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

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

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

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

1. 七个必须分开的空间

1.1 地理空间 Geographic

地理空间中的点写作:
g=( λdeg , φdeg ,hm) g=(\lambda_{deg},\varphi_{deg},h_m) g=(λdeg,φdeg,hm)

  • λ(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\in0,1 m=(xm,ym),xm,ym∈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 n=2z

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

本书还会给地址附加 wrap
t=(z,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 pt=(u,v),u,v 通常位于 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) pw=(X,Y,Z)

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

若一整个 Mercator 世界的渲染宽度为 worldSize,则:
X=xm⋅worldSize,Z=ym⋅worldSize X=x_m\cdot worldSize,\qquad Z=y_m\cdot worldSize X=xm⋅worldSize,Z=ym⋅worldSize

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

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

1.6 渲染局部空间 Render-local

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

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

1.7 裁剪、NDC 与屏幕空间

顶点经过模型、观察、投影矩阵后得到齐次裁剪坐标:
pc=(xc,yc,zc,wc) p_c=(x_c,y_c,z_c,w_c) pc=(xc,yc,zc,wc)

执行透视除法后得到标准化设备坐标(NDC):
pn= ( xcwc , ycwc , zcwc ) p_n=\left(\frac{x_c}{w_c},\frac{y_c}{w_c},\frac{z_c}{w_c}\right) pn=(wcxc,wcyc,wczc)

WebGL 可见范围为 x,y,z ∈ [−1,1]。最后,宽为 W、高为 H 的画布把 NDC 映射到 CSS 或设备像素:
xs= xn+1 2 W,ys= 1−yn 2 H x_s=\frac{x_n+1}{2}W,\qquad y_s=\frac{1-y_n}{2}H xs=2xn+1W,ys=21−ynH

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

2. 空间转换表

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

2.1 各空间轴向与原点对照

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

空间 原点位置 第 1 轴 ( X/U/λX / U / \lambda X/U/λ) 第 2 轴 ( Y/V/φY / V / \varphi Y/V/φ) 第 3 轴 ( Z/hZ / h Z/h)
Geographic (0∘,0∘)(0^{\circ}, 0^{\circ}) (0∘,0∘) (本初子午线与赤道) 东 (+) 北 (+) 相对参考面高度 hm h_m hm (+)
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 的精确公式,并解释为什么纬度必须截断。


相关推荐
何时梦醒1 小时前
🎯 从零彻底搞懂 React Context API —— 一篇带你穿越"组件树"的状态共享方案
前端·javascript·react.js
渣波1 小时前
拒绝页面假死!React 并发编程实战:Web Worker + useRef 深度解析与高性能计算架构
前端·javascript
用户2930750976691 小时前
Web Worker:让 JavaScript 拥有"多线程"能力
前端
渣波1 小时前
赋予 AI “灵魂”:LangChain.js 中临时与长期记忆的终极实战指南
前端·javascript
逍遥德2 小时前
ECMAScript 各个版本的语法列表
前端·javascript·ecmascript·es6
用户059540174462 小时前
把AI聊天机器人记忆存储测试从3小时压到5分钟,我用Pytest + Docker搭了一套自动化回归
前端·css
明月_清风2 小时前
显存即正义:不同显存容量能训多大的模型?一文说清硬件边界与训练策略
前端·后端·ai编程
Sterting2 小时前
第 9 节:本地存储 — 数据不丢的前端缓存
前端·javascript·缓存
IT_陈寒2 小时前
Vite的HMR在我项目上突然失效,排查三天找到离谱原因
前端·人工智能·后端