跳转至

VR 控制器射线不可见 — 调试记录

日期: 2026-06-01
项目: truth-lab 最速降线实验
设备: Pico Ultra VR 头显
作者: Hermes Agent (aiyoubu)


问题

点击"进入 VR"后,手柄没有紫色射线指示器,用户无法瞄准控制点进行拖拽。

调用链(已实现 3 版后仍未解决)

1
2
3
VrControllerRays (R3F 组件, return null)
  useEffect: gl.xr.getController(0/1) → 创建 Cylinder Mesh → controller.add(ray)
  useFrame: 每帧切换 ray.visible = true

调试过程

第 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) 内部调用链:

// WebGLRenderer.render()
function render(scene, camera) {
    if (scene.matrixWorldAutoUpdate === true) scene.updateMatrixWorld();
    // ... XR camera setup ...
    projectObject(scene, camera, 0, sortObjects);  // ← 只遍历 scene 的子对象!
}

// projectObject 只递归遍历 object.children
function projectObject(object, camera, ...) {
    if (object.visible === false) return;
    // ... 收集 Mesh、Light、Sprite ...
    for (let i = 0; i < object.children.length; i++) {
        projectObject(object.children[i], 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.jsgetTargetRaySpace):

1
2
3
4
5
6
7
8
getTargetRaySpace() {
    if (this._targetRay === null) {
        this._targetRay = new Group();
        this._targetRay.matrixAutoUpdate = false;
        this._targetRay.visible = false;
    }
    return this._targetRay;
}

返回的 Group 没有 parent,需要外部手动 scene.add()

Three.js 官方 XR 示例 中标准做法:

const controller = renderer.xr.getController(0);
scene.add(controller);  // ← 这步在 truth-lab 中缺失


修复

在 useEffect 中添加一行:

1
2
3
const controller = gl.xr.getController(ci);
// ... 创建 ray Mesh 并 controller.add(ray) ...
scene.add(controller);   // ★ 关键修复

并在 cleanup 中:

ctrl.removeFromParent();
// 同时 dispose 子 Mesh 的 geometry 和 material


经验教训

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)

子对象的 matrixWorldupdateMatrixWorld() 递归计算。不在场景树中的对象,即使手动调用 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 handleXRFrameupdate()gl.render() 路径