Files
english/.opencode/skills/threejs/references/14-physics-vr.md
2026-04-12 01:06:31 +07:00

6.7 KiB

Physics & VR/XR

Integrate physics simulations and virtual reality.

Physics Integration

Three.js doesn't include physics - use external libraries:

Rust-based, high-performance:

import { RapierPhysics } from 'three/addons/physics/RapierPhysics.js';

// Initialize
const physics = await RapierPhysics();

// Create physics body
const box = new THREE.Mesh(
  new THREE.BoxGeometry(),
  new THREE.MeshStandardMaterial()
);
scene.add(box);

// Add physics (mass > 0 = dynamic)
physics.addMesh(box, 1); // mass = 1

// Static ground
const ground = new THREE.Mesh(
  new THREE.BoxGeometry(10, 0.5, 10),
  new THREE.MeshStandardMaterial()
);
ground.position.y = -2;
scene.add(ground);
physics.addMesh(ground); // no mass = static

// Update in animation loop
function animate() {
  physics.step();
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}

Ammo Physics

Port of Bullet physics engine:

import { AmmoPhysics } from 'three/addons/physics/AmmoPhysics.js';

const physics = await AmmoPhysics();

// Same API as Rapier
physics.addMesh(mesh, mass);

function animate() {
  physics.step();
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}

Jolt Physics

High-performance alternative:

import { JoltPhysics } from 'three/addons/physics/JoltPhysics.js';

const physics = await JoltPhysics();
physics.addMesh(mesh, mass);

Physics Constraints

// After initialization
const physics = await RapierPhysics();

// Point-to-point constraint
physics.addConstraint(meshA, meshB, 'fixed');
physics.addConstraint(meshA, meshB, 'spring', { stiffness: 100 });

// Remove constraint
physics.removeConstraint(constraint);

VR/XR Setup

Basic WebXR

import { VRButton } from 'three/addons/webxr/VRButton.js';

// Enable XR
renderer.xr.enabled = true;

// Add VR button to page
document.body.appendChild(VRButton.createButton(renderer));

// Animation loop for VR
renderer.setAnimationLoop(() => {
  renderer.render(scene, camera);
});

// Stop using requestAnimationFrame, use setAnimationLoop instead

AR Mode

import { ARButton } from 'three/addons/webxr/ARButton.js';

renderer.xr.enabled = true;
document.body.appendChild(ARButton.createButton(renderer));

// AR-specific features
const session = renderer.xr.getSession();
session.requestHitTestSource({ space: viewerSpace }).then((hitTestSource) => {
  // Use hit testing for placing objects
});

VR Controllers

// Get controllers
const controller1 = renderer.xr.getController(0);
const controller2 = renderer.xr.getController(1);

scene.add(controller1);
scene.add(controller2);

// Controller events
controller1.addEventListener('selectstart', () => {
  console.log('Trigger pressed');
});

controller1.addEventListener('selectend', () => {
  console.log('Trigger released');
});

// Add visual controller models
import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js';

const controllerModelFactory = new XRControllerModelFactory();

const grip1 = renderer.xr.getControllerGrip(0);
grip1.add(controllerModelFactory.createControllerModel(grip1));
scene.add(grip1);

const grip2 = renderer.xr.getControllerGrip(1);
grip2.add(controllerModelFactory.createControllerModel(grip2));
scene.add(grip2);

Hand Tracking

import { OculusHandModel } from 'three/addons/webxr/OculusHandModel.js';

const hand1 = renderer.xr.getHand(0);
const handModel1 = new OculusHandModel(hand1);
hand1.add(handModel1);
scene.add(hand1);

const hand2 = renderer.xr.getHand(1);
const handModel2 = new OculusHandModel(hand2);
hand2.add(handModel2);
scene.add(hand2);

Teleportation

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

function handleController(controller) {
  const intersections = getIntersections(controller);

  if (intersections.length > 0) {
    const intersection = intersections[0];

    // Teleport on button release
    controller.addEventListener('selectend', () => {
      const offset = intersection.point.y;
      camera.position.y += offset;
    });
  }
}

function getIntersections(controller) {
  tempMatrix.identity().extractRotation(controller.matrixWorld);
  raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
  raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
  return raycaster.intersectObjects(scene.children, true);
}

Spatial Audio for VR

const listener = new THREE.AudioListener();
camera.add(listener);

const sound = new THREE.PositionalAudio(listener);
const audioLoader = new THREE.AudioLoader();

audioLoader.load('sound.ogg', (buffer) => {
  sound.setBuffer(buffer);
  sound.setRefDistance(1);
  sound.setLoop(true);
  sound.play();
});

// Attach to object
object.add(sound);

// Update listener in VR
renderer.setAnimationLoop(() => {
  // Listener automatically updates with camera in VR
  renderer.render(scene, camera);
});

Room-Scale VR

// Request room-scale experience
navigator.xr.requestSession('immersive-vr', {
  requiredFeatures: ['local-floor']
}).then((session) => {
  // Session setup
});

// Get play area bounds
session.requestReferenceSpace('bounded-floor').then((space) => {
  const bounds = space.boundsGeometry;
  // Create visual boundary
});

Performance Tips for VR

  • Target 90 FPS (11.1ms per frame)
  • Use lower polygon counts
  • Reduce shadow quality
  • Limit post-processing
  • Use instancing for repeated objects
  • Enable foveated rendering if available
  • Test on actual VR hardware
// Foveated rendering (Quest 2+)
const gl = renderer.getContext();
const ext = gl.getExtension('WEBGL_foveated_rendering');
if (ext) {
  ext.foveatedRenderingModeWEBGL(gl.FOVEATED_RENDERING_MODE_ENABLE_WEBGL);
}

Mixed Reality (MR)

import { XRButton } from 'three/addons/webxr/XRButton.js';

// Request MR features
document.body.appendChild(
  XRButton.createButton(renderer, {
    requiredFeatures: ['hand-tracking', 'layers'],
    optionalFeatures: ['local-floor', 'bounded-floor']
  })
);

// Passthrough mode (Quest Pro, etc.)
const session = renderer.xr.getSession();
const baseLayer = session.renderState.baseLayer;
baseLayer.compositionDisabled = true; // enable passthrough

Common VR Patterns

// Detect if in VR
if (renderer.xr.isPresenting) {
  // In VR mode
}

// Get VR camera (for raycasting)
const vrCamera = renderer.xr.getCamera(camera);

// Different behavior for VR vs desktop
renderer.setAnimationLoop(() => {
  if (renderer.xr.isPresenting) {
    // VR rendering logic
  } else {
    // Desktop rendering logic
  }
  renderer.render(scene, camera);
});