VR 控制器射线不可见 — 调试记录¶
日期: 2026-06-01
项目: truth-lab 最速降线实验
设备: Pico Ultra VR 头显
作者: Hermes Agent (aiyoubu)
问题¶
点击"进入 VR"后,手柄没有紫色射线指示器,用户无法瞄准控制点进行拖拽。
调用链(已实现 3 版后仍未解决)¶
调试过程¶
第 1 版(useFrame 手动同步位置/旋转)¶
方案: 在 useFrame 中每帧读取 controller.getWorldPosition() + controller.quaternion,写入 ray Mesh 的 position/quaternion。
失败原因分析: 控制器 Group 的 matrixAutoUpdate = false(由 WebXRManager 设置),虽然 position/quaternion 值本身正确,但手动同步存在时序不确定性。
第 2 版(子对象自动继承变换)¶
方案: 将 ray Mesh 作为 controller Group 的子对象(controller.add(ray)),ray 设置 quaternion = Y→-Z 映射指向控制器 forward。
失败原因分析: 仍然不可见。此时开始深度溯源。
第 3 版(3 个 Agent 源码级分析)¶
方法: 启动两个独立 Agent 分别阅读 Three.js r174 和 R3F v9.6.1 源码,第 3 个 Agent 综合结论。
根因(源码级证实)¶
gl.xr.getController(0/1) 返回的 Group 不在场景树中,parent === null。
Three.js 的 gl.render(scene, camera) 内部调用链:
由于控制器 Group 没有被 scene.add() 加入场景树,projectObject 从 scene 开始遍历时永远达不到控制器及其子 ray Mesh。
R3F 在 XR 模式下仍然调用 state.gl.render(state.scene, state.camera),但 state.scene 就是 Three.js 的 Scene 对象。问题不在 R3F 侧,而在 Three.js 渲染器侧。
关键源码证据¶
Three.js r174 WebXRController.js(getTargetRaySpace):
返回的 Group 没有 parent,需要外部手动 scene.add()。
Three.js 官方 XR 示例 中标准做法:
修复¶
在 useEffect 中添加一行:
并在 cleanup 中:
经验教训¶
1. gl.xr.getController() 返回的对象需要手动添加到场景¶
Three.js 的 WebXRManager 不会自动将控制器 Group 加入场景。这是开发者的责任。Three.js 官方示例全部有 scene.add(controller),但在 R3F + 原生 WebXR 模式下手写时很容易遗漏。
2. 子对象 + 场景树 = 可见性的两个必要条件¶
Three.js 中一个 Mesh 要可见需要同时满足: - 逻辑上:mesh.visible = true - 拓扑上:mesh 必须在场景树中(mesh.parent 链上溯到 scene)
子对象的 matrixWorld 由 updateMatrixWorld() 递归计算。不在场景树中的对象,即使手动调用 updateMatrixWorld() 更新了 matrixWorld,也**不会被 projectObject 遍历到**,所以不会被渲染。
3. R3F 返回 null 的组件是合法的¶
VrControllerRays 返回 null 是标准模式——useFrame / useThree 等 hooks 完全正常工作,只是不向 R3F 场景树贡献节点。但对于手动用 Three.js API 创建的 Mesh,必须保证其被加入到 state.scene。
4. 调试方法:独立 Agent 源码级分析¶
本次成功的关键是使用两个独立 Agent 分别追溯不同部分的源码(Agent A: Three.js WebXRManager; Agent B: R3F 渲染循环 + Three.js render),得出一致结论。独立验证 + 交叉确认比单个路径排查更可靠。
相关文件¶
| 文件 | 说明 |
|---|---|
src/experiments/brachistochrone/BrachistochroneScene.tsx | VrControllerRays 组件(含修复) |
src/hooks/useVrDrag.ts | VR 控制点拖拽 hook |
Three.js r174 WebXRController.js | getTargetRaySpace() 返回无 parent 的 Group |
Three.js r174 WebGLRenderer.js | render() → projectObject() 只遍历 scene.children |
Three.js r174 WebXRManager.js | onAnimationFrame() 更新 controller matrix 的时序 |
R3F v9.6.1 events-b389eeca.esm.js | handleXRFrame → update() → gl.render() 路径 |