从一次Cesium项目踩坑说起:经纬度、大地坐标系、度带,我到底该用哪个?

一个真实的 Cesium 项目踩坑记录:从测绘投影坐标到 WGS84 经纬度,从度带到 Cartesian3,一次搞懂经纬度、大地坐标系和投影坐标的原理与转换。

上个月接了个测绘可视化项目

客户丢过来一堆数据文件,说”直接加载到地图上就行”。我打开一看——全是 CGCS2000 高斯投影坐标,X 和 Y 都七八位数字,经纬度半个影子都没有。

项目用的是 Cesium,整颗地球是 WGS84 椭球体,经纬度才是它的母语。我手上这些投影坐标,就像拿着北京地图的公交卡要去刷上海地铁。不转换,寸步难行。

于是那个周末我把自己关在房间里,重新翻了一遍大学课本上关于大地测量学的内容——度带、中央子午线、高斯-克吕格投影、坐标反算……翻了两个小时后我意识到:这些东西一旦脱离考试场景,比想象中更容易搞混。

先把经纬度、大地坐标系、投影坐标理清楚

经纬度——最直观,但不适合算距离

经纬度是一个球面坐标系统。地球被近似为一个椭球体(WGS84 用的就是椭球,不是正球),经度从本初子午线向东/西量到 ±180°,纬度从赤道向北/南量到 ±90°。

核心问题:球面上的 1 度经度 ≠ 球面上的 1 度纬度。

  • 纬度 1 度 ≈ 111km(几乎恒定,因为子午线是大圆)
  • 经度 1 度 ≈ 111 × cos(纬度) km——赤道最宽,两极归零

所以如果你直接用经纬度算两点距离,比如 sqrt((lng1-lng2)² + (lat1-lat2)²),结果在赤道附近勉强能用,到高纬度就完全不准。这也是为什么经纬度不适合做几何计算——它本质上是角度,不是长度。

大地坐标系(Geodetic Coordinate System)——真正的三维

大地坐标系也叫地心地固坐标系(ECEF),在 Cesium 里就是 Cartesian3。

它把地球椭球体的质心作为原点,三条轴:

  • X 轴:赤道面与本初子午线交点方向
  • Y 轴:赤道面与东经 90° 交点方向
  • Z 轴:指向北极(地球自转轴)

任何一个地面点的坐标 (X, Y, Z) 单位都是。这就解决了经纬度的”角度量”问题——你可以直接算欧几里得距离、做向量运算、做插值。

经纬度转 Cartesian3 本质上是一个椭球几何计算:

X = (N + h) × cos(φ) × cos(λ)
Y = (N + h) × cos(φ) × sin(λ)
Z = (N × (1 - e²) + h) × sin(φ)

其中 φ = 纬度,λ = 经度,h = 高程,N = 卯酉圈曲率半径(纬度的函数),e² = 椭球第一偏心率。

反过来 Cartesian3 → 经纬度就是坐标反算,因为 N 依赖纬度,需要迭代求解。Cesium 的 Cartographic.fromCartesian 底层就是这个迭代逻辑。

投影坐标——把曲面压平

投影坐标系 = 经纬度 + 一个数学投影 + 一个分带方案。

核心思想:球面不可展平。你不可能把一个橘子皮完整地铺成一张长方形纸,要么撕开,要么变形。投影坐标系就是一种”怎么撕、怎么变形”的约定。

高斯-克吕格投影(横轴墨卡托)在中国的做法是:

  • 按经度分带——每 3° 或 6° 一个带,每带独立投影
  • 每带内:中央子午线投影为直线(X 轴),赤道投影为直线(Y 轴)
  • 中央子午线的长度比 = 1(即在中央子午线上没有变形),越远离中央子午线变形越大

这就是为什么分带是必须的。不分带,远离中央子午线的地方变形大到没法用。

坐标长什么样?比如一个 CGCS2000 3 度带 38 带的坐标:

X = 3888800.12    → 向北 3888 公里(赤道到本点的距离)
Y = 38500312.45   → 前两位 38 是带号,500312.45 是带内横坐标
                    (500000 是中央子午线的偏移值,避免负值)

注意 Y 坐标带带号,X 坐标没有。这是中国测绘数据的惯例。

拿到投影坐标后的反算流程:看 Y 前两位确定带号 → 根据分带约定算中央子午线 → 用高斯投影反算公式(或 proj4js)把 (X, Y) 算回 (λ, φ) → 喂给 Cesium 的 Cartesian3.fromDegrees

三者关系一张图看懂

   地表一个点
      │
      ▼
  经纬度 (λ, φ)  ──投影公式──► 投影坐标 (X, Y)
      │                              │
      │ 椭球几何                       │ 反算
      ▼                              ▼
  Cartesian3 (X, Y, Z)            经纬度 (λ, φ)
      │
      │ Cesium 内部使用
      ▼
  屏幕渲染、碰撞检测、相机

经纬度是你的”通用语言”——Cesium 说它,Google Maps 说它,GPS 也说它。投影坐标是测绘数据的”母语”——国内国土、规划、测绘部门的数据基本全是这个。Cartesian3 是 Cesium 的”内部语言”——它只存在于三维空间里做计算。

你在 Cesium 里看到的每一帧画面,都是这三套坐标之间来回转换的结果。

Cesium 里到底有几种坐标?

写 Cesium 的兄弟应该都跟这几个概念打过照面:

  • Cartographic(地理坐标)——经度、纬度、高程,WGS84 椭球上的球面坐标。单位是度、度、米。
  • Cartesian3(笛卡尔坐标/大地坐标)——以地心为原点的三维直角坐标系。X 轴指向赤道与本初子午线交点,Y 轴指向赤道与东经 90 度交点,Z 轴指向北极。单位是米。
  • 屏幕坐标——像素坐标,不做 GIS 分析的话基本用不上。

新手最容易卡在哪?就是把 Cartographic 和”经纬度”划等号,把 Cartesian3 当成”Cesium 内部自己用的神秘坐标”。其实说穿了就是一个球面和一个立方体的关系。球面上的一个点,在三维空间里总是能找到唯一的一组 (X, Y, Z) 跟它对应。

大地坐标(Cartesian3)怎么转成经纬度?

Cesium 给了现成的 API,复杂度基本为零:

// Cartesian3 → Cartographic(经纬度弧度值)
const cartographic = Cesium.Cartographic.fromCartesian(cartesian3);

// 弧度转角度
const lng = Cesium.Math.toDegrees(cartographic.longitude);
const lat = Cesium.Math.toDegrees(cartographic.latitude);
const height = cartographic.height;

// 反过来,经纬度 → Cartesian3
const cartesian3 = Cesium.Cartesian3.fromDegrees(lng, lat, height);

反向转换也简单:

const cartesian3 = Cesium.Cartesian3.fromDegrees(lng, lat, height);

但是注意一个隐藏细节:fromCartesian 返回的 longitude 和 latitude 是 弧度,不是度。很多人写完发现经纬度不对,就是因为少了 toDegrees 这一步。我也踩过,不丢人。

真正卡住我的是度带

回到那个让我翻课本的问题——客户给的是投影坐标,怎么变成 Cesium 能吃的经纬度?

中国常用的高斯-克吕格投影分带方式有两种:

  • 6 度带——从零度子午线开始,每 6 度一个带。中国横跨 13~23 带,北京大概在 20 带。
  • 3 度带——从东经 1.5 度开始,每 3 度一个带。中国横跨 25~45 带。

每投影带都有一个中央子午线,坐标反算时必须知道数据用的是几度带、中央子午线是多少,否则算出来的经纬度会偏差几度甚至十几度,画面上的点直接飞到隔壁省。

判断方法其实不复杂:

  • 拿到 Y 坐标(通常是 6~8 位数),看前两位——如果是带号,去掉带号,剩下的就是该带内的横坐标。
  • 6 度带中央子午线 = 带号 × 6 − 3
  • 3 度带中央子午线 = 带号 × 3

举个例子:Y 坐标是 20500312.45,前两位 20 就是带号。如果是 6 度带,中央子午线 = 20 × 6 − 3 = 117°。如果是 3 度带,中央子午线 = 20 × 3 = 60°(但这个在中国范围不太可能,所以如果数据是中国的,基本就是 6 度带)。

拿到中央子午线之后,用 proj4js 或者 Cesium 搭配一个投影转换库就能反算出经纬度:

import proj4 from "proj4";

// CGCS2000 / 3-degree Gauss-Kruger zone 38 (中央子午线 114°)
const srcCRS = "+proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs";
const wgs84 = "+proj=longlat +datum=WGS84 +no_defs";

const [lng, lat] = proj4(srcCRS, wgs84, [x, y]);
// 然后直接喂给 Cesium
const position = Cesium.Cartesian3.fromDegrees(lng, lat, height);

这一步做对之后,数据才真正在 Cesium 的地球上长对了位置。

另一个常见的坑:Cartesian3 不是你想的那样

很多刚从 Leaflet 或者 OpenLayers 转过来的开发,习惯把坐标当成 (lng, lat) 二元组到处传。到了 Cesium 发现所有 API 都要 Cartesian3,就开始到处 fromDegrees。

但 Cartesian3 是一个以为单位的直角坐标系,原点在地心。你可以把它理解为”把地球切成一块一块,每一块用 XYZ 定位”。

有一个很重要的认知:Cartesian3 的值在 Cesium 内部大量用于碰撞检测、相机控制、深度测试。你在屏幕上点击一个位置,Cesium 返回的是 Cartesian3,而不是经纬度。所以学会在这两个坐标之间熟练切换,是 Cesium 开发的必修课。

// 屏幕坐标 → Cartesian3
const cartesian3 = viewer.camera.pickEllipsoid(screenPosition);

// Cartesian3 → Cartographic
const carto = Cesium.Cartographic.fromCartesian(cartesian3);
const lng = Cesium.Math.toDegrees(carto.longitude);
const lat = Cesium.Math.toDegrees(carto.latitude);

总结一下这次踩坑学到的东西

  1. Cesium 核心坐标三件套: Cartographic(经纬度高程,球面)、Cartesian3(XYZ 米,三维直角)、ScreenCoordinates(像素)。理解这三者的关系和转换,比背 API 更重要。
  2. 度带不是玄学: 中国测绘数据大概率是高斯-克吕格投影。拿到数据先确认坐标系(北京54、西安80、CGCS2000),再确认分带方式(3 度还是 6 度),然后找到中央子午线。这一步错了后面全白干。
  3. 投影转经纬度用 proj4js: 不需要自己实现复杂的投影反算,proj4js 封装好了。正确配好源 CRS 的 PROJ string,一行代码完成转换。
  4. 别忽略弧度转角度: Cartographic 的经纬度是弧度,别拿着弧度当度数去算距离。Cesium 的 Cesium.Math.toDegrees 是你的好朋友。

做三维 GIS 开发,坐标系转换就是最基础的水和电。你不一定每天都要徒手画一遍转换公式,但至少得知道——当数据对不上的时候,问题大概率出在坐标系上,而不是 Cesium 的 bug。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注