How Camera Poses and Gizmos Are Handled and Rendered in Supersplat

Supersplat stores camera animation data as poses managed by CameraAnimTrack and renders debug visualizations via CameraPoseGizmos, using a central event bus to maintain synchronization without direct coupling between the systems.

In the playcanvas/supersplat repository, a WebGL-based Gaussian splat editor, camera animation workflows rely on a robust pose management system. The implementation splits concerns between data storage and visualization, enabling users to create camera paths while viewing positional gizmos directly in the 3D viewport. This architecture ensures that pose editing, interpolation, and visual debugging operate independently while remaining deterministically synchronized.

Pose Management Architecture

The pose management system centers on the CameraAnimTrack class defined in src/camera-poses.ts. This component maintains an ordered array of poses and handles interpolation between keyframes.

Data Structure and Event Subscription

Each pose contains name, frame, position (Vec3), target (Vec3), and optional fov values stored in an internal poses: Pose[] array. The constructor registers listeners for timeline events including timeline.time, timeline.frame, timeline.frames, timeline.smoothness, and scene.clear (lines 30‑48).

Key Operations and Spline Interpolation

The class exposes methods like addKey, removeKey, moveKey, and copyKey for manipulating the animation track. When poses change, rebuildSpline() constructs a looping CubicSpline from position, target, and FOV values (lines 28‑45). During playback, evaluating the spline at a specific frame fires the camera.setPose event (lines 33‑39), updating the scene camera without requiring direct references to rendering components.

Serialization and Public API

Pose data persists through docSerialize.poseSets and docDeserialize.poseSets (lines 74‑97), enabling project save/load functionality. The track registers its public API via registerCameraPosesEvents, making it accessible through events.function('camera.animTrack') and legacy helpers like camera.poses.

Gizmo Rendering System

Visual feedback for camera positions is handled by CameraPoseGizmos in src/camera-pose-gizmos.ts, which constructs debug geometry representing each pose's frustum.

Debug Element Implementation

The gizmo extends the base Element class with type debug (lines 28‑35) and utilizes a custom ShaderMaterial built from debug-shader GLSL sources (lines 42‑48). It generates a line-mesh (PRIMITIVE_LINES) where each pose requires VERTS_PER_CAMERA = 20 vertices to represent the camera frustum icon.

Mesh Rebuilding and Dirty Flags

Performance optimization relies on a dirty-flag pattern. The gizmo sets dirty = true when receiving pose-related events like track.keyAdded, track.keyRemoved, or track.keysLoaded, and when scene bounds change (lines 71‑86). During onPreRender, the mesh rebuilds only when dirty by calling rebuildMesh() (lines 98‑101).

Frustum Geometry Generation

The rebuildMesh() method queries current poses via scene.events.invoke('camera.poses') and computes forward, right, and up vectors for each position. It writes line segments forming a pyramid-shaped frustum with an up-indicator, applying cyan vertex colors (0, 255, 255, 255) (lines 23‑64).

Visibility Control

Gizmo visibility depends on two conditions checked in onPreRender: the camera.showPoses event flag and scene.camera.renderOverlays (line 94). Users toggle visibility by firing events.fire('camera.showPoses', true).

Event-Driven Integration Workflow

The architecture decouples pose editing from rendering through the central event bus (src/events.ts):

  1. Pose Mutation: When CameraAnimTrack.addKey(frame) executes, it stores the current camera state, rebuilds the spline, and fires track.keyAdded.
  2. Gizmo Update: CameraPoseGizmos listeners set dirty = true in response to track events.
  3. Render Loop: Each frame, onPreRender checks the dirty flag and regenerates the line mesh from current pose data.
  4. Camera Playback: Timeline updates trigger spline evaluation, firing camera.setPose to update the scene camera while gizmos render as overlays.

Working with Camera Poses: Code Examples

Adding a Camera Pose Programmatically

// Access the animation track through the event system
const animTrack = events.invoke('camera.animTrack') as CameraAnimTrack;

// Capture current camera state at frame 120
animTrack.addKey(120);

This captures the current position, target, and fov, rebuilds the interpolation spline, and triggers track.keyAdded (lines 58‑81).

Toggling Gizmo Visibility

// Enable pose visualization
events.fire('camera.showPoses', true);

// Hide gizmos
events.fire('camera.showPoses', false);

The gizmo system queries this state via scene.events.invoke('camera.showPoses') during pre-render (line 94).

Accessing Pose Data for Serialization

// Export pose sets for project saving
const poseSets = events.invoke('docSerialize.poseSets');
console.log(JSON.stringify(poseSets, null, 2));

This invokes the serialization handlers defined in registerCameraPosesEvents (lines 74‑94).

Retrieving the Gizmo Entity

const gizmo = scene.elements.find(e => e instanceof CameraPoseGizmos) as CameraPoseGizmos;
console.log(gizmo.entity.getWorldPosition());

The entity attaches to scene.app.root during initialization (line 69).

Summary

  • Separation of Concerns: CameraAnimTrack manages data and interpolation in src/camera-poses.ts, while CameraPoseGizmos handles visualization in src/camera-pose-gizmos.ts.
  • Event-Driven Architecture: Both systems communicate exclusively through the central event bus, using events like camera.setPose, track.keyAdded, and camera.showPoses.
  • Spline Interpolation: Poses interpolate using a looping CubicSpline rebuilt automatically when keys are added, removed, or modified.
  • Performance Optimization: Gizmos use dirty-flag pattern and line-mesh generation (PRIMITIVE_LINES) with 20 vertices per camera, only rebuilding when pose data changes.
  • Serialization: Complete pose sets persist through docSerialize.poseSets and docDeserialize.poseSets for project portability.

Frequently Asked Questions

How do I add a camera pose programmatically in Supersplat?

Invoke events.invoke('camera.animTrack') to retrieve the CameraAnimTrack instance, then call addKey(frame) where frame is the timeline position. This captures the current camera's position, target, and FOV, rebuilds the internal cubic spline, and fires the track.keyAdded event to update gizmos automatically.

What file handles the visualization of camera poses?

The src/camera-pose-gizmos.ts file contains the CameraPoseGizmos class, which extends the base Element type to render debug frustum icons. It generates line-mesh geometry using PRIMITIVE_LINES with cyan-colored vertices and updates automatically through event listeners for track.keyAdded, track.keyRemoved, and other pose-related events.

How does Supersplat interpolate between camera poses?

The CameraAnimTrack.rebuildSpline() method constructs a looping CubicSpline from all stored pose positions, targets, and FOV values. When the timeline advances, the spline evaluates the interpolated state for the current frame and fires camera.setPose, enabling smooth camera animations through complex paths without requiring manual tweening.

Can I customize the appearance of camera pose gizmos?

While the vertex color is hardcoded to cyan (0, 255, 255, 255) in rebuildMesh() (lines 66‑73), you can modify the debug-shader material defined in src/shaders/debug-shader.ts or extend CameraPoseGizmos to override mesh generation. Visibility toggling is available at runtime via the camera.showPoses event flag.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →