Intro to WebVR

XR

About Me

Matt Stow

@stowball
 

Lead UX Engineer at
National Rugby League

VR

AR

3DoF

6DoF

1977

1982

1987

1991

1992

1995

1998

2002

2011

2012

2014

2015

2016

2018

Unique experiences

FIFA World Cup 2018

Keep Talking & Nobody Explodes

VR Arcades, Japan

Zero Latency, Melbourne

Luke W’s AR Examples

What is WebXR?

  1. A draft, official spec and experimental JavaScript API that supports VR devices
     
  2. Basically, it provides performant
    Stereoscopic Rendering and Positional Tracking
     
  3. It doesn‘t do graphics. You need WebGL
     
  4. It extends the Gamepad and a few other APIs

Developing for XR

What you'll need

Vanilla

if (navigator.xr) {
  navigator.xr.requestDevice()
  .then(xrDevice => {
    // Advertise the AR/VR functionality to get a user gesture.
  })
  .catch(err => {
    if (err.name === 'NotFoundError') {
      // No XRDevices available.
      console.error('No XR devices available:', err);
    } else {
      // An error occurred while requesting an XRDevice.
      console.error('Requesting XR device failed:', err);
    }
  })
} else{
  console.log("This browser does not support the WebXR API.");
}
xrPresentationContext = htmlCanvasElement.getContext('xrpresent');
let sessionOptions = {
  // The immersive option is optional for non-immersive sessions;
  // the value defaults to false.
  immersive: false,
  outputContext: xrPresentationContext
}
xrDevice.requestSession(sessionOptions)
.then(xrSession => {
  // Use a WebGL context as a base layer.
  xrSession.baseLayer = new XRWebGLLayer(session, gl);
  // Start the render loop
})
xrSession.requestFrameOfReference('eye-level')
.then(xrFrameOfRef => {
  xrSession.requestAnimationFrame(onFrame(time, xrFrame) {
    let pose = xrFrame.getDevicePose(xrFrameOfRef);
    if (pose) {
      for (let view of xrFrame.views) {
        // Draw something to the screen.
      }
    }
    // Input device code will go here.
    frame.session.requestAnimationFrame(onFrame);
  }
}

// Oh, and don't forget to load the polyfill
// https://github.com/immersive-web/webxr-polyfill
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );

var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

var geometry = new THREE.BoxGeometry( 1, 1, 1 );
var material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
var cube = new THREE.Mesh( geometry, material );
scene.add( cube );

camera.position.z = 5;

var animate = function () {
	requestAnimationFrame( animate );

	cube.rotation.x += 0.01;
	cube.rotation.y += 0.01;

	renderer.render( scene, camera );
};

animate();

Unity

using UnityEngine;
using UnityEngine.VR;

public class UpdateEyeAnchors : MonoBehaviour
{
    GameObject[] eyes = new GameObject[2];
    string[] eyeAnchorNames = { "LeftEyeAnchor", "RightEyeAnchor" };  
  
    void Update()
    {
        for (int i = 0; i < 2; ++i)
        {
            // If the eye anchor is no longer a child of us, don't use it
            if (eyes[i] != null && eyes[i].transform.parent != transform)
            {
                eyes[i] = null;
            }

            // If we don't have an eye anchor, try to find one or create one
            if (eyes[i] == null)
            {
                Transform t = transform.Find(eyeAnchorNames[i]);
                if (t)
                    eyes[i] = t.gameObject;

                if (eyes[i] == null)
                {
                    eyes[i] = new GameObject(eyeAnchorNames[i]);
                    eyes[i].transform.parent = gameObject.transform;
                }
            }

            // Update the eye transform
            eyes[i].transform.localPosition = InputTracking.GetLocalPosition((VRNode)i);
            eyes[i].transform.localRotation = InputTracking.GetLocalRotation((VRNode)i);
        }
    }
}
private void OnEnable ()
{
    m_VrInput.OnSwipe += HandleSwipe;
}


private void HandleSwipe(VRInput.SwipeDirection swipeDirection)
{
    // If the game isn't playing or the camera is fading, return and don't handle the swipe.
    if (!m_MazeGameController.Playing)
        return;

    if (m_CameraFade.IsFading)
        return;

    // Otherwise start rotating the camera with either a positive or negative increment.
    switch (swipeDirection)
    {
        case VRInput.SwipeDirection.LEFT:
            StartCoroutine(RotateCamera(m_RotationIncrement));
            break;

        case VRInput.SwipeDirection.RIGHT:
            StartCoroutine(RotateCamera(-m_RotationIncrement));
            break;
    }
}

// Export with Mozilla's Unity WebVR Assets plugin
// https://github.com/mozilla/unity-webvr-export

BabylonJS

var scene = new BABYLON.Scene(engine);
var vrHelper = scene.createDefaultVRExperience();

// Initial camera before the user enters VR
vrHelper.deviceOrientationCamera;
// WebVR camera used after the user enters VR
vrHelper.webVRCamera;
// One of the 2 cameras above depending on which one is in use
vrHelper.currentVRCamera;

vrHelper.onControllerMeshLoaded.add((webVRController)=>{
    var controllerMesh = webVRController.mesh;
    webVRController.onTriggerStateChangedObservable.add(()=>{
        // Trigger pressed event
    });
});

vrHelper.enableInteractions();
var myBox = BABYLON.MeshBuilder.CreateBox('myBox', {
  height: 5, width: 2, depth: 0.5,
}, scene);

var mySphere = BABYLON.MeshBuilder.CreateSphere('mySphere', {
  diameter: 2, diameterX: 3,
}, scene);

var myPlane = BABYLON.MeshBuilder.CreatePlane('myPlane', {
  width: 5, height: 2,
}, scene);

var myGround = BABYLON.MeshBuilder.CreateGround('myGround', {
  width: 6, height: 4, subdivsions: 4,
}, scene);

PlayCanvas

var LookCamera = pc.createScript('lookCamera');

LookCamera.attributes.add("mouseLookSensitivity", {
  type: "number", default: 0, title: "Mouse Look Sensitivity", description: "",
});
LookCamera.attributes.add("touchLookSensitivity", {
  type: "number", default: 0, title: "Touch Look Sensitivity", description: "",
});

LookCamera.prototype.initialize = function () {
    // Camera euler angle rotation around x and y axes
    var quat = this.entity.getLocalRotation();
    this.ex = this.getPitch(quat) * pc.math.RAD_TO_DEG;
    this.ey = this.getYaw(quat) * pc.math.RAD_TO_DEG;
    
    this.targetEx = this.ex;
    this.targetEy = this.ey;
            
    this.moved = false;
    this.lmbDown = false;

    // Disabling the context menu stops the browser displaying a menu when
    // you right-click the page
    this.app.mouse.disableContextMenu();

    this.addEventCallbacks();
    
    this.lastTouchPosition = new pc.Vec2();
    
    this.on("destroy", function () {
        this.removeEventCallbacks();
    });
    
    if (this.app.vr && this.app.vr.display) {
        this.app.vr.display.on("presentchange", this.onVrPresentChange, this);
    }
    
    this.startCameraOrientation = this.entity.getLocalRotation().clone();
};

React 360

import {
  StyleSheet,
  Text,
  View,
  VrButton,
} from 'react-360';

class HelloWorld extends Component {
  render() {
    return (
      <View style={styles.wrapper}>
        <VrButton style={styles.button}>
          <Text style={styles.buttonText}>
            Hello World
          </Text>
        </VrButton>
      </View>
    );
  }
}

What is A-Frame?

  1. A web framework for quickly and easily building cross-platform VR experiences
     
  2. Created by Mozilla in 2014
     
  3. Built on top of and provides a declarative structure to three.js
     
  4. Bloody awesome

A-Frame

<script src="https://aframe.io/releases/0.8.2/aframe.min.js"></script>

<a-scene>
  <a-box
    position="-1 0.5 -3" rotation="0 45 0" color="#4cc3d9" shadow
  ></a-box>
  
  <a-sphere
    position="0 1.25 -5" radius="1.25" color="#ef2d5e" shadow
  ></a-sphere>

  <a-cylinder
    position="1 0.75 -3" radius="0.5" height="1.5" color="#ffc65d" shadow
  ></a-cylinder>

  <a-plane
    position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7bc8a4" shadow
  ></a-plane>
</a-scene>

How it works

<script src="https://aframe.io/releases/0.8.2/aframe.min.js"></script>

<a-scene>
  <a-box
    position="-1 0.5 -3" rotation="0 45 0" color="#4cc3d9" shadow
  ></a-box>
  
  <a-sphere
    position="0 1.25 -5" radius="1.25" color="#ef2d5e" shadow
  ></a-sphere>

  <a-cylinder
    position="1 0.75 -3" radius="0.5" height="1.5" color="#ffc65d" shadow
  ></a-cylinder>

  <a-plane
    position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7bc8a4" shadow
  ></a-plane>
</a-scene>

Handles 3D boilerplate, VR setup, default camera, lighting and controls

Convenient HTML element called a primitive
It's a wrapper around the generic <a-entity>




  <a-box
    
  ></a-box>
  
  

Attributes are called components. Dimension attribute values are in metres
Components can be standalone (shadow), accept a simple string (position), or accept multiple properties via an inline style syntax ("foo: bar; baz: qux;")




  
  
  
  
   
   
  

 
    position="1 0.75 -3" radius="0.5" height="1.5" color="#ffc65d" shadow
 

 
 
  
<script src="https://aframe.io/releases/0.8.2/aframe.min.js"></script>

<a-scene>
  
  
  
  
  
  
  

  
  
  

  
  
  
</a-scene>

Primitives

  • <a-box>
  • <a-camera>
  • <a-circle>
  • <a-collada-model>
  • <a-cone>
  • <a-cursor>
  • <a-curvedimage>
  • <a-cylinder>
  • <a-dodecahedron>
  • <a-gltf-model>
  • <a-icosahedron>
  • <a-image>
  • <a-light>
  • <a-link>
  • <a-obj-model>
  • <a-octahedron>
  • <a-plane>
  • <a-ring>
  • <a-sky>
  • <a-sound>
  • <a-sphere>
  • <a-tetrahedron>
  • <a-text>
  • <a-torus-knot>
  • <a-torus>
  • <a-triangle>
  • <a-video>
  • <a-videosphere>

Components

  • camera
  • collada-model
  • cursor
  • daydream-controls
  • debug
  • embedded
  • fog
  • gearvr-controls
  • geometry
  • gltf-model
  • hand-controls
  • keyboard-shortcuts
  • laser-controls
  • light
  • line
  • link
  • look-controls
  • material
  • obj-model
  • oculus-touch-controls
  • pool
  • position
  • raycaster
  • rotation
  • scale
  • screenshot
  • shadow
  • sound
  • stats
  • text
  • tracked-controls
  • visible
  • vive-controls
  • vr-mode-ui
  • wasd-controls
  • windows-motion-controls

A-Frame Inspector!

Ctrl+Alt+I

const box = document.querySelector('a-box');
const sphere = document.querySelector('a-sphere');
const cylinder = document.querySelector('a-cylinder');
const plane = document.querySelector('a-plane');

setTimeout(() => {
  box.setAttribute('color', 'dodgerblue');
}, 500);

setTimeout(() => {
  sphere.setAttribute('radius', 1.75);
}, 1000);

setTimeout(() => {
  cylinder.setAttribute('position', '1 2 -3');
}, 1500);

setTimeout(() => {
  plane.setAttribute('visible', false);
}, 2000);

Imperative 😬 

Declarative 🤔

Declarative! 

<div id="app">
  <a-scene background="color: #333">
    <a-box
      position="-1 0.5 -3" rotation="0 45 0" shadow
      v-bind:color="boxColor"
    ></a-box>

    <a-sphere
      position="0 1.25 -5" color="#ef2d5e" shadow
      v-bind:radius="sphereRadius"
    ></a-sphere>

    <a-cylinder
      radius="0.5" height="1.5" color="#ffc65d" shadow
      v-bind:position="cylinderPosition"
    ></a-cylinder>

    <a-plane
      position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7bc8a4" shadow
      v-bind:visible="planeVisibility.toString()"
    ></a-plane>
  </a-scene>
</div>
new Vue({
  el: '#app',
  data() {
    return {
      boxColor: '#4cc3d9',
      sphereRadius: 1.25,
      cylinderPosition: '1 0.75 -3',
      planeVisibility: true,
    };
  },
  mounted() { 
    setTimeout(() => { this.boxColor = 'dodgerblue'; }, 1000);

    setTimeout(() => { this.sphereRadius = 1.75; }, 1500);

    setTimeout(() => { this.cylinderPosition = '1 2 -3'; }, 2000);

    setTimeout(() => { this.planeVisibility = false; }, 2500);
  },
});

Declarative! 

<a-box
  color="#fff"
  depth="0.2"
  height="0.5"
  position="0 3.5 -4"
  ref="button"
  width="1"
></a-box>

…

mounted() {
  this.$refs.button.addEventListener('mouseenter', this.reset);
}
<a-box
  color="#fff"
  depth="0.2"
  height="0.5"
  position="0 3.5 -4"
  width="1"
  v-on:mouseenter="reset"
></a-box>

Declarative?

vue-aframe-directives

// my-component.vue
<a-entity
  v-a-bind:color="color"
  v-a-bind:depth="depth"
  v-a-bind:position="`0 ${positionY} 2`"
  v-a-bind:rotation="rotation"
  v-a-bind:scale="scale"
  v-a-bind:visible="visible"
></a-entity>

// app.vue
<my-component
  v-bind:color="#fff"
  v-bind:depth="0"
  v-bind:position-y="1"
  v-bind:rotation="{ x: 45 }"
  v-bind:scale="[3, 2, 1]"
  v-bind:visible="false"
></my-component>

// behind the scenes
el.setAttribute('color', '#fff');
el.setAttribute('depth', 0.001);
el.object3D.position.set(0, 1, 2);
el.object3D.rotation.x = 45;
el.object3D.scale.set(3, 2, 1);
el.object3D.visible = false;

WIP

2-day hackathon PoC

One last thing…

<script src="aframe-ar.min.j></script>

<a-scene
  arjs="debugUIEnabled: false; sourceType: webcam;"
  embedded
>
  <a-box
    color="#fff"
    depth="1.1"
    height="1.1"
    material="opacity: 0.3"
    width="1.1"
  />
  <a-sphere
    radius="0.5"
    src="earth_atmos_4096.jpg"
  >
    <a-animation
      attribute="rotation"
      dur="5000"
      easing="linear"
      repeat="indefinite"
      to="0 360 0"
    ></a-animation>
  </a-sphere>

  <a-marker-camera preset="hiro"></a-marker-camera>
</a-scene>

A-Frame AR!

Where to next?

  1. Learn more at A-Frame School
     
  2. Learn how to design for VR (Link 1, Link 2)
     
  3. Design 3D models with Vectary or Blender
     
  4. Have fun!

Thank you!

@stowball
 

 

Intro to WebXR

By Matt Stow

Intro to WebXR

A brief history of VR and how you can use A-Frame to quickly build and prototype VR experiences in the browser

  • 3,211