Renderer Module
src/renderer/ hosts the WebGPU renderer that consumes preprocessed splats, runs the GPU radix sorter, and issues a single indirect draw for every visible model. The module exposes GaussianRenderer, a high-level orchestrator that wires together preprocessors, sorter, global buffers, and render pipelines.
Responsibilities
- Create static GPU resources (pipeline layout, render pipelines, indirect draw buffer) once during
initialize(). - Maintain dual preprocessors (SH + raw RGB) so models with different colour encodings can share the same renderer.
- Manage a single
GPURSSorterinstance and cachePointCloudSortStuffper point cloud/point count. - Provide global buffers (
splat2D, sorter payload) that grow on demand and allow multi-model batching. - Drive preprocessing, sorting, and rendering via
prepareMulti/renderMulti, emitting a single indirect draw regardless of how many models are visible. - Offer optional depth-enabled rendering, statistics, and debug helpers (
readInstanceCountDebug,readPayloadSampleDebug, ONNX count tracing).
Key APIs
import { GaussianRenderer, DEFAULT_KERNEL_SIZE } from 'src/renderer';
import type { RenderArgs, RendererConfig, RenderStats } from 'src/renderer';
const renderer = new GaussianRenderer({
device,
format: navigator.gpu.getPreferredCanvasFormat(),
shDegree: 3,
debug: true,
});
await renderer.initialize();
renderer.prepareMulti(encoder, device.queue, pointClouds, {
camera,
viewport: [canvas.width, canvas.height],
maxSHDegree: 3,
kernelSize: DEFAULT_KERNEL_SIZE,
});
const pass = encoder.beginRenderPass(passDesc);
renderer.renderMulti(pass, pointClouds);
pass.end();
Refer to the API reference for the full surface area.
Pipeline Flow
initialize() -> create pipelines, preprocessors, sorter, indirect buffer
prepareMulti() -> ensure capacity -> reset sorter state -> per-model dispatchModel -> sorter.recordSortIndirect -> patch drawIndirect
renderMulti() -> bind global splat + sorter BGs -> drawIndirect()
prepareMultirespects eachPointCloudcolour mode (SH or RGB) and optional ONNXcountBuffer, writing splats into[baseOffset, baseOffset + numPoints)inside the globalsplat2Dbuffer.- After preprocessing, the single
GPURSSorterperforms an indirect radix sort using the counters populated by preprocessing. renderMultibinds the global bind groups and issues one indirect draw. For per-model rendering,render()still works using cached per-cloud sort resources.
Components
| Component | Description |
|---|---|
GaussianRenderer |
Public renderer implementation. Accepts either legacy (device, format, shDeg) parameters or a RendererConfig object. |
RenderArgs |
Camera + viewport + render settings passed to prepareMulti. |
RendererConfig |
Initialization options: device, format, shDegree, optional compressed, debug. |
RenderStats |
Lightweight stats (gaussianCount, visibleSplats, memoryUsage). |
DEFAULT_KERNEL_SIZE |
Shared default for kernel radius when models do not specify one. |
Resource Management
- Static resources: render pipelines (with and without depth), pipeline layout, indirect draw buffer, preprocessors, sorter.
- Per-point-cloud caches:
WeakMap<PointCloud, PointCloudSortStuff>so sorting resources rebuild only when the point count changes. - Global buffers:
ensureGlobalCapacity()allocates a sharedsplat2Dbuffer and sorter payload sized to the sum of all models. Capacity increases with a 1.25× growth factor to amortise reallocations.
Rendering Settings
buildRenderSettings() merges per-cloud metadata (bbox/center/mipSplatting) with RenderArgs:
maxSHDegreeclamps to both the renderer and the model.kernelSize,gaussianScaling,mipSplatting,showEnvMap,walltime,sceneCenter,clippingBox,sceneExtendare pushed into the preprocessor uniform buffer each dispatch.
Depth rendering can be toggled at runtime:
This recreates the depth-aware pipeline while leaving the non-depth pipeline untouched.
Integration Points
| Module | Interaction |
|---|---|
Point Cloud (src/point_cloud) |
Supplies bind-group layouts, splat buffers, transforms, and per-model params. The renderer binds pointCloud.renderBindGroup() at @group(0). |
Preprocess (src/preprocess) |
Two GaussianPreprocessor instances drive dispatchModel per point cloud. They write into the global splat2D buffer and sorter counters. |
Sorting (src/sort) |
A single GPURSSorter provides both preprocess and render bind-group layouts plus the radix pipelines. Sorting output controls drawIndirect. |
Shaders (src/shaders/gaussian.wgsl) |
Vertex shader reads all attributes from storage buffers; fragment shader implements Gaussian splatting with premultiplied alpha. |
Debug & Stats
renderer.getRenderStats(pointCloud)returns counts and a coarse memory estimate.readInstanceCountDebug()copies the current indirect draw instance count to the CPU for inspection.readPayloadSampleDebug(n)dumps the firstnpayload indices from the global sorter buffer.debugONNXCount()chains into the preprocessor debug flow to verify ONNX-driven counts.
Common Issues & Tips
- Capacity exceeded: If the sum of
pointCloud.numPointsexceedsglobalCapacity,ensureGlobalCapacityreallocates buffers. Expect a short spike but no crash. - Mixed colour modes: Ensure
PointCloud.colorModeis set correctly so the renderer picks the right preprocessor (SH vs RGB). - Depth artifacts: Enable the depth pipeline (
setDepthEnabled(true)) or disable it if splats must always render back-to-front purely via sorting. - Indirect draw stuck at zero: Call
prepareMultibeforerenderMulti; preprocessing populates the indirect buffer viaencoder.copyBufferToBuffer.
Related Docs
- Architecture – Render pipeline design, global buffer management, and multi-model batching architecture.
- API Reference – Renderer configuration structs, lifecycle methods, and debug helpers.
- Preprocess Module – Source of projected splats and counter buffers.
- Sorting Module – Explains how indirect draw payloads are ordered before rendering.