mcpField
C
cesiumjs-spatial-math

cesiumjs-spatial-math

github.com/cesiumgs开发193248 次安装6 周前更新
代码仓库

关于 cesiumjs-spatial-math

面向 CesiumJS 开发者的空间数学与变换参考技能,涵盖地理坐标、地心几何、坐标系转换、测量、投影和相交运算。

主要功能

  1. 01Cartesian 与 Cartographic 坐标转换
  2. 02向量与矩阵运算
  3. 03局部参考坐标系变换
  4. 04椭球测地线距离
  5. 05包围体与相交测试
  6. 06地理投影工具

适用场景

  1. 01将地理位置转换为三维坐标
  2. 02在局部 ENU 坐标系中定向模型
  3. 03计算地点间的地表距离
  4. 04投影坐标或测试几何相交

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/cesiumgs/cesiumjs-skills --skill cesiumjs-spatial-math

安装后在 Claude Code 中输入/cesiumjs-spatial-math确认已加载。

详细介绍

CesiumJS 空间数学与变换

基线版本:CesiumJS v1.144(2026-08-01)

每个 CesiumJS 应用的数学基础:坐标类型、单位换算、椭球体几何、参考系变换、包围体、相交测试和投影。

核心概念

CesiumJS 使用右手地心地固(ECEF)坐标系:

  • Cartesian3 —— ECEF 坐标(x、y、z),单位为米。所有 3D 位置的内部表示形式。
  • Cartographic ——(经度、纬度、高度)。角度单位为弧度,高度为椭球面以上的米数。

核心数学中的所有角度值均以弧度表示。使用 Math.toRadians() / Math.toDegrees()。数学类型采用静态方法搭配结果参数的模式:传入 result 参数以重用分配的对象。

Cartesian3 —— 位置与向量

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)

破坏性变更(1.139,#8359): Cartesian2、Cartesian3 和 Cartesian4 现在是 ES6 类。在静态工厂方法前调用 new 会抛出错误—— new Cartesian3.fromArray([...]) 和 new Cartesian3.fromDegrees(...) 都是 错误的。对于工厂方法,请去掉 new(Cartesian3.fromArray([...]));仅在真正的构造函数中保留 new(new Cartesian3(x, y, z))。更多类正迁移到 ES6 类,因此请在所有地方遵循此规则。

向量运算
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 —— 地理坐标

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 工具函数

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

椭球体

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 —— 参考系

Transforms 会构建关联局部坐标系与 ECEF 的 4x4 矩阵。最常用的函数是 eastNorthUpToFixedFrame。

东-北-上(ENU)

ENU:X = 东,Y = 北,Z = 上。在地球上放置模型时使用的标准坐标系。

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
航向-俯仰-横滚模型矩阵

定位和定向 3D 模型的标准方法。

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 = 绕 -Z 轴旋转(罗盘方位,顺时针)。Pitch = 绕 -Y 轴旋转。Roll = 绕 +X 轴旋转。单位为弧度。

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
其他局部坐标系
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 变换

采用列优先存储(WebGL 约定)。构造函数为便于阅读而接受行优先参数。

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 —— 旋转

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 组合模式

当需要明确控制模型朝向的轴角,然后将其与 ENU 局部坐标系组合时,请使用此模式:

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());

这是在地理位置放置具有任意旋转的模型的标准模式。Transforms.headingPitchRollToFixedFrame 是针对 HPR 旋转的便捷封装;需要轴角或四元数控制时,请使用上述手动组合方式。

大地测量距离

重要:“两个经纬度点之间的距离”几乎总是指大圆表面距离,而不是穿过地球的直线弦长。对两个 fromDegrees 结果使用 Cartesian3.distance 得到的是弦长,它比表面距离短,并且在跨大陆尺度上会产生明显误差(例如,纽约↔伦敦的弦长与约 5,837 km 的表面距离相差约 100 多 km)。要显示“这两个地点相距多远”,请始终使用 EllipsoidGeodesic.surfaceDistance。

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"

显示距离时,将 surfaceDistance(米)除以 1000,并四舍五入到最接近的千米。对于非常短的距离(< 约 1 km),弦长和表面距离的差异在四舍五入后可以忽略;对于任何跨大陆距离,优先使用 EllipsoidGeodesic。

将大地线采样为 Cartesian3 位置

interpolateUsingFraction 会返回 Cartographic。将每个采样点转换为 Cartesian3,再传给折线或其他几何 API。使用足够多的采样点(跨洋弧线至少约 64 个),使折线明显弯曲,而不是看起来像一条直线状的恒向线。

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` },
});

包围球

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 是一个 Cartesian3(ECEF),可直接用作实体位置。sphere.radius 的单位是米,可作为椭球半径用于可视化。请使用较低的 alpha 值(≈0.3),让包围的点和底层地理信息仍能透过球体显示——球体过于不透明会遮住它本该包围的数据:

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,
  },
});

射线和相交测试

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 —— 世界坐标转屏幕坐标

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);

地理投影

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);

常见模式

在本地 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());
按容差比较位置
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 */ }

性能提示

  1. 复用临时变量。 在循环外预分配 result 对象,避免 GC 暂停。
  2. 比较时使用 distanceSquared,而不是 distance,以避免调用 Math.sqrt。
  3. 优先使用 Cartesian3.fromDegrees,而不是手动创建 Cartographic 再进行转换。
  4. 缓存模型矩阵。 如果位置是静态的,只调用一次 Transforms.eastNorthUpToFixedFrame。
  5. 刚体变换使用 Matrix4.inverseTransformation——它比 inverse 更快、更稳定。
  6. 使用 fromDegreesArray / fromDegreesArrayHeights 批量创建位置,而不是循环调用 fromDegrees。
  7. 使用 Cartesian3.normalize 前先检查——零长度向量会导致它抛出错误。先检查模长。
  8. 浮点数比较使用 equalsEpsilon。CesiumMath.EPSILON7 是不错的默认容差。
  9. 在渲染循环外预先计算 HPR。 仅当方向发生变化时,才转换为四元数或矩阵。
  10. 选择合适的距离。 Cartesian3.distance = 穿过地球的弦长(很少适用于地理场景)。EllipsoidGeodesic.surfaceDistance = 球面大圆距离(适用于城市之间的标签)。

另请参阅

  • cesiumjs-camera —— 使用这些坐标类型进行相机定位和飞行动画
  • cesiumjs-primitives —— 使用 Transforms 中模型矩阵的 Geometry 和 Primitive API
  • cesiumjs-terrain-environment —— 地形高度查询和地球表面交互

评论

支持 Markdown