返回知识库

GIS

地形与 Worker

地形与 Worker 封面
GISengine-webgpu地形Web 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 是数据模型,不是文件格式。实际存储高程数据时,有三类主流方案:

  1. RGB 编码:把高程值编码到 PNG/JPG 的三个颜色通道里。
  2. 二进制浮点:直接存 float32int16,文件是 .bil.flt.tif 等。
  3. 云优化 GeoTIFF (COG):分块压缩的 GeoTIFF,支持 HTTP Range 请求按需读取。

大地水准面 vs 椭球面:海拔到底从哪量起

这是地形领域最容易混淆的概念之一:

  • 椭球面(Ellipsoid):一个完美的数学椭球,WGS84 定义了它的长半轴和扁率。GPS 直接测出的坐标就在椭球面上。
  • 大地水准面(Geoid):一个虚构的”平均海平面延伸面”,是地球重力场的等位面。实际的海拔高度从这里量。
  • 两者差异:同一地点,椭球面高度 ≠ 大地水准面高度,差值叫 Geoid undulation(大地水准面起伏),在中国能差几十到一百多米。

NOTE

简单说:GPS 告诉你”我在椭球面上方 3000 米”,但你要的可能是”我在海平面上方 2840 米”——这中间的 160 米差值就是 Geoid undulation。

异步管线:地形计算为什么不能在主线程

3D 地图的地形数据量巨大,处理链长:

  1. 网络请求下载高程瓦片(几十 KB 到几百 KB)
  2. CPU 解码(RGB → float、BIL 解析、COG 分块解压)
  3. CPU 生成地形网格(从 heightmap 生成 vertex buffer)
  4. 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 通过以下设计支持流式读取

  1. 分块(Tiling):把大图切成 512×512 或 256×256 的小块
  2. 多分辨率金字塔:每个块存多个 LOD 级别
  3. 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 相比传统方案的优势

特性传统 GeoTIFFCOG
文件大小可压缩(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-1060
上海5-510
拉萨3700-303730
珠峰8850-308880

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❌ 阻塞主线程
上传 GPU1-5msgl.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 的核心特性

  1. 零拷贝:ArrayBuffer 在上下文间转移所有权,不复制数据
  2. 单向转移:转出后原上下文无法访问
  3. 适用类型ArrayBufferMessagePortImageBitmapOffscreenCanvas
  4. 不适用:普通对象仍用 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 + Transferable0ms(不阻塞)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 没有 windowdocument 对象。如果尝试读取图片像素,要用 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 个高程值,如果需要更平滑的地形,需要插值(如双线性插值)。

延伸阅读与自测

延伸阅读

  1. Mapbox Terrain-RGB — 官方文档:https://docs.mapbox.com/data/tileset-reference/mapbox-terrain-rgb-v1/
  2. OGC GeoTIFF — 标准规范:https://www.ogc.org/standards/geotiff
  3. COG 规范https://www.cogeo.org/
  4. EGM2008 大地水准面模型https://earth-info.nga.mil/
  5. Web Workers APIhttps://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
  6. Transferable Objectshttps://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects

自测题

  1. 计算题:某点 RGB 值为 [130, 217, 35],用 Mapbox Terrain-RGB 公式解码,实际高程是多少?(提示:H = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1)

  2. 概念辨析:为什么大地水准面不是椭球面?从物理和数学两个角度解释。

  3. 方案设计:要为一个移动端 3D 地图应用设计地形系统,目标是在 3G 网络下 2 秒内加载初始视口地形。你会选择哪种高程编码方案?Worker 管线的关键设计点是什么?

  4. 精度分析:比较 RGB (0.1m)、BIL (float32 全精度)、COG (DEFLATE 压缩) 三种方案的精度、文件大小、解码复杂度,给出适用场景建议。

  5. 代码分析:以下代码有什么问题?如何修复?

    // 主线程
    const buf = await fetch('terrain.png').then(r => r.arrayBuffer());
    worker.postMessage({ buffer: buf });  // 没有 Transfer
    console.log(buf.byteLength);  // 预期输出什么?