@stowball
Lead UX Engineer at
National Rugby League
3DoF
6DoF
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();
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
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);
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();
};
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>
);
}
}
<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>
<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>
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);
<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);
},
});
<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>
// 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
<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>
@stowball