TeleportVR Locomotion Guide for Three.js WebXR

This article explores the TeleportVR utility pattern, a widely used design pattern for user locomotion in virtual reality. We will define what the TeleportVR pattern is, examine its benefits in mitigating motion sickness, and provide a step-by-step technical guide on how to implement this locomotion system within a Three.js WebXR environment.

Understanding the TeleportVR Utility Pattern

The TeleportVR utility pattern is a virtual reality locomotion design pattern that allows users to navigate large virtual spaces without physical movement or continuous virtual steering (smooth locomotion). Continuous artificial movement often causes motion sickness due to the sensory conflict between the visual system and the vestibular system. Teleportation solves this by instantly transitioning the user’s avatar from their current location to a targeted point in the 3D space.

In a Three.js environment, the TeleportVR pattern consists of three core visual and functional components: 1. The Pointer: A visual line or curve (often a parabolic arc) projected from the user’s VR controller. 2. The Target Marker: A ring or disk projected onto the floor at the intersection point of the pointer, indicating where the user will land. 3. The Rig Translation: The logic that shifts the coordinate system of the user’s virtual camera rig to the selected destination.

Step 1: Setting Up the VR Camera Rig

To implement teleportation in Three.js, you must place the PerspectiveCamera and the VR controllers inside a parent Group. This parent group is known as the “Camera Rig.”

In WebXR, the camera’s position is locked to the hardware tracking of the physical headset. If you attempt to update the camera’s position directly, WebXR will immediately overwrite it. Instead, you must translate the parent Camera Rig.

const cameraRig = new THREE.Group();
scene.add(cameraRig);

// Add camera and controllers to the rig
cameraRig.add(camera);
cameraRig.add(controller1);
cameraRig.add(controller2);

Step 2: Generating the Raycast and Intersection

To find where the user wants to teleport, you must project a ray from the controller into the scene. Three.js provides a Raycaster class that simplifies this task.

In your animation loop, obtain the controller’s matrix position and direction to configure the raycaster.

const raycaster = new THREE.Raycaster();
const tempMatrix = new THREE.Matrix4();

function getControllerRaycast(controller, walkableObjects) {
  tempMatrix.identity().extractRotation(controller.matrixWorld);
  raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
  raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);

  // Intersects with floor meshes
  return raycaster.intersectObjects(walkableObjects);
}

Step 3: Visualizing the Pointer and Target Marker

To guide the user, render a visual marker at the intersection point. Create a simple mesh (like a flat ring) and place it at the raycast collision coordinate.

// Create a target marker
const markerGeometry = new THREE.RingGeometry(0.1, 0.15, 32);
markerGeometry.rotateX(-Math.PI / 2); // Lay flat on the floor
const markerMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00, side: THREE.DoubleSide });
const targetMarker = new THREE.Mesh(markerGeometry, markerMaterial);
targetMarker.visible = false;
scene.add(targetMarker);

Update the marker’s position in the render loop if an intersection is detected:

const intersects = getControllerRaycast(activeController, [floorMesh]);

if (intersects.length > 0) {
  const intersectPoint = intersects[0].point;
  targetMarker.position.copy(intersectPoint);
  targetMarker.visible = true;
} else {
  targetMarker.visible = false;
}

Step 4: Executing the Teleport Translation

The actual teleportation occurs when the user triggers an input event, such as releasing a thumbstick or a button on the controller.

When the event is triggered, calculate the offset between the user’s physical position relative to the rig’s origin. This offset must be accounted for so the user lands exactly where they pointed, regardless of where they are physically standing in their room-scale play space.

function teleport(targetPosition) {
  // Calculate the user's offset from the rig center
  const userOffset = new THREE.Vector3();
  userOffset.setFromMatrixPosition(camera.matrixWorld);
  userOffset.sub(cameraRig.position);
  
  // We only care about horizontal offsets (X and Z axes)
  userOffset.y = 0; 

  // Move the rig to the target position, subtracting the physical offset
  cameraRig.position.copy(targetPosition).sub(userOffset);
}

Bind this teleport function to the select event listener of your VR controller:

controller1.addEventListener('selectend', () => {
  if (targetMarker.visible) {
    teleport(targetMarker.position);
  }
});

By encapsulating this behavior, you establish a clean TeleportVR pattern that provides intuitive, nausea-free navigation through any WebXR experience built with Three.js.