跳转至

Pico Ultra VR 适配 — 调试过程记录

背景

truth-lab 最速降线实验需要支持 WebXR VR,在 Pico Ultra 头显上运行。项目使用 React Three Fiber v9.6.1 + Three.js r174 + react-three/xr v6。

调试时间线

第 1 轮:极简测试页 → 场景不工作

症状:纯 Three.js 测试页(xr-test.html)能在 Pico 正常进入 VR 并看到方块,但完整 truth-lab 场景不行。

行动: - 从 @react-three/xr v6 方案切换到原生 WebXR API - onCreated 中设置 gl.xr.enabled = true - 使用 navigator.xr.requestSession('immersive-vr', { requiredFeatures: ['local-floor'] }) - 使用 gl.xr.setSession(session) + gl.xr.setReferenceSpaceType('local-floor')

结果:❌ 失败。报错 "failed to execute requestReferenceSpace on 'XRSession', this device does not support the requested reference space type"


第 2 轮:双 Agent 根因分析

方法:用 delegate_task 启动两个子 Agent: - Agent A:深度分析所有可能性(分析 R3F 源码、Three.js WebXRManager、pmndrs/xr 源码) - Agent B:审阅 Agent A 结论,评估可靠性

Agent A 核心发现: 1. R3F v9.6.1 自带 XR 支持(handleXRFrame + sessionstart/sessionend 监听) 2. @pmndrs/xr@react-three/xr 的内核)硬编码 'local-floor' 为 required feature 3. R3F 的 loop()isPresenting=true 时自动跳过

Agent B 审阅结论(高可靠性原因筛选): - ✅ 最可能:Pico 不支持 'local-floor' reference space type - ✅ 遗漏发现:@pmndrs/xr 内部 calls session.requestReferenceSpace('local-floor') - ✅ 遗漏发现:gl.xr.setReferenceSpaceType() 多次被覆盖的时序问题

行动: 1. 移除 requiredFeatures: ['local-floor'] 2. 删除 gl.xr.setReferenceSpaceType('local-floor') 错用 3. 改用 alert() 替换为页面内错误提示(VR 浏览器屏蔽 alert) 4. 添加 rendererRef 清理

结果:❌ 还是失败。"requestReferenceSpace failed" — @pmndrs/xr 内部依然用 'local-floor'


第 3 轮:猴子补丁 setReferenceSpaceType

发现@pmndrs/xr 库在 store.jsinit.js 中硬编码 'local-floor',无论外部怎么配置。

行动: 1. 在 onCreated 中对 gl.xr.setReferenceSpaceType 做猴子补丁,拦截 'local-floor' → 'local' 2. 使用 customSessionInit: { requiredFeatures: [] } 绕过 session 创建时的 feature 检查

结果:❌ 虽然 session 创建成功(不闪退了),但场景全黑——坐标问题


第 4 轮:VR 场景坐标偏移

发现:桌面场景在 z=0 平面,VR 相机在原点看向 -z,场景在用户**身后**。场景中心在 y=-4,在用户**脚下**。

行动: 1. 为 SceneContent 添加 <group position={vrOffset}> 2. VR 模式偏移 [-5, 2.8, -3](场景中心移到 [0, -1.2, -3],用户面前 3 米)

结果:❌ 还是全黑


第 5 轮:去掉 react-three/xr,纯 R3F 原生方案

发现:R3F v9.6.1 自带完整 XR 支持。@react-three/xr 添加了大量不必要的功能(手柄建模、手部追踪、teleport 等),可能干扰基本渲染。

行动: 1. 完全移除 @react-three/xr<XR> 组件 2. 移除 createXRStore() 3. 自己实现 VrButton 组件,直接调用 navigator.xr.requestSession() + gl.xr.setSession(session) 4. 用全局 _rendererRef 传递 renderer 引用

结果:❌ "requestReferenceSpace failed, device does not support requested reference space type" —— 猴子补丁没有阻止 THREE 内部使用 'local-floor'


第 6 轮:紧挨着 setSession 前设置 referenceSpaceType ✅

发现:之前在 onCreated 中设置 setReferenceSpaceType('local'),但在 R3F 初始化过程中可能有其他代码覆盖了这个设置。

行动: 1. 保留猴子补丁 2. 在 handleClick 中,gl.xr.setSession(session) 紧挨着之前 再调一次 gl.xr.setReferenceSpaceType('local') 3. 消除一切时序窗口

结果:✅ 成功进入 VR!场景正确显示


最终方案总结

3 个关键修复

修复 解决什么问题 方法
移除 requiredFeatures Pico 不支持 'local-floor' 作为 session 必需特性 navigator.xr.requestSession('immersive-vr') 无参数
猴子补丁 setReferenceSpaceType Three.js 和第三方库内部默认为 'local-floor' 拦截方法调用,'local-floor' → 'local'
紧挨着 setSession 前设置 消除初始化过程中的覆盖时序 gl.xr.setReferenceSpaceType('local') 紧贴 setSession()

最终架构

1
2
3
4
5
6
BrachistochroneScene
├── Canvas (R3F)
│   ├── onCreated: { gl.xr.enabled = true; monkeyPatch(); setRST('local'); }
│   └── SceneContent (含场景所有 3D 对象 + VR 偏移)
└── VrButton (独立组件)
    └── handleClick: requestSession → setRST('local') → setSession → 成功
  • 不使用 @react-three/xr / @pmndrs/xr
  • 完全依赖 R3F v9.6.1 内置 XR 支持(sessionstart/sessionend + handleXRFrame)
  • Vendor 体积:2.0 MB(vs 4.3MB 使用 react-three/xr 时)

代码要点

onCreated={({ gl }) => {
  gl.xr.enabled = true;
  const orig = gl.xr.setReferenceSpaceType.bind(gl.xr);
  gl.xr.setReferenceSpaceType = ((type) => {
    orig(type === 'local-floor' ? 'local' : type);  // 猴子补丁
  });
  gl.xr.setReferenceSpaceType('local');  // 立即设置一次
}}

// VR 按钮
const session = await navigator.xr.requestSession('immersive-vr');
gl = _rendererRef;
gl.xr.setReferenceSpaceType('local');  // 紧挨着 setSession 前再设一次
await gl.xr.setSession(session);

待改进

  • VR 模式下的控制点拖拽交互(需手柄支持)
  • VR 场景位置可能需要微调
  • 去掉 console.log 调试输出