cesiumjs-spatial-math
CesiumJS spatial math - Cartesian3, Cartographic, Matrix4, Quaternion, Transforms, Ellipsoid, BoundingSphere, projections, coordinate conversions. Use when converting between coordinate systems, computing positions on the ellipsoid, performing spatial intersection tests, building model matrices, or working with geographic projections.
How do I install this agent skill?
npx skills add https://github.com/cesiumgs/cesiumjs-skills --skill cesiumjs-spatial-mathIs this agent skill safe to install?
- Gen Agent Trust Hubpass
This skill provides comprehensive documentation and code examples for spatial math and coordinate transforms using the CesiumJS library. No security risks were detected.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
CesiumJS Spatial Math & Transforms
Version baseline: CesiumJS v1.144 (2026-08-01)
Mathematical foundation for every CesiumJS application: coordinate types, unit conversions, ellipsoid geometry, reference frame transforms, bounding volumes, intersection tests, and projections.
Core Concepts
CesiumJS uses a right-handed Earth-Centered Earth-Fixed (ECEF) coordinate system:
- Cartesian3 -- ECEF (x, y, z) in meters. Internal representation for all 3D positions.
- Cartographic -- (longitude, latitude, height). Angles are radians, height in meters above ellipsoid.
All angular values in core math are radians. Use Math.toRadians() / Math.toDegrees(). Math types use a static-method-with-result pattern: pass a result parameter to reuse allocations.
Cartesian3 -- Positions and Vectors
import { Cartesian3, Math as CesiumMath } from "cesium";
// From lon/lat degrees -- most common entry point
const pos = Cartesian3.fromDegrees(-105.0, 40.0);
const elevated = Cartesian3.fromDegrees(-105.0, 40.0, 1500.0); // with height
// Batch creation: [lon, lat, lon, lat, ...]
const ring = Cartesian3.fromDegreesArray([-105, 40, -100, 40, -100, 35]);
// With heights: [lon, lat, h, lon, lat, h, ...]
const wall = Cartesian3.fromDegreesArrayHeights([-105, 40, 500, -100, 40, 1000]);
// From raw ECEF or from radians
const raw = new Cartesian3(-1275096.0, -4797180.0, 4075270.0);
const fromRad = Cartesian3.fromRadians(-1.8326, 0.6981, 1500.0);
// Constants
Cartesian3.ZERO; // (0,0,0)
Cartesian3.UNIT_X; // (1,0,0)
Cartesian3.UNIT_Y; // (0,1,0)
Cartesian3.UNIT_Z; // (0,0,1)
Breaking change (1.139, #8359):
Cartesian2,Cartesian3, andCartesian4are now ES6 classes. Callingnewon a static factory method now throws --new Cartesian3.fromArray([...])andnew Cartesian3.fromDegrees(...)are errors. Dropnewfor factory methods (Cartesian3.fromArray([...])); keep it only for the real constructor (new Cartesian3(x, y, z)). More classes are migrating to ES6 classes, so apply this rule everywhere.
Vector Operations
const a = new Cartesian3(1.0, 2.0, 3.0);
const b = new Cartesian3(4.0, 5.0, 6.0);
const r = new Cartesian3(); // reusable scratch
Cartesian3.add(a, b, r); // a + b
Cartesian3.subtract(a, b, r); // a - b
Cartesian3.multiplyByScalar(a, 2.0, r); // a * 2
Cartesian3.negate(a, r); // -a
Cartesian3.cross(a, b, r); // cross product
Cartesian3.normalize(a, r); // unit vector
Cartesian3.lerp(a, b, 0.5, r); // linear interpolation
Cartesian3.midpoint(a, b, r); // midpoint
const dot = Cartesian3.dot(a, b); // dot product
const len = Cartesian3.magnitude(a); // ||a||
const dist = Cartesian3.distance(a, b); // Euclidean distance
const distSq = Cartesian3.distanceSquared(a, b); // faster for comparisons
const angle = Cartesian3.angleBetween(a, b); // radians
Cartographic -- Geographic Coordinates
import { Cartographic, Cartesian3, Math as CesiumMath } from "cesium";
const carto = Cartographic.fromDegrees(-105.0, 40.0, 1500.0);
const cartoRad = Cartographic.fromRadians(-1.8326, 0.6981, 1500.0);
// Cartesian3 <-> Cartographic
const position = Cartesian3.fromDegrees(-105.0, 40.0, 1500.0);
const geo = Cartographic.fromCartesian(position);
const lonDeg = CesiumMath.toDegrees(geo.longitude); // -105.0
const latDeg = CesiumMath.toDegrees(geo.latitude); // 40.0
const backToCart = Cartographic.toCartesian(geo);
CesiumMath Utilities
import { Math as CesiumMath } from "cesium";
// Degree/radian conversion
const rad = CesiumMath.toRadians(90.0); // PI/2
const deg = CesiumMath.toDegrees(Math.PI); // 180
// Constants: PI, TWO_PI, PI_OVER_TWO, PI_OVER_FOUR, RADIANS_PER_DEGREE
// EPSILON1 (0.1) through EPSILON21 (1e-21)
const clamped = CesiumMath.clamp(value, 0.0, 1.0);
const interp = CesiumMath.lerp(0.0, 100.0, 0.5); // 50
const norm = CesiumMath.negativePiToPi(angle); // [-PI, PI]
const pos = CesiumMath.zeroToTwoPi(angle); // [0, 2*PI]
const safeLon = CesiumMath.convertLongitudeRange(angle); // [-PI, PI)
const eq = CesiumMath.equalsEpsilon(a, b, CesiumMath.EPSILON7); // float compare
Ellipsoid
import { Ellipsoid, Cartesian3, Cartographic } from "cesium";
// Built-in ellipsoids
Ellipsoid.WGS84; // Earth (default)
Ellipsoid.UNIT_SPHERE; // radius 1
Ellipsoid.MOON; // lunar sphere
Ellipsoid.MARS; // Mars (v1.133+)
// Change default (affects Ellipsoid.default everywhere)
Ellipsoid.default = Ellipsoid.MOON;
// Conversions on a specific ellipsoid
const cart = Ellipsoid.WGS84.cartographicToCartesian(
Cartographic.fromDegrees(-75.0, 40.0, 100.0),
);
const carto = Ellipsoid.WGS84.cartesianToCartographic(cart);
// Surface normal at a position
const normal = Ellipsoid.WGS84.geodeticSurfaceNormal(cart, new Cartesian3());
// Project point onto ellipsoid surface
const onSurface = Ellipsoid.WGS84.scaleToGeodeticSurface(cart, new Cartesian3());
Transforms -- Reference Frames
Transforms builds 4x4 matrices relating local frames to ECEF. The most commonly used function is eastNorthUpToFixedFrame.
East-North-Up (ENU)
ENU: X = east, Y = north, Z = up. Standard frame for placing models on the globe.
import { Cartesian3, Transforms, Matrix4 } from "cesium";
const origin = Cartesian3.fromDegrees(-105.0, 40.0);
const enuMatrix = Transforms.eastNorthUpToFixedFrame(origin);
// Columns: [east, north, up, origin] in ECEF
Heading-Pitch-Roll Model Matrix
Standard way to position and orient a 3D model.
import { Cartesian3, Transforms, HeadingPitchRoll, Math as CesiumMath } from "cesium";
const position = Cartesian3.fromDegrees(-105.0, 40.0, 0.0);
const hpr = new HeadingPitchRoll(
CesiumMath.toRadians(90.0), // heading: 90 deg east
0.0, // pitch: level
0.0, // roll: none
);
const modelMatrix = Transforms.headingPitchRollToFixedFrame(position, hpr);
// Just the orientation quaternion (e.g., for Entity.orientation)
const orientation = Transforms.headingPitchRollQuaternion(position, hpr);
HeadingPitchRoll
Heading = rotation about -Z (compass bearing, clockwise). Pitch = about -Y. Roll = about +X. Radians.
import { HeadingPitchRoll, Math as CesiumMath } from "cesium";
const hpr = new HeadingPitchRoll(CesiumMath.toRadians(45.0), CesiumMath.toRadians(-10.0), 0.0);
const hprDeg = HeadingPitchRoll.fromDegrees(45.0, -10.0, 0.0); // convenience
Other Local Frames
import { Transforms, Cartesian3 } from "cesium";
const origin = Cartesian3.fromDegrees(-105.0, 40.0);
Transforms.northEastDownToFixedFrame(origin); // NED (aviation)
Transforms.northUpEastToFixedFrame(origin); // NUE
// Custom frame from any combo of east|north|up|west|south|down
const customFn = Transforms.localFrameToFixedFrameGenerator("north", "west");
const matrix = customFn(origin);
// Recover heading/pitch/roll from an existing model matrix
const hpr = Transforms.fixedFrameToHeadingPitchRoll(modelMatrix);
Matrix4 -- 4x4 Transforms
Column-major storage (WebGL convention). Constructor takes row-major for readability.
import { Matrix4, Matrix3, Cartesian3, Quaternion } from "cesium";
// Factory methods
Matrix4.fromTranslation(new Cartesian3(10, 20, 30));
Matrix4.fromRotationTranslation(Matrix3.fromRotationZ(Math.PI / 4), new Cartesian3(100, 0, 0));
Matrix4.fromTranslationQuaternionRotationScale(
new Cartesian3(0, 0, 0), Quaternion.IDENTITY, new Cartesian3(2, 2, 2),
);
Matrix4.fromUniformScale(5.0);
// Combine, transform, invert
const combined = Matrix4.multiply(matA, matB, new Matrix4());
const worldPt = Matrix4.multiplyByPoint(enuMatrix, new Cartesian3(100, 0, 0), new Cartesian3());
const inv = Matrix4.inverseTransformation(enuMatrix, new Matrix4()); // rigid-body only
// Decompose
Matrix4.getTranslation(enuMatrix, new Cartesian3());
Matrix4.getMatrix3(enuMatrix, new Matrix3());
Matrix4.getScale(enuMatrix, new Cartesian3());
Quaternion -- Rotation
import { Quaternion, Cartesian3, HeadingPitchRoll, Math as CesiumMath, Matrix3, Matrix4, Transforms } from "cesium";
Quaternion.IDENTITY; // (0, 0, 0, 1)
const q1 = Quaternion.fromAxisAngle(Cartesian3.UNIT_Z, CesiumMath.toRadians(45.0));
const q2 = Quaternion.fromHeadingPitchRoll(new HeadingPitchRoll(CesiumMath.toRadians(90), 0, 0));
const q3 = Quaternion.fromRotationMatrix(Matrix3.fromRotationZ(Math.PI / 2));
const mid = Quaternion.slerp(q1, q2, 0.5, new Quaternion()); // interpolate
const composed = Quaternion.multiply(q1, q2, new Quaternion()); // compose
Quaternion → Matrix3 → Matrix4 Composition Pattern
Use this pattern when you need explicit axis-angle control over model orientation, then must compose with an ENU local frame:
import { Cartesian3, Quaternion, Matrix3, Matrix4, Transforms, Math as CesiumMath } from "cesium";
const origin = Cartesian3.fromDegrees(-115.17, 36.11, 3000.0);
// 1. Build local-to-ECEF frame at origin
const enuFrame = Transforms.eastNorthUpToFixedFrame(origin);
// 2. Build quaternion for 45-deg yaw about local up axis
const q = Quaternion.fromAxisAngle(Cartesian3.UNIT_Z, CesiumMath.toRadians(45.0));
// 3. Convert quaternion → Matrix3 → Matrix4 (zero translation in local frame)
const rot3 = Matrix3.fromQuaternion(q, new Matrix3());
const rotMatrix4 = Matrix4.fromRotationTranslation(rot3, Cartesian3.ZERO, new Matrix4());
// 4. Compose: ENU frame * local rotation = final model matrix
const modelMatrix = Matrix4.multiply(enuFrame, rotMatrix4, new Matrix4());
This is the canonical pattern for placing a model with arbitrary rotation at a geographic position. Transforms.headingPitchRollToFixedFrame is a convenience wrapper for HPR rotations; use the manual composition above when you need axis-angle or quaternion control.
Geodesic Distance
Critical: "Distance between two lon/lat points" almost always means great-circle surface distance, not the straight-line chord through the Earth. Using Cartesian3.distance on two fromDegrees results gives the chord, which is shorter than the surface distance and grows materially wrong over continental scales (e.g., NYC↔London chord is ~100+ km off the ~5,837 km surface distance). Always use EllipsoidGeodesic.surfaceDistance for "how far apart are these two places" labels.
import { Cartographic, EllipsoidGeodesic, Cartesian3 } from "cesium";
// Surface distance (great-circle via Vincenty) -- correct for "distance between cities"
const geodesic = new EllipsoidGeodesic(
Cartographic.fromDegrees(-73.985, 40.758), // New York
Cartographic.fromDegrees(-0.1276, 51.5074), // London
);
const surfaceDist = geodesic.surfaceDistance; // ~5,837,000 m (~5,837 km)
const midCarto = geodesic.interpolateUsingFraction(0.5); // midpoint on surface
// Chord (straight-line through ellipsoid interior) -- rarely what you want for geography
const chord = Cartesian3.distance(
Cartesian3.fromDegrees(-73.985, 40.758),
Cartesian3.fromDegrees(-0.1276, 51.5074),
); // shorter than surfaceDistance; do NOT use for "distance between cities"
When labeling distances, format from surfaceDistance (meters) divided by 1000 and rounded to the nearest km. For very short distances (< ~1 km) the chord and surface distance agree to within rounding; for anything continental, prefer EllipsoidGeodesic.
Sampling a Geodesic into Cartesian3 Positions
interpolateUsingFraction returns a Cartographic. Convert each sample to Cartesian3 before passing to polylines or other geometry APIs. Use enough samples (>= ~64 for transoceanic arcs) so the polyline visibly curves rather than appearing as a straight rhumb-like line.
import { Cartographic, EllipsoidGeodesic, Cartesian3 } from "cesium";
const start = Cartographic.fromDegrees(-73.985, 40.758); // NYC
const end = Cartographic.fromDegrees(-0.1276, 51.507); // London
const geodesic = new EllipsoidGeodesic(start, end);
const N = 64;
const positions = [];
for (let i = 0; i <= N; i++) {
const carto = geodesic.interpolateUsingFraction(i / N);
// Convert Cartographic (radians) to Cartesian3
positions.push(Cartesian3.fromRadians(carto.longitude, carto.latitude, carto.height));
}
// positions is now a Cartesian3[] suitable for polyline entity positions
// Label the midpoint with the great-circle distance, not the chord
const midCarto = geodesic.interpolateUsingFraction(0.5);
const midPos = Cartesian3.fromRadians(midCarto.longitude, midCarto.latitude, midCarto.height);
const km = (geodesic.surfaceDistance / 1000).toFixed(0);
viewer.entities.add({
position: midPos,
label: { text: `Distance: ${km} km` },
});
BoundingSphere
import { BoundingSphere, Cartesian3 } from "cesium";
const sphere = BoundingSphere.fromPoints(
Cartesian3.fromDegreesArray([-105, 40, -100, 40, -100, 35]),
); // sphere.center (Cartesian3), sphere.radius (number)
const inside = Cartesian3.distance(sphere.center, Cartesian3.fromDegrees(-102, 37.5)) <= sphere.radius;
sphere.center is a Cartesian3 (ECEF) and can be used directly as an entity position. sphere.radius is in meters and can be passed as ellipsoid radii for visualization. Use a low alpha (≈0.3) so the enclosed points and underlying geography remain visible through the sphere -- an overly opaque sphere hides exactly the data it is meant to bound:
import { Color, Cartesian3 } from "cesium";
viewer.entities.add({
position: sphere.center,
ellipsoid: {
radii: new Cartesian3(sphere.radius, sphere.radius, sphere.radius),
material: Color.YELLOW.withAlpha(0.3), // translucent so input points stay visible
outline: true,
outlineColor: Color.YELLOW,
},
});
Ray and Intersection Tests
import { Ray, IntersectionTests, Plane, Cartesian3, Ellipsoid } from "cesium";
const ray = new Ray(new Cartesian3(0, 0, 6378137), new Cartesian3(0, 0, -1)); // auto-normalized
const ptOnRay = Ray.getPoint(ray, 1000.0, new Cartesian3());
// Ray-plane: returns Cartesian3 or undefined
const plane = Plane.fromPointNormal(Cartesian3.ZERO, Cartesian3.UNIT_Z);
const hit = IntersectionTests.rayPlane(ray, plane);
// Ray-ellipsoid: returns Interval {start, stop} or undefined
const camRay = new Ray(new Cartesian3(0, 0, 20000000), new Cartesian3(0, 0, -1));
const interval = IntersectionTests.rayEllipsoid(camRay, Ellipsoid.WGS84);
if (interval) {
const nearPt = Ray.getPoint(camRay, interval.start, new Cartesian3());
}
// Ray-triangle: returns parametric t or undefined
const t = IntersectionTests.rayTriangleParametric(ray, p0, p1, p2, true);
SceneTransforms -- World to Screen
import { SceneTransforms, Cartesian3 } from "cesium";
// World -> pixel coordinates (Cartesian2 or undefined if off-screen)
const winPos = SceneTransforms.worldToWindowCoordinates(viewer.scene, Cartesian3.fromDegrees(-105, 40));
// High-DPI aware variant
const bufPos = SceneTransforms.worldToDrawingBufferCoordinates(viewer.scene, worldPos);
Geographic Projections
import { GeographicProjection, WebMercatorProjection, Cartographic, Ellipsoid } from "cesium";
const carto = Cartographic.fromDegrees(-105.0, 40.0);
// Plate Carree: project/unproject between Cartographic and Cartesian3
const geoProj = new GeographicProjection(Ellipsoid.WGS84);
const xy = geoProj.project(carto); // Cartesian3
const back = geoProj.unproject(xy); // Cartographic
// Web Mercator (EPSG:3857)
const merc = new WebMercatorProjection(Ellipsoid.WGS84);
const mercXY = merc.project(carto);
Common Patterns
Offset a Position in Local ENU
import { Cartesian3, Transforms, Matrix4 } from "cesium";
const origin = Cartesian3.fromDegrees(-105.0, 40.0, 0.0);
const enu = Transforms.eastNorthUpToFixedFrame(origin);
// Move 500m east, 200m north, 100m up in local frame
const worldPt = Matrix4.multiplyByPoint(enu, new Cartesian3(500, 200, 100), new Cartesian3());
Compare Positions with Tolerance
import { Cartesian3, Math as CesiumMath } from "cesium";
const a = Cartesian3.fromDegrees(-105.0, 40.0);
const b = Cartesian3.fromDegrees(-105.0001, 40.0001);
Cartesian3.equalsEpsilon(a, b, CesiumMath.EPSILON7); // preferred over ===
if (Cartesian3.distance(a, b) < 10.0) { /* within 10m */ }
Performance Tips
- Reuse scratch variables. Pre-allocate
resultobjects outside loops to avoid GC pauses. - Use
distanceSquaredinstead ofdistancewhen comparing -- avoidsMath.sqrt. - Prefer
Cartesian3.fromDegreesover manual Cartographic creation then conversion. - Cache model matrices. Call
Transforms.eastNorthUpToFixedFrameonce if position is static. - Use
Matrix4.inverseTransformationfor rigid-body transforms -- faster and more stable thaninverse. - Batch position creation with
fromDegreesArray/fromDegreesArrayHeightsinstead of loopingfromDegrees. - Guard
Cartesian3.normalize-- it throws on zero-length vectors. Check magnitude first. - Use
equalsEpsilonfor float comparisons.CesiumMath.EPSILON7is a good default tolerance. - Pre-compute HPR outside render loops. Convert to quaternion/matrix only when orientation changes.
- Choose the right distance.
Cartesian3.distance= chord through Earth (rarely what you want for geography).EllipsoidGeodesic.surfaceDistance= great-circle surface distance (use this for city-to-city labels).
See Also
- cesiumjs-camera -- Camera positioning and flight animations that consume these coordinate types
- cesiumjs-primitives -- Geometry and Primitive API that uses model matrices from Transforms
- cesiumjs-terrain-environment -- Terrain height queries and globe surface interactions
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/cesiumgs/cesiumjs-skills/cesiumjs-spatial-math">View cesiumjs-spatial-math on skillZs</a>