Three.js教程
Cesium
cesium-3dtiles
cesium-3dtiles

用 AI 写了个脚本,把 13 万栋北京建筑全渲染进了 Cesium

城市建筑白模是 Cesium 里很常见的一种可视化效果:一栋栋灰白色的几何体铺满城市,没有贴图,没有材质,但能让人一眼看清城市的空间格局。

要在 Cesium 里跑起来这个效果,需要经历两段路:一是找到原始数据并完成格式转换,二是在前端正确加载和渲染。本文从最原始的 OpenStreetMap 数据出发,经过一段 Python 脚本的加工,最终在 Cesium 里加载出北京的建筑白模,把这两段路完整走一遍。

最终效果如下:

北京建筑白模效果

前半部分聚焦 Cesium 这一侧——如何加载 3D Tiles,以及底图、相机、样式等常用 API 的用法;后半部分回到数据生产,看 OSM 原始数据是怎么一步步变成 Cesium 可以加载的格式的。

案例仓库:https://github.com/calmound/cesium-osm-building (opens in a new tab)

这个仓库已经包含所有必要文件,clone 下来即可直接运行,不需要自己跑转换脚本:

在 Cesium 里加载建筑白模

案例启动

clone 仓库之后,进入前端工程目录:

cd frontend-demo
npm install
npm run dev

打开终端输出的本地地址(通常是 http://localhost:5800),将相机高度往下移动到合适的位置,就能看到北京建筑白模。

Cesium 加载的是什么格式

先弄清楚一件事:Cesium 不认识 .osm.pbf,也不认识 GeoJSON 或 Shapefile。

Cesium 只认识 3D Tiles

3D Tiles 是 Cesium 生态里的空间数据格式,专门为大规模三维场景设计。你可以把它理解为一套"分级切片"的三维数据包——类似于地图服务里的瓦片,但存储的是三维几何体。

一套 3D Tiles 的结构是这样的:

  • tileset.json:入口文件,描述整个数据集的空间组织方式
  • *.b3dm:实际的瓦片文件,每个文件包含一批三维建筑的几何数据

所以 Cesium 加载建筑白模的过程,本质上是:读取 tileset.json,按需请求对应的 b3dm 文件,渲染成屏幕上的几何体。

OSM 数据是什么

OpenStreetMap(OSM)是一个由全球志愿者协作维护的开放地图数据库,任何人都可以编辑和使用其中的地理数据。

OSM 的数据模型基于三种基本要素:

  • Node(节点):带经纬度的点,可以是单独的兴趣点,也可以是构成 Way 的顶点
  • Way(路径):由一组节点连成的线或面,道路、建筑轮廓都属于这一类
  • Relation(关系):多个 Way 或 Node 的组合,用于表示复杂要素,比如带内环的建筑多边形

每个要素都可以附加任意标签(tag),采用键值对形式,建筑数据长这样:

building        = yes
building:levels = 10
height          = 32
name            = 国贸大厦

.osm.pbf 是 OSM 数据的二进制压缩格式(Protocol Buffer Format),相比 XML 体积小很多,是 Geofabrik 等平台分发 OSM 数据的标准格式。一个北京区域的 .osm.pbf 包含了这座城市几乎所有地物——道路、建筑、水系、公园等。

但这套数据是二维的。它记录了建筑的地面轮廓和属性标签,没有三维几何。要把它变成 Cesium 可以渲染的建筑白模,需要一层加工,第二部分专门处理这件事。


了解了 Cesium 所需的数据格式和 OSM 原始数据的结构之后,接下来结合示例工程 frontend-demo/src/main.js (opens in a new tab) 的代码,逐步介绍 Cesium 加载建筑白模涉及的核心 API。

初始化 Viewer

Cesium 里一切的起点是 Viewer,它负责创建渲染容器、管理相机、底图、场景等所有东西。

const viewer = new Viewer("cesiumContainer", {
  animation: false,
  baseLayerPicker: false,
  fullscreenButton: false,
  geocoder: false,
  homeButton: true,
  infoBox: false,
  sceneModePicker: false,
  selectionIndicator: false,
  timeline: false,
  navigationHelpButton: false
});

第一个参数是 HTML 容器的 id,第二个参数是配置项。默认情况下 Cesium 会在页面上渲染一大堆内置 UI 控件(时间轴、动画、地理搜索框等),对于只展示建筑白模的场景来说这些都用不到,所以这里把多余的控件全部关掉,只保留了 homeButton(返回初始视角的按钮)。

创建 Viewer 之后,可以进一步调整场景的细节:

// 关闭深度测试(防止白模被地形遮挡,地形起伏不大时常用此设置)
viewer.scene.globe.depthTestAgainstTerrain = false;
 
// 大气层亮度微调,让画面不那么曝
viewer.scene.skyAtmosphere.brightnessShift = -0.1;
 
// 限制最大俯仰角,防止相机翻转到地球背面
viewer.scene.screenSpaceCameraController.maximumTiltAngle = CesiumMath.toRadians(89);

加载 3D Tiles

把一套 3D Tiles 加入场景,核心代码如下:

const tileset = await Cesium3DTileset.fromUrl("/tileset_output/tileset.json", {
  maximumScreenSpaceError: 64
});
viewer.scene.primitives.add(tileset);
  • fromUrl 是异步方法,它会请求 tileset.json 并解析出整个数据集的空间结构
  • maximumScreenSpaceError 控制 tile 的加载精度:值越小,加载越精细,性能消耗越高;值越大,加载越粗糙,但性能更好。64 是一个适合城市白模概览的折中值
  • viewer.scene.primitives.add(tileset) 把数据集注册进场景。Cesium 里的 primitives 是直接渲染三维几何体的容器,3D Tiles 走的就是这条路

为什么用 primitives 而不是 entities

Cesium 有两套添加内容的 API:entities 适合管理少量有属性的地理要素(比如标注点、路线);primitives 适合大规模几何渲染,性能更高。建筑白模有十万级别的要素,必须走 primitives

底图加载与切换

Cesium 的底图系统通过 viewer.imageryLayers 管理,它是一个图层栈——可以叠加多个底图,也可以随时增删。

本示例工程里预置了两套底图:

const imageryProviders = {
  // Cesium Ion 的卫星影像底图
  ion: await createWorldImageryAsync(),
  // CartoDB 的浅色矢量底图(通过标准 XYZ 瓦片 URL 接入)
  light: new UrlTemplateImageryProvider({
    url: "https://basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",
    credit: "CARTO"
  })
};

createWorldImageryAsync() 是 Cesium 内置的卫星影像,分辨率高,适合配合建筑白模做整体鸟瞰。UrlTemplateImageryProvider 则是通用的 XYZ 瓦片接入方式,只要提供符合 {z}/{x}/{y} 格式的底图服务地址就能使用,市面上大多数地图服务都支持这种格式。

切换底图时,先清空当前图层,再加入新的:

viewer.imageryLayers.removeAll();
viewer.imageryLayers.addImageryProvider(provider);

示例工程里还有第三种模式——"关闭"底图,也就是完全隐藏地球:

viewer.scene.globe.show = false;

关掉地球之后,背景变成纯色,建筑白模的颜色也会同步调亮,形成高对比的浏览效果,适合截图或仔细查看建筑细节。

alt text

相机控制

Cesium 里的相机定位不像普通地图那样只有经纬度,它是真正的三维相机,需要指定位置、朝向和俯仰角。

最常用的方式是 camera.flyTo

await viewer.camera.flyTo({
  destination: Cartesian3.fromDegrees(116.397, 39.908, 1800),
  orientation: {
    heading: CesiumMath.toRadians(20),   // 方位角(偏北多少度)
    pitch: CesiumMath.toRadians(-38),    // 俯仰角(负值表示向下看)
    roll: 0
  },
  duration: 1.8  // 飞行动画时长(秒)
});

Cartesian3.fromDegrees(经度, 纬度, 高度) 把地理坐标转换成 Cesium 内部的三维笛卡尔坐标,高度单位是米。

示例工程里预置了三个相机位置:

| 预设 | 高度 | 用途 | |||| | 核心区 | 1800m | 俯瞰北京城区,建筑密集 | | 近景 | 650m | 接近街道高度,感受单栋建筑体量 | | 远景 | 12000m | 整体鸟瞰,看城市空间分布 |

alt text

另外一种常用方式是 flyToBoundingSphere,它根据数据的实际空间范围自动计算飞行目标,不需要手动指定坐标:

await viewer.camera.flyToBoundingSphere(
  tileset.boundingSphere,
  {
    duration: 1.8,
    offset: new HeadingPitchRange(
      CesiumMath.toRadians(18),   // heading
      CesiumMath.toRadians(-38),  // pitch
      range                       // 距离包围球边缘的距离
    )
  }
);

这在数据加载完成后首次定位时很好用,因为不需要提前知道数据在哪里。

白模样式与调试

给白模设置颜色

3D Tiles 支持通过 Cesium3DTileStyle 统一设置样式,颜色用 CSS 颜色字符串表示:

tileset.style = new Cesium3DTileStyle({
  color: "color('#dbe4ec', 1.0)"  // 浅蓝灰色,第二个参数是不透明度
});

示例工程里在切换底图时也会同步调整白模颜色——普通底图用浅灰蓝,关闭底图后用更亮的近白色,让白模在不同背景下都有合适的对比度。

调试 tile 结构

开发过程中排查数据或渲染问题时,两个调试属性很常用:

// 显示每个 tile 的三角面网格
tileset.debugWireframe = true;
 
// 显示每个 tile 的空间包围盒
tileset.debugShowBoundingVolume = true;

线框模式可以直观看出每个 tile 包含多少几何,包围盒模式可以看出 tile 的空间切分方式,以及哪些 tile 当前在相机视锥内被加载。

alt text

监听加载事件

3D Tiles 是流式加载的,Cesium 提供了几个事件可以监听加载进度:

// 初始视角内的 tile 加载完成
tileset.initialTilesLoaded.addEventListener(() => { ... });
 
// 所有当前请求的 tile 加载完成
tileset.allTilesLoaded.addEventListener(() => { ... });
 
// 某个 tile 加载失败
tileset.tileFailed.addEventListener((event) => {
  console.error(event.url, event.message);
});

tileFailed 在排查白模加载不完整的问题时很有用,通常是路径错误或文件缺失导致的。

第一部分小结

Cesium 这一层只负责"显示"。核心的几个概念:

  • Viewer 是整个场景的容器
  • imageryLayers 管理底图,支持随时切换
  • camera.flyTo 控制相机飞行,需要指定位置和朝向
  • 3D Tiles 通过 primitives.add 加入场景,流式按需加载
  • Cesium3DTileStyle 控制白模外观

只要有一套格式正确的 3D Tiles,以上这些就足以把它展示出来。

接下来的问题是:这套 3D Tiles 是怎么从 OSM 原始数据变出来的?

从 OSM 数据到 3D Tiles

数据从哪里来

本教程使用的原始数据https://www.geofabrik.de/data/download.html来自 (opens in a new tab) Geofabrik:https://www.geofabrik.de/data/download.html。 (opens in a new tab)

Geofabrik 是一个专门提供 OpenStreetMap 数据导出的服务,数据按国家和地区切分,可以免费下载。

北京区域的下载页面:Geofabrik Beijing:https://download.geofabrik.de/asia/china/beijing.html (opens in a new tab)

alt text

用 AI 生成转换脚本

完成这个转换任务,有不少现成工具可以用,比如 py3dtilesosm2worldFME 等,各有侧重。但去评估和学习一套新工具本身就要花时间。这里选择了另一条路:直接让大语言模型来做。

整个过程分三步。

第一步:让 AI 分析数据

beijing-260327.osm.pbf 下载到目录后,先让 AI 分析这份数据的结构和内容。

(AI 对话截图:目录下的数据分析)

AI 扫描出这份 PBF 文件包含五类 OSM 图层,其中 multipolygons 图层统计到建筑面约 131,046 个,并逐一列出了各建筑类型的数量分布——yes 类型最多有 88,903 个,apartments 有 14,929 个,以此类推。数据的边界范围、字段覆盖情况也都给出来了。有了这份分析,后续怎么处理心里就有数了。

第二步:问 AI 转换方案

接着追问:把这份数据转成 3D Tiles 用于 Cesium 加载,要怎么做?

(AI 对话截图:转换方案说明)

AI 直接指出了这份数据的现实情况:13.1 万个建筑面,但真正带 height 字段的只有约 1733 个,带 building:levels 的约 1.9 万个。大部分建筑没有高度信息,因此不能直接生成高质量 3D Tiles,需要先做高度推断。

给出的方案是:提取建筑底面 → 推断高度 → 按网格切片 → 每个 tile 挤出 glb → 生成 tileset.json。并主动提出可以直接生成一套可运行脚本。

第三步:让 AI 生成脚本

确认方案后,给出具体要求:使用 Python 准备转换脚本,执行时需要显示进度。

(AI 对话截图:生成 Python 脚本)

AI 直接生成了完整的转换脚本,包含各阶段的进度输出。整个对话从数据分析到拿到可运行脚本,没有查任何工具文档。

脚本完整代码见仓库:osm_buildings_to_3dtiles.py (opens in a new tab),可以直接取用。

数据加工的五个步骤

整条转换链路可以拆成五步:

第一步:提取建筑面

原始 .osm.pbf 里有道路、水系、绿地等各种要素。脚本用 ogr2ogr(GDAL 的命令行工具)从中筛选出带 building 标签的多边形,输出为 GeoJSON 格式,存到 work/buildings.geojsons

第二步:整理建筑属性

每个建筑要素保留核心字段:heightbuilding:levelsbuilding(建筑类型)等,过滤掉无效几何。

第三步:补全建筑高度

OSM 数据里的建筑并不都带高度信息——有的有精确的 height,有的只有楼层数,有的什么都没有。脚本采用分层策略补全:

  1. 优先使用 height 字段(单位:米)
  2. 没有 height 就用 building:levels × 3.2 估算
  3. 两者都没有,按建筑类型给默认值(apartments 约 18m,commercial 约 24m,house 约 9m)

这是合理的近似,不是精确值,但对于体量白模来说足够用。

第四步:挤出三维几何

把每个建筑的二维底面多边形,沿垂直方向拉伸到对应高度,生成封闭的三维 mesh。

第五步:打包成 3D Tiles

把所有建筑按地理位置切分成若干空间格子,每个格子打包成一个 b3dm 文件,再生成统一的 tileset.json 入口。

切成多个 tile 而不是一个大文件,是 3D Tiles 格式的核心设计:相机只看到哪块区域,就只加载那块区域的数据,不需要把整个城市的几何都塞进内存。

环境准备

安装 GDAL

脚本的第一步(提取建筑面)依赖 ogr2ogr,它是 GDAL 的一部分。

macOS:

brew install gdal
ogr2ogr --version  # 确认安装成功

Linux(Ubuntu/Debian):

sudo apt-get install gdal-bin

Windows:建议通过 OSGeo4W (opens in a new tab) 安装。

准备 Python 环境

脚本需要 Python 3.9 及以上版本。在仓库根目录创建虚拟环境并安装依赖:

python3 -m venv .venv

macOS / Linux:

.venv/bin/python -m pip install -r data-pipeline/requirements.txt

Windows:

.\.venv\Scripts\python -m pip install -r data-pipeline/requirements.txt

依赖清单在 requirements.txt

执行转换

环境准备好之后,运行:

macOS / Linux:

cd data-pipeline
../.venv/bin/python osm_buildings_to_3dtiles.py beijing-260327.osm.pbf --output tileset_output --work-dir work --overwrite

Windows:

cd data-pipeline
..\.venv\Scripts\python osm_buildings_to_3dtiles.py beijing-260327.osm.pbf --output tileset_output --work-dir work --overwrite

脚本会依次输出阶段进度:

Extracting building polygons...
Normalize...
Tiles...
Writing tileset manifest...

跑完之后,tileset_output/ 里就是 Cesium 可以直接加载的数据。

转换结果

当前仓库包含的是北京 2026 年 3 月底快照的转换结果:

  • 处理建筑要素:131,046
  • 生成 b3dm tile:536

详细摘要可以在 /tileset_output/summary.json:里查看。

第二部分小结

整条链路用一句话概括:

从 OSM 地图数据中提取建筑轮廓,补全高度,挤出为三维几何,切片打包成 Cesium 可加载的 3D Tiles。

三层关系:

输入层   →   .osm.pbf(二维地图要素)
加工层   →   提取、补高、挤出、切片
输出层   →   tileset.json + b3dm(三维瓦片数据)

Cesium 只和输出层打交道,不关心加工过程。只要输出层格式正确,它就能显示。

后续可以怎么扩展

这条流程跑通之后,有三个方向可以继续:

优化视觉:按高度或建筑类型给白模着色,增加轮廓线,让体量层次更清晰。Cesium3DTileStyle 支持基于属性字段写条件表达式,可以实现相当灵活的样式控制。

优化性能:调整 tile 的切分粒度,降低每个 tile 的三角面数,或者引入 LOD(Level of Detail)让远景加载更粗糙的版本。

提升数据质量:OSM 的高度数据本身不完整,可以引入其他高度数据源(比如建筑数据库或遥感数据)做补全,或者保留更多建筑语义字段用于后续样式处理。

参考链接