# How Camera Poses and Gizmos Are Handled and Rendered in Supersplat

> Discover how Supersplat handles camera poses and gizmos. Learn about CameraAnimTrack, CameraPoseGizmos, and synchronized rendering via an event bus for efficient visualization.

- Repository: [PlayCanvas/supersplat](https://github.com/playcanvas/supersplat)
- Tags: deep-dive
- Published: 2026-05-10

---

**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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
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`](https://github.com/playcanvas/supersplat/blob/main/src/camera-poses.ts), while `CameraPoseGizmos` handles visualization in [`src/camera-pose-gizmos.ts`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/src/shaders/debug-shader.ts) or extend `CameraPoseGizmos` to override mesh generation. Visibility toggling is available at runtime via the `camera.showPoses` event flag.