305 lines
6.7 KiB
Markdown
305 lines
6.7 KiB
Markdown
# Physics & VR/XR
|
|
|
|
Integrate physics simulations and virtual reality.
|
|
|
|
## Physics Integration
|
|
|
|
Three.js doesn't include physics - use external libraries:
|
|
|
|
### Rapier Physics (Recommended)
|
|
|
|
Rust-based, high-performance:
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```javascript
|
|
import { JoltPhysics } from 'three/addons/physics/JoltPhysics.js';
|
|
|
|
const physics = await JoltPhysics();
|
|
physics.addMesh(mesh, mass);
|
|
```
|
|
|
|
### Physics Constraints
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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)
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
// 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);
|
|
});
|
|
```
|