# How to Implement World-Stable Heading Projection for Icons in CesiumJS

> Learn how to implement world-stable heading projection for icons in CesiumJS. This guide explains ENU forward vector projection and dead-band filtering for smoother results.

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

---

**The `gods-eye-view` repository solves world-stable heading projection by computing a screen-space rotation from an entity’s ENU forward vector projected onto the camera’s basis, then smoothing the result with a dead-band filter.**

Maintaining a consistent icon heading in CesiumJS regardless of camera angle requires **world-stable heading projection** that decouples the billboard’s orientation from the current view. In the `bilawalsidhu/gods-eye-view` project, this behavior is implemented in [`src/data/iconOrientation.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/iconOrientation.js) through a set of geometric helpers that transform a world-space course vector into a stable screen rotation. These utilities ensure that icons always point along their true ground track, even when the underlying forward probe falls behind the camera or drifts off-screen.

## Core Helpers in [`src/data/iconOrientation.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/iconOrientation.js)

Three inter-related functions in [`src/data/iconOrientation.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/iconOrientation.js) form the backbone of the projection pipeline.

### `screenProjectedRotation(scene, position, courseDeg, previous?)`

This function computes a rotation in radians that aligns an icon with its real-world heading. It first builds a forward probe vector of magnitude `FORWARD_PROBE_M` (2000 m) in the entity’s local **ENU** frame using `Cesium.Transforms.eastNorthUpToFixedFrame`. After transforming this vector into world space, it projects the result onto **`camera.rightWC`** and **`camera.upWC`** to obtain screen-space components `dx` and `dy`. The returned value is `atan2(-dx, -dy)`, which orients the icon so its up direction matches the projected course.

When the probe is off-screen, behind the camera, or yields very small screen components, the helper falls back to the supplied `previous` rotation to avoid sudden flips.

### `stabilizeScreenRotation(previous, next, deadbandRad?)`

To suppress sub-degree jitter caused by floating-point noise, this helper calculates the shortest wrapped angular difference using `atan2(sinΔ, cosΔ)`. If the absolute difference falls below the dead-band threshold—defaulting to approximately `0.5°` represented by `ROTATION_DEADBAND_RAD`—the previous rotation is kept. Otherwise, the new rotation is accepted, providing visually stable output across frames.

### `cameraPoseSignature(camera)`

This utility generates an inexpensive string signature by rounding the camera position to roughly 10 m and its angles to about `0.06°`. The rendering loop compares this signature frame-to-frame to skip expensive recalculations whenever the camera has not moved appreciably.

## How World-Stable Heading Projection Works

The projection achieves its stability through five architectural decisions implemented in [`src/data/iconOrientation.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/iconOrientation.js).

- **Fixed-magnitude world probe.** By constructing the forward vector in the ENU frame with a constant length of `FORWARD_PROBE_M = 2000 m`, the pipeline guarantees a camera-independent magnitude before any screen-space math occurs.

- **Camera-basis projection.** Rather than relying on `worldToWindowCoordinates`—which fails when the probe point lies behind the camera—the algorithm dots the world forward vector with `camera.rightWC` and `camera.upWC`. This yields a valid 2-D screen direction for extreme viewing angles and tracked-entity orbits.

- **Orthographic approximation.** The projection is exact at the viewport center and falls back to an orthographic approximation elsewhere. This ensures continuous rotation when the forward probe goes off-screen, eliminating sudden jumps.

- **Noise guardrails.** Two thresholds protect against unstable results: `MIN_SCREEN_COMPONENT_M` discards projections with negligible screen components, while `ROTATION_DEADBAND_RAD` filters out sub-degree rotational noise.

- **Horizon utilities.** Although separate from heading logic, the same file exports `horizonOccluder` and `skyBackdropFactor` for labels positioned near the horizon, keeping related geospatial visual effects in one module.

## Step-by-Step Implementation

Follow these steps to integrate the orientation pipeline into a CesiumJS application.

1. Import `screenProjectedRotation`, `stabilizeScreenRotation`, and `cameraPoseSignature` from [`src/data/iconOrientation.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/iconOrientation.js).

2. Inside the per-frame update, call `screenProjectedRotation` with the `scene`, the entity’s world `position`, its heading in degrees clockwise from north (`courseDeg`), and the previous rotation value.

3. Pass the raw rotation into `stabilizeScreenRotation` to suppress jitter before assigning it to the billboard.

4. Cache calculations by comparing the output of `cameraPoseSignature(scene.camera)` to the previous frame’s signature; skip recomputation when they match.

## Complete Working Example

The snippet below demonstrates a minimal integration that updates a billboard each frame using the helpers from [`src/data/iconOrientation.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/iconOrientation.js):

```js
import * as Cesium from 'cesium';
import {
  screenProjectedRotation,
  stabilizeScreenRotation,
  cameraPoseSignature,
} from './data/iconOrientation.js';

// Assume `viewer` is an existing Cesium.Viewer
const scene = viewer.scene;
let lastCamSig = '';
let prevRot = null;

// Create a test billboard
const billboard = scene.primitives.add(
  new Cesium.BillboardCollection().add({
    image: 'plane.png',
    position: Cesium.Cartesian3.fromDegrees(-122.4, 37.8, 1000),
    // rotation will be set each frame
  })
);

// Entity’s course (e.g., 45° clockwise from north)
const courseDeg = 45;

scene.preRender.addEventListener(() => {
  const camSig = cameraPoseSignature(scene.camera);
  if (camSig !== lastCamSig) {
    const pos = billboard.position;
    const rawRot = screenProjectedRotation(scene, pos, courseDeg, prevRot);
    prevRot = stabilizeScreenRotation(prevRot, rawRot);
    billboard.rotation = prevRot;
    lastCamSig = camSig;
  }
});

```

## Real-World Usage in the Repository

The pipeline is consumed across several data modules in `bilawalsidhu/gods-eye-view`:

- **[`src/data/aircraftIcons.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/aircraftIcons.js)** – Applies the orientation helpers to aircraft billboards.
- **[`src/data/flights.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/flights.js)** – Uses the rotation pipeline for commercial flight icons.
- **[`src/data/aisLiveVessles.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/aisLiveVessles.js)** – Adapts the utilities for AIS vessel markers.
- **[`src/camera.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/camera.js)** – Provides the camera basis vectors (`rightWC`, `upWC`) that `screenProjectedRotation` relies on.

## Summary

- The world-stable heading projection lives in [`src/data/iconOrientation.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/iconOrientation.js) and centers on three helpers: `screenProjectedRotation`, `stabilizeScreenRotation`, and `cameraPoseSignature`.
- A fixed 2000 m ENU forward probe is projected onto `camera.rightWC` and `camera.upWC` to produce a screen-space angle that remains valid behind the camera.
- Sub-degree jitter is eliminated by `stabilizeScreenRotation` via a default dead-band of roughly `0.5°`.
- Camera-pose caching through `cameraPoseSignature` avoids redundant math when the view is static.
- The same file also supplies horizon and sky-backdrop utilities for advanced labeling effects.

## Frequently Asked Questions

### Why not use `worldToWindowCoordinates` for heading projection?

The `worldToWindowCoordinates` approach fails when the forward probe point lies behind the camera, which is a common failure mode during tracked-entity orbits. The `gods-eye-view` implementation instead projects the world vector onto `camera.rightWC` and `camera.upWC`, producing a valid screen-space direction regardless of whether the probe is in front of or behind the camera.

### How does the system prevent icon jitter from floating-point noise?

The `stabilizeScreenRotation` helper computes the shortest wrapped angular difference with `atan2(sinΔ, cosΔ)`. If the change is smaller than the default dead-band of about `0.5°`, it retains the previous rotation, suppressing sub-degree jitter.

### What is the purpose of `FORWARD_PROBE_M = 2000`?

This constant defines the length of the forward vector built in the entity’s ENU frame before transformation to world space. A fixed, camera-independent magnitude ensures that the projected screen components remain stable and do not vary with distance or view angle.

### When should `cameraPoseSignature` be checked?

Check the signature at the start of your render loop. If it matches the previous frame’s signature, you can skip recomputing rotations for all icons because the camera pose has not changed appreciably.