This guide is intended to help developers that have created content with the now deprecated WebVR API migrate to the WebXR API that replaced it. For simplicity this document is going to primarily focus on the differences between how WebVR and WebXR presenting VR content to a headset, since that’s the primary draw of the API. Differences in displaying inline content are not covered.
The WebXR API is considered a "powerful feature" and thus only available on secure origins (ie: URLs using HTTPS). For development purposes localhost
counts as a secure origin, and other domains can be temporarily treated as secure via browser-specific mechanisms:
WebVR applications start by calling navigator.getVRDisplays()
, which returns a list of connected VR hardware. The developer then chooses a VRDisplay
from the list and keeps a reference to it. This object is what almost all further interaction is done with for the remainder of the application. Changes to the available set of VR hardware is indicated with the vrdisplayconnect
and vrdisplaydisconnect
events on the Window
object.
In WebXR a list of connected hardware cannot be retrieved to avoid fingerprinting. Instead a single active “XR device” is implicitly picked by the UA and all operations are performed against it. (System support for multiple XR devices at once is almost unheard of, so this isn’t problematic for most any real world scenario.) Changes to the available VR hardware are indicated by the devicechange
event of the navigator.xr
object.
Both WebVR and WebXR applications have a need to advertise to users that VR content can be shown, usually by enabling a button that will be used to initiate the VR content’s presentation to the display.
WebVR applications test to see if the VRDisplay
will allow VR content to be shown on it by checking the vrDisplay.capabilities.canPresent
boolean attribute.
WebXR applications call navigator.xr.isSessionSupported()
with the desired XRSessionMode
(which indicates the type of content they want to display, such as “immersive-vr”
) and check the resolved boolean to see if the implicit XR Device supports it.
WebVR
// All code samples will omit error checking for clarity.
let displays = await navigator.getVRDisplays();
let vrDisplay = displays[0];
if (vrDisplay.capabilities.canPresent) {
ShowEnterVRButton();
}
WebXR
let supported = await navigator.xr.isSessionSupported('immersive-vr');
if (supported) {
ShowEnterVRButton();
}
WebVR applications begin presenting VR content by calling vrDisplay.requestPresent()
.
WebXR applications begin presenting VR content by calling navigator.xr.requestSession()
with the XRSessionMode
of “immersive-vr”
. This returns a promise that resolves to an XRSession
, which the developer keeps a reference to. This object is what almost all further interaction is done with for the remainder of the VR content presentation.
Both methods must be called during a user activation event.
Both APIs render VR content using WebGL, though the way that imagery is supplied to the API differs.
WebVR applications pass an HTMLCanvasElement
, expected to have an attached WebGLRenderingContext
. as the source
of a VRLayerInit
dictionary, which is then passed to vrDisplay.requestPresent()
. From then on any content rendered to the canvas’ WebGL context’s backbuffer is presented to the VR hardware. For best results, it is expected that the application will resize the canvas to match the combined renderWidth
and renderHeight
reported by the VREyeParameters
for both eyes, reported by calling vrDisplay.getEyeParameters()
with the desired VREye
.
WebXR applications must first make the WebGL context compatible with the active XR device. This ensures that the WebGL resources reside on the GPU that is optimal for VR rendering (for example, the one that the headset is physically connected to on a multi-GPU desktop PC). This is done by either setting the xrCompatible
key to true
in the WebGLContextCreationAttributes
when creating the context or calling gl.makeXRCompatible()
on the context after it’s been created (which may trigger a context loss). Then the developer constructs a new XRWebGLLayer
, passing in both the XRSession
and an XR compatible WebGLRenderingContext
. This layer is then set as the source of the content the VR hardware will display by passing it to xrSession.updateRenderState()
as the baseLayer
of the XRRenderStateInit
dictionary. The canvas does not need to be resized for best results.
WebVR
let glCanvas = document.createElement('canvas');
let gl = document.getContext('webgl');
await vrDisplay.requestPresent([{ source: glCanvas }]);
let leftEye = vrDisplay.getEyeParameters("left");
let rightEye = vrDisplay.getEyeParameters("right");
glCanvas.width = Math.max(leftEye.renderWidth, rightEye.renderWidth) * 2;
glCanvas.height = Math.max(leftEye.renderHeight, rightEye.renderHeight);
// Now presenting to the headset.
WebXR
let glCanvas = document.createElement('canvas');
let gl = glCanvas.getContext('webgl', { xrCompatible: true });
let xrSession = await navigator.xr.requestSession('immersive-vr');
let xrLayer = new XRWebGLLayer(session, gl);
session.updateRenderState({ baseLayer: xrLayer });
// Now presenting to the headset.
WebVR has a single implicit tracking environment that all poses are delivered in. In order to allow the user to feel like the floor of their virtual environment aligns with the floor of their physical environment they must transform all poses by the sittingToStandingTransform
matrix that’s reported by the vrDisplay.stageParameters
attribute. If the developers wants to know the boundaries of the user’s play space they can look at the sizeX
and sizeZ
attributes of the vrDisplay.stageParameters
, which describe an axis aligned rectangle centered on the origin defined by the sittingToStandingTransform
. (This limited form of boundaries reporting may force the reported size to be significantly smaller than the actual boundaries the user configured.)
WebXR requires the developer to define the tracking environment they want poses communicated in. This is both to enable a wider range of hardware (like AR devices) and to simplify the creation of floor-aligned content by removing much of the matrix math that WebVR required. The tracking space is specified by calling xrSession.requestReferenceSpace()
with the desired XRReferenceSpaceType
, which returns a promise that resolves to an XRReferenceSpace
. The developer will supply this object any time poses are requested. A “local”
reference space closely aligns with WebVR’s implicit tracking environment, while a “local-floor”
reference space aligns the virtual environment with the floor of the user’s physical environment similar to WebVR’s sittingToStandingTransform
(but with less math expected of the developer in order to make it work.) A “bounded-floor”
reference space also aligns with the user’s physical floor, with the addition of reporting boundsGeometry
, which gives a full polygonal boundary that’s more flexible/accurate than WebVR’s rectangular equivalent.
WebVR
// No equivalent
WebXR
xrReferenceSpace = await xrSession.requestReferenceSpace("local");
If the developer wants to use reference spaces other than "local"
during an ["immersive-vr"
] session they must also request consent to use it at session creation time by passing the desired type to either the requiredFeatures
or optionalFeatures
members of the XRSessionInit
dictionary passed to navigator.xr.requestSession()
. This will cause the UA to prompt the user for their consent to use the more detailed levels of tracking if necessary.
WebVR
// No equivalent
WebXR
let xrSession = await navigator.xr.requestSession('immersive-vr', {
requiredFeatures: ["local-floor"]
});
let xrReferenceSpace = await xrSession.requestReferenceSpace("local-floor");
Both VRDisplay
and XRSession
have a requestAnimationFrame
function that is called to process rendering callbacks at the appropriate refresh rate for the VR hardware.
In WebVR during the vrDisplay.requestAnimationFrame()
callback the user’s pose is queried by calling vrDisplay.getFrameData()
, which is passed an application-allocated VRFrameData
object to populate with the current pose data. The VRFrameData
contains projection and view matrices for the user’s left and right eyes, as well as a VRPose
that describes the position, orientation, velocity, and acceleration of the user at the time of the frame. The application is expected to use the projection and view matrices as-is, even though it may appear they could be computed from the VRPose
and values given in the VREyeParameters
.
In WebXR an XRFrame is passed into the callback provided to xrSession.requestAnimationFrame()
. The user’s pose is queried from the XRFrame by calling xrFrame.getViewerPose()
with the XRReferenceSpace
the developer wants the pose reported in. The XRViewerPose
that’s returned contains an array of XRView
s, each of which reports a projectionMatrix
and a transform that indicates the required position of the “camera” for that view. The projectionMatrix
is expected to be used as-is, but the transform
(which is an XRRigidTransform
) providing a position
vector and orientation
quaternion, as well as a matrix
representation of the same transform.) The XRViewerPose
also has a top-level transform
that gives the position and orientation for the VR hardware. No velocity or acceleration is exposed by WebXR at this time.
In WebVR the application is always expected to render the scene twice, once for the left eye to the left half of the default WebGL framebuffer, and once for the right eye to the right half of the default WebGL framebuffer. The resolution that the app renders at can be controlled two ways: By resizing the WebGL backbuffer (with the canvas width and height attributes) or by changing the left and right viewports by setting the leftBounds
and rightBounds
of the VRLayerInit
when calling vrDisplay.requestPresent()
.
In WebXR the application renders the scene N times, once for each XRView
that’s reported by the XRViewerPose
. The number of views reported may change from frame to frame. Content is rendered into the framebuffer
of the XRWebGLLayer
, which is allocated by the UA to match the VR hardware's needs. (The default WebGL framebuffer is not used by WebXR for "immersive-vr"
sessions, and can be rendered into for display on the page as usual during VR presentation.) The viewport for each view is determined by passing the XRView
into xrWebGLLayer.getViewport()
. The size of the XRWebGLLayer
framebuffer
is determined by the VR hardware (and reported on the layer as framebufferWidth
and framebufferHeight
) but can be scaled at layer creation time by setting the framebufferScaleFactor
in the XRWebGLLayerInit
dictionary.
When a WebVR application is done rendering it must call vrDevice.submitFrame()
to capture the content rendered to the WebGL context’s default framebuffer and display it on the VR hardware.
WebXR automatically presents the content of the XRWebGLLayer
’s framebuffer
to the VR hardware when the xrSession.requestAnimationFrame()
callback returns.
WebVR
let vrFrameData = new VRFrameData();
function onFrame (t) {
// Queue a request for the next frame to keep the animation loop going.
vrDisplay.requestAnimationFrame(onFrame);
// Get the frame data, which contains the required matrices.
vrDisplay.getFrameData(frameData);
// Ensure we're rendering to the default backbuffer.
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// Set the default left eye viewport, assuming that it wasn't changed during
// the call to vrDisplay.requestPresent().
gl.viewport(0, 0, webglCanvas.width * 0.5, webglCanvas.height);
// Render the scene using a fictional rendering library with the left eye's
// projection and view matrix.
scene.setProjectionMatrix(frameData.leftProjectionMatrix);
scene.setViewMatrix(frameData.leftViewMatrix);
scene.render();
// Set the default right eye viewport.
gl.viewport(webglCanvas.width * 0.5, 0, webglCanvas.width * 0.5, webglCanvas.height);
// Render the scene using a fictional rendering library with the right eye's
// projection and view matrix.
scene.setProjectionMatrix(frameData.rightProjectionMatrix);
scene.setViewMatrix(frameData.rightViewMatrix);
scene.render();
vrDisplay.submitFrame();
}
WebXR
function onFrame(t, frame) {
let session = frame.session;
// Queue a request for the next frame to keep the animation loop going.
session.requestAnimationFrame(onXRFrame);
// Get the XRDevice pose relative to the Reference Space we created
// earlier. The pose may not be available for a variety of reasons, so
// we'll exit the callback early if it comes back as null.
let pose = frame.getViewerPose(xrReferenceSpace);
if (!pose) {
return;
}
// Ensure we're rendering to the layer's backbuffer.
let layer = session.renderState.baseLayer;
gl.bindFramebuffer(gl.FRAMEBUFFER, layer.framebuffer);
// Loop through each of the views reported by the viewer pose.
for (let view of pose.views) {
// Set the viewport required by this view.
let viewport = layer.getViewport(view);
gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
// Render the scene using a fictional rendering library with the view's
// projection matrix and view transform.
scene.setProjectionMatrix(view.projectionMatrix);
scene.setCameraTransform(view.transform.position, view.transform.orientation);
// Alternatively, the view matrix can be retrieved directly like so:
// scene.setViewMatrix(view.transform.inverse.matrix);
scene.render(view.projectionMatrix, view.transform);
}
}
WebVR applications receive VR controller input from the navigator.getGamepads()
API. Some of the Gamepad
objects returned will have a non-zero displayId
, which indicates they are associated with a VRDisplay
. The buttons
and axes
arrays indicate input states as usual, while a non-standard pose
attribute on the Gamepad
indicates the VRPose
of the controller in WebVR’s implicit tracking space. If the developer wants the controller’s pose relative to the user’s floor it must be manually transformed with the vrDisplay.stageParameters.sittingToStandingTransform
. A non-standard hand
attribute on the Gamepad
indicates which hand the controller is associated with, if known. The id
attribute of the Gamepad
indicates the name of the controller, and can be used to load an appropriate mesh to represent the device in the virtual scene.
WebXR applications surface input from multiple sources through xrSession.inputSources
, which is an array of XRInputSource
objects. The XRInputSource
contains a targetRaySpace
(for point and click tracking) and optional gripSpace
(for handheld objects) which can be passed to the xrFrame.getPose()
method along with the tracking space they should be reported relative to in order to get the XRPose
of the input source. The XRInputSource
also has a handedness
attribute to indicate which hand the input source is associated with, if known. For button and axis state the XRInputSource
has an optional gamepad
attribute, which is a Gamepad
object (that notably lacks the non-standard extensions used by WebVR and does not appear in the array returned by navigator.getGamepads()
). The profiles
attribute of the XRInputSource
contains an array of strings that indicate, with decreasing specificity, the type of input device and can be used to load an appropriate mesh to represent the device in the virtual scene.
It is not possible to trigger user activation events with WebVR input.
The selectstart
, select
, and selectend
events fired on an XRSession
indicate when the primary trigger, button, or gesture of an XRInputSource
is being interacted with, and can be used to facilitate basic interaction without the need to observe the gamepad
state. The select
event is a user activation event and can be used to begin media playback, among other things.
There is also a corresponding set of squeezestart
, squeeze
, and squeezeend
events that are fired when either a grip button or squeeze gesture is being interacted with. The squeeze
event also is a user activation event.
WebVR
function onFrame (t) {
// Queue a request for the next frame to keep the animation loop going.
vrDisplay.requestAnimationFrame(onFrame);
// Loop through all gamepads and identify the ones that are associated with
// the vrDisplay.
let gamepads = navigator.getGamepads();
for (let i = 0; i < gamepads.length; ++i) {
let gamepad = gamepads[i];
// The array may contain undefined gamepads, so check for that as
// well as a non-null pose.
if (gamepad && gamepad.displayId && gamepad.pose) {
scene.showControllerAtTransform(gamepad.pose.position, gamepad.pose.orientation, gamepad.hand);
}
}
// Handle rendering as shown above...
}
WebXR
function onFrame(t, frame) {
// Queue a request for the next frame to keep the animation loop going.
xrSession.requestAnimationFrame(onXRFrame);
// Loop through all input sources.
for (let inputSource of xrSession.inputSources) {
// Show the input source if it has a grip space
if (inputSource.gripSpace) {
let inputPose = frame.getPose(inputSource.gripSpace, xrReferenceSpace);
scene.showControllerAtTransform(inputPose.position, inputPose.orientation, inputSource.handedness);
}
}
// Handle rendering as shown above...
);
Both WebVR and WebXR may have their presentation of VR content ended by the UA at any time.
WebVR applications may explicitly end the presentation of VR content by calling vrDisplay.exitPresent()
. A vrdisplaypresentchange
event is fired on the Window
object when presentation is started or ended by either the application or the UA. The presentation state is determined by checking the vrDisplay.isPresenting
boolean attribute.
WebXR applications may explicitly end the presentation of VR content by calling xrSession.end()
, at which point the XRSession
object becomes unusable. An end
event is fired on the XRSession
when it is ended by either the application or UA.
WebVR
window.addEventListener("vrdisplaypresentchange", () => {
if (!vrDisplay.isPresenting) {
// VR presentation has ended. Do any necessary cleanup.
}
});
WebXR
xrSession.addEventListener("end", () => {
// VR presentation has ended. Do any necessary cleanup.
});
WebXR currently has no equivalent for the vrdisplayactivate
, vrdisplaydeactivate
, vrdisplaypointerrestricted
, and vrdisplaypointerunrestricted
events. WebXR events also have no equivalent of the reason
enum reported for VRDisplayEvent
types.
WebXR currently has no equivalent for the VRDisplayCapabilities.hasExternalDisplay
attribute.
WebXR has no method for reporting projection parameters in terms of field of view.
Developers looking for more information about how to use WebXR can also refer to the following resources: