跳转至

Three.js Integration 架构

Visionary 在同一个 WebGPU 设备上渲染高斯 Splat 和经典的 Three.js 内容。本文档解释了协调层是如何连接的,深度和覆盖层是如何捕获的,以及低级集成(GaussianSplattingThreeWebGPU)如何在编辑器之外保持可复用性。

高层布局

THREE.WebGPURenderer (GPUDevice/GPUCanvasContext 的所有者)
│
├─ Scene Graph (网格, 灯光, Gizmo, GaussianModel 实例)
│   └─ GaussianThreeJSRenderer (继承自 THREE.Mesh)
│        ├─ 挂钩到 onBeforeRender 以运行高斯预处理
│        ├─ 通过 renderThreeScene() 捕获颜色+深度
│        ├─ 使用共享的命令编码器发布 drawSplats()
│        └─ 合成可选的 Gizmo 覆盖层
│
└─ Low-level helper: GaussianSplattingThreeWebGPU
        ├─ 拥有 GaussianRenderer (计算 + 渲染管道)
        ├─ 管理 PointCloud / DynamicPointCloud 生命周期
        └─ 使用 DirectCameraAdapter 进行相机转换

动机

  1. 单 GPU 设备 – WebGL 和 WebGPU 上下文之间没有握手,零纹理拷贝,没有画布堆叠逻辑。
  2. 确定性深度 – 场景网格和 Splat 读/写相同的深度值(每帧捕获一次)。
  3. 可扩展性 – GaussianThreeJSRenderer 存在于场景图中,因此 XR、后期处理、编辑器 Gizmo 和 OrbitControls 均可正常工作。

帧时间线

function animate(nowMs: number) {
  requestAnimationFrame(animate);

  gaussianRenderer.updateDynamicModels(camera, nowMs * 0.001);  // (1) ONNX 变形
  gaussianRenderer.renderOverlayScene(gizmoScene, camera);      // (2) 可选辅助
  gaussianRenderer.renderThreeScene(camera);                    // (3) 捕获场景颜色+深度
  gaussianRenderer.drawSplats(threeRenderer, scene, camera);    // (4) Splat + 覆盖层合成
}
  1. 动态更新 – 相机矩阵(通过 CameraAdapter 转换)驱动基于 ONNX 的 DynamicPointCloud,因此它们可以烘焙视差感知着色数据。
  2. 覆盖层渲染 – Gizmo 或 HUD 场景渲染到 gizmoOverlayRT 中,使用相同的 Three.js 渲染器以确保一致的色调映射。
  3. 场景捕获 – 主场景渲染到一个内部的 HalfFloat 渲染目标,带有 Float 深度纹理。WebGPU 全屏通道将颜色缓冲区 Blit(传输)到实际画布,处理 Three.js 期望的线性→sRGB 转换。
  4. 高斯通道 – onBeforeRender 预计算变换并分发计算工作。drawSplats() 提交一个渲染通道,该通道读取捕获的深度纹理(当自动深度开启时),并可选择在交换链之上合成 Gizmo 覆盖层。

共享设备命令流

  • GaussianThreeJSRenderer 从 THREE.WebGPURenderer.backend 获取 GPUDevice 和 GPUCanvasContext。
  • 每一帧它为每个阶段(prepareMulti 和 drawSplats)创建一个单一命令编码器。没有二级上下文或画布——Splat 直接绘制到 context.getCurrentTexture() 中。
  • 渲染器依赖于 GaussianRenderer.prepareMulti(...) / renderMulti(...),因此所有可见模型在一个计算/渲染对中批处理。这使得即使有许多模型,队列提交也保持最小。

相机转换

DirectCameraAdapter(由 GaussianSplattingThreeWebGPU 使用)和 CameraAdapter(在应用的其他地方使用)遵循相同的规则集:

  • 从 camera.matrixWorldInverse(Three 的视图矩阵)开始,乘以 R_y(π) 以采用渲染器的 +Z 向前约定,而不改变手性。
  • 将投影矩阵乘以相同的 R_y(π),以保持 P * V 与 Three.js 输出相同。
  • 在预处理后将第二行取反,以抵消打包 Splat 时发生的单次 Y 翻转。
  • 从活动视口推导像素空间中的焦距,以便计算着色器接收准确的镜头数据。

适配器暴露 viewMatrix(), projMatrix(), position(), frustumPlanes(), 以及一个与高斯渲染器期望相匹配的 projection.focal() 垫片(shim)。

深度捕获与覆盖层

自动深度模式(默认)

  1. renderThreeScene() 懒加载分配一个 THREE.RenderTarget(width, height, HalfFloat) 和一个匹配的 THREE.DepthTexture(width, height, FloatType)。
  2. Three.js 场景每帧渲染到该目标一次。
  3. WebGPU 渲染通道将目标颜色纹理 Blit 到交换链,同时从线性转换为 sRGB。
  4. 当 drawSplats() 运行时,它获取支持 DepthTexture 的 WebGPU 纹理句柄,并将其作为 depthStencilAttachment(load/store = load)插入渲染通道描述符。因此,Splat 会自动遵守所有网格遮挡。
  5. Gizmo 覆盖层随后使用简单的带纹理全屏四边形进行合成,并使用预乘 Alpha 混合渲染。

禁用自动深度(setAutoDepthMode(false))将控制权交还给调用者。旧项目随后可以调用 setOccluderMeshes(...),这会在每一帧将精简后的场景渲染到临时深度纹理中。保留此路径仅出于兼容性原因。

诊断

  • diagnoseDepth() 打印自动深度是否启用、渲染目标尺寸和当前 GaussianRenderer 深度状态。
  • disposeDepthResources() 清除缓存的渲染目标,以便下一帧重新创建它们——这在设备丢失或画布调整大小异常后很有用。

GaussianSplattingThreeWebGPU 内部结构

GaussianSplattingThreeWebGPU 被有意设计得很小:

const gs = new GaussianSplattingThreeWebGPU();
await gs.initialize(renderer.backend.device);
await gs.loadPLY('/models/room.ply');

const encoder = device.createCommandEncoder();
gs.render(encoder, swapChainView, camera, [width, height], depthView);
device.queue.submit([encoder.finish()]);
  • initialize(device) 实例化 GaussianRenderer(device, 'bgra8unorm', 3) 并确保 GPU 排序器管道已就绪。
  • loadPLY / loadFile 依赖于 defaultLoader 并将结果包装在 PointCloud 中。
  • render(...) 更新内部 DirectCameraAdapter,运行 prepareMulti,通过 setDepthEnabled 可选地切换深度,并发出一个渲染通道。
  • 该助手暴露 setVisible, numPoints, setDepthEnabled, 和 dispose 用于生命周期管理。

动态模型处理

  • GaussianModel.update() 执行 ONNX 推理,将当前变换、视图矩阵、投影矩阵和可选动画时间传递给 DynamicPointCloud。
  • GaussianThreeJSRenderer.updateDynamicModels() 简单地迭代每个注册的模型并等待 model.update(...)。这在任何 GPU 工作之前运行,因此 Splat 缓冲区始终包含最新的位置/SH 数据。
  • 渲染器还暴露了细粒度的 setter(setModelGaussianScale, setModelOpacityScale, setModelRenderMode 等)以及对每个 GaussianModel 进行操作的全局变体(setGlobalTimeScale, startAllAnimations 等)。

错误处理

  • 缺少 WebGPU 支持:调用者应保护渲染器的创建(参见 initThreeContext)并回退到仅 WebGL 的场景。
  • 相机不匹配:将所有转换保持在 CameraAdapter / DirectCameraAdapter 中——临时的矩阵调整很快会导致输出镜像。
  • 设备丢失:在 device.lost 事件后调用 disposeDepthResources() 和 diagnoseDepth();下一次 renderThreeScene() 调用将重新创建目标。

值得阅读的文件

  • src/app/GaussianThreeJSRenderer.ts – 编排层,自动深度,覆盖层,运行时控制。
  • src/three-integration/GaussianSplattingThreeWebGPU.ts – 用于直接集成的轻量级助手。
  • src/camera/CameraAdapter.ts – 可复用的相机转换逻辑。
  • src/app/GaussianModel.ts – 带有自动同步和动画助手的 Object3D 包装器。
  • src/renderer/gaussian_renderer.ts – 共享计算/渲染管道。