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 的精确公式,并解释为什么纬度必须截断。


相关推荐
AlienZHOU4 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc8 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师9 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen10 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒10 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow10 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81810 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007810 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明10 小时前
第 6 章 组件进阶
前端·vue.js
江华森10 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端