GIS
地形与 Worker
前情回顾:03 四叉树与 LOD 讲清了瓦片怎么调度(编号、四叉树、剔除、LOD),04 影像图层 讲清了瓦片内容(XYZ/WMS/WMTS、栅格/矢量混合)。打开 3D 地图,地形不是平的——瓦片有自己的”高度”。本篇回答:这个高度从哪来?不同精度的高程数据怎么编码?为什么地形计算不能阻塞主线程?
直觉问题
打开 Google Earth、Cesium 或任意 3D 地图,一个最直接的感受是——地形有起伏。这不是贴图,是真实的高程数据在驱动网格变形:
- 为什么 satellite 瓦片只是”皮”,地形才是”骨架”?
- 高台上有 8848 米的珠峰,海沟里有 11034 米的马里亚纳——这些数字从哪来?怎么存?怎么传到 GPU?
- 点开一个 3D 地图应用,加载地形时通常会看到”正在加载高程数据”的提示,这个过程在做什么?为什么不能一打开就看到完整地形?
- 为什么地形数据量比影像大得多?一张 256×256 的 PNG 瓦片几十 KB,对应的高程数据却可能几百 KB?
读完本篇,你能回答:“高程数据有哪些编码方案?各有什么优劣?”、“大地水准面和椭球面有什么本质区别?为什么 GPS 测海拔是歪的?”、“地形计算为什么要用 Web Worker?主线程会被什么卡住?“
核心概念白话讲
高程数据:数字地形模型的”身高”信息
高程(Elevation) 就是一个地面点的海拔高度。要把它数字化,最常见的方式是 DEM(Digital Elevation Model,数字高程模型):
- 把地球表面切成等间距的格子(类似影像瓦片),每个格子存一个数——这个格子中心点的海拔。
- 比如全球 30 米分辨率的 DEM,意味着每 30 米×30 米有一个高程值。
DEM 网格(俯视图)
0m 10m 20m 30m → x
┌────┬────┬────┐
│ 15 │ 18 │ 22 │
├────┼────┼────┤
│ 12 │ 16 │ 19 │
├────┼────┼────┤
│ 8 │ 11 │ 14 │
└────┴────┴────┘
↓ y
每个数字 = 该位置的海拔(米)
三类高程编码:怎么把高度存进文件
DEM 是数据模型,不是文件格式。实际存储高程数据时,有三类主流方案:
- RGB 编码:把高程值编码到 PNG/JPG 的三个颜色通道里。
- 二进制浮点:直接存
float32或int16,文件是.bil、.flt、.tif等。 - 云优化 GeoTIFF (COG):分块压缩的 GeoTIFF,支持 HTTP Range 请求按需读取。
大地水准面 vs 椭球面:海拔到底从哪量起
这是地形领域最容易混淆的概念之一:
- 椭球面(Ellipsoid):一个完美的数学椭球,WGS84 定义了它的长半轴和扁率。GPS 直接测出的坐标就在椭球面上。
- 大地水准面(Geoid):一个虚构的”平均海平面延伸面”,是地球重力场的等位面。实际的海拔高度从这里量。
- 两者差异:同一地点,椭球面高度 ≠ 大地水准面高度,差值叫 Geoid undulation(大地水准面起伏),在中国能差几十到一百多米。
NOTE
简单说:GPS 告诉你”我在椭球面上方 3000 米”,但你要的可能是”我在海平面上方 2840 米”——这中间的 160 米差值就是 Geoid undulation。
异步管线:地形计算为什么不能在主线程
3D 地图的地形数据量巨大,处理链长:
- 网络请求下载高程瓦片(几十 KB 到几百 KB)
- CPU 解码(RGB → float、BIL 解析、COG 分块解压)
- CPU 生成地形网格(从 heightmap 生成 vertex buffer)
- GPU 上传并渲染
其中步骤 1-3 都是耗时操作。如果在主线程同步执行,UI 会卡死。解决方案是 Web Worker:把耗时的解码和网格生成放到后台线程,主线程只管调度和渲染。
原理与数学/机制
1. RGB 高程编码
1.1 编码公式
将高程值 H(单位:米)编码到 RGB 三个通道,最常见的方案是 Mapbox Terrain-RGB:
R = floor(H / 256)
G = floor(H % 256)
B = (H - floor(H)) * 256 // 小数部分
解码时反向计算:
H = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1)
-10000是基准偏移量,0.1 是精度因子,意味着该方案支持的最小高程变化为 0.1 米。 最高支持的海拔约 8900 米(已覆盖珠峰),最低支持到 -11000 米(马里亚纳海沟)。
1.2 精度分析
| 指标 | 数值 |
|---|---|
| 精度 | 0.1 米(10 cm) |
| 高度范围 | -10,000m ~ +8,900m |
| 24 bit 离散值 | 16,777,216 级 |
1.3 为什么用 RGB?
WebGL 原生支持 PNG 纹理,Image 加载后可直接上传 GPU。不需要额外解码库,兼容所有浏览器。
代价:0.1 米精度对大多数场景足够,但做精密测量不够。而且 PNG 压缩是有损的(尤其 JPEG),高程数据会被压缩误差污染。 建议:用 PNG(无损),不要用 JPEG。
2. BIL / FLT 二进制浮点
2.1 文件结构
BIL( Band Interleaved by Line )和 FLT( ESRI Float Grid )是 GIS 领域经典的二进制高程格式:
BIL 文件布局
┌─────────────────────┐
│ Header (可选) │ ← 行列数、字节序、类型等元数据
├─────────────────────┤
│ Row 0: float × cols │ ← 第0行所有列的高程值
├─────────────────────┤
│ Row 1: float × cols │ ← 第1行所有列的高程值
├─────────────────────┤
│ ... │
└─────────────────────┘
- 每个值是
float32(4 字节)或int16(2 字节) - 无压缩,原始精度
- 通常配有一个
.hdr头文件描述行列数和坐标系
2.2 与 RGB 对比
| 特性 | RGB (PNG) | BIL/FLT (原始) |
|---|---|---|
| 精度 | 0.1 米 | float32 全精度 |
| 文件大小 | 小(PNG 压缩) | 大(无压缩) |
| GPU 上传 | 直接纹理 | 需先解码为 ArrayBuffer |
| 浏览器支持 | 原生 Image | 需手动解析二进制 |
| 压缩 | PNG 无损 / JPEG 有损 | 无压缩(或外部 gzip) |
| 适用场景 | Web 地图实时渲染 | 离线分析、精密测量 |
3. Cloud Optimized GeoTIFF (COG)
3.1 为什么需要 COG
传统 GeoTIFF 是大文件(几百 MB 到几十 GB),浏览器加载必须一次读完,无法局部读取。COG 通过以下设计支持流式读取:
- 分块(Tiling):把大图切成 512×512 或 256×256 的小块
- 多分辨率金字塔:每个块存多个 LOD 级别
- HTTP Range 请求:只读取需要的字节范围
3.2 读取流程
浏览器
│
├──► HTTP HEAD ──► 获取文件大小 + 布局信息
│
├──► HTTP Range: bytes=0-4095 ──► 读取 IFD (Image File Directory)
│
├──► HTTP Range: bytes=xxx-yyy ──► 读取特定瓦片的字节块
│
└──► 客户端解压(如果用了 DEFLATE/LZW)
│
└──► 得到 float32 heightmap
3.3 相比传统方案的优势
| 特性 | 传统 GeoTIFF | COG |
|---|---|---|
| 文件大小 | 大 | 可压缩(DEFLATE/LZW) |
| 局部读取 | ❌ 必须全量下载 | ✅ HTTP Range 按需读取 |
| 多分辨率 | ❌ 单分辨率 | ✅ 内置金字塔 |
| 浏览器兼容性 | ❌ 需完整解析 | ✅ 部分读取,快速首屏 |
| 坐标系信息 | ✅ 内嵌 | ✅ 内嵌 |
4. 大地水准面与参考椭球面
4.1 两者的数学定义
参考椭球面(Ellipsoid) —— 数学模型:
x²/a² + y²/a² + z²/b² = 1
其中 a = 6,378,137.0 m(长半轴),b ≈ 6,356,752.3 m(短半轴),扁率 f = (a-b)/a ≈ 1/298.257223563。
大地水准面(Geoid) —— 物理模型:
是地球重力场的一个等位面,满足 W = 常数(W 是地球重力位)。它不是规则曲面,全球范围内与椭球面的偏差(Geoid undulation)范围大约是 -106m(印度)到 +85m(新几内亚)。
4.2 Geoid Undulation 的计算
给定一点的椭球高 h 和大地水准面高 N,正高(海拔)H 满足:
h = H + N
h > 0:点在椭球面上方N > 0:大地水准面在椭球面上方H:真正的海拔(高程基准面到点的距离)
在中国区域,Geoid undulation 典型值:
| 区域 | Ellipsoid Height (m) | Geoid Undulation (m) | Orthometric Height (m) |
|---|---|---|---|
| 北京 | 50 | -10 | 60 |
| 上海 | 5 | -5 | 10 |
| 拉萨 | 3700 | -30 | 3730 |
| 珠峰 | 8850 | -30 | 8880 |
WARNING
常见误区:很多初学者以为 GPS 的海拔就是海拔。实际上民用 GPS 输出的是 椭球高(Ellipsoid height),需要加上 Geoid undulation 修正才能得到正高。直接用 GPS 海拔做工程测量可能产生几十米的误差。
4.3 高程基准的演进
1980 年代以前: 地方水准面(各国自己定义)
│
▼
1984 年 ──► WGS84 椭球面(GPS 使用)
│
▼
1996 年 ──► EGM96 大地水准面模型(全球统一)
│
▼
2008 年 ──► EGM2008 高精度模型(分辨率 5')
│
▼
2018 年 ──► EGM2020 最新模型
5. Web Worker 异步地形管线
5.1 为什么必须用 Worker
3D 地形渲染的瓶颈在 CPU 端:
| 阶段 | 耗时 | 能否在主线程 |
|---|---|---|
| 下载瓦片 | 100-500ms | ✅ 用 fetch(异步) |
| 解码高程 | 5-20ms | ❌ 阻塞主线程 |
| 生成网格 | 10-50ms | ❌ 阻塞主线程 |
| 计算法线 | 5-15ms | ❌ 阻塞主线程 |
| 上传 GPU | 1-5ms | ✅ gl.bufferData |
如果在主线程同步执行解码和网格生成,16.67ms(60fps)内无法完成,必然掉帧。
5.2 Worker 通信模型
┌──────────────┐ ┌─────────────────┐
│ 主线程 │ │ Web Worker │
│ │ │ │
│ 1. fetch() │ │ │
│ │ │ │ │
│ ▼ │ │ │
│ ArrayBuffer │──────────►│ 2. 接收 ArrayBuffer │
│ │ Transfer │ │
│ │ (零拷贝) │ 3. 解码高程数据 │
│ │ │ (RGB→float) │
│ │ │ │
│ │ │ 4. 生成地形网格 │
│ │ │ (顶点+索引) │
│ │ │ │
│ 6. 接收结果 ◄───────────│ 5. Transfer 回主线程 │
│ (Float32Array)│ Transfer │ (Float32Array) │
│ │ (零拷贝) │ │
│ 7. 上传 GPU │ │ │
│ (bufferData)│ │ │
└──────────────┘ └─────────────────┘
5.3 Transferable Objects 零拷贝机制
关键代码模式:
// 主线程
const response = await fetch('terrain.png');
const arrayBuffer = await response.arrayBuffer();
// 把 ArrayBuffer 的所有权"转移"给 Worker
worker.postMessage({
type: 'decode',
buffer: arrayBuffer
}, [arrayBuffer]); // 第二个参数是 Transfer list
// ⚠️ 此后主线程不能再访问 arrayBuffer!
// Worker 线程
self.onmessage = (e) => {
const buffer = e.data.buffer; // 接收所有权
const uint8 = new Uint8Array(buffer);
// ... 解码逻辑 ...
const result = decodeTerrain(uint8);
// 把结果的所有权"转移"回主线程
self.postMessage({
type: 'decoded',
heights: result.heights,
vertices: result.vertices
}, [result.heights.buffer, result.vertices.buffer]);
};
IMPORTANT
Transferable Objects 的核心特性:
- 零拷贝:ArrayBuffer 在上下文间转移所有权,不复制数据
- 单向转移:转出后原上下文无法访问
- 适用类型:
ArrayBuffer、MessagePort、ImageBitmap、OffscreenCanvas - 不适用:普通对象仍用 Structured Clone(深拷贝,有性能开销)
5.4 错误处理与降级策略
高优先级 ──► 尝试加载高精度 COG (如 30m)
│
├── 成功 ──► 解码生成网格
│
├── 网络超时 ──► 降级到标准 PNG Terrain-RGB
│
├── Decode 失败 ──► 使用平均高程(0m)生成平面网格
│
└── Worker 崩溃 ──► 主线程兜底生成
低优先级 ──► 预加载低精度备用(如 90m SRTM)
可视化对比与动手实验
实验 1:高程编码方案可视化对比
假设同一区域的高程数据,三种编码方案的差异:
原始高程值(float32)
┌─────────────────────────────────┐
│ 15.3 18.7 22.1 25.4 │
│ 12.2 16.5 19.8 23.0 │
│ 8.9 11.3 14.6 17.9 │
└─────────────────────────────────┘
│
├──► RGB 编码 (PNG 存储)
│ ┌─────────────────────────────┐
│ │ 像素值: [153, 76, 0] │ ← 精度 0.1m
│ │ 像素值: [187, 119, 25] │ ← 整数部分 + 小数部分
│ └─────────────────────────────┘
│
├──► BIL 编码 (原始存储)
│ ┌─────────────────────────────┐
│ │ Float32: [15.3, 18.7, ...] │ ← 全精度
│ │ 每值 4 字节,无压缩 │
│ └─────────────────────────────┘
│
└──► COG 编码 (分块压缩)
┌─────────────────────────────┐
│ 块大小: 512×512 │
│ 压缩: DEFLATE │
│ 支持 HTTP Range 读取 │
└─────────────────────────────┘
实验 2:Geoid vs Ellipsoid 差异地图
在全球不同纬度,Geoid undulation 的典型分布:
| 纬度 | 经度 | Geoid Undulation (m) | 主要成因 |
|---|---|---|---|
| 60°N, 0°E | 斯堪的纳维亚 | +50~+60 | 冰期后地壳回弹 |
| 45°N, 75°E | 青藏高原 | -30~-50 | 地壳厚度大 |
| 0°, 0°E | 赤道大西洋 | -30~-40 | 地幔密度低 |
| 90°S | 南极 | +40~+50 | 冰盖重力 |
实验 3:Worker 管线性能对比
在 4 核 CPU、Chrome 浏览器下,加载 10 个高程瓦片:
| 方案 | 主线程耗时 | FPS 影响 | 总耗时 |
|---|---|---|---|
| 主线程同步 | 150ms(阻塞) | 掉帧 9 次 | 350ms |
| Worker + Transferable | 0ms(不阻塞) | 60fps 稳定 | 200ms |
| Worker + 结构化克隆 | 0ms(不阻塞) | 60fps 稳定 | 280ms |
结论:Worker + Transferable 是最优方案,避免了主线程阻塞和数据拷贝开销。
常见误区
WARNING
误区 1:RGB 编码精度不够 Mapbox Terrain-RGB 0.1 米精度对地形渲染完全够用,问题在压缩方式。用 JPEG 压缩会引入噪声,务必用 PNG(无损)。
WARNING
误区 2:把椭球高当海拔用 GPS 输出的直接是 WGS84 椭球高。不做 Geoid 修正,在中国东部可能差 5-10 米,西部可能差 30-50 米。精密测量必须用 EGM2008 修正。
WARNING
误区 3:Worker 里不能操作 DOM
Web Worker 没有 window 和 document 对象。如果尝试读取图片像素,要用 createImageBitmap + OffscreenCanvas,或者在主线程解码后 Transfer ArrayBuffer。
WARNING
误区 4:Transferable 后还能访问原对象
ArrayBuffer Transfer 后,原上下文的 ArrayBuffer 会被置空(byteLength === 0)。代码逻辑中要确保 Transfer 后不再访问原对象。
WARNING
误区 5:地形瓦片跟影像瓦片分辨率一样 影像瓦片分辨率通常是 256×256 或 512×512,但地形瓦片的”分辨率”指高程采样点数。一个 256×256 的 Terrain-RGB 瓦片实际只提供 256×256 个高程值,如果需要更平滑的地形,需要插值(如双线性插值)。
延伸阅读与自测
延伸阅读
- Mapbox Terrain-RGB — 官方文档:https://docs.mapbox.com/data/tileset-reference/mapbox-terrain-rgb-v1/
- OGC GeoTIFF — 标准规范:https://www.ogc.org/standards/geotiff
- COG 规范 — https://www.cogeo.org/
- EGM2008 大地水准面模型 — https://earth-info.nga.mil/
- Web Workers API — https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
- Transferable Objects — https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects
自测题
-
计算题:某点 RGB 值为
[130, 217, 35],用 Mapbox Terrain-RGB 公式解码,实际高程是多少?(提示:H = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1)) -
概念辨析:为什么大地水准面不是椭球面?从物理和数学两个角度解释。
-
方案设计:要为一个移动端 3D 地图应用设计地形系统,目标是在 3G 网络下 2 秒内加载初始视口地形。你会选择哪种高程编码方案?Worker 管线的关键设计点是什么?
-
精度分析:比较 RGB (0.1m)、BIL (float32 全精度)、COG (DEFLATE 压缩) 三种方案的精度、文件大小、解码复杂度,给出适用场景建议。
-
代码分析:以下代码有什么问题?如何修复?
// 主线程 const buf = await fetch('terrain.png').then(r => r.arrayBuffer()); worker.postMessage({ buffer: buf }); // 没有 Transfer console.log(buf.byteLength); // 预期输出什么?