easy-three

WebXR Support

What You Can Do

easy-three provides a small wrapper for WebXR.
You can add a VR button, enable controller models, enable hand tracking models, and pick objects with a controller ray.

WebXR works only in supported browsers and devices. For example, use Meta Quest Browser or a WebXR-compatible desktop browser connected to a VR headset.

WebXR requires a secure context. Make sure to use https.

Start From the Template

If you are new to easy-three, start from the template on the Getting Started page. The template already includes the importmap needed to load three.js and easy-three.

When using CDN directly, use the same importmap style as below.

index.html
<script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.185.1/build/three.module.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.185.1/examples/jsm/",
      "@pixiv/three-vrm": "https://cdn.jsdelivr.net/npm/@pixiv/three-vrm@3/lib/three-vrm.module.min.js",
      "@masabando/easy-three": "https://cdn.jsdelivr.net/gh/masabando/easy-three@1.15.0/dist/easy-three.js"
    }
  }
</script>

Minimum VR Scene

Get xr from init(), then call xr.setup(). This adds the VR button to the page and enables WebXR rendering.

main.js
import { init } from "@masabando/easy-three";

const { camera, create, animate, xr } = init();

camera.position.set(0, 1.6, 3);
create.ambientLight();
create.directionalLight();
create.cube({
  position: [0, 1.6, -2],
});

xr.setup();

animate();
Do not use camera-control helpers such as controls or fpv after entering XR. In XR mode, the headset controls the camera position and rotation.

Play in VR

To play in VR, click the VR button added to the page. Put on your VR headset and use the controllers to interact with the scene.

WebXR requires HTTPS.
Recommended static hosting services include GitHub Pages, Vercel, and Netlify.

When checking locally, it is recommended to use VSCode's Live Server extension in combination with port forwarding.
The Live Server extension allows you to start a server with HTTP communication locally.
Then, open the terminal in VSCode from the menu "View" → "Terminal", and set the Live Server port (usually 5500) from "Port Forwarding" in the "Ports" tab to issue a URL.
By setting the display range to "Public", you can also access it from the browser of the VR headset.
After use, it is recommended to disable port forwarding.

When Not in Fullscreen

If the original (non-XR) rendering is not fullscreen, the container passed to init() must have position: relative for the VR button to display correctly.

Initial Camera Position

The camera position set in init() is used only in non-XR mode. In XR mode, the camera position is controlled by the headset.

To change the initial camera position, put the camera returned from init() into a group, and change the position of that group. In XR mode, the position of the group is used as the initial camera position.

main.js
// カメラを格納するグループを作成
const cameraGroup = create.group({
  position: [0, 0, 3],
})
// カメラをグループに追加
cameraGroup.add(camera);

Pick Objects With a Controller

Pass objects to selectableObjects. When an object is selected by the controller, the selection information is stored in controller.userData.selected. You can get the selected object with controller.userData.selected[0].object.

main.js
const { create, animate, xr } = init();

create.ambientLight();
create.directionalLight();

const cube1 = create.cube({
  position: [-0.7, 1.5, -2],
});
const cube2 = create.cube({
  position: [0.7, 1.5, -2],
});

const { rightController } = xr.setup({
  selectableObjects: [cube1, cube2],
});

animate(({ delta }) => {
  cube1.material.color.set(0x3366ff);
  cube2.material.color.set(0x3366ff);

  const selected = rightController.userData.selected?.[0];
  if (selected) {
    selected.object.rotation.y += delta;
    selected.object.material.color.set(0xff3333);
  }
});

Move Selected Objects

The controller has no mousemove event. Instead, use the getIntersections() method of the controller to get information about objects that intersect with the controller ray.

const cube = create.cube()

const { rightController } = xr.setup({
  selectableObjects: [cube],
});

animate(() => {
  if (rightController.userData.selected?.[0]) {
    const intersections = rightController.getIntersections()
    if (intersections.length > 0) {
      const p = intersections[0].point;
      const object = rightController.userData.selected[0].object;
      object.position.x = p.x;
      object.position.y = p.y;
    }
  }
})

setup Options

OptionDefaultDescription
leftControllertrueShows the left controller model.
rightControllertrueShows the right controller model.
leftHandtrueShows the left hand model when hand tracking is available.
rightHandtrueShows the right hand model when hand tracking is available.
buttonTargetdomElementSets where the VR button is appended.
selectableObjects[]Sets the objects that can be selected by the controller ray.

Using With React

In React, call init() and xr.setup() inside useEffect. Return destroy() from the effect so the renderer is cleaned up when the component is removed.

MyXRScene.jsx
import { init } from "@masabando/easy-three";
import { useEffect, useRef } from "react";

export function MyXRScene() {
  const ref = useRef();

  useEffect(() => {
    const { camera, create, animate, xr, destroy } = init(ref.current);

    camera.position.set(0, 1.6, 3);
    create.ambientLight();
    create.directionalLight();
    create.cube({
      position: [0, 1.6, -2],
    });

    xr.setup();
    animate();

    return () => {
      destroy();
    };
  }, []);

  return <div
           ref={ref}
           style={{
             width: "100%",
             height: "400px",
             position: "relative",
           }}
         />;
}

More Details

For a longer sample using sky, ocean, and controller selection, see the xr reference page.