Three.js Integration Module Architecture
Visionary renders Gaussian splats and classic Three.js content on the same WebGPU device. This document explains how the orchestration layer is wired, how depth and overlays are captured, and how the low-level integration (GaussianSplattingThreeWebGPU) stays reusable outside of the editor.
High-level layout
THREE.WebGPURenderer (owner of GPUDevice/GPUCanvasContext)
│
├─ Scene Graph (meshes, lights, gizmos, GaussianModel instances)
│ └─ GaussianThreeJSRenderer (extends THREE.Mesh)
│ ├─ Hooks into onBeforeRender to run Gaussian preprocess
│ ├─ Captures color+depth via renderThreeScene()
│ ├─ Issues drawSplats() with shared command encoder
│ └─ Composites optional gizmo overlay
│
└─ Low-level helper: GaussianSplattingThreeWebGPU
├─ Owns GaussianRenderer (compute + render pipelines)
├─ Manages PointCloud / DynamicPointCloud lifetime
└─ Uses DirectCameraAdapter for camera conversion
Motivations
- Single GPU device – No handshake between WebGL and WebGPU contexts, zero texture copies, no canvas stacking logic.
- Deterministic depth – Scene meshes and splats read/write the same depth values (captured once per frame).
- Extensibility – GaussianThreeJSRenderer lives in the scene graph, so XR, post-processing, editor gizmos, and OrbitControls continue to function normally.
Frame timeline
function animate(nowMs: number) {
requestAnimationFrame(animate);
gaussianRenderer.updateDynamicModels(camera, nowMs * 0.001); // (1) ONNX deformation
gaussianRenderer.renderOverlayScene(gizmoScene, camera); // (2) optional helpers
gaussianRenderer.renderThreeScene(camera); // (3) capture scene color+depth
gaussianRenderer.drawSplats(threeRenderer, scene, camera); // (4) splat + overlay composite
}
- Dynamic update – Camera matrices (converted through
CameraAdapter) drive ONNX-basedDynamicPointClouds so they can bake parallax-aware shading data. - Overlay render – Gizmos or HUD scenes render into
gizmoOverlayRT, using the same Three.js renderer to ensure consistent tone mapping. - Scene capture – The main scene renders to an internal HalfFloat render target with a Float depth texture. A WebGPU fullscreen pass blits the color buffer to the actual canvas, handling the linear→sRGB conversion that Three.js expects.
- Gaussian pass –
onBeforeRenderprecomputes transforms and dispatches compute work.drawSplats()submits one render pass that reads the captured depth texture (when auto depth is on) and optionally composites the gizmo overlay on top of the swap chain.
Shared-device command flow
GaussianThreeJSRendererobtains theGPUDeviceandGPUCanvasContextfromTHREE.WebGPURenderer.backend.- Every frame it creates a single command encoder per stage (
prepareMultianddrawSplats). There is no secondary context or canvas—splats draw directly intocontext.getCurrentTexture(). - The renderer relies on
GaussianRenderer.prepareMulti(...)/renderMulti(...)so all visible models are batched in one compute/render pair. This keeps queue submissions minimal even with many models.
Camera conversion
Both DirectCameraAdapter (used by GaussianSplattingThreeWebGPU) and CameraAdapter (used elsewhere in the app) follow the same set of rules:
- Start from
camera.matrixWorldInverse(Three’s view matrix) and multiply byR_y(π)to adopt the renderer’s +Z-forward convention without changing handedness. - Multiply the projection matrix by the same
R_y(π)to keepP * Videntical to Three.js output. - Negate the second row after preprocess to counter the single Y-flip that happens when packing splats.
- Derive focal lengths in pixel space from the active viewport so compute shaders receive accurate lens data.
The adapters expose viewMatrix(), projMatrix(), position(), frustumPlanes(), and a projection.focal() shim that matches what the Gaussian renderer expects.
Depth capture & overlays
Auto Depth Mode (default)
renderThreeScene()lazily allocates aTHREE.RenderTarget(width, height, HalfFloat)and a matchingTHREE.DepthTexture(width, height, FloatType).- The Three.js scene renders into that target once per frame.
- A WebGPU render pass blits the target color texture to the swap chain while converting from linear to sRGB.
- When
drawSplats()runs, it fetches the WebGPU texture handle that backsDepthTextureand plugs it into the render pass descriptor asdepthStencilAttachment(load/store = load). Splats therefore respect all mesh occluders automatically. - Gizmo overlays are composited afterwards using a simple textured full-screen quad rendered with premultiplied-alpha blending.
Disabling auto depth (setAutoDepthMode(false)) hands control back to the caller. Legacy projects can then call setOccluderMeshes(...), which renders a reduced scene into a temporary depth texture each frame. This path is kept solely for compatibility reasons.
Diagnostics
diagnoseDepth()prints whether auto depth is enabled, render-target sizes, and current GaussianRenderer depth settings.disposeDepthResources()clears cached render targets so the next frame recreates them—useful after device loss or canvas resize anomalies.
GaussianSplattingThreeWebGPU internals
GaussianSplattingThreeWebGPU is intentionally minimal:
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)instantiatesGaussianRenderer(device, 'bgra8unorm', 3)and ensures the GPU sorter pipelines are ready.loadPLY/loadFilerely ondefaultLoaderand wrap the result in aPointCloud.render(...)updates the internalDirectCameraAdapter, runsprepareMulti, optionally toggles depth withsetDepthEnabled, and emits one render pass.- The helper exposes
setVisible,numPoints,setDepthEnabled, anddisposefor lifecycle management.
Dynamic model handling
GaussianModel.update()executes ONNX inference by passing the current transform, view matrix, projection matrix, and an optional animation time toDynamicPointCloud.GaussianThreeJSRenderer.updateDynamicModels()simply iterates over every registered model and awaitsmodel.update(...). This runs before any GPU work so the splat buffers always contain fresh positions/SH data.- The renderer also exposes granular setters (
setModelGaussianScale,setModelOpacityScale,setModelRenderMode, etc.) plus global variants (setGlobalTimeScale,startAllAnimations, …) that operate on everyGaussianModel.
Error handling
- Missing WebGPU support: callers should guard renderer creation (see
initThreeContext) and fall back to WebGL-only scenes. - Camera mismatches: keep all conversions in
CameraAdapter/DirectCameraAdapter—ad-hoc matrix tweaks quickly lead to mirrored outputs. - Device loss: call
disposeDepthResources()anddiagnoseDepth()after adevice.lostevent; the nextrenderThreeScene()invocation will recreate the targets.
Files worth reading
src/app/GaussianThreeJSRenderer.ts– orchestration layer, auto depth, overlays, runtime controls.src/three-integration/GaussianSplattingThreeWebGPU.ts– lightweight helper for direct integrations.src/camera/CameraAdapter.ts– reusable camera conversion logic.src/app/GaussianModel.ts– Object3D wrapper with auto-sync and animation helpers.src/renderer/gaussian_renderer.ts– shared compute/render pipelines.