我第一次接触Cesium,是被一个数字孪生项目硬逼的。当时打开官网,满屏英文文档,Quick Start里new一个Viewer,切到Sandcastle又全是代码,看了一个小时只觉得头晕。后来被项目追着跑了两个月,回头再读官网,才发现文档其实写得很清楚,只是缺一个“中译中”的过程——把英文术语翻译成人话,再拿真实需求去验证。这篇教程就想做这件事:把Cesium官网的核心概念、入门路径和常见坑,用我自己实操过来的理解重新讲一遍,适合刚接触三维GIS、要做数字孪生或城市可视化、又被官网英文劝退的前端和GIS开发同学。
1. 官网在讲什么:先把这个地球引擎的家底摸清楚
1.1 Cesium到底是一个“地图库”还是“游戏引擎”
官网对Cesium的定义是:A geospatial 3D mapping platform for creating virtual globes。翻译过来的意思是“一个用来创建虚拟地球的地理空间三维地图平台”。这句话信息密度很高,但新手容易忽略几个关键词:geospatial、3D、virtual globe、platform。它不是Leaflet那种二维瓦片地图的简单升级,而是一个自带坐标系、时间轴、相机系统、数据源管理的WebGL引擎。
我用一个不太严谨但很实用的理解:Cesium和Three.js最大的区别在于——Three.js给你一个空白的3D场景,坐标单位多是米,场景内容全靠自己搭;Cesium给你一个半径6378137米的地球,坐标是经纬度和海拔,场景里已经包含地球椭球体、地形、大气、太阳和相机。你在Cesium里写代码,本质上是在处理“球面坐标、相机视野、时间驱动、数据调度”这些事。所以入门第一课不是背API,而是先把“地球是场景主体”这个思维立起来。
有了这层理解,再去看官网的Viewer、Scene、DataSource文档,就不会觉得它们是一堆平级的类了。官网经常说“Create a Viewer”,但从不强调Viewer是什么。在我看来,Viewer就是一个“开箱即用的地球应用外壳”,它把界面、渲染、数据管理和基础控件都打包好,让你一行代码看到地球。而真正干活的核心,在Viewer背后的Scene和Globe里。
1.2 从“虚拟地球”到“数据中心”:Cesium的顶层设计
官网Quick Start里你会反复看到这几个大写词汇:Viewer、Scene、Globe、Camera、DataSource、Clock。第一遍看英文文档,很容易误以为它们是互不相干的组件。我后来才想明白,它们其实是一条从用户界面到渲染内核的链路。
- Viewer:最外层容器,负责把Cesium界面和widgets(动画控件、时间轴、图层选择器、信息框)打包起来;
- Scene:真正的渲染管理器,所有可见对象都在Scene里被绘制,它掌握全局光照、雾效、透明度和渲染顺序;
- Globe:Scene内置的一个地球体,管理影像图层、地形、大气层;
- Camera:描述当前相机的位置和朝向,决定“你从哪个角度看到哪里”;
- DataSource:数据源管理集合,把GeoJSON、KML、CZML、3D Tiles统一到一套接口下;
- Clock:Cesium世界的“时间控制器”,所有动态效果都受它驱动。
官网Tutorials里Quick Start只让你new Cesium.Viewer('cesiumContainer'),但如果你不理解这条链路,后面读到viewer.dataSources.add会困惑“到底加到哪儿去了”,看到viewer.clock又会疑惑“这和系统时间有什么关系”。我的习惯是把这套东西映射成一个舞台场景:Viewer是整个剧院,Scene是舞台,Globe是舞台中央的地球模型,Camera是观众席,DataSource是道具组,Clock是灯光师。这个类比不严格,但撑过入门期完全够用。
真正入了门之后,你会发现官网还有一个更底层的CesiumWidget,它不带任何控件,只帮你创建一个Scene和渲染循环。早期不必深挖,知道Viewer是Widget的“豪华版”就行。
2. 搭第一个地球,别一上来就被token卡住
2.1 一条script标签跑起来
现在官网推荐用npm包和import方式引入,这是工程化正路,但对刚入门的人并不友好。我建议第一个Demo用CDN方式,把注意力放在“看到地球”这个目标上。新建一个html文件,内容如下:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>Cesium First Globe</title> <link href="https://cesium.com/downloads/cesiumjs/releases/1.119/Build/Cesium/Widgets/widgets.css" rel="stylesheet"> <style> html, body, #cesiumContainer { width: 100%; height: 100%; margin: 0; padding: 0; } </style> </head> <body> <div id="cesiumContainer"></div> <script src="https://cesium.com/downloads/cesiumjs/releases/1.119/Build/Cesium/Cesium.js"></script> <script> const viewer = new Cesium.Viewer('cesiumContainer'); </script> </body> </html>从官网下载的release包解压后,Build/Cesium目录里同样有Cesium.js和Widgets文件夹,完全可以下载到本地自己引用。我不建议直接复制网上某篇老博客里的CDN地址,版本太老会导致API对不上。
2.2 Access Token:为什么官网Demo经常黑屏或只有天空
很多教程为了图省事,直接new Viewer()就不管了,然后你会发现画面卡在天蓝色或者黑色,控制台飘红字。这里头最大的坑就是Cesium Ion的Access Token。
Cesium Ion是Cesium官方的在线服务,托管了全球影像、全球地形、大量示例3D Tiles和卫星影像。官网默认Demo会去Ion拉取这些资源,而Ion要求请求携带token。没配token或者token瞎填,影像图层就会加载失败,表现在页面上就是:天空有、地球黑、或者干脆一片灰。这其实不是Cesium库本身的问题,是网络请求和鉴权的问题。
入门时有两个选择:
- 注册Cesium Ion账号,在控制台创建一个token,然后设置:
Cesium.Ion.defaultAccessToken = 'your_token_here';- 完全绕开Ion,改用其他免费影像源,比如ArcGIS World Imagery:
const viewer = new Cesium.Viewer('cesiumContainer', { imageryProvider: new Cesium.ArcGisMapServerImageryProvider({ url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer' }), baseLayerPicker: false });这里要提醒一句:Cesium新版本里imageryProvider这个构造函数选项已经开始被调整,如果你用的是最新版,建议打开官方文档看当前推荐的写法,别拿老代码硬套。
网上一搜“cesium ion 的 图片无法访问”,基本离不开三类原因:token没生效、token安全限制太严(比如只允许特定域名)、当前网络访问Ion不稳定。排查顺序是:先看控制台网络请求返回什么状态码,再检查token是否有效,最后确认请求的域名是否可达。如果不想依赖Ion,就老老实实换离线或第三方影像源。
2.3 影像源和地形源怎么选
影像源不是越清晰越好,而是越贴合项目越好。做全球宏观展示,用公共影像源就够;做城市级项目,最好加载本地的瓦片服务,避免把公网带宽打满,也避免上线后因为外网抖动导致地图空白。
Cesium里默认的地球表面是椭球体,没有山也没有坑。要想看到起伏地形,需要给Scene配置TerrainProvider:
const viewer = new Cesium.Viewer('cesiumContainer'); const terrain = await Cesium.createWorldTerrainAsync(); viewer.scene.setTerrain(new Cesium.Terrain(Cesium.Math.RADIANS_PER_DEGREE, terrain));这里结合“高程数据 webgl cesium”这个热搜词多说一句:Cesium高程数据的核心概念是DEM(数字高程模型),但Cesium不直接用普通TIFF,而是要求转成quantized-mesh或第三方地形服务格式。Ion可以直接上传GeoTIFF生成地形,本地地形则要用工具转。入门阶段,直接用createWorldTerrainAsync体验效果最省事。地形和影像一定要分开理解:影像决定地球表面“长什么样”,地形决定地球表面“隆起到哪里”。
3. 官网术语的“中译中”:把Scene、Camera、Clock彻底搞懂
3.1 Scene不是SceneManager,Camera也不是地图上的箭头
官网API文档里,Scene的属性和方法有上百个,新手很容易被吓跑。实际上入门阶段只需要记住三件事:
viewer.scene已经是一个创建好的Scene实例,不需要自己new;- Scene负责所有对象的渲染,包括Primitive、Entity、3D Tiles、光照和天空;
- 很多常见操作,例如
scene.pick、scene.globe、scene.camera,都是在这个实例上进行的。
Camera则是“观众的眼睛”。Cesium的相机默认是透视相机,控制方式不是直接传经纬度,而是传世界坐标。新手最容易踩坑的代码是:
// 错误示范:以为可以传经纬度 viewer.camera.flyTo({ destination: [116.39, 39.9, 1000] });正确写法是用Cartesian3转换:
viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 1000) });fromDegrees内部会把经纬度转成弧度,再算成地心坐标系下的XYZ。官网没说这个细节,导致很多初学者“镜头飞到莫名其妙的地方”。我在教程里反复强调坐标系问题,因为这是所有Cesium开发的底层逻辑。
camera.setView、camera.flyTo、camera.lookAt这三个是最高频的API。setView瞬间跳转,flyTo带飞行动画,lookAt让镜头始终看向某个目标。做数字孪生巡检动画时,lookAt配合时间轴效果很好;做业务跳转,flyTo更自然。
3.2 Clock、JulianDate和世界的时间观
官网在Dynamic Scenes教程里会花很大篇幅讲Clock、JulianDate、SampledPositionProperty。很多人第一遍看会跳过,直到发现粒子、模型动画、CZML里带时间的对象全都不动,才回头补课。
Cesium不用JavaScript的Date,而用JulianDate,因为它要处理跨世纪、跨星历的天文计算。你不需要自己实现,但要知道这几个API:
viewer.clock.currentTime:当前场景时间;viewer.clock.clockRange:时间到达起点/终点后的行为,例如Loop循环、Clamped保持;viewer.clock.shouldAnimate:是否自动推进时间。
举一个实际例子:你要让一个飞机模型沿路径飞,通常用SampledPositionProperty给每个时间点写入位置,然后把它赋给实体的position。这个动画背后是Clock在驱动,不是requestAnimationFrame在驱动。如果你忘了viewer.clock.shouldAnimate = true,模型会原地不动,但代码又不报错,排查半天才发现是时间被暂停了。
3.3 Entity还是Primitive,这是个老问题
官网文档里,Entity被描述为“高层抽象”,Primitive是“底层渲染对象”。这两个词太抽象了。我的理解是:Entity是面向业务开发的API,你告诉它“这里有一个点、一个面、一个模型”,它帮你处理创建和销毁;Primitive是面向性能的API,你得自己管理几何体、材质、矩阵变换,代码量多但更灵活。
实际项目里的选型原则,我总结成一个表格:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 几十个标绘点、业务弹窗 | Entity | 开发效率高,样式切换方便 |
| 上万级热力点、海量标绘 | Primitive或第三方扩展 | 避免Entity对象的创建销毁开销 |
| 精细控制模型节点、自定义Shader | Primitive / ModelExperimental | 底层能力更直接 |
| 快速和GeoJSON交互 | Entity + GeoJsonDataSource | 现成的样式和数据绑定 |
很多人一上来就学Primitive,觉得“底层才专业”,结果被几何矩阵绕晕。我的建议是反过来的:先用Entity跑通业务,等出现性能瓶颈,再下钻到Primitive。Cesium的Entity内部也包了一层Primitive,学会使用和排查再深入,效率更高。
4. 往地球上塞数据:3D Tiles、倾斜摄影、模型和高程
4.1 3D Tiles是Cesium的“亲儿子”
3D Tiles是Cesium团队主导的开源规范,用来流式传输海量三维数据。它可以理解为“三维世界的瓦片”,类似二维地图的切片,但加上了层级细节(LOD)。一栋楼是一个块,一个小区是很多块,页面只加载当前视角能看到的部分。
加载3D Tiles的代码非常简单:
const tileset = await Cesium.Cesium3DTileset.fromUrl('/data/tileset.json'); viewer.scene.primitives.add(tileset); await viewer.zoomTo(tileset);但这里的隐藏坑很多。最常见的是本地文件直接打开html,tileset请求被浏览器跨域拦截。你需要在项目目录起一个静态服务,比如:
npx serve或者:
python -m http.server 8080然后再访问http://localhost:8080。这个细节官网不会教你,但几乎每个本地加载3D Tiles失败的人都会遇到。
热搜词里的“cesium 3dtiles 单体化”,本质是让3D Tiles里的每个建筑或构件能单独选中、高亮。实现思路有两层:一层是数据侧,建模或转换时给每个构件写入唯一标识和业务属性;另一层是前端侧,通过viewer.scene.pick拾取到feature,再修改features颜色。这是个进阶话题,但入门知道“单体化=几何+属性+可交互”就够了。
4.2 模型加载:glTF/glb是标准,SU和3ds Max不能直接上
“cesium 模型可以直接加载su吗”这个搜索词,我在各个社区见到太多次了。答案是:不能直接加载skp,Cesium认的标准三维模型格式是glTF和glb。SketchUp导出时选glb,3ds Max可以通过插件或DCC管线导出glTF,然后再加载。
加载glb最简单的方式:
viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(120.1, 30.2, 100), model: { uri: '/models/building.glb', scale: 1.0, heightReference: Cesium.HeightReference.RELATIVE_TO_GROUND } });如果你要控制模型内部的零件,比如数字孪生里“开启阀门”“升起机械臂”,就要深入glTF的节点层级。Cesium的ModelExperimental支持遍历节点、设置变换、显隐节点。官网这块文档偏少,最好的学习资料是Sandcastle里的Model示例,直接跑起来改代码,比看API快。
4.3 GeoJSON和矢量数据:入门最常见的需求
大多数业务系统的第一需求不是模型,而是“在地图上标点线面”。GeoJsonDataSource是官网开箱即用的方案:
const dataSource = await Cesium.GeoJsonDataSource.load('/data/cities.geojson'); viewer.dataSources.add(dataSource); dataSource.entities.values.forEach(entity => { entity.polygon.material = Cesium.Color.YELLOW.withAlpha(0.5); entity.polygon.outline = true; });这里再说回“cesium绘制矩形”。Cesium里矩形有专门图形rectangle,多边形用polygon,圆形用ellipse。如果你要做鼠标交互式绘制,官网没有现成的DrawHandler,得自己监听鼠标事件,或者在社区找DrawHelper工具。我建议新手先手写一遍鼠标事件逻辑,理解坐标转换和吸附逻辑,再去用第三方封装。
4.4 高程数据到底怎么用
高程数据在Cesium里不是叠加图层,而是通过TerrainProvider改变地球表面高度。影像源管颜色,地形源管高度。这个二元关系一旦理解,很多“高程不生效”的问题就能定位。
一个典型坑是:你加了地形,但视角看着还是平的。原因是地形数据没加载成功,或者当前视角没有地形覆盖。解决办法是看viewer.scene.terrainProvider赋值情况,同时打开开发者工具看网络请求。
本地高程数据转换我建议用CesiumLab或类似工具,把DEM转成Cesium可用的terrain tiles,再配置到TerrainProvider。入门阶段别在这块纠结太久,先用Ion的全球地形把流程跑通,等要离线部署再优化格式和工具链。
5. 实战里高频出现的需求:鹰眼、热力图、雷达和动态光照
5.1 鹰眼(MiniMap)实现思路
鹰眼在智慧城市项目中几乎必做。核心思路很简单:页面角落再放一个Cesium.Viewer,关掉所有控件,然后同步主视图和鹰眼视图的相机。
代码结构大致如下:
const miniMap = new Cesium.Viewer('miniMapContainer', { animation: false, timeline: false, baseLayerPicker: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false }); viewer.camera.changed.addEventListener(() => { syncViewerCamera(viewer, miniMap); });关键点是同步循环。如果主视图和鹰眼互相监听camera.changed,会造成抖动。常规做法是加一个isSyncing标志位,或者只在鹰眼上监听postRender。我在项目里用过mousemove同步鹰眼相机,效果不错,但要注意鹰眼分辨率低,相机fov要稍微调大一点。
5.2 热力图:从点数据到HeatmapLayer
很多人搜“cesium 热力图”,第一个找的是heatmap.js。heatmap.js不是Cesium插件,但它画出的canvas可以贴到Cesium的实体上。实现思路是:把经纬度范围映射到canvas像素,用heatmap.js生成热力canvas,再把canvas作为image material贴到一个RectangleEntity上。
入门版本:
const heatCanvas = heatmap.createCanvas(); heatCanvas.width = 512; heatCanvas.height = 512; // ... 用heatmap库渲染热力数据 ... viewer.entities.add({ rectangle: { coordinates: Cesium.Rectangle.fromDegrees(west, south, east, north), material: new Cesium.ImageMaterialProperty({ image: heatCanvas }) } });但要注意,这种方式的贴图在跨大范围时会变形失真,只能算Demo级方案。生产项目通常会把热力数据切到瓦片或使用GPU逐像素渲染。入门阶段能跑通“小范围矩形热力”就够用了,关键是要记住经纬度范围和canvas像素坐标必须一一对应,不然热力点全部错位。
5.3 雷达扫描效果和动态光照
“cesium雷达”这个搜索词,背后对接的往往是军工仿真或智慧安防。Cesium没有内置雷达组件,需要用自定义Material实现扫描效果。最简单的做法是给Polygon写一个自定义fragment shader,让颜色随时间变化,形成一个旋转的扫描扇区。如果你不想写shader,也可以用扇形图片贴图,配合实体的rotation旋转属性实现伪扫描效果。
动态光照相对简单:
viewer.scene.globe.enableLighting = true;开启后,地球会按照太阳位置产生白天黑夜阴影变化。但如果你加载的是自定义3D Tiles,光照不会精确到模型的每一个面,因为Cesium的光照主要作用于地球和表面图层,模型本身的阴影需要模型带法线和环境光遮蔽信息。这里和Three.js的光照逻辑不一样,Cesium的“动态光照”更多是大尺度的阳光效果,而不是精细的室内补光。
5.4 “3D地球滚动出现崩溃”这类问题的通病
搜索“cesium 3d地球滚动出现崩溃”,你会发现大量帖子。这类崩溃多半不是Cesium库本身的问题,而是页面里同时存在多个WebGL上下文、GPU内存溢出、或者Viewer没有正确销毁。
入门阶段最容易踩的三个点:
- 页面里反复
new Viewer,旧Viewer没有销毁,导致GPU上下文堆积; - 同时加载过多高精度3D Tiles,显存爆掉;
- 浏览器硬件加速和旧显卡驱动冲突。
排查方法很朴素:先关掉自己的业务代码,只留一个Viewer和一个3D Tiles,看还崩不崩。如果还崩,换一个浏览器或检查显卡驱动;如果好了,就二分法把业务功能一个个加回来,直到定位到罪魁祸首。这个习惯比任何高级API都重要。
6. 被项目逼出来的性能优化和报错排查笔记
6.1 那些年我遇到过的高频报错
Cesium项目跑多了,高频报错其实就那么几类。我自己列了一个排查清单:
| 报错现象 | 常见原因 | 处理思路 |
|---|---|---|
| CORS policy报错 | 本地file协议直连加载模型/瓦片 | 起http静态服务,或给服务端配跨域头 |
| Ion token 401/403 | 没配token或token过期 | 注册Ion并正确设置defaultAccessToken |
| TerrainProvider加载失败 | 地形地址失效或跨域 | 换公共地形或本地地形服务 |
| Entity不显示 | 位置高度不对,或地形遮挡 | 先flyTo该位置,确认坐标和heightReference |
| 页面滚动卡死 | 多Viewer未销毁/GPU内存溢出 | 生命周期里调用viewer.destroy() |
最容易被忽视的是本地文件直连。网上很多教程默认你知道起一个http服务,导致新手直接双击打开html,然后加载失败。我现在每次带新人第一句话都是:Cesium项目必须在http协议下运行,不要用file协议双击。
6.2 做过一次几十栋楼的数字孪生后的优化经验
我之前接触过一个小规模城市数字孪生项目,几十栋楼,客户要求秒开、不卡。最后验证下来,最有效的优化手段不是换显卡,而是:
- 给3D Tiles设置合适的
maximumScreenSpaceError,默认值是16,对大场景可以放宽到64,让低精度块更早出现; - 不要一次性加载所有LOD,Cesium本身会动态调度,但请求并发要控制;
- 模型贴图用压缩纹理,glb文件尽量减面;
- 开启
viewer.scene.requestRenderMode = true,并设置maximumRenderTimeChange,让页面静止时不重复渲染。
viewer.scene.requestRenderMode = true; viewer.scene.maximumRenderTimeChange = 0.5;这些参数官网文档都有,但新手很难把它们串起来。我的体会是:Cesium性能优化不是“调一个参数就飞起”,而是围绕“渲染频率、资源总量、LOD调度”三个维度一起做减法。
6.3 Cesium for Unity / Unreal 值得关注
如果你是从游戏引擎入门的,或者公司要做UE5大屏,会搜到Cesium for Unity、Cesium for Unreal。这两个插件本质是把3D Tiles、地形和影像能力搬进游戏引擎。入门Web版之后再去看它们,会容易理解很多:Viewer对应引擎里的CesiumGeoreference,DataSource对应Cesium3DTilesetActor。
有人问“ue5中cesium for unreal不显示版权”,这是Cesium在引擎里的Credits控件被关闭或没有正确添加。你需要在场景里找到CesiumCreditSystem组件,确保它被激活。做项目时版权合规要重视,Cesium本来就需要保留必要的attribution信息,尤其是在标注“自定义数据源”和自己打包发布的时候,把版权信息去掉会给自己惹麻烦。
7. 入门后怎么继续:面试题、复习方向和几条“真香”路线
7.1 从官网文档翻译到自己的知识体系
官网全是英文,翻译资料又散落各处,很多初学者陷入“看一遍忘一遍”的循环。我自己的方法是给每个核心模块建一个“一句话笔记”:
- Viewer:地球应用的外壳;
- Scene:真正的渲染现场;
- Camera:镜头语言;
- Clock:时间引擎;
- 3D Tiles:三维瓦片流;
- Entity:上层数据封装。
然后把Sandcastle里跑通过的Demo按功能分类存到书签,做项目时直接搜Demo改参数。这比囤一堆学习资料有用得多。官网Sandcastle就是最好的题库,每个示例都有完整代码,改一行立即看到效果,这种反馈速度是看文档替代不了的。
7.2 常见面试题背后考的是这些
Cesium面试题翻来覆去就那些:Viewer和Scene区别、Cartographic和Cartesian3怎么转换、Camera的flyTo原理、3D Tiles的加载优化、怎么做单体化、Entity和Primitive选哪个、怎么实现鹰眼和量算。这些问题表面考API,实际考的是你有没有理解Cesium的坐标和渲染链路。
举一个最常见的:
const position = Cesium.Cartesian3.fromDegrees(120, 30, 100);如果面试官问“为什么不直接new Cartesian3(120, 30, 100)”,其实在考你:Cesium内部用世界坐标(米),经纬度是角度,需要先换算,你知不知道fromDegrees做了什么。能把坐标系这条线讲清楚,并且说明自己踩过的坑,就比背一百个API更强。
7.3 Three.js + Cesium做工业数字孪生的两种集成思路
最后聊下热搜词里的“three.js、cesium 工业数字孪生”。这个组合很常见,但集成思路其实有两条。
第一种是以Cesium为底座,Three.js只渲染Cesium里的复杂设备模型。可以在同一个canvas上叠加TwoRenderer,也可以把Three.js渲染结果通过纹理贴到Cesium实体上。缺点是要处理两个渲染器的深度缓冲和坐标同步,代码量不小。
第二种是以Three.js为主场景,Cesium只负责提供地球和倾斜摄影背景。实际做法是把Cesium的场景作为背景纹理,然后在Three.js里叠加工业设备和动画。这种方式更适合纯前端展示型数字孪生,但地球和设备的空间关系会比较难对齐。
我个人的倾向是:如果数字孪生业务里GIS数据占比高,优先学Cesium原生;如果偏工业设备展示、偏游戏化交互,先用Three.js把效果做出来,再接Cesium的地球底座。两条路线不冲突,但目标要分清,不然会在“双渲染引擎”里挣扎很久。
如果让我重新学一遍,我不会再囤各种教程,而是打开官网Sandcastle,从一个带3D Tiles的Demo开始,改坐标、换模型、加交互,遇到不懂的再去翻对应文档。真正值钱的不是“知道Cesium怎么用”,而是知道“一个三维地球应用从零到上线会经历哪些必然的坑”,而这些坑,只有自己动手填过才会记得牢。