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 loadevents, 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-cylinderinstances 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 tickcallback, 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