# How to Render Viewsheds for CCTV Cameras on a CesiumJS Globe

> Learn how to render CCTV viewsheds on a CesiumJS globe using frustum geometry, golden angle algorithm, and dynamic primitive volumes. Achieve precise camera coverage visualization.

- Repository: [Bilawal Sidhu/gods-eye-view](https://github.com/bilawalsidhu/gods-eye-view)
- Tags: how-to-guide
- Published: 2026-09-04

---

**You render CCTV viewsheds on a CesiumJS globe by computing five-point frustum geometry from camera poses, generating deterministic hues using the golden angle algorithm, and instantiating translucent `Cesium.Primitive` volumes that dynamically rebuild when coverage mode changes.**

The **gods-eye-view** repository (`bilawalsidhu/gods-eye-view`) provides a complete implementation for visualizing three-dimensional camera coverage as colored frustum volumes. This approach transforms surveillance metadata into immediate spatial context on the CesiumJS globe, allowing operators to see exactly where each CCTV camera points and how far it sees.

## Architecture of the Viewshed System

The implementation splits responsibility across three logical layers in [`src/data/cctvViewshed.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/cctvViewshed.js) and [`src/data/cctv.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/cctv.js). This separation ensures that geometry calculations remain pure while scene management handles Cesium-specific lifecycle concerns.

### Colour Generation with Golden Angle Spacing

Every camera receives a deterministic hue through the `cameraHue(index)` function in [`src/data/cctvViewshed.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/cctvViewshed.js). The algorithm spaces hues using `GOLDEN_ANGLE_DEG` (approximately 137.5 degrees), guaranteeing that neighboring cameras in the id-sorted catalog receive perceptually distinct colors across sessions.

The `viewshedColors(hueDeg)` function converts this hue to HSL color space and derives two alpha levels: `FILL_ALPHA_IDLE` for inactive cameras and `FILL_ALPHA_ACTIVE` for selected or highlighted units. This ensures consistent visual semantics without randomization.

### Frustum Geometry Construction

The `computeFrustumGeometry()` function in [`src/data/cctv.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/cctv.js) calculates five Cartesian3 positions for each camera: the **mount point** (camera location) and four far-plane corners (**tl**, **tr**, **br**, **bl**). These vertices define the view frustum based on the camera's pose, ground altitude, and intrinsic parameters.

The `createFrustumVolumePrimitive(positions, color)` function (line 88 in [`src/data/cctvViewshed.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/cctvViewshed.js)) builds a `Cesium.Geometry` instance from these five vertices (forming six triangles) and wraps it in a `Cesium.Primitive` with back-face culling explicitly disabled (`cull: { enabled: false }`). This configuration allows the viewer to navigate inside the cone while maintaining visibility of the volume walls.

### Lifecycle and Scene Management

The `rebuildViewshedVolume(record, isActive)` and `destroyViewshedVolume()` functions in [`src/data/cctv.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/cctv.js) manage the primitive's existence in `viewer.scene.primitives`. Each primitive is tagged with `primitive._gevViewshed = record.camera.id` to enable test harness verification and counting.

Coverage mode switching occurs through the `_coverageMode` variable, which accepts `'off'`, `'on'` (wireframe), or `'viewshed'`. When `setParams({ coverageMode: 'viewshed' })` is called, the `refreshCoverage()` cycle iterates visible cameras and invokes `rebuildViewshedVolume()` accordingly.

## Step-by-Step Rendering Flow

Follow this sequence to render a viewshed volume:

1. **Compute geometry** – Call `computeFrustumGeometry(record.camera, record.groundAltM)` to derive the five frustum vertices.
2. **Generate colors** – Execute `cameraHue(record.camera.index)` followed by `viewshedColors(hue)` to obtain fill and line colors for the current state.
3. **Construct primitive** – Pass the positions and fill color to `createFrustumVolumePrimitive()` to build the translucent geometry.
4. **Tag and insert** – Set the `_gevViewshed` property on the primitive and add it to the scene via `viewer.scene.primitives.add()`.
5. **Maintain reference** – Store the primitive in `record.viewshedPrimitive` for future updates or destruction.
6. **React to changes** – When the camera pose updates, destroy the old primitive and rebuild using the same flow.

## Code Implementation Examples

### Enable Viewshed Mode Programmatically

Toggle the global coverage mode to render all visible camera viewsheds:

```javascript
// Assuming cctvLayer references the initialized CCTV data module
cctvLayer.setParams({ coverageMode: 'viewshed' });

```

### Manually Rebuild a Single Camera Viewshed

For custom integrations or unit testing, manually construct a viewshed volume:

```javascript
import { cameraHue, viewshedColors, createFrustumVolumePrimitive } from './data/cctvViewshed.js';
import { computeFrustumGeometry } from './data/cctv.js';

function rebuildOneViewshed(record, viewer) {
  // 1. Generate stable hue based on camera catalog index
  const hue = cameraHue(record.camera.index);
  
  // 2. Derive idle and active color states
  const colors = viewshedColors(hue);
  record.viewshedColors = colors;

  // 3. Compute frustum geometry (mount + four corners)
  const frustum = computeFrustumGeometry(record.camera, record.groundAltM);
  const positions = {
    mount: Cesium.Cartesian3.fromDegrees(record.camera.lon, record.camera.lat, frustum.mount.alt),
    tl: Cesium.Cartesian3.fromDegrees(frustum.corners.tl.lon, frustum.corners.tl.lat, frustum.corners.tl.alt),
    tr: Cesium.Cartesian3.fromDegrees(frustum.corners.tr.lon, frustum.corners.tr.lat, frustum.corners.tr.alt),
    br: Cesium.Cartesian3.fromDegrees(frustum.corners.br.lon, frustum.corners.br.lat, frustum.corners.br.alt),
    bl: Cesium.Cartesian3.fromDegrees(frustum.corners.bl.lon, frustum.corners.bl.lat, frustum.corners.bl.alt),
  };

  // 4. Build primitive with translucent fill
  const primitive = createFrustumVolumePrimitive(positions, colors.fill);
  primitive._gevViewshed = record.camera.id; // Tag for test harness
  
  // 5. Add to Cesium scene
  viewer.scene.primitives.add(primitive);
  record.viewshedPrimitive = primitive;
}

```

### UI Control Integration

Wire a tri-state button to cycle through coverage modes:

```javascript
// In src/ui.js or your interface controller
let currentMode = 'off';

document.getElementById('coverageBtn').addEventListener('click', () => {
  const next = currentMode === 'off' ? 'on' 
             : currentMode === 'on' ? 'viewshed' 
             : 'off';
  
  cctvLayer.setParams({ coverageMode: next });
  currentMode = next;
});

```

## Key Robustness Features

The production implementation in `bilawalsidhu/gods-eye-view` maintains performance and correctness through several architectural decisions:

- **Pure mathematics** – All vertex positions derive from camera pose parameters without querying the Cesium scene, preserving the zero-steady-state-work invariant documented at line 90 of [`src/data/cctvViewshed.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/cctvViewshed.js).
- **Deterministic colour mapping** – The golden-angle hue calculation ensures that camera colors remain identical across browser sessions and application restarts, preventing confusion in multi-operator environments.
- **Efficient resource reuse** – Volumes rebuild only when wireframe geometry changes (pose edits, coverage mode switches, or visibility changes). No per-frame allocation occurs during static observation.
- **Interior visibility** – Disabling back-face culling allows the camera volume to remain visible when the viewer navigates inside the frustum, satisfying the UI requirement for immersive inspection of coverage boundaries.

## Summary

- **Viewshed rendering** requires computing five-point frustum geometry from CCTV pose data and ground altitude.
- **Colour consistency** relies on the golden angle algorithm in `cameraHue()` to generate distinct, session-stable hues for each camera.
- **Primitive construction** occurs in `createFrustumVolumePrimitive()`, which builds translucent `Cesium.Primitive` volumes with disabled back-face culling.
- **Lifecycle management** through `rebuildViewshedVolume()` and `destroyViewshedVolume()` ensures efficient scene updates without memory leaks.
- **Mode switching** between wireframe and solid viewshed uses the `coverageMode` parameter (`'on'` vs `'viewshed'`) in [`src/data/cctv.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/cctv.js).

## Frequently Asked Questions

### What defines a viewshed in CesiumJS surveillance mapping?

A viewshed is a three-dimensional, translucent frustum volume representing the field-of-view coverage of a CCTV camera. In the `gods-eye-view` implementation, it appears as a colored geometric primitive anchored to the globe surface, extending from the camera mount point to the far-plane corners calculated from focal length and sensor parameters.

### How does the golden angle algorithm prevent color collisions between cameras?

The `cameraHue()` function multiplies the camera's catalog index by `GOLDEN_ANGLE_DEG` (approximately 137.5 degrees) modulo 360. This irrational angle distribution ensures that even with thousands of cameras, adjacent indices receive maximally separated hues on the color wheel, eliminating the need for a centralized color registry or randomization that could cause collisions.

### Why is back-face culling disabled for viewshed volumes?

Back-face culling is disabled (`cull: { enabled: false }` in the primitive constructor) to ensure the frustum remains visible when the user navigates inside the cone. Without this setting, Cesium would hide the interior walls of the viewshed when the camera enters the volume, breaking the immersive inspection experience required for verifying coverage overlap.

### How do I switch between wireframe cones and solid viewshed volumes?

The system exposes a tri-state coverage mode controlled by `setParams({ coverageMode: mode })`. Set `mode` to `'on'` for wireframe cones, `'viewshed'` for translucent solid volumes, or `'off'` to hide coverage indicators entirely. This state change triggers `refreshCoverage()` in [`src/data/cctv.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/cctv.js), which manages primitive creation and destruction accordingly.