跳转至

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 覆盖层。

共享设备命令流

  • GaussianThreeJSRendererTHREE.WebGPURenderer.backend 获取 GPUDeviceGPUCanvasContext
  • 每一帧它为每个阶段(prepareMultidrawSplats)创建一个单一命令编码器。没有二级上下文或画布——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 – 共享计算/渲染管道。