上个月接了个测绘可视化项目
客户丢过来一堆数据文件,说”直接加载到地图上就行”。我打开一看——全是 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);
总结一下这次踩坑学到的东西
- Cesium 核心坐标三件套: Cartographic(经纬度高程,球面)、Cartesian3(XYZ 米,三维直角)、ScreenCoordinates(像素)。理解这三者的关系和转换,比背 API 更重要。
- 度带不是玄学: 中国测绘数据大概率是高斯-克吕格投影。拿到数据先确认坐标系(北京54、西安80、CGCS2000),再确认分带方式(3 度还是 6 度),然后找到中央子午线。这一步错了后面全白干。
- 投影转经纬度用 proj4js: 不需要自己实现复杂的投影反算,proj4js 封装好了。正确配好源 CRS 的 PROJ string,一行代码完成转换。
- 别忽略弧度转角度: Cartographic 的经纬度是弧度,别拿着弧度当度数去算距离。Cesium 的 Cesium.Math.toDegrees 是你的好朋友。
做三维 GIS 开发,坐标系转换就是最基础的水和电。你不一定每天都要徒手画一遍转换公式,但至少得知道——当数据对不上的时候,问题大概率出在坐标系上,而不是 Cesium 的 bug。