How to Keep Orbit Rings Aligned with GMST in CesiumJS
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, 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.
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 (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:
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 (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:
if (_params.showOrbits && now - _lastRingRotation >= RING_ROTATION_MS) {
_updateOrbitPathRotations(new Date(now));
_lastRingRotation = now;
}
Source: 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:
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 (lines 624‑631)
Complete Implementation Example
Here is a standalone pattern for implementing GMST‑aligned orbit rings in your own CesiumJS application:
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
PolylineGeometryat a specific GMST and store that reference time alongside the primitive. - Rotate continuously: Calculate
-deltaGMSTaround 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
preRenderlistener to balance accuracy and performance. - Modify modelMatrix: Assign the result to the primitive’s
modelMatrixproperty 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.
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 →