Building VR experiences with A-Frame

Belén Albeza

belenalbeza.com // @ladybenko

Deck:
https://belen-albeza.github.io/building-vr

Why VR in the Web?

Which browsers?

Which headsets?

The WebVR API

Tools to author VR content

A-Frame

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

Which primitives we have?

The ECS pattern

A-Painter

VR drawing app

A-Blast

Arcade game

Let's make a demo!

Step 1: The barebones

<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-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

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

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

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!

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!

Thanks!

Questions?

@ladybenko

Source code:
https://github.com/belen-albeza/building-vr