关于 threejs-impl-xr
这是面向 Three.js 开发者的 WebXR 实现参考,适用于构建浏览器中的 VR 和 AR 场景。内容涵盖会话设置、控制器、手部追踪、命中测试、移动方式和性能要点。
主要功能
- 01WebXR 会话配置与参考空间
- 02VR 和 AR 场景初始化
- 03控制器与手部追踪
- 04AR 表面命中测试
- 05XR 渲染性能优化
适用场景
- 01为 Three.js 场景添加 VR 会话
- 02配置 AR 会话并放置虚拟物体
- 03实现控制器或手势交互
- 04调整 XR 场景的帧率与渲染开销
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ impertio-studio/ three.js-claude-skill-package --skill threejs-impl-xr安装后在 Claude Code 中输入/threejs-impl-xr确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/anti-patterns.mdexamples.mdmethods.md详细介绍
threejs-impl-xr
快速参考
WebXRManager 属性
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
enabled | boolean | false | 启用 XR 渲染 |
isPresenting | boolean | 只读 | XR 会话是否处于活动状态 |
cameraAutoUpdate | boolean | true | 根据 XR 设备姿态自动更新相机 |
WebXRManager 方法
| 方法 | 签名 | 描述 |
|---|---|---|
getSession() | () => XRSession | null | 当前 XR 会话 |
setSessionInit(options) | (XRSessionInit) => void | 进入会话前配置会话功能 |
setReferenceSpaceType(type) | (string) => void | 设置参考空间类型 |
getController(index) | (number) => Group | 获取控制器目标射线空间 |
getControllerGrip(index) | (number) => Group | 获取控制器握持空间 |
getHand(index) | (number) => Group | 获取手部追踪组 |
setFoveation(level) | (number) => void | 设置注视点渲染(0.0–1.0) |
getFoveation() | () => number | 获取当前注视点渲染级别 |
getEnvironmentBlendMode() | () => string | 获取混合模式(不透明、叠加、Alpha 混合) |
setFramebufferScaleFactor(scale) | (number) => void | 调整 XR 渲染分辨率 |
会话类型
| 类型 | 用例 |
|---|---|
'immersive-vr' | 完整的 VR 头显体验 |
'immersive-ar' | 头显或手机上的 AR 透视 |
'inline' | 浏览器窗口中的非沉浸式 XR |
参考空间类型
| 类型 | 原点 | 用例 |
|---|---|---|
'viewer' | 头部位置 | HUD 元素、视线锁定 UI |
'local' | 初始头部位置 | 坐姿体验 |
'local-floor' | 起始时的地面高度 | 站立式 VR,房间规模体验始终优先使用 |
'bounded-floor' | 带边界的地面 | 带安全边界的房间规模体验 |
'unbounded' | 世界原点 | 大范围 AR 体验 |
XR 附加组件类
| 类 | 导入路径 | 用途 |
|---|---|---|
VRButton | three/addons/webxr/VRButton.js | 创建带功能检测的“进入 VR”按钮 |
ARButton | three/addons/webxr/ARButton.js | 创建带功能检测的“进入 AR”按钮 |
XRControllerModelFactory | three/addons/webxr/XRControllerModelFactory.js | 加载适用的控制器 3D 模型 |
XRHandModelFactory | three/addons/webxr/XRHandModelFactory.js | 创建手部追踪可视化 |
XRHandPrimitiveModel | three/addons/webxr/XRHandPrimitiveModel.js | 简单的几何手部表示 |
XREstimatedLight | three/addons/webxr/XREstimatedLight.js | AR 环境光照估算 |
XRPlanes | three/addons/webxr/XRPlanes.js | AR 平面检测可视化 |
重要警告
绝不要使用 requestAnimationFrame() 进行 XR 渲染——始终使用 renderer.setAnimationLoop()。WebXR API 需要使用自己的帧计时;XR 会话处于活动状态时,requestAnimationFrame 会停止触发。
绝不要在 VR 中应用繁重的后期处理——每种效果都会运行两次(每只眼睛各一次),使 GPU 开销翻倍。丢帧会导致晕动症。
始终以 72fps(Quest)或 90fps(PC VR)为目标——丢帧会导致恶心和不适。更低的目标帧率不可接受。
始终在创建 VRButton/ARButton 之前设置 renderer.xr.enabled = true——按钮会检查此属性以检测功能。
绝不要忘记将控制器添加到场景中——getController() 会返回一个 Group,必须通过 scene.add() 添加,否则它不会渲染或触发事件。
VR 设置
最简 VR 场景
import * as THREE from 'three';
import { VRButton } from 'three/addons/webxr/VRButton.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// Enable XR BEFORE creating VRButton
renderer.xr.enabled = true;
renderer.xr.setReferenceSpaceType('local-floor');
document.body.appendChild(VRButton.createButton(renderer));
// MUST use setAnimationLoop — NEVER requestAnimationFrame
renderer.setAnimationLoop((time, frame) => {
renderer.render(scene, camera);
});
AR 设置
import * as THREE from 'three';
import { ARButton } from 'three/addons/webxr/ARButton.js';
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.xr.enabled = true;
// Configure AR features BEFORE creating ARButton
renderer.xr.setSessionInit({
requiredFeatures: ['hit-test'],
optionalFeatures: ['dom-overlay'],
domOverlay: { root: document.getElementById('overlay') }
});
document.body.appendChild(ARButton.createButton(renderer, {
requiredFeatures: ['hit-test']
}));
renderer.setAnimationLoop((time, frame) => {
renderer.render(scene, camera);
});
控制器
控制器空间
Three.js 为每个物理控制器提供了三个不同的空间:
| 方法 | 空间 | 使用场景 |
|---|---|---|
getController(index) | 目标射线 | 指向方向、激光指示器 |
getControllerGrip(index) | 握持空间 | 手握住控制器的位置 |
getHand(index) | 手部 | 完整的手部追踪骨架 |
始终将所有用到的空间添加到场景中。每个方法都会返回一个 THREE.Group。
控制器事件
| 事件 | 触发条件 |
|---|---|
selectstart | 按下主扳机 |
selectend | 松开主扳机 |
select | 按下并松开主扳机 |
squeezestart | 按下握持按钮 |
squeezeend | 松开握持按钮 |
squeeze | 按下并松开握持按钮 |
connected | 检测到控制器(event.data = XRInputSource) |
disconnected | 控制器断开 |
控制器模型
import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js';
const factory = new XRControllerModelFactory();
// Target ray space — for laser pointer / interaction ray
const controller0 = renderer.xr.getController(0);
controller0.addEventListener('selectstart', onSelectStart);
controller0.addEventListener('selectend', onSelectEnd);
scene.add(controller0);
// Grip space — for rendering the controller model
const grip0 = renderer.xr.getControllerGrip(0);
grip0.add(factory.createControllerModel(grip0));
scene.add(grip0);
激光指示器视觉效果
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 0, -5)
]);
const material = new THREE.LineBasicMaterial({ color: 0xffffff });
const line = new THREE.Line(geometry, material);
controller0.add(line);
手部追踪
import { XRHandModelFactory } from 'three/addons/webxr/XRHandModelFactory.js';
// Request hand-tracking feature
renderer.xr.setSessionInit({
optionalFeatures: ['hand-tracking']
});
const handFactory = new XRHandModelFactory();
const hand0 = renderer.xr.getHand(0);
hand0.add(handFactory.createHandModel(hand0, 'mesh'));
scene.add(hand0);
const hand1 = renderer.xr.getHand(1);
hand1.add(handFactory.createHandModel(hand1, 'mesh'));
scene.add(hand1);
手部模型配置: 'mesh'(逼真模型)、'spheres'(关节球体)、'boxes'(关节方块)。
手部组会触发以下事件:
connected— 检测到手部disconnected— 手部断开pinchstart/pinchend— 拇指与食指捏合手势
AR 命中测试
命中测试可将虚拟对象放置在真实世界的表面上。
let hitTestSource = null;
let hitTestSourceRequested = false;
const reticle = new THREE.Mesh(
new THREE.RingGeometry(0.15, 0.2, 32).rotateX(-Math.PI / 2),
new THREE.MeshBasicMaterial()
);
reticle.matrixAutoUpdate = false;
reticle.visible = false;
scene.add(reticle);
renderer.setAnimationLoop((time, frame) => {
if (frame) {
const session = renderer.xr.getSession();
const referenceSpace = renderer.xr.getReferenceSpace();
if (!hitTestSourceRequested) {
session.requestReferenceSpace('viewer').then((viewerSpace) => {
session.requestHitTestSource({ space: viewerSpace }).then((source) => {
hitTestSource = source;
});
});
hitTestSourceRequested = true;
}
if (hitTestSource) {
const results = frame.getHitTestResults(hitTestSource);
if (results.length > 0) {
const pose = results[0].getPose(referenceSpace);
reticle.visible = true;
reticle.matrix.fromArray(pose.transform.matrix);
} else {
reticle.visible = false;
}
}
}
renderer.render(scene, camera);
});
传送模式
const tempMatrix = new THREE.Matrix4();
const raycaster = new THREE.Raycaster();
const marker = new THREE.Mesh(
new THREE.CircleGeometry(0.25, 32).rotateX(-Math.PI / 2),
new THREE.MeshBasicMaterial({ color: 0x00ff00 })
);
scene.add(marker);
const controller = renderer.xr.getController(0);
controller.addEventListener('selectend', () => {
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) {
const point = intersects[0].point;
// Move the XR camera rig, NOT the camera directly
cameraRig.position.set(point.x, 0, point.z);
}
});
scene.add(controller);
始终移动包含摄像机的摄像机架组,绝不要直接移动摄像机——WebXR API 会控制摄像机相对于其父级的位置。
VR 性能
目标帧率
| 平台 | 目标 FPS | 备注 |
|---|---|---|
| Meta Quest 2/3 | 72–120 fps | 默认 72,可选 90/120 |
| PC VR (SteamVR) | 90 fps | 标准目标 |
| PSVR2 | 90–120 fps | 取决于平台 |
优化技巧
- 注视点渲染 — 使用
renderer.xr.setFoveation(1.0)以获得最高性能。范围为 0.0(无)到 1.0(最高)。 - 帧缓冲区缩放 — 在 GPU 受限时,使用
renderer.xr.setFramebufferScaleFactor(0.75)降低分辨率。 - 尽量减少绘制调用 — 对重复对象使用
THREE.InstancedMesh。目标是少于 100 次绘制调用。 - 避免后期处理 — 每种效果在立体渲染中都会渲染两次。尽可能移除 bloom、SSAO 和抗锯齿通道。
- 使用烘焙光照 — 实时阴影的开销会翻倍。尽可能预先烘焙。
- LOD(细节层次) — 使用
THREE.LOD降低远处对象的多边形数量。 - 纹理压缩 — 使用 KTX2/Basis 纹理以减少 GPU 内存占用。
摄像机架模式
VR 移动始终使用摄像机架组:
const cameraRig = new THREE.Group();
cameraRig.add(camera);
scene.add(cameraRig);
// Move the rig, not the camera
cameraRig.position.set(0, 0, 5);
WebXR API 每帧都会设置摄像机相对于其父级的位置和旋转。直接移动摄像机会立即被覆盖。
XR 估算光照(AR)
import { XREstimatedLight } from 'three/addons/webxr/XREstimatedLight.js';
const xrLight = new XREstimatedLight(renderer);
xrLight.addEventListener('estimationstart', () => {
scene.add(xrLight);
scene.environment = xrLight.environment;
// Remove default lights
});
xrLight.addEventListener('estimationend', () => {
scene.remove(xrLight);
scene.environment = null;
// Restore default lights
});
参考链接
- references/methods.md — WebXRManager 和 XR 附加类的完整 API 签名
- references/examples.md — VR、AR、控制器和手部追踪的可运行代码示例
- references/anti-patterns.md — WebXR 开发中应避免的做法