# How to Keep Orbit Rings Aligned with GMST in CesiumJS

> Learn to align orbit rings with GMST in CesiumJS by baking geometry and applying a Z-axis rotation matrix each frame. Keep your visualizations accurate and synchronized with Earth's rotation.

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

---

**To keep orbit rings aligned with GMST in CesiumJS, bake the polyline geometry once at a specific Greenwich Mean Sidereal Time (GMST), then apply a continuous Z‑axis rotation matrix each frame that compensates for the Earth's rotation using the delta between the baked GMST and the current GMST.**

In the `bilawalsidhu/gods-eye-view` repository, satellite orbit rings are rendered as static `Cesium.Primitive` objects to maximize rendering performance. Because the Earth rotates underneath the orbits, rings baked at a specific GMST will drift out of alignment with live satellite positions unless continuously re‑oriented via the primitive’s **model matrix**.

## The GMST Alignment Challenge

When you generate a polyline representing a satellite's orbit path, the coordinates are computed in the Earth‑Centered Earth‑Fixed (ECEF) frame at a specific moment in time. As the Earth rotates, the ECEF frame rotates with it, but your baked vertices remain fixed in world space. This causes the visual ring to appear to drift away from the satellite's actual ground track. The solution is to treat the geometry as invariant while rotating the coordinate frame via `modelMatrix` updates.

## Baking and Rotating Orbit Geometry

### Computing the Rotation Matrix

In [`src/data/satellites.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/satellites.js), the `orbitFrameModelMatrix` function calculates the transformation required to align the baked geometry with the current Earth rotation. It computes the difference between the GMST at bake‑time and the current GMST, then constructs a rotation matrix around the Z‑axis.

```javascript
export function orbitFrameModelMatrix(gmstAtBake, nowDate, result = new Cesium.Matrix4()) {
  const deltaGmst = gstime(nowDate) - gmstAtBake;
  const rotation = Cesium.Matrix3.fromRotationZ(-deltaGmst, _scratchRingRotation);
  return Cesium.Matrix4.fromRotationTranslation(rotation, Cesium.Cartesian3.ZERO, result);
}

```

*Source: [`src/data/satellites.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/satellites.js) (lines 441‑453)*

### Storing the Baked Reference Frame

When the user first toggles an orbit ring visible, the system captures the current GMST and stores it alongside the primitive in a `Map`:

```javascript
const bakeDate = new Date();
const basePositions = computeOrbitPath(sat.satrec, bakeDate);
// ... primitive creation code ...
_orbitPaths.set(noradId, {
  primitive,
  gmstAtBake: gstime(bakeDate),
});

```

*Source: [`src/data/satellites.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/satellites.js) (lines 663‑695)*

## The Pre‑Render Update Loop

To maintain alignment without rebuilding GPU buffers, the system updates the `modelMatrix` property of each primitive during Cesium’s `preRender` event.

### Throttled Updates with RING_ROTATION_MS

The update logic checks the elapsed time against `RING_ROTATION_MS` (set to **1000 ms**) to throttle calculations and prevent excessive matrix math:

```javascript
if (_params.showOrbits && now - _lastRingRotation >= RING_ROTATION_MS) {
  _updateOrbitPathRotations(new Date(now));
  _lastRingRotation = now;
}

```

*Source: [`src/data/satellites.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/satellites.js) (lines 250‑256)*

### Iterating Stored Primitives

The helper function `_updateOrbitPathRotations` iterates all active rings and applies the rotation matrix in‑place:

```javascript
function _updateOrbitPathRotations(nowDate) {
  for (const path of _orbitPaths.values()) {
    if (!path.primitive) continue;
    orbitFrameModelMatrix(path.gmstAtBake, nowDate, path.primitive.modelMatrix);
  }
}

```

*Source: [`src/data/satellites.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/satellites.js) (lines 624‑631)*

## Complete Implementation Example

Here is a standalone pattern for implementing GMST‑aligned orbit rings in your own CesiumJS application:

```javascript
import * as Cesium from 'cesium';
import { orbitFrameModelMatrix, gstime } from './data/satellites.js';

// 1. Bake the ring geometry at the current GMST.
const bakeDate = new Date();
const positions = computeOrbitPath(satrec, bakeDate);

const primitive = new Cesium.Primitive({
  geometryInstances: new Cesium.GeometryInstance({
    geometry: new Cesium.PolylineGeometry({
      positions: positions,
      width: 2,
      vertexFormat: Cesium.PolylineColorAppearance.VERTEX_FORMAT
    }),
    attributes: {
      color: Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.CYAN)
    }
  }),
  appearance: new Cesium.PolylineColorAppearance({ translucent: true }),
  asynchronous: false
});

viewer.scene.primitives.add(primitive);

// 2. Store the GMST used at bake-time.
const gmstAtBake = gstime(bakeDate);

// 3. Update rotation in the pre-render tick.
viewer.scene.preRender.addEventListener(() => {
  const now = new Date();
  const model = orbitFrameModelMatrix(gmstAtBake, now);
  primitive.modelMatrix = model; // Zero-cost GPU update
});

```

## Summary

- **Bake once**: Generate `PolylineGeometry` at a specific GMST and store that reference time alongside the primitive.
- **Rotate continuously**: Calculate `-deltaGMST` around the Z‑axis to align the baked ECEF frame with the current ECEF frame.
- **Update efficiently**: Throttle matrix updates to approximately every **1000 ms** using a `preRender` listener to balance accuracy and performance.
- **Modify modelMatrix**: Assign the result to the primitive’s `modelMatrix` property rather than reconstructing geometry, keeping rendering flicker‑free.

## Frequently Asked Questions

### What is GMST and why does it affect orbit rings?

Greenwich Mean Sidereal Time (GMST) measures the rotation of the Earth relative to the fixed stars. Because satellite orbits are defined in inertial space but displayed in the rotating Earth‑Centered Earth‑Fixed (ECEF) frame, the ground track appears to move westward as the Earth turns. Without GMST alignment, a static ring baked at one moment will drift away from the satellite’s current position over time.

### Why not rebuild the geometry every frame?

Reconstructing `PolylineGeometry` every frame creates garbage‑collection pressure and forces continuous GPU buffer uploads. By baking the geometry once and applying a `modelMatrix` rotation, the GPU reuses the same vertex buffer while only updating the transformation matrix uniform, resulting in high‑performance rendering suitable for visualizing thousands of simultaneous satellite tracks.

### How often should the modelMatrix update?

According to the `gods-eye-view` source code, updating every **1000 milliseconds** (controlled by `RING_ROTATION_MS`) provides sufficient visual accuracy for orbital visualization while minimizing CPU overhead. For applications requiring sub‑meter precision, you may increase the frequency, but once per second is adequate for most satellite tracking use cases.

### Can this technique work with other CesiumJS geometry types?

Yes. This approach works with any `Cesium.Primitive` that accepts a `modelMatrix`, including `PolygonGeometry`, `PolylineVolumeGeometry`, and custom `GeometryInstance` objects. The requirement is that the geometry must be defined in the ECEF frame at bake‑time, allowing the Z‑axis rotation to correctly align it with the current Earth rotation regardless of the specific primitive type.