Docs

Walk a scene in VR

Let a visitor stand in a scene with a headset such as a Meta Quest 3, starting at a view you chose and moving between views with the trigger.

The three.js renderer can show a scene in a VR headset through WebXR. It is off unless you ask for it: pass xr to ThreeView (or xr: true to createThreeRenderer), and nothing about a page that does not ask changes.

vr.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import { KloradProvider, SceneObject } from "@klorad/react";
import { vrSupported, type CameraView } from "@klorad/engine-three";
import { ThreeView, type ThreeViewHandle } from "@klorad/engine-three/react";

// Each stop is an ordinary camera view: the visitor stands where its camera stands and faces its heading.
const stops: CameraView[] = [
  { target: { east: 0, north: 0, up: 1.6 }, heading: 0, tilt: 5, distance: 6 },
  { target: { east: 0, north: 0, up: 1.2 }, heading: 135, tilt: 15, distance: 2.5 },
];

export default function Exhibit() {
  const view = useRef<ThreeViewHandle>(null);
  const [canVR, setCanVR] = useState(false);
  const [stop, setStop] = useState(0);
  useEffect(() => void vrSupported().then(setCanVR), []);
  const next = () => {
    const i = (stop + 1) % stops.length;
    setStop(i);
    view.current?.moveVR(stops[i]!);
  };
  return (
    <KloradProvider options={{ coordinateSystem: { origin: { lat: 40.632, lon: 22.951, height: 12 } } }}>
      <SceneObject id="piece" name="Scanned piece" position={{ east: 0, north: 0 }} geometry={{ kind: "model", uri: "/models/piece.stl" }} />
      {canVR ? <button onClick={() => void view.current?.enterVR(stops[stop])}>Enter VR</button> : null}
      <div style={{ height: 480 }}>
        <ThreeView ref={view} xr onXRSelect={next} />
      </div>
    </KloradProvider>
  );
}

Where the visitor stands

A VR stop is an ordinary CameraView, the same value view() returns and setView takes. enterVR(view) starts an immersive-vr session on the headset's floor and places the visitor so their eyes are where that view's camera is (an eye height of 1.6 m, EYE_HEIGHT) and they face its heading. The tilt of the view does not tilt the visitor: they look up and down with their head. Called without a view, it starts from the view on the screen.

moveVR(view) moves the visitor to another stop while the session runs, by moving the floor under them; nothing in the scene moves. Outside a session it does nothing, so you can call it next to setView in the same handler.

The trigger

onXRSelect fires when either controller's trigger is pressed. The renderer does not decide what the trigger means: the example walks the stops in order.

Which browsers

vrSupported() resolves to true in a browser that can start an immersive VR session (the Meta Quest browser can) and false elsewhere, including a desktop without a headset. Show the button only when it is true. A browser asks the visitor before a session starts, and enterVR only works from a click or a tap.

WebXR runs only on a secure page: https, or localhost while you develop. To try a page from your machine on a headset, open it over https or forward the port to the headset.

When the visitor leaves VR, the page's camera comes back to where it was.

Run it liveSandbox: VRA room of exhibits walked in a headset, starting at the first stop and moving on with the trigger.

Something wrong or unclear? Tell us.