How to Render Viewsheds for CCTV Cameras on a CesiumJS Globe
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 and 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. 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 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) 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 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:
- Compute geometry – Call
computeFrustumGeometry(record.camera, record.groundAltM)to derive the five frustum vertices. - Generate colors – Execute
cameraHue(record.camera.index)followed byviewshedColors(hue)to obtain fill and line colors for the current state. - Construct primitive – Pass the positions and fill color to
createFrustumVolumePrimitive()to build the translucent geometry. - Tag and insert – Set the
_gevViewshedproperty on the primitive and add it to the scene viaviewer.scene.primitives.add(). - Maintain reference – Store the primitive in
record.viewshedPrimitivefor future updates or destruction. - 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:
// 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:
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:
// 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. - 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 translucentCesium.Primitivevolumes with disabled back-face culling. - Lifecycle management through
rebuildViewshedVolume()anddestroyViewshedVolume()ensures efficient scene updates without memory leaks. - Mode switching between wireframe and solid viewshed uses the
coverageModeparameter ('on'vs'viewshed') insrc/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, which manages primitive creation and destruction accordingly.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →