WebXR and stereo#

Radix projects every splat once for both eyes of an XR frame, and sorts once for both, because sort keys are distances from the head and stay valid in every direction. WebXR runs on either backend: WebGPU through the WebXR / WebGPU binding (XRGPUBinding), with the GPU sort every frame, or the WebGL2 backend's XR, with the CPU sort on a worker.

Code: src/render/splatCamera.ts (the head and eyes); the example page examples/xr/main.ts (/build/examples/xr/index.html).

// An XR-compatible WebGPU adapter
const renderer = await createRenderer({ xr: true });
renderer.xr.setFramebufferScaleFactor(1);

const splatRenderer = new SplatRenderer({ renderer, collection });
// One viewpoint for every camera that shows the head
splatRenderer.view = splatRenderer.viewFor("head");
scene.add(splatRenderer);

const session = await navigator.xr.requestSession("immersive-vr", {
  requiredFeatures: ["webgpu", "local-floor"],
});
await renderer.xr.setSession(session);

renderer.setAnimationLoop(() => renderer.render(scene, camera));

Head and eyes#

three renders an XR frame through an ArrayCamera with one camera per eye. Radix splits the work:

Only the draw doubles. In the emulator, a stereo frame took about 1.15x the time of a mono frame, with one projection pass and one sort. The words per splat grow from 4 to 8 for ewa, from 8 to 10 for 3dgut and htgs, and from 7 to 10 for raygs (Projection).

A stereo preview on the desktop takes the same path: render through an ArrayCamera of two cameras with viewports side by side (/build/examples/xr/index.html?stereo=1).

One view for the head#

An XR page renders one head through several camera objects: the desktop camera before a session, a stereo preview, and three's XR camera during one. On the CPU sort each camera object would make its own view and its own sort. Set one explicit view instead:

splatRenderer.view = splatRenderer.viewFor("head");

WebGPU XR#

WebGL2 XR#

The WebGL2 backend's XR uses the CPU sort. The order lags the head by a few frames:

Controls#

CameraControls moves an XR rig with the controllers while presenting: pass xr: renderer.xr and rig (default: the camera's parent, whose parent must be the scene). The left stick moves along the head's horizontal facing, the left trigger and grip move down and up, the right stick turns about the head, and the right trigger and grip are fast and slow (Camera controls).

/build/examples/xr/index.html#

The example page reads its options from the URL and from radixXr.configure(): url, layout, method, bits, sort (gpu or cpu), gpu (webgpu or webgpu-gl), xrScale (the eye-buffer scale), multiview, stereo, profile, threshold (the re-sort distance), and guard (the sortGuard). With ?link=<url> (?link=1: http://127.0.0.1:8001) it polls scripts/dev-link.mjs for configuration changes and posts its stats there. radixXr.measure({ seconds, turn }) turns the head and returns frame rates, GPU time per phase, and sort statistics.

Gotchas#

Source: docs/xr.md