Skip to content

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 GPURSSorter instance and cache PointCloudSortStuff per 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()
  • prepareMulti respects each PointCloud colour mode (SH or RGB) and optional ONNX countBuffer, writing splats into [baseOffset, baseOffset + numPoints) inside the global splat2D buffer.
  • After preprocessing, the single GPURSSorter performs an indirect radix sort using the counters populated by preprocessing.
  • renderMulti binds 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 shared splat2D buffer 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:

  • maxSHDegree clamps to both the renderer and the model.
  • kernelSize, gaussianScaling, mipSplatting, showEnvMap, walltime, sceneCenter, clippingBox, sceneExtend are pushed into the preprocessor uniform buffer each dispatch.

Depth rendering can be toggled at runtime:

renderer.setDepthEnabled(true);
renderer.setDepthFormat('depth24plus');

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 first n payload 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.numPoints exceeds globalCapacity, ensureGlobalCapacity reallocates buffers. Expect a short spike but no crash.
  • Mixed colour modes: Ensure PointCloud.colorMode is 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 prepareMulti before renderMulti; preprocessing populates the indirect buffer via encoder.copyBufferToBuffer.
  • 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.