Building VR experiences with A-Frame
Belén Albeza
Why VR in the Web?
- The power of an URL
- You can leverage your existing know-how
Which browsers?
- Firefox, Chrome, Samsung Internet, Edge
- Check the status at webvr.rocks
- Polyfill other browsers
Which headsets?
- HTC Vive
- Oculus Rift
- Samsung Gear
- Daydream
The WebVR API
- It's an standard API (spec at W3C)
- It doesn't render, but provides the data to do it
- We can get:
- Characteristics of the headset
- The pose
- The stage
- VR controllers input
Tools to author VR content
- THREE.js
- Some game engines (Unity, PlayCanvas…)
- A-Frame!
A-Frame
- aframe.io
- It's a WebVR framework that simplifies a lot creating VR experiences
- Created at Mozilla, open-source
Creating a VR scene is easy…
<a-scene>
<a-sphere radius="1" position="0 1 -4" color="#ff0040">
</a-sphere>
<a-plane rotation="-90 0 0" position="0 0 -5" width="10"
height="10" color="#fff">
</a-plane>
<a-sky color="#eee"></a-sky>
</a-scene>
Advantages of this approach
- It's in the DOM!
querySelector
, etc. work! - You can manipulate the scene with JS by adding, removing and modifying these DOM elements
- You can use existing tools and frameworks (such as a template library)
Which primitives we have?
- 3D primitives:
a-sphere
,a-box
,a-torus
… - Images and video:
a-video
,a-videosphere
,… - Other:
a-gltf
,a-text
,a-light
,a-sound
…
The ECS pattern
- Entity-Component-System
- Favors compoistion over inheritance
- Entities (
a-entity
,a-box
…): barebones structure - Component (
position
,color
…): attached to entities - System: handles business logic of components
A-Painter
VR drawing app
A-Blast
Arcade game

Let's make a demo!
- It leverages A-Frame's entity-component-system
- It uses A-Frame's asset manager
- It synthesizes chords with the Web Audio API
- It uses positional audio
Step 1: The barebones
- We are creating the basic, static elements of the scene
- We are setting the sky to be (temporarily) red
<a-scene>
<a-camera position="0 1 0"></a-camera>
<a-circle rotation="-90 0 0" radius="10" color="#fff"
transparent opacity="0.5"></a-circle>
<a-sky color="red"></a-sky>
</a-scene>
Step 2: the asset manager
- A-Frame incorporates an asset manager so it loads images, audio files, models, etc.
- We don't need to listen to
load
events, etc. - Use
a-assets
- We are going to use an image in the sky
<a-scene>
<a-assets>
<img id="city" src="city.jpg">
</a-assets>
<!-- ... -->
<a-sky src="#city"></a-sky>
</a-scene>
Step 3: Create the pillars procedurally
- Why? DRY and we don't need to manually calculate positions
- We can just use the standard API's to manipulate the DOM
Document.createElement
,Element.appendChild
,Element.setAttribute
…- We will create
a-cylinder
instances and attach them to the scene
const PILLARS = [
{ color: 'rgb(253, 162, 40)' },
{ color: 'rgb(254, 229, 65)' },
{ color: 'rgb(35, 224, 64)' },
// ...
];
const RADIUS = 5;
PILLARS.forEach(function (p, index, arr) {
let angle = index * (2 * Math.PI / arr.length);
let x = Math.cos(angle) * RADIUS;
let z = Math.sin(angle) * RADIUS;
let el = document.createElement('a-cylinder');
el.setAttribute('position', `${x} 0 ${z}`);
el.setAttribute('color', p.color);
document.querySelector('a-scene').appendChild(el);
});
Step 4: Create a component
- For now it will animate the entity by nesting an
a-animation
(later on it will play a chord) - The component will hold the data to operate
- The system will provide the behaviour
AFRAME.registerComponent('pipe', {
schema: {
chord: { type: 'string', default: 'C4' },
dur: { type: 'number', default: 2000 }
},
init: function () {
this.el.appendChild(this.system.createAnimation(this.data));
},
});
AFRAME.registerSystem('pipe', {
createAnimation: function (data) {
let anim = document.createElement('a-animation');
anim.setAttribute('attribute', 'scale');
anim.setAttribute('dur', data.dur / 3);
anim.setAttribute('from', '1 1 1');
// more attribute setup here
// ...
return anim;
}
});
const PILLARS = [
{ chord: 'C4', color: 'rgb(253, 162, 40)', dur: 2500 },
// ...
];
PILLARS.forEach(function (p, index, arr) {
// ...
// ex: <a-cylinder pipe="chord: C4; dur: 2500">
el.setAttribute('pipe', `chord: ${p.chord};
${p.dur ? 'dur: ' + p.dur : '' }`);
// ...
});
Step 5: play sounds
- We have an audio synth created with Web Audio
- It can play major chords (3 notes at the same time)
- A note is a wave with a given frequency (ex: A4 is 440 Hz)
- It can handle positional audio
AFRAME.registerSystem('pipe', {
init: function () {
this.ctx = new AudioContext();
this.instrument = new Instrument(this.ctx);
},
// ...
};
AFRAME.registerSystem('pipe', {
// ...
playChord: function (entity) {
let data = entity.components.pipe.data;
let position = entity.getAttribute('position');
let rotY = // ... camera rotation
let orientation = // ... orientation to camera
this.instrument.playChord(data.chord, data.dur / 1000,
position, orientation);
entity.emit('sound');
},
//...
};
Final step: orchestrate!
- Systems have a
tick
callback, which is executed every frame - We will use it to select a random pillar and play it, once the previous note has finished
- The system now needs to track every existing component
AFRAME.registerSystem('pipe', {
init: function () {
this.entities = [];
// ...
},
subscribe: function (el) {
this.entities.push(el);
},
unsubscribe: function (el) {
let index = this.entities.indexOf(el);
this.entities.splice(index, 1);
},
// ...
};
AFRAME.registerSystem('pipe', {
// ...
tick: function () {
let timestamp = this.ctx.currentTime;
if (timestamp - this.lastNoteTime > this.lastNoteDuration) {
let entity = this.entities[
Math.floor(Math.random() * this.entities.length)];
this.lastNoteDuration = this.playChord(entity);
this.lastNoteTime = timestamp;
}
},
// ...
};
Try A-Frame yourself!
- Get started at aframe.io
- Mozilla's A-Frame codepens codepen.io/mozvr
Thanks!
Questions?
Source code:
https://github.com/belen-albeza/building-vr