Introducing the WebVR 1.0 API Proposal

2016 is shaping up to be a banner year for Virtual Reality. Many consumer VR products will finally be available and many top software companies are ramping up to support these new devices. The new medium has also driven demand for web-enabled support from browser vendors. Growth in WebVR has centered on incredible viewing experiences and the tools used to create online VR content.

VR_images
The Mozilla VR team has been working hard to support the online creation and display of VR content in the browser. This week marks a WebVR milestone. Working closely with Brandon Jones of the Google Chrome team, the Mozilla team is excited to announce the version 1.0 release of the WebVR API proposal.

Recent VR technology advances and community feedback have allowed us to improve the API to address developer needs.

Some of the improvements include:

  • VR-specific handling of device rendering and display.
  • The ability to traverse links between WebVR pages.
  • An input handling scheme that can enumerate VR inputs, including six degrees of freedom (6DoF) motion controllers.
  • Accommodation of both sitting and standing experiences.
  • Suitability for both desktop and mobile usage.

We are excited to share these improvements to the API. Keep in mind the list above represents a small sample of what has changed. For all the details, take a look at the full API draft and check out Brandon’s blog post.

This article is focused on basic usage of the proposed API, which requires an understanding of some complex concepts like matrix math. As an alternative, you can get a quick start in WebVR by looking at A-Frame or the WebVR boilerplate, both built on top of the API.

Before we dive in, we’d like to give special thanks to Chris Van Wiemeersch (Mozilla), Kearwood “Kip” Gilbert (Mozilla), Brandon Jones (Google), and Justin Rogers (Microsoft) for contributing to the creation of this specification.

Implementation roadmap

We plan to land a stable implementation of the 1.0 APIs in Firefox Nightly in the first half of the year. You can follow along on Bugzilla for all the details, or see status updates on platform support on iswebvrready.org.

Want to get started today? Currently developers can experiment with a proof-of-concept implementation of the new API using Brandon Jones’ experimental builds of Chromium.

Both three.js and the WebVR Polyfill (used by the WebVR Boilerplate mentioned above) have open pull requests to support the latest APIs.

Components of a VR experience

Let’s take a look at the key components required for any VR experience:

  1. The VR display that we are rendering content to.
  2. The user’s pose. The orientation and positioning of the headset in space.
  3. Eye parameters that define stereo separation and field of view.

Here’s a look at the workflow sequence for getting content into the headset:

  1. navigator.getVRDisplays() to retrieve a VR Display.
  2. Create a <canvas> element which we will use to render content.
  3. Use VRDisplay.requestPresent() to pass in the canvas element.
  4. Create the VR device-specific animation loop in which we will perform content rendering.
    1. VRDisplay.getPose() to update the user’s pose.
    2. Perform calculations and rendering.
    3. Use VRDisplay.submitFrame() to indicate to the compositor when the canvas element content is ready to be presented in the VR display.

The following sections describe each of these actions in detail.

Working with VR displays

Devices that display VR content have very specific display requirements for frame rate, field of view, and content presentation that are handled separately from standard desktop displays.

Enumerating VR displays

To retrieve VR displays available to the browser, use the navigator.getVRDisplays() method, which returns a Promise that resolves with an array of VRDisplay objects:

navigator.getVRDisplays().then(function (displays) {
  if (!displays.length) {
    // WebVR is supported, no VRDisplays are found.
    return;
  }

  // Handle VRDisplay objects. (Exposing as a global variable for use elsewhere.)
  vrDisplay = displays.length[0];
}).catch(function (err) {
  console.error('Could not get VRDisplays', err.stack);
});

Keep in mind:

  • You must have your VR headset plugged in and powered on before any VR devices will be enumerated.
  • If you do not have a VR headset, you can simulate a device by opening about:config and setting dom.vr.cardboard.enabled to true.
  • Users of Firefox Nightly for Android or Firefox for iOS will enumerate a Cardboard VR device for use with Google Cardboard.

Creating a render target

To determine the render target size (i.e., your canvas size), create a render target large enough to hold both the left and right eye viewports. To find the size (in pixels) of each eye:

// Use 'left' or 'right'.
var eyeParameter = vrDisplay.getEyeParameters('left');

var width = eyeParameter.renderWidth;
var height = eyeParameter.renderHeight;

Presenting content into the headset

To present content into the headset, you’ll need to use the VRDisplay.requestPresent() method. This method takes a WebGL <canvas> element as a parameter which represents the viewing surface to be displayed.

To ensure that the API is not abused, the browser requires a user-initiated event in order for a first-time user to enter VR mode. In other words, a user must choose to enable VR, and so we wrap this into a click event handler on a button labeled “Enter VR”.

// Select WebGL canvas element from document.
var webglCanvas = document.querySelector('#webglcanvas');
var enterVRBtn = document.querySelector('#entervr');

enterVRBtn.addEventListener('click', function () {
  // Request to present WebGL canvas into the VR display.
  vrDisplay.requestPresent({source: webglCanvas});
});

// To later discontinue presenting content into the headset.
vrDisplay.exitPresent();

Device-specific requestAnimationFrame

Now that we have our render target set up and the necessary parameters to render and present the correct view into the headset, we can create a render loop for the scene.

We will want to do this at an optimized refresh rate for the VR display. We use the VRDisplay.requestAnimationFrame callback:

var id = vrDisplay.requestAnimationFrame(onAnimationFrame);

function onAnimationFrame () {  
  // Render loop.
  id = vrDisplay.requestAnimationFrame(onAnimationFrame);
}

// To cancel the animation loop.
vrDisplay.cancelRequestAnimationFrame(id);

This usage is identical to the standard window.requestAnimationFrame() callback that you may already be familiar with. We use this callback to apply position and orientation pose updates to our content and to render to the VR display.

Retrieving pose information from a VR display

We will need to retrieve the orientation and position of the headset using the VRDisplay.getPose() method:

var pose = vrDisplay.getPose();

// Returns a quaternion.
var orientation = pose.orientation;

// Returns a three-component vector of absolute position.
var position = pose.position;

Please note:

  • Orientation and position return null if orientation and position cannot be determined.
  • See VRStageCapabilities and VRPose for details.

Projecting a scene to the VR display

For proper stereoscopic rendering of the scene in the headset, we need eye parameters such as the offset (based on interpupillary distance or IPD) and field of view (FOV).

// Pass in either 'left' or 'right' eye as parameter.
var eyeParameters = vrDisplay.getEyeParameters('left');

// After translating world coordinates based on VRPose, transform again by negative of the eye offset.
var eyeOffset = eyeParameters.offset;

// Project with a projection matrix.
var eyeMatrix = makeProjectionMatrix(vrDisplay, eyeParameters);


// Apply eyeMatrix to your view.
// ...

/**
 * Generates projection matrix
 * @param {object} display - VRDisplay
 * @param {number} eye - VREyeParameters
 * @returns {Float32Array} 4×4 projection matrix
 */
function makeProjectionMatrix (display, eye) {
  var d2r = Math.PI / 180.0;
  var upTan = Math.tan(eye.fieldOfView.upDegrees * d2r);
  var downTan = Math.tan(eye.fieldOfView.leftDegrees * d2r);
  var rightTan = Math.tan(eye.fieldOfView.rightDegrees * d2r);
  var leftTan = Math.tan(eye.fieldOfView.leftDegrees * d2r);
  var xScale = 2.0 / (leftTan + rightTan);
  var yScale = 2.0 / (upTan + downTan);

  var out = new Float32Array(16);
  out[0] = xScale;
  out[1] = 0.0;
  out[2] = 0.0;
  out[3] = 0.0;
  
  out[4] = 0.0;
  out[5] = yScale;
  out[6] = 0.0;
  out[7] = 0.0;
  
  out[8] = -((leftTan - rightTan) * xScale * 0.5);
  out[9] = (upTan - downTan) * yScale * 0.5;
  out[10] = -(display.depthNear + display.depthFar) / (display.depthFar - display.depthNear);

  out[12] = 0.0;
  out[13] = 0.0;
  out[14] = -(2.0 * display.depthFar * display.depthNear) / (display.depthFar - display.depthNear);
  out[15] = 0.0;

  return out;
}

Submitting frames to the headset

VR is optimized to minimize the discontinuity between the user’s movement and the content rendered into the headset. This is important for a comfortable (non-nauseating) experience. This is enabled by giving direct control of how this happens using the VRDisplay.getPose() and VRDisplay.submitFrame() methods:

// Rendering and calculations not dependent on pose.
// ...

var pose = vrDisplay.getPose();

// Rendering and calculations dependent on pose. Apply your generated eye matrix here to views.
// Try to minimize operations done here.
// ...

vrDisplay.submitFrame(pose);

// Any operations done to the frame after submission do not increase VR latency. This is a useful place to render another view (such as mirroring).
// ...

The general rule is to call VRDisplay.getPose() as late as possible and VRDisplay.submitFrame() as early as possible.

Demos, feedback and resources

Looking for ways to get started? Here’s a collection of example apps that use the WebVR 1.0 API. Also, check out the resources listed below.

And please keep sharing your feedback!

The development of this API proposal has been in direct response to evolving VR technology, and also from community feedback and discussion. We’re off to a good start, and your ongoing feedback can help us make it better.

We invite you to submit issues and pull requests to the WebVR API specification GitHub repository.

Resources

About Casey Yee

I work on the WebVR team at Mozilla and work on researching how we can use web technology to build high performance Virtual Reality experiences.

More articles by Casey Yee…


5 comments

  1. 动感小前端

    excited! thx for share!

    March 2nd, 2016 at 17:30

  2. Finbar Maginn

    Would love to see the output formatted for cardboard VR!

    March 16th, 2016 at 07:28

    1. Casey Yee

      @Finbar,
      You get a Cardboard formatted output when using the WebVR API’s on Firefox for Android. For all other browsers, take a look at Boris Smus’ webvr-boilerplate (https://github.com/borismus/webvr-boilerplate/) which polyfills the behavior using requestFullscreen().

      March 22nd, 2016 at 10:49

  3. Jonas

    Looks interesting. I’ll need to give this a try. Two questions:

    How do you plan to deal with latency and GC interrupts?
    I tried to build a twitchy FPS with WebGL a few (2?) years ago and the input to output latency was quite bad in firefox and terrible in google chrome (approaching 100ms iirc). In firefox there was the additional problem of gc pauses even with fairly optimized code (not sure why the GC was even triggered of the heap didn’t grow but it still interrupted the game). Is there a separate shorter rendering pipeline for WebVR? Do you completely rely on reprojection? And lastly could this be applied to non VR webgl rendering too?

    March 16th, 2016 at 07:47

    1. Casey Yee

      @jonas,
      We are currently tracking at about 7ms from when we retrieve orientation and position data to when the actual pixels are painted into the headset. That falls within the 13ms we have per-frame at 75hz using the Oculus DK2 with our reference scene mozvr.com Sechelt example.

      Additionally, with recent implementation of pose prediction, we have even lower perceived latency and smoother tracking (http://mozvr.ghost.io/webvr-oculus-pose-prediction-and-hw-latency-testing/).

      As far as the GC pauses are concerned. This is definetly an ongoing issue that needs fixing, but for VR there are a two ways we can help mask these issues:

      – WebGL in workers, would allow us to move rendering out from the main thread and any janks and hangups (https://blog.mozilla.org/research/2014/07/22/webgl-in-web-workers-today-and-faster-than-expected/)

      – frame re-projection/async time-warp where we generate intermediate frames to cover for situations where frame rate dips below the required VR refresh. (https://developer.oculus.com/blog/asynchronous-timewarp-examined/)

      This is all definetly a work in progress, but the results have been encouraging. In many cases, the results are pretty close to or even indiscernible from native.

      March 22nd, 2016 at 12:53

Comments are closed for this article.