把整个国家变成体素:一条地理数据管线的实践
一条体素管线如何把瑞士开放地理数据变成1:1的Minecraft世界,并讲解分块、细节层级与空间数据流式加载的经验。

瑞士政府的开放数据门户上,有一份埃格尔北壁的高程图,采样精细到你几乎能摸到岩石的纹理。一位开发者把这类文件喂进体素管线,最终做成了一款可玩的产品:整个瑞士被还原为1:1的Minecraft世界,每个方块代表一米,马特洪峰的高度和陡峭程度都与真实一致。下载包压缩后有24 GB,光是免费试玩区域就约2400平方公里。底层工程——分块、细节层级、按需生成——堪称如何处理海量空间数据而不让系统崩溃的教科书级范例。
问题所在:整个国家的体素装不进内存
我们来算一笔账。瑞士国土面积41,285平方公里。按每平方米一个方块计算,还没挖一个洞,就有413亿个地表柱体。假设每根柱体只有100个方块的深度——岩石、泥土、湖床——总数就达到四万亿个方块。即使每个方块只占一个字节,原始几何数据也有4 TB。但最终的世界压缩后只有24 GB下载包,这说明一件重要的事:几乎没有任何几何数据是以几何形式存储的,它存的是一份“配方”。
垂直方向也让问题更棘手。原版Minecraft的世界建造高度上限是384格。瑞士从马焦雷湖畔的海拔193米,一直到杜富尔峰的4634米,跨度达4441米,还没挖矿就已经超过原版上限十一倍多。因此这个世界需要Cubic Chunks模组,它用16×16×16的立方体取代了Minecraft基于柱体的存储方式,想堆多高就堆多高。这个模组只能在Minecraft Java 1.12.2上稳定运行,所以这个至今仍在发布的项目,锁定的还是好几年前的游戏版本。
内存是第三道墙。玩家第一次进入某个区域时,从源数据现场生成方块非常吃算力:项目要求至少16 GB内存,测试机型包括搭载32 GB内存的Ryzen 7 7800X3D。任何在游戏或GIS领域与流式加载预算搏斗过的人,都会认出这种局面。这和显存不够用时的权衡如出一辙——装不下全部,于是你必须非常擅长决定“此刻要留住什么”。
失败的尝试一:提前把整个世界烘焙出来
最直接的办法是一次大型离线任务:读取地理数据,输出Minecraft的区域文件(Anvil格式将世界按512×512方块划分为一个区域),再打包压缩发布。丹麦在2014年确实做过类似的事:丹麦地理数据局把全国做成了一张Minecraft地图。这是开放数据领域的里程碑式演示,证明了国家规模的地图是可行的。那为什么不这么做呢?
因为提前烘焙的结构性成本会随着野心一起膨胀。每个区域的每个方块都必须在玩家进入之前生成、写入并压缩,这意味着每次数据更新都要完整重建,并重新分发几十GB的文件。每位玩家都要下载大量他们永远不会去的地形——而41,000多平方公里里的大部分,恐怕根本没有人会去。最糟的是,一旦发布,烘焙好的世界就冻结在数据快照的那一刻。它是一次性产物,除非你重新生成并重新分发全部内容,否则无法改进,而用户则要把整个星球重新下载一遍。
失败的尝试二:一张高程图,一种分辨率
第二个诱惑是标准化:把所有输入都转换成单一的全局栅格、统一分辨率,这样管线只需一条代码路径。瑞士境内你有高精度地形模型swissALTI3D;国境之外则是哥白尼GLO-30 DEM,每30米一个高程点。如果为了统一把所有数据降采样到30米,埃格尔北壁就会变成一堆阶梯状的沙堆。如果把所有数据升采样到可用的最高分辨率,存储和处理量会爆炸,而边境地区依然没有真实数据——你只是把无知升采样了而已。
异构性对语义的冲击和对高程一样大。swissBUILDINGS3D只覆盖瑞士,因此生成器可以在真实建筑轮廓上生成简化的体素建筑——但只限于国境之内。在日内瓦附近跨过国境后,完全没有建筑图层,于是管线只能采取唯一合理的兜底方案:在本该有房子的地方种树。水体需要自己的数据源(湖床用swissBATHY3D),冰川、森林和葡萄园也一样。并不存在单一的输入文件。地理数据里从来都没有。
这个教训远不止适用于Minecraft:分辨率是每个瓦片的元数据,而不是全局常量。一条假设输入统一的管线,要么浪费精度,要么凭空编造精度——而凭空编造的精度比诚实的粗糙更糟,因为从内部根本无法分辨两者的区别。

真正有效的做法:分块管线与按需生成
瑞士项目之所以成功,是因为它反转了“全部烘焙”的直觉。你下载的不是一个完成的世界,而是一个世界包——预处理过的地理数据加上定制生成器——游戏会在你第一次真正踏入某处时才构建其中的方块。新区域的首次访问需要几分钟的运算,之后它就像其他Minecraft世界一样,以缓存区块的形式保存下来。计算成本是按访问次数支付的,而不是预先支付,你也永远不会为没人踩过的地形生成数据。瓦片是一切的基本单位:存储、缓存、生成和故障隔离都以它为准。
分块以LV95为锚点,这是瑞士官方的测量坐标系。每个瓦片记录了哪些数据集覆盖它以及分辨率是多少,而测量坐标到方块坐标的转换只是一个纯粹的仿射变换。项目甚至公开了这个公式,方便你从国家地图查看器中获取坐标,然后在游戏里传送到对应位置:
def lv95_to_block(east: float, north: float, height: float) -> tuple[int, int, int]:
"""Convert Swiss LV95 survey coordinates to Minecraft block coordinates."""
x = int(east - 2_480_000) # strip the LV95 easting offset
z = int(1_300_000 - north) # Minecraft's z axis grows southward
y = int(height) # one block = one meter above sea level
return x, y, z
# The summit of the Jungfrau, 4,158 m:
print(lv95_to_block(2_640_188, 1_154_072, 4158))
# -> (160188, 4158, 145928)
# x and z match the in-game teleport command exactly; the game just
# drops you a few meters higher so you land on the summit, not in it.
在分块网格之上,生成过程是对每一列逐层计算的图层栈。基础高程来自swissALTI3D。地表覆盖——水体、森林、道路、铁路、堤坝、雪崩防护设施——来自swissTLM3D。湖床来自swissBATHY3D,树高来自swissSURFACE3D,冰川轮廓来自可追溯到1850年的GLAMOS冰川清单,冰层厚度则来自苏黎世联邦理工学院的数据集,因此阿莱奇冰川是真正雕刻出来的冰体,而不是披在岩石上的一张白色贴图。草甸、岩屑坡和葡萄园来自联邦栖息地地图;甚至苏黎世的行道树也来自该市自己的树木地籍。总共大约十五个开放数据集,全部免费使用——瑞士国家测绘局在2021年开放了数据,包括商业用途。
体素世界并不是地形的图片,而是你的管线在每平方米上做出的数百万个微小决定的缓存。世界的质量,正是这些决定的质量。
细节层级是数据问题,而不是渲染技巧
Minecraft默认的渲染距离只能看到几百米。项目的截图里,阿莱奇冰川却能绵延数公里地向远方弯曲——这种远景(“Fernsicht”)是游戏在首次启动时一次性构建的低精度表示。这是典型的细节层级(LOD)方案,但关键决定在于LOD存放在哪里:它是在数据导入时从源数据计算出来的,而不是由渲染器临时凑出来的。
如果你做过地图瓦片金字塔,你就明白这笔数学账。每一级缩放在两个轴上都把分辨率减半,因此完整金字塔的成本约为基础层的 1 + 1/4 + 1/16 + … ≈ 4/3 倍——为了在每个尺度上都保持流畅,需要承担约33%的存储开销。体素地形的LOD就是把这个金字塔竖起来:远处是粗糙的缩放级别,近处是精细的级别,真正的难点在于选择一套键值方案,让你能快速取到正确的单元。我们在geohash与真正的空间索引之间的权衡中也遇到过同样的问题——键的选择决定了下游所有环节的查询成本。
我最喜欢的LOD决策其实根本不是视觉上的。免费下载包是一片2400平方公里的区域,即Jungfrau-Aletsch联合国教科文组织遗产区:埃格尔峰、门希峰、少女峰,以及阿尔卑斯山最大的冰川——它与付费的全国版本运行的是完全相同的管线。这就是把金丝雀发布变成产品功能。它只有1.1 GB而不是24 GB,无需注册,而且如果它能在你的机器上跑,完整地图也一定能跑。如果你在做大型数据产品,直接照搬这个思路:你的免费版应该是最难地形的一个有代表性的切片,而不是最简单功能的残缺演示。

在浏览器中流式加载空间瓦片
同样的克制也体现在项目朴素的网页上。它的概览地图提供一个滑块,可以对比体素世界与真实的航拍照片——而这些影像根本不来自项目自己的服务器。只有当你拖动滑块时,浏览器才会直接从瑞士国家测绘局的服务中拉取SWISSIMAGE瓦片。按需加载、直连源站、对缓存友好:大量带宽由专为此类用途存在的公共基础设施承担,而项目自己的托管账单几乎为零。
网络上的瓦片寻址已经是个成熟的问题,如果你涉足地理空间工作,值得熟练掌握:
import math
def latlon_to_tile(lat: float, lon: float, zoom: int) -> tuple[int, int]:
"""Standard slippy-map (XYZ) tile addressing used by OSM and most WMTS fronts."""
n = 2 ** zoom
x = int((lon + 180.0) / 360.0 * n)
lat_rad = math.radians(lat)
y = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
return x, y
# At zoom 15, any point on Earth lands in one of 1,073,741,824 tiles.
# Fetch the few that intersect the viewport. Skip the other billion.
比公式更重要的是两个细节。第一,请求要等到用户意图明确之后才发出——也就是滑块真正被拖动的时候——这既是带宽优化,也是对一项以公共经费运行的免费公共服务的礼貌。第二,版权信息随瓦片一同传递:页面在地图旁直接标注了SWISSIMAGE、swissBOUNDARIES3D和swissNAMES3D。当你的产品是开放数据的衍生作品时,在展示处注明出处就不是装饰,而是许可协议在按设计发挥作用。
从瑞士管线中可以借鉴什么
先说反方观点,因为实践者提出了不少好意见。像Build the Earth这样的手工项目,动员了数千名志愿者来建模真实建筑,没有任何自动化管线能达到那样的精细度——经过体素化的swissBUILDINGS3D建筑轮廓只是粗略的近似,如果你关心的是建筑,那么只导出地形可能是更干净的产物。这点说得有道理。但覆盖面与精细度直接相互制约:自动化今天就能覆盖41,285平方公里,手工建造则最终能做出一座精美的老城。我的立场是:对于数据产品而言,带有公开出处的自动化生成更胜一筹,而手工打造的精品地标可以在它们真正重要的地方叠加上去。
而出处说明恰恰是大家最容易忽略的部分。这个项目发布了一份已知缺陷清单:边境附近的空白区域(只有30米的哥白尼网格可用)、斑驳或平坦的区域、道路和桥梁错误或缺失,以及以抽样而非逐米方式完成的质量检查。这份清单确实发挥了作用——它把“你的世界这里是错的”这类漏洞报告,转化为“是的,这份数据集到此为止”的文档说明。每一个衍生数据产品都应该公布它的出处矩阵和已知错误清单。那些不这样做的产品并不是更准确,只是不够诚实。
- 一切都要分块。瓦片是存储、缓存、生成和故障隔离的基本单位。整块式结构无法优雅降级。
- 导入时绝不降采样。保留每个数据源的原始分辨率,在渲染时再重采样。
- 按需生成。当世界大到超出任何人的游玩时间时,按访问次数计算的成本就胜过预先烘焙。
- 在导入时构建LOD金字塔。预留约33%的额外开销,换来在任何距离上都平滑的缩放。
- 发布金丝雀切片。一个免费、有代表性的最难数据切片,胜过任何规格参数表。
- 公布出处与已知缺陷。它们是产品的一部分,而不是为产品道歉。
完整版售价15瑞士法郎,包含今后所有版本——而且出于法律原因,仅面向瑞士居民销售,对于一件纯数字商品来说,这是个颇为有趣的限制。但真正的产品并不是这张地图,而是一个证明:单个人凭借公开数据、分块管线和诚实的文档,就能比大多数团队的地理空间基础设施更快地交付成果。你的数据集大概没有41,285平方公里那么大。你的借口也同样更小。
