WebXR 技术栈深度解析 — truth-lab Pico Ultra 适配实战¶
目录¶
- 方案演进全景
- 技术层次总览
- WebGL:底层画布
- Three.js:WebGL 的渲染框架
- React Three Fiber:React 化 Three.js
- Xr 库:打通 R3F 与 WebXR
- 技术关系图谱
- Pico Ultra 兼容性专题
- 常见误区与教训
1. 方案演进全景¶
truth-lab 进入 VR 的功能经历了 6 轮方案迭代,每轮都换了一条技术路径。
方案 A:@react-three/xr v6(原始实现)¶
- 原理:
@react-three/xrv6 基于@pmndrs/xr内核,提供完整的 XR 管理(session 创建、控制器、手柄、交互) - 结果:首次能进 VR(用户确认一次),但手柄无法操控小球
- 问题:
@pmndrs/xr内部buildXRSessionInit()硬编码requiredFeatures: ['local-floor']- Pico Ultra 不支持
'local-floor'作为必需特性 - 尝试用
customSessionInit覆盖 → 但setReferenceSpaceType仍被库覆盖为'local-floor'
方案 B:原生 WebXR + R3F(中间尝试)¶
- 原理:跳过 XR 库,直接用浏览器 WebXR API
- 结果:多次报错、黑屏
- 问题:R3F v9.6.1 内部有 XR 支持(
handleXRFrame),但referenceSpaceType默认值'local-floor'在setSession内部被读取,猴子补丁时序不可靠
方案 C:@react-three/xr + customSessionInit + 猴子补丁¶
- 结果:session 创建成功,但渲染全黑
- 问题:
@react-three/xr添加的控制器、手柄、输入源管理等可能干扰基本渲染
方案 D:最终方案 — 纯 R3F + 原生 WebXR¶
- 原理:
- 不加载任何 XR 库(
@react-three/xr/@pmndrs/xr) - R3F v9.6.1 内置
handleXRFrame自动接管 XR 渲染循环 navigator.xr.requestSession('immersive-vr')无参数gl.xr.setReferenceSpaceType('local')紧贴setSession()之前- 猴子补丁作为安全网
- 结果:✅ 成功
2. 技术层次总览¶
3. WebGL:底层画布¶
是什么¶
WebGL(Web Graphics Library)是浏览器中访问 GPU 的 JavaScript API。本质上是 OpenGL ES 的浏览器绑定。
渲染管线¶
关键概念¶
| 概念 | 说明 |
|---|---|
WebGLRenderingContext | 渲染上下文,通过 canvas.getContext('webgl') 获得 |
xrCompatible | 上下文属性,标记此上下文可用于 XR 渲染 |
makeXRCompatible() | 将现有 WebGL 上下文切换到 XR 兼容模式(可能重建上下文!) |
| Shader (着色器) | GPU 上运行的小程序(顶点/片段) |
| Framebuffer (帧缓冲) | 渲染目标,可以是屏幕或纹理 |
与 XR 的关系¶
当进入 VR 时,Three.js 会: 1. 检查 gl.getContextAttributes().xrCompatible 是否为 true 2. 如果不是,调用 gl.makeXRCompatible() — 这可能导致 WebGL 上下文丢失并重建 3. 创建 XRWebGLLayer 或 XRProjectionLayer,绑定到 XR session 4. XR 帧渲染使用 XR 的帧缓冲而不是默认 canvas
这就是猴子补丁方案没解决的问题:makeXRCompatible() 即使成功,也可能重置上下文状态。
4. Three.js:WebGL 的渲染框架¶
是什么¶
Three.js 是 WebGL 的**高层封装**,提供场景图、相机、材质、光照、几何体等抽象。
核心类¶
| 类 | 职责 |
|---|---|
WebGLRenderer | 管理 WebGL 上下文,驱动渲染循环 |
Scene | 场景图根节点,包含所有 3D 对象 |
PerspectiveCamera | 透视相机,定义视锥体 |
WebXRManager | 与管理 XR session 的核心 |
WebXRManager 内部机制¶
WebGLRenderer 内部持有一个 WebXRManager 实例(renderer.xr)。
关键设计:referenceSpaceType 是一个闭包变量,只能通过 setReferenceSpaceType() 修改。这就是为什么猴子补丁能工作的原因——我们拦截了方法调用。
setAnimationLoop 的陷阱¶
R3F 不调用 renderer.setAnimationLoop(),它用自己的 RAF 循环。XR 模式下,R3F 通过 gl.xr.setAnimationLoop(handleXRFrame) 设置回调。这调的是 WebXRManager.setAnimationLoop,而非 WebGLRenderer 的。
WebGLAnimation(XR 的驱动)¶
5. React Three Fiber:React 化 Three.js¶
是什么¶
R3F 是一个 React Reconciler,将 Three.js 的声明式转换为 React 组件树。你用 JSX 写 Three.js:
渲染循环¶
R3F 不用 Three.js 的 setAnimationLoop()。它有自己的 RAF 循环:
内置 XR 支持(关键!)¶
R3F v9.6.1 已经内置了 XR 支持,不需要任何额外库:
这就是为什么最终方案不需要任何 XR 库——R3F 已经做好了。
R3F vs Three.js¶
| Three.js | R3F | |
|---|---|---|
| 渲染循环 | renderer.setAnimationLoop() | 自己的 RAF loop() |
| 场景管理 | 手动物件添加/移除 | React 声明式 |
| XR 支持 | WebXRManager(原始 API) | 内置 handleXRFrame + session 事件 |
| 状态管理 | 无 | Zustand store |
| 适用场景 | 任意 JS 项目 | React 项目 |
6. XR 库:打通 R3F 与 WebXR¶
@react-three/xr v6¶
定位:R3F 的 XR 官方扩展库。
功能: - 提供 <XR> 组件包裹场景 - 提供 VRButton / ARButton / XRButton 进入 XR - 提供 useController() / useHand() / useXR() hooks - 封装控制器(手柄)建模和视觉 - 支持手部追踪、Teleport、DOM Overlay 等高级功能
内部架构:
@pmndrs/xr 核心分析¶
store.ts 关键代码:
init.js 关键代码:
问题根源: - store.js 第 168 行:xrManager.setReferenceSpaceType(bounded ? 'bounded-floor' : 'local-floor') - init.js 第 7 行:const requiredFeatures = ['local-floor'] - 没有提供 'local' 作为选项 - bounded 参数只能选 undefined / true / false,都不对应 'local'
为什么不使用 react-three/xr¶
| 方面 | react-three/xr | 纯 R3F + 原生 |
|---|---|---|
| 依赖体积 | 4.3 MB vendor | 2.0 MB vendor |
| 控制器建模 | 自动添加 | 无(暂不需要) |
| 手部追踪 | 支持 | 无 |
| 配置灵活性 | 受限于库的选项 | 完全控制 |
| Pico 兼容性 | 硬编码 'local-floor' | 可自定义 |
| 维护复杂 | 依赖版本更新 | 自己控制 |
结论:对于基本 VR 渲染需求,R3F 内置支持就够了。@react-three/xr 在需要手柄交互、手部追踪、Teleport 等高级功能时才有必要。
7. 技术关系图谱¶
启动渲染的调用链¶
桌面模式:
XR 模式(最终方案):
各层分工¶
| 层 | 负责 | 不负责 |
|---|---|---|
| WebGL | GPU 绘制、着色器、纹理 | 场景管理、渲染循环 |
| Three.js | 场景图、相机、材质、WebXRManager | React 集成 |
| R3F | React 声明式、渲染循环、useFrame | XR session 管理 |
| XR 库 | XR session、控制器、交互 | 基础渲染 |
数据流¶
8. Pico Ultra 兼容性专题¶
local-floor vs local¶
| 类型 | 含义 | 适用 |
|---|---|---|
viewer | 以头显当前位置为原点 | 基础 AR |
local | 以用户初始位置为原点,Y 向上 | Pico Ultra 支持 |
local-floor | 以用户脚下地面为原点,Y 向上 | PC VR、Quest(Pico 不支持!) |
bounded-floor | 有边界的 local-floor | 房间规模 VR |
unbounded | 无边界的 AR | AR |
Pico Ultra 使用 local 而不支持 local-floor,意味着参考空间原点在用户**眼睛高度**而非地面。场景物体需要放在用户面前(负 Z)和眼睛高度以下(负 Y)。
关键适配点¶
navigator.xr.requestSession()不能带requiredFeatures: ['local-floor']gl.xr.setReferenceSpaceType()必须设为'local'- **场景位置**需要偏移到用户面前(
[-5, 2.8, -3]经验值) makeXRCompatible()在 Pico 上可能不是问题(GPU 统一)
9. 常见误区与教训¶
误区 1:"R3F 需要 react-three/xr 才能做 XR"¶
事实:R3F v9+ 内置 handleXRFrame 和 session 事件监听。@react-three/xr 是为了高级交互功能(手柄、手部追踪),不是基础 XR 渲染的前提。
误区 2:"gl.xr.setReferenceSpaceType('local-floor') 是必需的"¶
事实:这行代码实际上是**多余的**——THREE 默认为 'local-floor'。用 'local' 在 Pico 上更兼容。
误区 3:"alert() 能显示 XR 错误"¶
事实:VR 浏览器中 alert() 通常被屏蔽或不可见。应使用页面内嵌错误提示。
误区 4:"猴子补丁在任何时机设置都行"¶
事实:onCreated 中设 setReferenceSpaceType('local') 可能在 R3F 后续初始化中被覆盖。紧挨着 setSession 之前设置 才可靠。
误区 5:"gl.xr.setSession() 和 navigator.xr.requestSession() 是独立的"¶
事实:setSession 内部调用 session.requestReferenceSpace(),使用当前 referenceSpaceType。requestSession 只是创建 session,setSession 才将其绑定到渲染器。
附录:关键源码位置(three.js r174)¶
| 文件 | 关键行 |
|---|---|
src/renderers/WebGLRenderer.js | renderer.setAnimationLoop() (~1117) |
src/renderers/WebGLRenderer.js | onXRSessionStart() (~1108) |
src/renderers/webxr/WebXRManager.js L28 | let referenceSpaceType = 'local-floor' |
src/renderers/webxr/WebXRManager.js L252 | this.setSession() |
src/renderers/webxr/WebXRManager.js L363 | session.requestReferenceSpace(referenceSpaceType) |
src/renderers/webxr/WebXRManager.js L702 | onAnimationFrame() — XR 帧回调 |
src/renderers/webxr/WebXRManager.js L826 | onAnimationFrameCallback(time, frame) |
src/renderers/webgl/WebGLAnimation.js | WebGLAnimation 类 |
R3F v9.6.1(dist/events-b389eeca.esm.js):
| 行 | 内容 |
|---|---|
| ~15737 | handleSessionChange() — XR 事件处理 |
| ~15741 | gl.xr.setAnimationLoop(handleXRFrame) |
| ~16072 | loop() — 主 RAF 循环 |
| ~16086 | !state.gl.xr.isPresenting — XR 活跃时跳过 |
| ~16041 | update() — 渲染一帧 |