three.js地图数学基础(一)

0 阅读5分钟

坐标不是一个数字:先建立空间账本

地图引擎里最危险的坐标,往往不是算错的坐标,而是没有写明属于哪个空间、采用什么单位、轴朝向哪里的坐标。{ x: 0.5, y: 0.5 } 可以是世界中心,可以是瓦片内部的一个点,也可以是屏幕中央;只看数值无法判断。

本章先建立全书共用的“空间账本”。后续所有算法——视口覆盖、MVT 解码、几何构建、相机投影、瓦片替换——都必须明确从哪个空间读入数据,又向哪个空间输出数据。

ScreenShot_2026-08-07_184112_796.png

1. 七个必须分开的空间

1.1 地理空间 Geographic

地理空间中的点写作:

g=(λdeg,φdeg,hm)g=(\lambda_{deg},\varphi_{deg},h_m)
  • λ(longitude)是经度,向东为正,单位为度;
  • φ(latitude)是纬度,向北为正,单位为度;
  • h 是相对参考面的高度,单位为米。

经纬度不是平面笛卡尔坐标。相同的一度经度,在赤道和高纬地区对应的地表距离不同。因此不能直接把经纬度当作 Three.js 的 x/z 位置,也不能直接对它们做屏幕距离判断。

1.2 归一化 Mercator 空间 Normalized Mercator

将经纬度投影后,本书用一个单位正方形表示完整世界:

m=(xm,ym),xm,ym[0,1]m=(x_m,y_m),\qquad x_m,y_m\in[0,1]

约定如下:

  • x = 0 对应经度 −180°x = 1 对应 +180°
  • y = 0 在北方,y = 1 在南方;
  • xy 都是无量纲比例,不是米;
  • 水平方向允许超出 [0, 1],以表达跨越反经线后的重复世界。

“归一化”把投影公式、瓦片金字塔和渲染尺度解耦。世界可以在某一帧映射为 5122^20 或任意数量的世界单位,瓦片寻址仍只依赖 [0,1] 比例。

1.3 瓦片地址空间 Tile Address

缩放级别 z 把单位世界沿每个轴分成:

n=2zn=2^z

份。整数三元组 (z,x,y) 标识一个瓦片。这里的 x/y瓦片索引,不是点坐标;它们没有长度单位。

本书还会给地址附加 wrap

t=(z,x,y,wrap)t=(z,x,y,wrap)

其中 x 始终保持在规范范围 [0,n-1]wrap 记录它位于第几个重复世界。这样缓存身份可以使用规范地址,而场景位置仍能区分相邻世界。

1.4 瓦片局部空间 Tile-local

MVT 几何并不直接存储经纬度,而是在每个瓦片内部使用整数网格。若某图层声明 extent = E,则点写作:

pt=(u,v),u,v 通常位于 [0,E]p_t=(u,v),\qquad u,v\text{ 通常位于 }[0,E]
  • 单位是 1 / extent 个瓦片边长;
  • u 向右增大;
  • v 向下增大;
  • 为了缓冲裁剪边缘,实际几何可以越出 [0,E]

因此 u=4096 不是 4096 米,也不保证落在瓦片右边界;只有同时知道 extent,它才有含义。

1.5 世界空间 World

渲染器需要一个三维、右手系的笛卡尔空间。本书采用 Three.js 友好的约定:

pw=(X,Y,Z)p_w=(X,Y,Z)
  • X:Mercator 水平方向,向东为正;
  • Y:高度方向,向上为正;
  • Z:Mercator 垂直方向,向南为正;
  • 地图平面位于 X-Z 平面;
  • Y 与 MVT 的二维 y 完全不是同一轴。

若一整个 Mercator 世界的渲染宽度为 worldSize,则:

X=xmworldSize,Z=ymworldSizeX=x_m\cdot worldSize,\qquad Z=y_m\cdot worldSize

高度需要单独决定“米到世界单位”的尺度。第一阶段暂时让 Y 直接使用米,因为我们只展示坐标关系;建筑章节会给出一致的比例换算。

提示(纬度与高程尺度):在球面 Web Mercator 投影下,地面平面的“一米”对应的世界单位比例会随纬度 φ\varphi 改变(换算系数正比于 1cosφ\frac{1}{\cos\varphi})。虽然当前阶段统一视 YY 为米,但在后续实现 3D 建筑与真实高程时,必须引入与纬度相关的尺度修正因子。

1.6 渲染局部空间 Render-local

世界坐标的绝对值会随缩放和世界重复迅速变大,而 GPU 顶点属性通常使用 32 位浮点数。在图形学与地图引擎(如 Cesium、Mapbox)中,这被称为 RTC(Relative to Center)RTE(Relative to Eye) 技术。为保留局部几何细节,每帧选择一个邻近视口的高精度原点 owo_w,先在 CPU 的 Float64 中相减:

pr=(pwow)p_r=(p_w-o_w)

再上传 prp_r。渲染局部空间的原点会移动,但几何之间的相对关系不变。它是数值精度策略,不是新的地理投影。

1.7 裁剪、NDC 与屏幕空间

顶点经过模型、观察、投影矩阵后得到齐次裁剪坐标:

pc=(xc,yc,zc,wc)p_c=(x_c,y_c,z_c,w_c)

执行透视除法后得到标准化设备坐标(NDC):

pn=(xcwc,ycwc,zcwc)p_n=\left(\frac{x_c}{w_c},\frac{y_c}{w_c},\frac{z_c}{w_c}\right)

WebGL 可见范围为 x,y,z ∈ [−1,1]。最后,宽为 W、高为 H 的画布把 NDC 映射到 CSS 或设备像素:

xs=xn+12W,ys=1yn2Hx_s=\frac{x_n+1}{2}W,\qquad y_s=\frac{1-y_n}{2}H

这里再次发生了 y 轴翻转:NDC 的 y 向上,而 DOM 屏幕坐标的 y 向下。

2. 空间转换表

输入输出核心信息是否可逆常见损失
经纬度normalized Mercator球面 Mercator 投影限定纬度后可逆极区被裁去
normalized Mercator瓦片地址floor(2^z x/y)不可逆丢失瓦片内位置
瓦片地址 + tile-localnormalized Mercatorextentwrap可逆到量化精度MVT 整数量化
MercatorworldworldSize 与轴映射可逆通常无
worldrender-local高精度局部原点知道原点即可逆GPU 转 Float32
render-localclipM/V/P 矩阵矩阵可逆时可逆深度与浮点误差
clipNDC除以 w保留 w 才可逆丢弃 w 后不可逆
NDCscreenviewport 与像素比知道 viewport 即可逆像素采样

2.1 各空间轴向与原点对照

为了防止在工程编码中混淆轴向翻转(特别是 YY 轴与 VV 轴),下表汇总了各空间的原点及正轴朝向:

空间原点位置第 1 轴 (X/U/λX / U / \lambda)第 2 轴 (Y/V/φY / V / \varphi)第 3 轴 (Z/hZ / h)
Geographic(0,0)(0^{\circ}, 0^{\circ}) (本初子午线与赤道)东 (+)北 (+)相对参考面高度 hmh_m (+)
Normalized Mercator左上角 (西北角)东 (+)南 (+)
Tile-local (MVT)瓦片左上角右 (+)下 (+)
World (Three.js)场景中心 / 世界原点东 (+)上 (+)南 (+)
NDC视口中心右 (+)上 (+)屏幕里 / 远平面 (+)
Screen (DOM)画布左上角右 (+)下 (+)

3. 三条工程不变量

不变量一:名字必须暴露空间

优先使用 mercatorXtileYlocalPositionscreenPoint,不要在跨模块接口中只写 x/y。内部的短变量可以简洁,但边界必须自描述。

不变量二:转换发生在明确的所有者中

  • 经纬度投影属于数学模块;
  • MVT extent 到瓦片比例属于几何构建阶段;
  • 世界到 render-local 的减法属于渲染帧或 GPU runtime;
  • NDC 到屏幕像素属于视口模块。

如果多个模块都“顺手”完成一半转换,就很难判断重复缩放、轴翻转和精度损失发生在哪里。

不变量三:比较之前先统一空间

不能直接比较“相机到瓦片的世界距离”和“屏幕上的像素阈值”。视口选择使用 Mercator/世界空间的覆盖关系,交互命中使用屏幕射线,缓存优先级使用已经定义过的屏幕贡献或树距离。任何跨空间比较都必须展示换算。

4. 一次完整点变换

以上海教学视图中心 (121.4737°, 31.2304°) 为例:

  1. 地理坐标投影为 normalized Mercator,约为 (0.8374269444, 0.4086037247)
  2. z=13,乘以 8192 后取整,得到瓦片列/行;
  3. 根据瓦片左上角和 MVT extent,把局部整数恢复为 Mercator 比例;
  4. worldSize 并将 Mercator y 放到 Three.js 的世界 Z
  5. 在 Float64 中减去视口附近原点,得到小数值的 render-local 坐标;
  6. P·V·M 变换为 clip,再除以 w 得 NDC;
  7. 由 viewport 转为屏幕像素。

每一步都应能回答四个问题:输入空间是什么、输出空间是什么、单位是什么、是否丢失信息。这四问将贯穿整本手册。

5. 本章边界

本书采用球面 Web Mercator 和二维平面地图。椭球测地线、地形表面、地球曲面和三维地心坐标会改变距离、高度和可见性计算,但不会改变“先明确空间再写算法”的原则。后续章节只在边界说明这些差异,不把它们混入当前实现。

下一章将推导从 (λ,φ) 到 normalized Mercator 的精确公式,并解释为什么纬度必须截断。