{T}

WebXR 与 VR

WebXR API 允许在浏览器中创建沉浸式 VR(虚拟现实)和 AR(增强现实)体验。Three.js 提供了完整的 WebXR 支持,可以轻松创建跨平台的 XR 应用。

系统架构

plaintext
┌─────────────────────────────────────────────────────────────────────────┐
│                        WebXR 系统架构                                     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐  │
│  │   Three.js 层    │    │   WebXR API     │    │   XR 设备        │  │
│  │                  │    │                  │    │                  │  │
│  │  • WebGLRenderer │◀──▶│  • XRSession    │◀──▶│  • VR 头显       │  │
│  │  • XRManager     │    │  • XRReference  │    │  • 控制器        │  │
│  │  • Scene/Camera  │    │    Space        │    │  • 手部追踪      │  │
│  │  • Controllers   │    │  • XRFrame      │    │  • AR 设备       │  │
│  └──────────────────┘    └──────────────────┘    └──────────────────┘  │
│           │                      │                       │              │
│           ▼                      ▼                       ▼              │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                       渲染管线                                    │   │
│  │                                                                  │   │
│  │  正常模式:Scene → Camera → Renderer → Screen                    │   │
│  │  XR 模式:Scene → Camera → XRSession → XRDisplay → HMD           │   │
│  │                                                                  │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

核心组件说明

组件说明职责
XRSessionXR 会话管理 XR 体验的生命周期
XRReferenceSpace参考空间定义坐标系统和边界
XRFrameXR 帧包含每帧的追踪数据
XRViewXR 视图表示单个眼睛的视图
XRInputSource输入源控制器、手部追踪等输入设备
XRPoseXR 姿态位置和方向信息

概述

WebXR 是 Web 平台的沉浸式技术标准,支持 VR(虚拟现实)和 AR(增强现实)两种模式。Three.js 通过 WebXRManager 实现对 XR 设备的支持。

WebXR 模式

模式说明特点
immersive-vr沉浸式 VR完全沉浸式体验,替代现实世界
immersive-ar沉浸式 AR增强现实,虚拟物体叠加在现实世界
inline内联模式在页面内显示,无需 XR 设备

设备支持

设备类型支持的模式示例
VR 头显immersive-vrOculus Quest、HTC Vive、Valve Index
AR 设备immersive-ar手机 AR、Hololens、Magic Leap
移动设备inline智能手机、平板电脑
桌面浏览器inlinePC、Mac

浏览器支持

javascript
// 检测 WebXR 支持
async function checkXRSupport() {
  if ('xr' in navigator) {
    // VR 支持
    const vrSupported = await navigator.xr.isSessionSupported('immersive-vr');
    // AR 支持
    const arSupported = await navigator.xr.isSessionSupported('immersive-ar');
    
    console.log('VR 支持:', vrSupported);
    console.log('AR 支持:', arSupported);
    
    return { vr: vrSupported, ar: arSupported };
  }
  console.log('WebXR 不支持');
  return { vr: false, ar: false };
}

WebXR 基础

检测 WebXR 支持

javascript
// 完整的支持检测
async function detectXRFeatures() {
  const features = {
    webXR: 'xr' in navigator,
    vr: false,
    ar: false,
    handTracking: false,
    hitTest: false,
    anchors: false,
    depthSensing: false,
    domOverlay: false,
    lightEstimation: false
  };
  
  if (features.webXR) {
    features.vr = await navigator.xr.isSessionSupported('immersive-vr');
    features.ar = await navigator.xr.isSessionSupported('immersive-ar');
    
    // 检测可选功能
    const session = await navigator.xr.requestSession('immersive-vr', {
      optionalFeatures: [
        'hand-tracking',
        'hit-test',
        'anchors',
        'depth-sensing',
        'dom-overlay',
        'light-estimation'
      ]
    }).catch(() => null);
    
    if (session) {
      features.handTracking = session.enabledFeatures?.includes('hand-tracking') ?? false;
      features.hitTest = session.enabledFeatures?.includes('hit-test') ?? false;
      features.anchors = session.enabledFeatures?.includes('anchors') ?? false;
      features.depthSensing = session.enabledFeatures?.includes('depth-sensing') ?? false;
      features.domOverlay = session.enabledFeatures?.includes('dom-overlay') ?? false;
      features.lightEstimation = session.enabledFeatures?.includes('light-estimation') ?? false;
      await session.end();
    }
  }
  
  return features;
}
 
// 使用示例
detectXRFeatures().then(features => {
  console.log('XR 功能检测:', features);
});

初始化 VR

javascript
import * as THREE from 'three';
import { VRButton } from 'three/addons/webxr/VRButton.js';
 
// ==================== 创建基础场景 ====================
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x505050);
 
const camera = new THREE.PerspectiveCamera(
  70,                                      // FOV
  window.innerWidth / window.innerHeight,  // 宽高比
  0.1,                                     // 近裁剪面
  1000                                     // 远裁剪面
);
camera.position.set(0, 1.6, 3);  // 默认站立高度
 
const renderer = new THREE.WebGLRenderer({ 
  antialias: true,
  alpha: false
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
 
// ==================== 启用 XR ====================
renderer.xr.enabled = true;
 
document.body.appendChild(renderer.domElement);
 
// ==================== 添加 VR 按钮 ====================
const vrButton = VRButton.createButton(renderer);
document.body.appendChild(vrButton);
 
// ==================== 创建场景内容 ====================
// 地板
const floorGeometry = new THREE.PlaneGeometry(10, 10);
const floorMaterial = new THREE.MeshStandardMaterial({ 
  color: 0x888888,
  roughness: 0.8,
  metalness: 0.2
});
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
 
// 物体
const boxGeometry = new THREE.BoxGeometry(0.3, 0.3, 0.3);
const boxMaterial = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const box = new THREE.Mesh(boxGeometry, boxMaterial);
box.position.set(0, 0.15, -1);
box.castShadow = true;
box.receiveShadow = true;
scene.add(box);
 
// 光源
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
 
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
 
// ==================== XR 动画循环 ====================
// 使用 setAnimationLoop 而非 requestAnimationFrame
renderer.setAnimationLoop((time, frame) => {
  // 更新物体
  box.rotation.x += 0.01;
  box.rotation.y += 0.01;
  
  // 渲染
  renderer.render(scene, camera);
});
 
// ==================== 窗口调整 ====================
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

会话生命周期

javascript
// 监听 XR 会话事件
renderer.xr.addEventListener('sessionstart', (event) => {
  console.log('XR 会话开始');
  const session = renderer.xr.getSession();
  
  // 监听会话结束
  session.addEventListener('end', () => {
    console.log('XR 会话结束');
  });
  
  // 监听可见性变化
  session.addEventListener('visibilitychange', (event) => {
    console.log('可见性变化:', session.visibilityState);
    // visible, visible-blurred, hidden
  });
  
  // 监听输入源变化
  session.addEventListener('inputsourceschange', (event) => {
    console.log('输入源变化');
    console.log('添加:', event.added);
    console.log('移除:', event.removed);
  });
});
 
renderer.xr.addEventListener('sessionend', () => {
  console.log('XR 会话已结束');
});

VRButton 配置

创建 VR 按钮

javascript
import { VRButton } from 'three/addons/webxr/VRButton.js';
 
// 基础创建
const vrButton = VRButton.createButton(renderer);
document.body.appendChild(vrButton);
 
// 自定义样式
vrButton.style.position = 'absolute';
vrButton.style.bottom = '20px';
vrButton.style.left = '50%';
vrButton.style.transform = 'translateX(-50%)';
vrButton.style.padding = '12px 24px';
vrButton.style.fontSize = '16px';
vrButton.style.borderRadius = '8px';
vrButton.style.cursor = 'pointer';

VRButton 配置选项

javascript
// 完整配置
const vrButton = VRButton.createButton(renderer, {
  // 参考空间类型
  referenceSpaceType: 'local-floor',  // 默认值
  
  // 会话初始化选项
  sessionInit: {
    // 必需功能
    requiredFeatures: ['local-floor'],
    
    // 可选功能
    optionalFeatures: [
      'bounded-floor',   // 边界
      'hand-tracking',   // 手部追踪
      'layers',          // 图层
      'dom-overlay',     // DOM 叠加
      'hit-test',        // 命中测试(AR)
      'anchors',         // 锚点(AR)
      'light-estimation' // 光照估计(AR)
    ],
    
    // DOM 叠加元素(AR)
    domOverlay: {
      root: document.getElementById('overlay')
    }
  }
});
 
document.body.appendChild(vrButton);

自定义按钮行为

javascript
// 手动创建 VR 按钮
async function createCustomVRButton(renderer) {
  const button = document.createElement('button');
  button.textContent = '进入 VR';
  button.style.cssText = `
    position: absolute;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    padding: 12px 24px;
    font-size: 16px;
    background: #000;
    color: #fff;
    border: none;
    border-radius: 8px;
    cursor: pointer;
  `;
  
  // 检测支持
  if ('xr' in navigator) {
    const supported = await navigator.xr.isSessionSupported('immersive-vr');
    
    if (supported) {
      button.addEventListener('click', async () => {
        const session = await navigator.xr.requestSession('immersive-vr', {
          requiredFeatures: ['local-floor'],
          optionalFeatures: ['hand-tracking', 'bounded-floor']
        });
        
        renderer.xr.setSession(session);
        
        session.addEventListener('end', () => {
          button.textContent = '进入 VR';
        });
        
        button.textContent = '退出 VR';
      });
    } else {
      button.textContent = 'VR 不支持';
      button.disabled = true;
    }
  } else {
    button.textContent = 'WebXR 不支持';
    button.disabled = true;
  }
  
  return button;
}
 
const customButton = await createCustomVRButton(renderer);
document.body.appendChild(customButton);

控制器交互

XR 控制器基础

javascript
import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js';
 
// 获取控制器
const controller1 = renderer.xr.getController(0);
const controller2 = renderer.xr.getController(1);
 
// 添加到场景
scene.add(controller1);
scene.add(controller2);
 
// ==================== 控制器模型 ====================
const controllerModelFactory = new XRControllerModelFactory();
 
const controllerGrip1 = renderer.xr.getControllerGrip(0);
controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1));
scene.add(controllerGrip1);
 
const controllerGrip2 = renderer.xr.getControllerGrip(1);
controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2));
scene.add(controllerGrip2);
 
// ==================== 控制器射线 ====================
const geometry = new THREE.BufferGeometry().setFromPoints([
  new THREE.Vector3(0, 0, 0),
  new THREE.Vector3(0, 0, -1)
]);
 
const lineMaterial = new THREE.LineBasicMaterial({
  color: 0xffffff,
  linewidth: 2
});
 
const line1 = new THREE.Line(geometry, lineMaterial);
line1.name = 'line';
line1.scale.z = 5;
 
const line2 = line1.clone();
 
controller1.add(line1);
controller2.add(line2);

控制器事件

javascript
// ==================== 基础事件 ====================
controller1.addEventListener('selectstart', (event) => {
  console.log('控制器 1 选择开始(按下扳机键)');
  const controller = event.target;
  controller.userData.isSelecting = true;
});
 
controller1.addEventListener('selectend', (event) => {
  console.log('控制器 1 选择结束(释放扳机键)');
  const controller = event.target;
  controller.userData.isSelecting = false;
});
 
controller1.addEventListener('select', (event) => {
  console.log('控制器 1 选择(点击)');
});
 
// ==================== 其他事件 ====================
controller1.addEventListener('squeezestart', (event) => {
  console.log('握持开始(按下手柄握持键)');
});
 
controller1.addEventListener('squeezeend', (event) => {
  console.log('握持结束');
});
 
controller1.addEventListener('squeeze', (event) => {
  console.log('握持点击');
});
 
// ==================== 摇杆/触摸板事件 ====================
controller1.addEventListener('connected', (event) => {
  console.log('控制器已连接:', event.data);
  // event.data.hand: "left" | "right" | ""
  // event.data.targetRayMode: "tracked-pointer" | "gaze" | "screen"
  // event.data.profiles: 支持的配置文件
});
 
controller1.addEventListener('disconnected', (event) => {
  console.log('控制器已断开');
});

控制器交互实现

javascript
// ==================== 完整的交互系统 ====================
const raycaster = new THREE.Raycaster();
const tempMatrix = new THREE.Matrix4();
const intersected = [];
const objects = [];  // 可交互物体列表
 
// 添加可交互物体
function addInteractiveObject(mesh) {
  objects.push(mesh);
}
 
// 控制器交互
function handleController(controller) {
  // 设置射线
  tempMatrix.identity().extractRotation(controller.matrixWorld);
  raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
  raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
  
  // 检测相交
  const intersects = raycaster.intersectObjects(objects, false);
  
  // 处理高亮
  if (intersects.length > 0) {
    const intersect = intersects[0];
    
    // 高亮
    if (intersected.indexOf(intersect.object) === -1) {
      intersected.push(intersect.object);
      intersect.object.material.emissive.setHex(0x444444);
    }
    
    // 选择
    if (controller.userData.isSelecting) {
      // 拖拽
      if (!controller.userData.selected) {
        controller.attach(intersect.object);
        controller.userData.selected = intersect.object;
      }
    }
  } else {
    // 取消高亮
    intersected.forEach(obj => {
      obj.material.emissive.setHex(0x000000);
    });
    intersected.length = 0;
  }
}
 
// 释放物体
function onSelectEnd(event) {
  const controller = event.target;
  if (controller.userData.selected) {
    scene.attach(controller.userData.selected);
    controller.userData.selected = undefined;
  }
}
 
controller1.addEventListener('selectend', onSelectEnd);
controller2.addEventListener('selectend', onSelectEnd);
 
// ==================== 动画循环 ====================
renderer.setAnimationLoop((time, frame) => {
  handleController(controller1);
  handleController(controller2);
  renderer.render(scene, camera);
});

控制器可视化

javascript
// ==================== 自定义控制器模型 ====================
function createControllerModel(controller) {
  const group = new THREE.Group();
  
  // 圆环(表示握持位置)
  const ringGeometry = new THREE.TorusGeometry(0.02, 0.002, 16, 32);
  const ringMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
  const ring = new THREE.Mesh(ringGeometry, ringMaterial);
  ring.rotation.x = Math.PI / 2;
  group.add(ring);
  
  // 射线终点
  const sphereGeometry = new THREE.SphereGeometry(0.01, 16, 16);
  const sphereMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
  const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
  sphere.position.z = -1;
  group.add(sphere);
  
  return group;
}
 
controller1.add(createControllerModel(controller1));
controller2.add(createControllerModel(controller2));

手部追踪

检测手部追踪支持

javascript
async function checkHandTrackingSupport() {
  if (!('xr' in navigator)) return false;
  
  const isVRSupported = await navigator.xr.isSessionSupported('immersive-vr');
  
  if (isVRSupported) {
    try {
      const session = await navigator.xr.requestSession('immersive-vr', {
        optionalFeatures: ['hand-tracking']
      });
      
      const hasHandTracking = session.enabledFeatures?.includes('hand-tracking') ?? false;
      await session.end();
      
      return hasHandTracking;
    } catch (error) {
      console.error('检测手部追踪失败:', error);
      return false;
    }
  }
  
  return false;
}

使用手部追踪

javascript
import { XRHandModelFactory } from 'three/addons/webxr/XRHandModelFactory.js';
 
const handModelFactory = new XRHandModelFactory();
 
// ==================== 获取手部追踪 ====================
const hand1 = renderer.xr.getHand(0);  // 左手
const hand2 = renderer.xr.getHand(1);  // 右手
 
// 添加手部模型
hand1.add(handModelFactory.createHandModel(hand1, 'mesh'));  // mesh | points | spheres
hand2.add(handModelFactory.createHandModel(hand2, 'mesh'));
 
scene.add(hand1);
scene.add(hand2);
 
// ==================== 手部事件 ====================
hand1.addEventListener('pinchstart', (event) => {
  console.log('左手捏合开始');
  hand1.userData.isPinching = true;
});
 
hand1.addEventListener('pinchend', (event) => {
  console.log('左手捏合结束');
  hand1.userData.isPinching = false;
});
 
hand2.addEventListener('pinchstart', (event) => {
  console.log('右手捏合开始');
});
 
hand2.addEventListener('pinchend', (event) => {
  console.log('右手捏合结束');
});
 
// ==================== 手部关节 ====================
// WebXR 定义了 25 个手部关节
const HAND_JOINTS = {
  WRIST: 0,
  THUMB_METACARPAL: 1,
  THUMB_PHALANX_PROXIMAL: 2,
  THUMB_PHALANX_DISTAL: 3,
  THUMB_TIP: 4,
  INDEX_METACARPAL: 5,
  INDEX_PHALANX_PROXIMAL: 6,
  INDEX_PHALANX_INTERMEDIATE: 7,
  INDEX_PHALANX_DISTAL: 8,
  INDEX_TIP: 9,
  MIDDLE_METACARPAL: 10,
  MIDDLE_PHALANX_PROXIMAL: 11,
  MIDDLE_PHALANX_INTERMEDIATE: 12,
  MIDDLE_PHALANX_DISTAL: 13,
  MIDDLE_TIP: 14,
  RING_METACARPAL: 15,
  RING_PHALANX_PROXIMAL: 16,
  RING_PHALANX_INTERMEDIATE: 17,
  RING_PHALANX_DISTAL: 18,
  RING_TIP: 19,
  LITTLE_METACARPAL: 20,
  LITTLE_PHALANX_PROXIMAL: 21,
  LITTLE_PHALANX_INTERMEDIATE: 22,
  LITTLE_PHALANX_DISTAL: 23,
  LITTLE_TIP: 24
};
 
// 访问特定关节
function getJointPosition(hand, jointIndex) {
  const joint = hand.joints[jointIndex];
  if (joint) {
    return new THREE.Vector3().setFromMatrixPosition(joint.matrixWorld);
  }
  return null;
}
 
// 获取指尖位置
function getFingerTipPosition(hand) {
  return {
    thumb: getJointPosition(hand, HAND_JOINTS.THUMB_TIP),
    index: getJointPosition(hand, HAND_JOINTS.INDEX_TIP),
    middle: getJointPosition(hand, HAND_JOINTS.MIDDLE_TIP),
    ring: getJointPosition(hand, HAND_JOINTS.RING_TIP),
    little: getJointPosition(hand, HAND_JOINTS.LITTLE_TIP)
  };
}

手势识别

javascript
// 简单的手势识别
function detectGesture(hand) {
  const tips = getFingerTipPosition(hand);
  const wrist = getJointPosition(hand, HAND_JOINTS.WRIST);
  
  if (!tips || !wrist) return null;
  
  // 检测握拳
  const isFist = [
    tips.thumb,
    tips.index,
    tips.middle,
    tips.ring,
    tips.little
  ].every(tip => tip && tip.distanceTo(wrist) < 0.1);
  
  if (isFist) return 'fist';
  
  // 检测张开手掌
  const isOpenPalm = [
    tips.thumb,
    tips.index,
    tips.middle,
    tips.ring,
    tips.little
  ].every(tip => tip && tip.distanceTo(wrist) > 0.15);
  
  if (isOpenPalm) return 'open';
  
  // 检测指向
  if (tips.index && tips.index.distanceTo(wrist) > 0.15) {
    const otherFingersClosed = [tips.middle, tips.ring, tips.little]
      .every(tip => tip && tip.distanceTo(wrist) < 0.12);
    
    if (otherFingersClosed) return 'pointing';
  }
  
  // 检测和平手势
  if (tips.index && tips.middle && 
      tips.index.distanceTo(wrist) > 0.15 && 
      tips.middle.distanceTo(wrist) > 0.15) {
    const otherFingersClosed = [tips.thumb, tips.ring, tips.little]
      .every(tip => tip && tip.distanceTo(wrist) < 0.12);
    
    if (otherFingersClosed) return 'peace';
  }
  
  return 'unknown';
}

AR 支持

初始化 AR

javascript
import { ARButton } from 'three/addons/webxr/ARButton.js';
 
// ==================== 创建 AR 按钮 ====================
const arButton = ARButton.createButton(renderer, {
  // 必需功能
  requiredFeatures: ['hit-test'],
  
  // 可选功能
  optionalFeatures: [
    'dom-overlay',
    'anchors',
    'light-estimation',
    'depth-sensing'
  ],
  
  // DOM 叠加层
  domOverlay: {
    root: document.getElementById('ar-ui')
  }
});
 
document.body.appendChild(arButton);
 
// AR 需要透明背景
scene.background = null;
renderer.alpha = true;

AR 命中测试

javascript
let hitTestSource = null;
let hitTestSourceRequested = false;
let reticle = null;
 
// ==================== 创建标记点 ====================
function createReticle() {
  const geometry = new THREE.RingGeometry(0.15, 0.2, 32);
  const material = new THREE.MeshBasicMaterial({
    color: 0xffffff,
    opacity: 0.75,
    transparent: true,
    side: THREE.DoubleSide
  });
  const reticle = new THREE.Mesh(geometry, material);
  reticle.rotation.x = -Math.PI / 2;
  reticle.visible = false;
  return reticle;
}
 
reticle = createReticle();
scene.add(reticle);
 
// ==================== AR 渲染循环 ====================
function render(timestamp, frame) {
  if (frame) {
    const referenceSpace = renderer.xr.getReferenceSpace();
    const session = renderer.xr.getSession();
    
    // 请求命中测试源
    if (hitTestSourceRequested === false) {
      session.requestReferenceSpace('viewer').then((viewerSpace) => {
        session.requestHitTestSource({ space: viewerSpace }).then((source) => {
          hitTestSource = source;
        });
      });
      hitTestSourceRequested = true;
    }
    
    // 执行命中测试
    if (hitTestSource) {
      const hitTestResults = frame.getHitTestResults(hitTestSource);
      
      if (hitTestResults.length) {
        const hit = hitTestResults[0];
        const pose = hit.getPose(referenceSpace);
        
        // 更新标记点位置
        reticle.visible = true;
        reticle.position.setFromMatrixPosition(pose.transform.matrix);
        reticle.quaternion.setFromRotationMatrix(pose.transform.matrix);
      } else {
        reticle.visible = false;
      }
    }
  }
  
  renderer.render(scene, camera);
}
 
renderer.setAnimationLoop(render);
 
// ==================== 放置物体 ====================
const placedObjects = [];
 
function placeObject() {
  if (reticle.visible) {
    // 创建物体
    const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
    const material = new THREE.MeshStandardMaterial({ 
      color: Math.random() * 0xffffff 
    });
    const object = new THREE.Mesh(geometry, material);
    
    // 设置位置
    object.position.copy(reticle.position);
    object.quaternion.copy(reticle.quaternion);
    
    scene.add(object);
    placedObjects.push(object);
  }
}
 
// 点击控制器时放置
controller1.addEventListener('select', () => {
  placeObject();
});

AR 锚点

javascript
// 使用锚点固定虚拟物体
let anchor = null;
 
async function createAnchor(position, orientation) {
  const session = renderer.xr.getSession();
  const referenceSpace = renderer.xr.getReferenceSpace();
  
  if (session && 'anchors' in session) {
    // 创建锚点
    const anchorPose = new XRRigidTransform(
      { x: position.x, y: position.y, z: position.z },
      { x: orientation.x, y: orientation.y, z: orientation.z, w: orientation.w }
    );
    
    anchor = await session.createAnchor(anchorPose, referenceSpace);
    console.log('锚点已创建:', anchor);
    
    return anchor;
  }
  
  return null;
}
 
// 更新锚点位置
function updateAnchor(frame) {
  if (anchor) {
    const pose = frame.getPose(anchor.anchorSpace, renderer.xr.getReferenceSpace());
    if (pose) {
      // 更新物体位置
      object.position.setFromMatrixPosition(pose.transform.matrix);
    }
  }
}

AR 光照估计

javascript
let lightProbe = null;
let lightEstimate = null;
 
// 请求光照估计
async function setupLightEstimation() {
  const session = renderer.xr.getSession();
  
  if (session && 'lightEstimation' in session) {
    try {
      lightProbe = await session.initLightProbe();
      console.log('光照估计已启用');
    } catch (error) {
      console.warn('光照估计不支持:', error);
    }
  }
}
 
// 更新场景光照
function updateLighting(frame) {
  if (lightProbe) {
    lightEstimate = frame.getLightEstimate(lightProbe);
    
    if (lightEstimate) {
      // 更新环境光
      ambientLight.intensity = lightEstimate.primaryLightIntensity;
      
      // 更新主光源方向
      if (lightEstimate.primaryLightDirection) {
        const direction = lightEstimate.primaryLightDirection;
        directionalLight.position.set(
          direction.x,
          direction.y,
          direction.z
        );
      }
      
      // 更新主光源颜色
      if (lightEstimate.primaryLightColor) {
        const color = lightEstimate.primaryLightColor;
        directionalLight.color.setRGB(
          color.r,
          color.g,
          color.b
        );
      }
      
      // 使用球谐光照数据
      if (lightEstimate.sphericalHarmonicsCoefficients) {
        // 更新环境贴图
        // ...
      }
    }
  }
}

参考空间

参考空间类型

类型说明使用场景
viewer观察者空间以用户当前位置为原点
local本地空间固定起始位置,可移动
local-floor本地地板空间站立体验,地板高度为原点
bounded-floor有边界的地板空间需要在指定区域内移动
unbounded无界空间大范围移动体验
javascript
// 请求参考空间
async function setupReferenceSpace() {
  const session = renderer.xr.getSession();
  
  // 查看支持的参考空间
  const supportedReferenceSpaces = await session.supportedReferenceSpaces;
  console.log('支持的参考空间:', supportedReferenceSpaces);
  
  // 请求特定参考空间
  const referenceSpace = await session.requestReferenceSpace('local-floor');
  renderer.xr.setReferenceSpace(referenceSpace);
  
  return referenceSpace;
}
 
// 获取边界
async function getBounds() {
  const session = renderer.xr.getSession();
  
  if (session.enabledFeatures?.includes('bounded-floor')) {
    const boundedSpace = await session.requestReferenceSpace('bounded-floor');
    const bounds = boundedSpace.boundsGeometry;
    
    // bounds 是一个点数组,表示边界多边形
    console.log('边界:', bounds);
    
    return bounds;
  }
  
  return null;
}
 
// 可视化边界
function visualizeBounds(bounds) {
  if (!bounds) return;
  
  const points = bounds.map(p => new THREE.Vector3(p.x, 0, p.z));
  points.push(points[0]);  // 闭合
  
  const geometry = new THREE.BufferGeometry().setFromPoints(points);
  const material = new THREE.LineBasicMaterial({ color: 0xff0000 });
  const line = new THREE.Line(geometry, material);
  
  scene.add(line);
}

设置参考空间

javascript
// 创建 VR 会话并设置参考空间
async function createVRSession() {
  const session = await navigator.xr.requestSession('immersive-vr', {
    requiredFeatures: ['local-floor'],
    optionalFeatures: ['bounded-floor', 'hand-tracking']
  });
  
  // 设置参考空间
  const referenceSpace = await session.requestReferenceSpace('local-floor');
  renderer.xr.setReferenceSpace(referenceSpace);
  
  // 获取会话信息
  console.log('参考空间类型:', referenceSpace.type);
  
  return session;
}
 
// 重置参考空间(重置位置)
function resetReferenceSpace() {
  const referenceSpace = renderer.xr.getReferenceSpace();
  const newReferenceSpace = referenceSpace.getOffsetReferenceSpace(
    new XRRigidTransform({ x: 0, y: 0, z: 0 })
  );
  renderer.xr.setReferenceSpace(newReferenceSpace);
}

VR 场景优化

优化渲染性能

javascript
// ==================== 基础优化 ====================
// 降低渲染分辨率
renderer.setPixelRatio(1);  // 不使用设备像素比
 
// 禁用抗锯齿(或使用低质量)
renderer.antialias = false;
 
// 限制阴影
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.BasicShadowMap;  // 最简单的阴影
 
// ==================== 材质优化 ====================
// 使用简化的材质
const material = new THREE.MeshLambertMaterial({ color: 0x00ff00 });
 
// 减少材质复杂度
const simpleMaterial = new THREE.MeshStandardMaterial({
  color: 0x00ff00,
  roughness: 1,
  metalness: 0,
  flatShading: true
});
 
// ==================== 几何体优化 ====================
// 减少几何体细分
const geometry = new THREE.BoxGeometry(1, 1, 1, 1, 1, 1);
 
// 使用简化的几何体
const sphereGeometry = new THREE.IcosahedronGeometry(0.5, 1);  // 低细分
 
// ==================== LOD(细节层次)====================
const lod = new THREE.LOD();
lod.addLevel(highDetailMesh, 0);
lod.addLevel(mediumDetailMesh, 5);
lod.addLevel(lowDetailMesh, 10);
lod.addLevel(lowestDetailMesh, 20);
scene.add(lod);
 
// ==================== 视锥剔除 ====================
// 确保启用视锥剔除(默认启用)
object.frustumCulled = true;
 
// 计算正确的边界
geometry.computeBoundingBox();
geometry.computeBoundingSphere();

固定注视点渲染

javascript
// 检测固定注视点支持
async function checkFixedFoveation() {
  const session = renderer.xr.getSession();
  
  if (session) {
    // 获取支持的帧率
    const supportedFrameRates = await session.supportedFrameRates;
    console.log('支持的帧率:', supportedFrameRates);
    
    // 设置目标帧率
    await session.updateTargetFrameRate(72);
    
    // 固定注视点渲染(如果支持)
    const baseLayer = session.renderState.baseLayer;
    if (baseLayer && 'fixedFoveation' in baseLayer) {
      baseLayer.fixedFoveation = 0.5;  // 0-1,越高越省性能
      console.log('固定注视点已设置');
    }
  }
}

VR 性能监控

javascript
// ==================== 帧率监控 ====================
let frameCount = 0;
let lastTime = performance.now();
let fps = 0;
 
renderer.setAnimationLoop((time, frame) => {
  // 计算帧率
  frameCount++;
  if (time - lastTime >= 1000) {
    fps = frameCount;
    frameCount = 0;
    lastTime = time;
    console.log('FPS:', fps);
  }
  
  renderer.render(scene, camera);
});
 
// ==================== 使用 Stats.js ====================
import Stats from 'stats.js';
 
const stats = new Stats();
stats.showPanel(0);  // 0: fps, 1: ms, 2: mb
document.body.appendChild(stats.dom);
 
renderer.setAnimationLoop(() => {
  stats.begin();
  renderer.render(scene, camera);
  stats.end();
});

API 参考

XRSession

属性/方法类型说明
enabledFeaturesArray已启用的功能列表
visibilityStateString可见性状态
inputSourcesArray输入源列表
renderStateObject渲染状态
requestReferenceSpace(type)Method请求参考空间
requestHitTestSource(options)Method请求命中测试源
updateTargetFrameRate(rate)Method更新目标帧率
end()Method结束会话

XRReferenceSpace

属性/方法类型说明
typeString参考空间类型
boundsGeometryArray边界几何数据
getOffsetReferenceSpace(origin)Method获取偏移参考空间

XRFrame

属性/方法类型说明
sessionXRSession关联的会话
getPose(space, baseSpace)Method获取姿态
getViewerPose(referenceSpace)Method获取观察者姿态
getHitTestResults(hitTestSource)Method获取命中测试结果

XRInputSource

属性类型说明
handednessString"left" | "right" | "none"
targetRayModeString"tracked-pointer" | "gaze" | "screen"
profilesArray支持的配置文件
gripSpaceXRSpace握持空间
targetRaySpaceXRSpace射线空间
handXRHand手部追踪数据

Three.js WebXRManager

属性/方法类型说明
enabledBoolean是否启用 XR
isPresentingBoolean是否正在呈现
getSession()Method获取当前会话
setSession(session)Method设置会话
getReferenceSpace()Method获取参考空间
setReferenceSpace(space)Method设置参考空间
getController(index)Method获取控制器
getControllerGrip(index)Method获取握持控制器
getHand(index)Method获取手部追踪

配置参数详解

会话初始化选项

javascript
const sessionInit = {
  // 必需功能(缺失会导致创建失败)
  requiredFeatures: [
    'local-floor',  // 地板空间
    'viewer'        // 观察者空间
  ],
  
  // 可选功能(缺失不会导致失败)
  optionalFeatures: [
    'bounded-floor',    // 边界地板
    'hand-tracking',    // 手部追踪
    'layers',           // 图层
    'dom-overlay',      // DOM 叠加
    'hit-test',         // 命中测试
    'anchors',          // 锚点
    'depth-sensing',    // 深度感知
    'light-estimation', // 光照估计
    'mesh-detection'    // 网格检测
  ],
  
  // DOM 叠加配置
  domOverlay: {
    root: document.getElementById('overlay')
  }
};

渲染状态配置

javascript
// 更新渲染状态
session.updateRenderState({
  // 基础图层
  baseLayer: new XRWebGLLayer(session, gl, {
    alpha: true,
    antialias: false,
    depth: true,
    stencil: false,
    framebufferScaleFactor: 1.0  // 分辨率缩放
  }),
  
  // 深度/远近裁剪面
  depthNear: 0.1,
  depthFar: 1000.0,
  
  // 内联视图(用于 inline 模式)
  inlineVerticalFieldOfView: Math.PI / 3
});

控制器配置

javascript
// 控制器射线配置
const rayConfig = {
  length: 5,           // 射线长度
  color: 0xffffff,     // 射线颜色
  opacity: 0.5,        // 射线透明度
  lineWidth: 2         // 线宽
};
 
// 创建自定义射线
function createCustomRay(config) {
  const geometry = new THREE.BufferGeometry().setFromPoints([
    new THREE.Vector3(0, 0, 0),
    new THREE.Vector3(0, 0, -config.length)
  ]);
  
  const material = new THREE.LineBasicMaterial({
    color: config.color,
    opacity: config.opacity,
    transparent: true
  });
  
  return new THREE.Line(geometry, material);
}

最佳实践

1. 性能优化

javascript
// 遵循 72 FPS 目标
// - 减少绘制调用
// - 使用 LOD
// - 简化材质
// - 减少后处理

2. 舒适度

javascript
// - 避免突然移动
// - 提供移动选项
// - 使用渐变过渡
// - 避免闪烁

3. 无障碍

javascript
// - 提供多种交互方式
// - 支持坐姿和站姿
// - 支持手部追踪和控制器
// - 提供视觉反馈

4. 错误处理

javascript
async function startVR() {
  try {
    // 检测支持
    if (!('xr' in navigator)) {
      throw new Error('WebXR 不支持');
    }
    
    const supported = await navigator.xr.isSessionSupported('immersive-vr');
    if (!supported) {
      throw new Error('VR 设备未检测到');
    }
    
    // 创建会话
    const session = await navigator.xr.requestSession('immersive-vr', {
      requiredFeatures: ['local-floor']
    });
    
    renderer.xr.setSession(session);
    
  } catch (error) {
    console.error('VR 启动失败:', error);
    // 显示友好的错误信息
    showError(error.message);
  }
}

常见问题

Q1: VR 模式无法启动?

A: 检查以下问题:

javascript
// 1. 检测 WebXR 支持
if (!('xr' in navigator)) {
  console.log('浏览器不支持 WebXR');
  console.log('建议使用 Chrome、Edge 或 Firefox Reality');
}
 
// 2. 检测 VR 设备
const supported = await navigator.xr.isSessionSupported('immersive-vr');
if (!supported) {
  console.log('未检测到 VR 设备');
}
 
// 3. 检查 HTTPS
if (location.protocol !== 'https:') {
  console.log('WebXR 需要 HTTPS');
}
 
// 4. 检查 VRButton 是否正确添加
const vrButton = VRButton.createButton(renderer);
if (!document.body.contains(vrButton)) {
  document.body.appendChild(vrButton);
}

Q2: 控制器不显示或不工作?

A: 排查步骤:

javascript
// 1. 确保控制器已添加到场景
const controller1 = renderer.xr.getController(0);
scene.add(controller1);
 
// 2. 监听连接事件
controller1.addEventListener('connected', (event) => {
  console.log('控制器已连接:', event.data);
});
 
// 3. 检查 XR 会话状态
renderer.xr.addEventListener('sessionstart', () => {
  console.log('XR 会话已开始');
  // 此时控制器应该可用
});
 
// 4. 确保渲染器 XR 已启用
renderer.xr.enabled = true;

Q3: 如何处理移动端 VR?

A: 移动端可能不支持沉浸式 VR:

javascript
// 检测设备类型
const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
 
// 移动端可能需要不同的处理
if (isMobile) {
  // 检查 Cardboard 支持
  const supported = await navigator.xr.isSessionSupported('immersive-vr');
  
  if (!supported) {
    // 提供 inline 模式作为后备
    console.log('提供普通浏览模式');
  }
}

Q4: 如何实现瞬移移动?

A: 瞬移移动实现:

javascript
// 创建瞬移标记
const teleportMarker = new THREE.Mesh(
  new THREE.CircleGeometry(0.5, 32),
  new THREE.MeshBasicMaterial({ color: 0x00ff00, opacity: 0.5, transparent: true })
);
teleportMarker.rotation.x = -Math.PI / 2;
teleportMarker.visible = false;
scene.add(teleportMarker);
 
// 监听摇杆输入
let isTeleporting = false;
 
controller1.addEventListener('selectstart', () => {
  isTeleporting = true;
});
 
controller1.addEventListener('selectend', () => {
  if (teleportMarker.visible) {
    // 瞬移到目标位置
    const referenceSpace = renderer.xr.getReferenceSpace();
    const offset = new XRRigidTransform({
      x: teleportMarker.position.x,
      y: 0,
      z: teleportMarker.position.z
    });
    renderer.xr.setReferenceSpace(referenceSpace.getOffsetReferenceSpace(offset));
  }
  isTeleporting = false;
  teleportMarker.visible = false;
});
 
// 更新瞬移标记位置
function updateTeleportMarker(controller) {
  if (!isTeleporting) return;
  
  const raycaster = new THREE.Raycaster();
  const tempMatrix = new THREE.Matrix4();
  
  tempMatrix.identity().extractRotation(controller.matrixWorld);
  raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
  raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
  
  const intersects = raycaster.intersectObject(floor);
  
  if (intersects.length > 0) {
    teleportMarker.position.copy(intersects[0].point);
    teleportMarker.visible = true;
  } else {
    teleportMarker.visible = false;
  }
}

Q5: 如何检测用户是否在移动?

A: 监听位置变化:

javascript
let lastPosition = new THREE.Vector3();
const moveThreshold = 0.01;
 
renderer.setAnimationLoop((time, frame) => {
  if (frame) {
    const referenceSpace = renderer.xr.getReferenceSpace();
    const pose = frame.getViewerPose(referenceSpace);
    
    if (pose) {
      const position = new THREE.Vector3().setFromMatrixPosition(
        pose.transform.matrix
      );
      
      if (position.distanceTo(lastPosition) > moveThreshold) {
        console.log('用户正在移动');
        // 触发移动事件
      }
      
      lastPosition.copy(position);
    }
  }
  
  renderer.render(scene, camera);
});

Q6: 如何在 VR 中显示 UI?

A: 使用 VR 空间中的 3D UI:

javascript
// 创建 3D UI 面板
function createVRUI() {
  const uiGroup = new THREE.Group();
  
  // 创建画布纹理
  const canvas = document.createElement('canvas');
  canvas.width = 512;
  canvas.height = 512;
  const ctx = canvas.getContext('2d');
  
  // 绘制 UI
  ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
  ctx.fillRect(0, 0, 512, 512);
  ctx.fillStyle = 'white';
  ctx.font = '32px Arial';
  ctx.fillText('VR 菜单', 200, 50);
  
  // 按钮绘制
  ctx.fillStyle = '#4CAF50';
  ctx.fillRect(50, 100, 200, 50);
  ctx.fillStyle = 'white';
  ctx.fillText('按钮 1', 100, 135);
  
  const texture = new THREE.CanvasTexture(canvas);
  
  // 创建面板
  const geometry = new THREE.PlaneGeometry(1, 1);
  const material = new THREE.MeshBasicMaterial({
    map: texture,
    transparent: true,
    side: THREE.DoubleSide
  });
  const panel = new THREE.Mesh(geometry, material);
  
  // 跟随相机
  uiGroup.add(panel);
  uiGroup.position.set(0, 1.5, -2);
  
  return uiGroup;
}
 
// UI 跟随相机(始终可见)
function updateUIFollowCamera(ui, camera) {
  const cameraPosition = new THREE.Vector3();
  camera.getWorldPosition(cameraPosition);
  
  ui.position.x = cameraPosition.x;
  ui.position.z = cameraPosition.z - 2;
  ui.lookAt(cameraPosition);
}

Q7: 如何处理不同高度的玩家?

A: 使用 local-floor 参考空间:

javascript
// 使用 local-floor 确保地板高度正确
const sessionInit = {
  requiredFeatures: ['local-floor']
};
 
// 如果设备不支持 local-floor,手动设置高度
renderer.xr.addEventListener('sessionstart', async () => {
  const session = renderer.xr.getSession();
  
  try {
    const referenceSpace = await session.requestReferenceSpace('local-floor');
    renderer.xr.setReferenceSpace(referenceSpace);
  } catch (error) {
    // 使用 local 并手动调整
    const referenceSpace = await session.requestReferenceSpace('local');
    const offset = new XRRigidTransform({ x: 0, y: -1.7, z: 0 });  // 假设平均身高
    renderer.xr.setReferenceSpace(referenceSpace.getOffsetReferenceSpace(offset));
  }
});

相关链接