干了几年GIS可视化接手的Cesium项目少说也有七八个了。最开始啃官网文档的时候说实话挺痛苦的API又多又杂英文文档翻译过来总感觉隔着一层有些概念官网写得云里雾里自己踩了坑还得回头再看一遍文档才恍然大悟。后来带新人也发现大家卡住的地方其实都差不多——不是看不进文档而是看完了不知道怎么落到项目里。这个教程算是把我自己翻译官网加实践理解的笔记整理出来从环境搭建一路写到倾斜摄影、单体化、高程地形、动态光照这些高频项目需求。Cesium这东西入门不难难的是把官网那些概念串起来知道什么时候该用什么遇到报错知道去哪查。这篇内容就是干这个的适合刚接触Cesium的开发者、想转型WebGIS的同行也适合项目里要用Cesium但还没系统梳理过知识的人。1. 内容整体设计与思路拆解1.1 Cesium到底解决什么问题官网第一句话就说Cesium是一个用于构建全球尺度三维地理空间应用的JavaScript库。这句话信息量很大我拆解一下第一它是JavaScript库不是桌面软件也不是游戏引擎这意味着它可以无缝嵌入到现有的Web项目中不需要单独部署客户端。第二全球尺度这是Cesium和Three.js最本质的区别。Three.js也能画地球但Cesium原生就考虑了WGS84坐标系、椭球体、切片地图服务、大地水准面这些GIS底层问题。你在Three.js里得自己处理经纬度到笛卡尔坐标的转换还要自己写瓦片加载逻辑而在Cesium里这些都是内置的。第三核心是可视化加上一些空间分析能力。Cesium本身不是GIS分析软件它不会帮你做缓冲区分析、拓扑检查它的核心是把数据放到一个精确的三维地球上展示出来并提供相机控制、图元拾取、属性信息查询这些交互能力。再聊聊Cesium在技术栈里的位置。现在数字孪生项目很流行常见的技术组合有Three.js方案、UE5加Cesium for Unreal方案、纯Cesium方案。我的经验是如果只做设备级、厂区级的精细模型展示Three.js更合适它更轻、更灵活模型加载效率也高。但如果涉及园区级、城市级需要接入真实地理坐标、加载倾斜摄影或BIM数据、叠加多源地图服务Cesium是更稳妥的选择省掉一大堆坐标转换和瓦片调度的麻烦。至于UE5加Cesium for Unreal优点是渲染效果顶级适合做高保真可视化但开发和部署成本也高资源占用大一般用在专门的展示终端上。另外UE5里Cesium for Unreal有个常见困扰就是默认大数据版权标识的显示问题其实在Cesium for Unreal插件里有配置项可以控制后面我单独说。1.2 学习路线怎么安排才算不绕路我见过不少新人学Cesium上来就下载个最新版然后照着示例开始改代码改不通就去搜答案。这样也能进步但效率很低因为知识是散点没有形成体系。我建议按这条路线走先把最基础的地球加载跑通理解Viewer、Scene、Camera这三者的关系。然后逐个研究数据源影像底图、地形、矢量数据、三维模型把这四类数据源吃透。接着看Entity API和Primitive API的区别搞清楚什么时候用哪个。最后再看扩展功能粒子系统、动态光影、模型裁剪、单体化这些。官网的示例页面其实做得很用心每个示例都有代码、有运行效果但问题是示例之间没有递进关系你不知道先看哪个后看哪个。我的翻译策略是先把官方教程的四个核心章节吃透再回头去看示例。官网的核心章节值得反复读的有这些Visualizing Data - Entities、Graphics and Rendering、3D Tiles、Camera。我第一次读的时候跳过了Camera后来做项目发现相机控制才是交互体验的关键飞行动画、视角限定、坐标拾取全都依赖对Camera的理解。所以我把这部分单独拎出来后面会重点讲。2. 基础搭建与官方示例的本地化运行2.1 环境准备Node和静态服务器是最短路径Cesium官方推荐用npm包管理的方式引入但如果你只是想快速跑起来看效果我更推荐直接用CDN或者下载静态文件。先说CDN方式最简单的用法!DOCTYPE html html langzh-CN head meta charsetutf-8 titleCesium 入门/title link hrefhttps://cesium.com/downloads/cesiumjs/releases/1.119/Build/Cesium/Widgets/widgets.css relstylesheet script srchttps://cesium.com/downloads/cesiumjs/releases/1.119/Build/Cesium/Cesium.js/script /head body div idcesiumContainer stylewidth:100%;height:100vh;/div script const viewer new Cesium.Viewer(cesiumContainer, { infoBox: false, selectionIndicator: false }); /script /body /html这种方式的缺点也很明显每次版本更新可能导致CDN链接失效而且国内访问Cesium官方CDN有时候会很慢甚至完全加载不出来。所以项目正式开发我更推荐用npm方式npm install cesium装完后用Vite配一下处理Cesium的静态资源路径。这里有个典型的坑Cesium需要把Workers、Assets、Widgets这些静态目录暴露出来直接用默认配置会报错找不到资源。Vite的配置大概是这样的// vite.config.js import { defineConfig } from vite; import path from path; export default defineConfig({ resolve: { alias: { cesium: path.resolve(node_modules/cesium/Build/Cesium) } } });然后在入口文件里import * as Cesium from cesium; import cesium/Build/Cesium/Widgets/widgets.css; window.CESIUM_BASE_URL /cesium/;如果你用Vue或React官方有个vite-plugin-cesium插件能自动处理这些静态资源省心很多。2.2 首次运行时必须处理的Token问题写完第一行代码运行页面你会发现地球是黑的控制台报了一个401或者403的错误。这就是Cesium Ion Token没有配置。Cesium Ion是Cesium官方提供的数据服务免费注册一个账号后在Access Token页面可以创建一个默认token。把它配置到代码里Cesium.Ion.defaultAccessToken 你的token;这里我想多说一句。很多人在这个环节就卡住了因为注册Ion账号有网络门槛Cesium的官方服务在国内访问不太稳定。我测试过Ion的DOM、卫星影像、地形服务都托管在官方云上一旦网络不稳定就会出现图片无法访问、地形加载失败这类情况。我的建议是个人学习阶段可以去注册拿token这是最快捷的方式。正式项目里影像底图、地形数据最好换成国内可访问的数据源比如天地图、高德、或者自己用GeoServer发布的服务。这样不仅不依赖国外网络加载速度和稳定性反而更好。那问题来了换成天地图之后代码里怎么写我后面专门讲影像加载这一节。2.3 官网示例复制后跑不起来的常见原因官网的示例大多很精简直接粘到HTML里有可能跑不起来。最常见的原因有三个一是Cesium Viewer的默认参数问题。官网很多示例为了突出功能把一些控件藏起来了但你如果照抄它的初始化参数可能连底图都没了。比如ArcGisMapServerImageryProvider这类示例如果你没有正确传入url参数地图就加载不了。二是官方示例经常依赖Cesium ion上的数据集。比如加载3D Tiles的示例用的是一个Cesium Ion上托管的模型ID如果你没有把对应的数据集添加到自己的账号下代码复制过来就是白屏。三是最容易踩的坑官网示例通常只写JavaScript部分省略了HTML和CSS结构。如果你没注意容器的高度地球就是一个100%但高度为0的div页面看起来就是一片空白。这个我真的是每次带新人都要强调一次容器必须先有确定的高度。3. Cesium核心概念拆解官网怎么说实际怎么理解3.1 Viewer与Scene的关系官网文档对Viewer的定义是一个用于显示Cesium场景的交互式小部件它提供了多种控件并集成了场景渲染。这个描述太正式了。我通俗解释一下Viewer就是那个带各种按钮、时间轴、图层管理器的外壳Scene才是真正渲染三维内容的场景。Viewer封装了Scene还封装了Camera、Canvas、数据源集合等等。实际开发中我们需要区分什么时候操作Viewer什么时候操作Scene。操作Viewer的场景创建和销毁应用、控制控件的显隐、添加数据源viewer.dataSources.add、获取当前相机状态。操作Scene的场景拾取物体scene.pick、绘制辅助对象scene.debugShowFramesPerSecond、控制大气和光照效果、监听渲染循环事件scene.postUpdate。官网是这么分层的Viewer ├── Scene三维场景 │ ├── Camera相机 │ ├── Globe地球 │ ├── PrimitiveCollection图元集合 │ └── postUpdate更新事件 ├── DataSourceCollection │ └── CustomDataSource / CzmlDataSource / GeoJsonDataSource ├── Entities实体集合 ├── ImageryLayers影像图层集合 └── TerrainProvider地形数据记忆方法很简单凡是跟绘制、渲染、相机相关的找Scene凡是跟数据管理、交互控件相关的找Viewer。3.2 Camera控制会转视角才算入门Cesium的相机控制官网文档写得很细但读起来累。我提炼出的实际经验是要掌握四个核心操作setView、flyTo、lookAt、move。setView是瞬间把相机放到指定位置不带动画。你需要在某个时刻直接切换视角时用它。viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 10000) });flyTo是相机飞行过渡适合用户点击某个按钮后平滑地飞到感兴趣的位置。支持带一定的视角方向viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 1000), orientation: { heading: Cesium.Math.toRadians(0), pitch: Cesium.Math.toRadians(-45), roll: 0 }, duration: 3 });lookAt是盯着某个目标点不动适合围绕一个建筑或设备旋转观察。我记得有次项目里要做一个雷达扫描的效果就靠相机加一个Entity的雷达波效果配合。雷达效果本身是用Entity的ellipse或sensorVolume实现的但相机要始终追踪目标这时候lookAt就很合适。另外有些项目需要限制相机不能飞到地下、不能反转可以在screenSpaceCameraController上配置viewer.scene.screenSpaceCameraController.minimumZoomDistance 100; viewer.scene.screenSpaceCameraController.maximumZoomDistance 50000;这个在官网里叫constrained camera“约束相机”实际情况里经常被忽略但不限制的话用户很容易把视角滑到地底下体验很糟糕。3.3 数据源DataSource与实体Entity的选择标准官网把数据加载分成了两种模式一种是面向数据源的加载比如加载GeoJSON、KML、CZML另一种是直接向场景添加Entity对象。我个人的选择标准是这样的单个业务对象比如一个摄像头点位、一个设备模型用Entity最方便因为可以直接给它绑属性、做事件绑定代码也直观。如果是一整批数据比如几千个点、几百个面用Entity会拖慢性能这时候应该用GeoJsonDataSource加载或者在极致性能场景下用Primitive API。Entity本质上是个高层封装最后也是转成Primitive去渲染。但Entity的优势在于属性驱动和事件机制比如你要让一个飞机模型沿着航线飞用Entity的position属性配合SampledPositionProperty就够了用Primitive就要自己算每一帧的变换矩阵开发量大得多。初学者先学Entity完全没问题官网的教程也是从Entity开始的。但不要止步于Entity理解了Primitive之后你对Cesium的性能边界才有感觉。有些项目一上来要加载十万个点你还在Entity里循环add帧率就直接掉到个位数了这时候就得考虑Primitive或者用聚合功能EntityCluster。3.4 官网里没写透的Event机制Cesium的事件系统是学习曲线比较陡的部分。官网有Event、EventHelper这些类但并没有很通俗地解释什么时候用哪个。实际开发中我最常用的是这几个viewer.clock.onTick每一帧都触发用来做实时动画。viewer.screenSpaceEventHandler处理鼠标键盘交互比如点击拾取、拖拽绘制。viewer.scene.postRender渲染完成后触发适合做一些渲染后的处理。viewer.dataSourceChanged数据源变化时触发。鼠标点击拾取物体是这当中最有代表性的场景。基本套路是const handler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction((movement) { const picked viewer.scene.pick(movement.position); if (Cesium.defined(picked)) { console.log(拾取到, picked.id); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);这里有个细节scene.pick拾取的是Primitive如果你添加的是Entity需要靠picked.id来拿到对应的Entity对象。Entity底层会创建一个Primitive这个Primitive的id属性会指回原来的Entity。官网文档里这点写得很隐晦不实际用一次根本理解不了。4. 底图加载从官方数据源到国内实践4.1 影像底图的Provider选型官网默认会用Ion提供的Bing Maps影像做底图但实际国内项目里这个默认方案基本都得换。我在项目里常用的方案有这么几种第一种是天地图。国家地理信息公共服务平台提供WMTS服务中科星图等公司也做了功能相同的国内瓦片服务关键是免费、稳定、合规。接入方式// 需要先到天地图官网申请key const tiandituToken 你的天地图key; const imageryProvider new Cesium.UrlTemplateImageryProvider({ url: https://t{s}.tianditu.gov.cn/img_w/wmts?servicewmtsrequestGetTileversion1.0.0LAYERimgtileMatrixSetwformattilestileMatrix{z}tileCol{x}tileRow{y}tk tiandituToken, subdomains: [0, 1, 2, 3, 4, 5, 6, 7], maximumLevel: 18 });注意这里的变量占位符是{t}、{z}、{x}、{y}这是UrlTemplateImageryProvider的标准写法。有的老教程还在用WebMapServiceImageryProvider的方式请求天地图但我实测下来WMTS的方式更稳定解析速度也更快。第二种是高德、腾讯等商业互联网地图。高德的瓦片地址可以拼出来但要注意版权要求一般建议在正式商用前仔细确认合规问题。高德瓦片的URL规则是https://webrd0{s}.is.autonavi.com/appmaptile?langzh_cnsize1scale1style8x{x}y{y}z{z}这里s是子域0到3。第三种是离线瓦片。很多项目在内网运行或者有历史积累的瓦片数据这时候直接把瓦片文件夹发布成静态服务再用UrlTemplateImageryProvider加载就行。我做过一个项目用的是MapServer发布的瓦片URL模板就是http://内网IP:端口/切片目录/{z}/{x}/{y}.png一样能跑得很好。4.2 加载MVT格式矢量瓦片最近很多人在问Cesium能不能加载MVT。MVT是矢量瓦片格式Cesium原生不直接支持MVT渲染需要通过中间转换或第三方库实现。目前可行的方案有几个一是用deck.gl的MVTLayer配合Cesium把MVT数据作为图层叠加到三维地球上二是把MVT解码成GeoJSON再走GeoJsonDataSource加载三是用Mapbox的Cesium集成方案。我在实际项目里用的是第二种思路原因很简单它不需要引入额外的渲染引擎和Cesium的坐标系、事件系统融合得最好。具体步骤是先用mapbox/mvt这样的库解码MVT拿到features数组然后构造GeoJSON对象再加载import { decode } from mapbox/mvt; async function loadMVTLayer(url, viewer) { const res await fetch(url); const buffer await res.arrayBuffer(); const features decode(buffer); const geojson { type: FeatureCollection, features: features.map(f ({ type: Feature, properties: f.properties, geometry: f.geometry })) }; viewer.dataSources.add( Cesium.GeoJsonDataSource.load(geojson, { stroke: Cesium.Color.YELLOW, fill: Cesium.Color.fromAlpha(Cesium.Color.YELLOW, 0.3), strokeWidth: 2 }) ); }这个方案的缺点是如果MVT数据量很大动态解码会有性能压力。更好的做法是在服务端把MVT转成GeoJSON或者直接发布3D Tiles的矢量数据这样在Cesium里加载就非常平滑了。4.3 加载3D Tiles的两种数据来源3D Tiles是Cesium处理海量三维模型的核心格式官网的详细教程值得好好读。但实际开发时大家最常卡住的是数据从哪来。第一种是官方Ion的数据集。官网示例里会写const tileset await Cesium.Cesium3DTileset.fromIonAssetId(40866); viewer.scene.primitives.add(tileset);但这里的问题就是我在前面提到的只有你账号下添加过对应AssetId的数据集才能加载。而且Ion的数据在国内网络环境下经常加载失败如果你遇到3DTiles白屏优先检查是不是Ion的资源没加载出来。第二种是自建数据。用倾斜摄影和BIM模型生成3D Tiles常见工具有ContextCapture、Smart3D还有开源的py3dtiles、cesiumlab。生成之后把3DTiles文件夹放到静态服务器或者对象存储上const tileset await Cesium.Cesium3DTileset.fromUrl(/data/tileset.json); viewer.scene.primitives.add(tileset); await viewer.zoomTo(tileset);这里有几个提升细节的技巧在fromUrl之前先设置好maximumScreenSpaceError这是一个控制LOD切换的参数默认值是16如果模型加载后很糊就调小到8或4如果加载很慢卡顿就调大到32。这个参数对比官网文档里给了建议但大家总忽略。用tileset.style可以做样式过滤。比如高亮显示置信度低于某阈值的倾斜模型面片。tileset.modelMatrix用来修正坐标偏移。倾斜摄影数据经常坐标系有偏移需要手动调整到正确位置。动态光照时可以用tileset.customShader增加自定义着色逻辑不过这个功能对WebGL版本有要求后面我单独说。4.4 倾斜摄影与SU、Revit模型加载的处理办法很多设计师和工程师手里有SketchUp和Revit文件他们问能不能在Cesium里直接加载SU模型。Cesium不支持直接加载SKP格式也不支持Revit的RVT格式官网文档对支持的模型格式写得很明确glTF、GLB、3D Tiles、3D Tiles Next以及部分旧格式。但实际项目里我们经常要加载SU或Revit模型。成熟的流程是这样的SU模型先导出为glTF或GLB。SKP导出glTF这个操作官方没有直接支持需要装一个插件比如SketchUp的glTF导出插件。导出成glb后如果模型不太大可以直接用viewer.entities.add里的model属性加载const entity viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0), model: { uri: /models/building.glb, scale: 1.0, heightReference: Cesium.HeightReference.CLAMP_TO_GROUND } });如果模型有几十上百MB那就需要用模型处理工具把它转成3D Tiles。常见的处理工具是CesiumLab或ModelTiler传进去SU导出的glTF输出3D Tiles格式再按上面3D Tiles的加载方式加载即可。Revit模型同理一般建议先用Revit的插件导出成FBX再转glTF再转3D Tiles。这个链路过程有不少坑比如FBX转glTF后坐标偏移、材质丢失但整体流程是可以跑通的。关于UE5里Cesium for Unreal不显示版权信息的问题也说一句。Cesium for Unreal默认会在场景里显示Cesium的Logo和数据来源信息这是出于开源协议的合规要求。如果你确实需要在项目中隐藏需要到Cesium for Unreal插件里找到相关设置调整为不显示但前提是你必须满足对应的协议条件。我自己做的经验是合规的标准做法是保留版权信息或者按协议要求在应用的关于页面里说明使用了Cesium的相关技术。千万别在不知情的情况下直接擅自隐藏。5. 核心功能实操绘制、标注、量测与热力图5.1 用Entity绘制点线面和矩形、多边形Cesium里画点、线、面本质是创建Entity并传入相应的图形实例。这里有一套很成熟的口诀点用point、线用polyline、面用polygon、矩形用rectangle。画一个矩形const rectangle viewer.entities.add({ rectangle: { coordinates: Cesium.Rectangle.fromDegrees( 116.30, 39.80, 116.50, 40.00 ), material: Cesium.Color.RED.withAlpha(0.5), outline: true, outlineColor: Cesium.Color.WHITE } });画一个多边形需要提供线性环坐标const polygon viewer.entities.add({ polygon: { hierarchy: Cesium.Cartesian3.fromDegreesArray([ 116.30, 39.80, 116.50, 39.80, 116.40, 40.00 ]), material: Cesium.Color.GREEN.withAlpha(0.4), perPositionHeight: true } });这里要注意perPositionHeight这个参数官网的解释是每个顶点的绝对高度。如果不设这个参数默认会把多边形贴到地面上。所以如果你想让多边形抬升到一定高度需要给每个顶点传三维坐标并设置perPositionHeight为true。点的话通常还会配一个label用于标注名称const point viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 100), point: { pixelSize: 10, color: Cesium.Color.ORANGE }, label: { text: 设备A, font: 14px sans-serif, pixelOffset: new Cesium.Cartesian2(0, -20), fillColor: Cesium.Color.WHITE, showBackground: true, backgroundColor: Cesium.Color.BLACK.withAlpha(0.6) } });5.2 坐标拾取和鼠标绘制用户在地图上点一下我们要拿到经纬度和高度这是很常见的需求。代码套路是这样的const handler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction((movement) { const cartesian viewer.camera.pickEllipsoid(movement.position, viewer.scene.globe.ellipsoid); if (cartesian) { const cartographic Cesium.Cartographic.fromCartesian(cartesian); const lon Cesium.Math.toDegrees(cartographic.longitude); const lat Cesium.Math.toDegrees(cartographic.latitude); const height cartographic.height; console.log(经度: ${lon}, 纬度: ${lat}, 高度: ${height}); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);注意这里用的是pickEllipsoid它是在椭球面上求交拿到的是地面上的点。如果你要拾取的是3D Tiles模型表面的点位比如倾斜摄影模型的某一点就要用scene.pickPositionconst pickedPosition viewer.scene.pickPosition(movement.position);这个API依赖于深度缓冲区如果场景支持深度拾取就能拿到三维世界坐标。用这个功能做倾斜摄影的量测是很实用的。鼠标绘制矩形或多边形社区里有一些现成的工具库比如cesium-drawhelper但更灵活的做法是自己控制drawingMode。基本流程是监听LEFT_CLICK添加顶点监听MOUSE_MOVE显示预览双击结束绘制。关键点在于要区分预览图形和最终图形预览图形每帧更新最终图形只在双击时创建。5.3 热力图在Cesium中的实现方式很多做数据可视化的朋友会问到热力图怎么实现。Cesium自身不带热力图功能常见的实现有两种思路离线渲染方案先用heatmap.js在前端渲染一张平面热力图片然后将图片作为纹理贴到一个矩形Entity上。这种方式实现简单性能也好但只能绘制平面的热力效果无法弯曲贴合地形。三维热力方案将热力值转为不同高度和颜色的柱体或立方体。比如你想展示区域人流密度可以把每个格网的热力值映射为不同高度的圆柱颜色从蓝到红渐变效果立体。我自己的经验是如果是平面热力图展示用离线渲染方案是最快的。核心代码分两步第一步用heatmap.js生成图片import heatmap from heatmap.js; const heatLayer heatmap.create({ container: document.querySelector(.heatmap-container), radius: 30 }); heatLayer.setData({ max: 100, data: [{ x: 100, y: 100, value: 80 }] }); const dataURL heatLayer.getDataURL();第二步把图片贴到Cesium的矩形上viewer.entities.add({ rectangle: { coordinates: Cesium.Rectangle.fromDegrees(116.20, 39.70, 116.60, 40.10), material: new Cesium.ImageMaterialProperty({ image: dataURL, transparent: true }) } });这种方式生成的热力图能随相机视角变化而缩放但因为是平面贴图不适合地形起伏很大的区域。如果要做地形贴合的热力图就需要逐个采样点创建带有颜色和高度的primitive开发量会大不少。5.4 鹰眼图的实现思路鹰眼图也叫小地图是在页面一角显示一个整体视角用户拖拽小地图大场景视角跟着变。官网没有提供现成的鹰眼组件但实现思路很清晰在页面上内嵌一个额外的Cesium Viewer设置为小尺寸同步主场景的相机状态。同步相机可以通过监听主视图的preRender或postUpdate事件viewer.camera.changed.addEventListener(() { const camera viewer.camera; eagleEyeViewer.camera.setView({ destination: camera.positionWC, orientation: { heading: camera.heading, pitch: camera.pitch, roll: camera.roll } }); });注意鹰眼图里的地球不用加载太精细的底图可以用一张简单的全球影像甚至纯色球体代替否则小窗会消耗太多性能。还有要设置鹰眼viewer的控件全关只保留一个干净的地球。鹰眼viewer的初始化const eagleEyeViewer new Cesium.Viewer(eagleEyeContainer, { baseLayer: false, geocoder: false, homeButton: false, sceneModePicker: false, baseLayerPicker: false, navigationHelpButton: false, animation: false, timeline: false, fullscreenButton: false });5.5 雷达波和动态光照效果雷达效果在项目中常用来表示监控范围或信号覆盖建模逻辑是画一个扇形或者椭圆并用材质动画实现扫描效果。官网有雷达扫描的示例但实际上手时总是要改。核心是用Entity的ellipse实现圆盘式雷达再用动画材质让它动起来const radarEntity viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0), ellipse: { semiMajorAxis: 500, semiMinorAxis: 500, material: new Cesium.RadarScanMaterialProperty({ color: Cesium.Color.CYAN }) } });不过这个RadarScanMaterialProperty是示例代码里的自定义材质Cesium原生没有需要自己继承MaterialProperty实现。如果你想偷懒可以直接用Cesium自带的PolylineGlowMaterialProperty做边界发光再用半透明圆面做填充同样有雷达的感觉。动态光照在官网上叫Dynamic Lighting其实是基于glTF模型的灯光效果模拟。Cesium本身没有光源系统但你可以用模型的PBR材质配合自定义Shader或者用3D Tiles的customShader在渲染时加入时间动态的光照参数。这个实现起来代码量不小如果只做建筑物灯光的开关效果建议用entity.model.lightColor属性直接改模型光源颜色简单有效。6. 地形与高程数据处理6.1 Cesium地形体系理解官网文档把地形处理分了三部分地形服务端TerrainProvider、地形图层terrainProvider、和地形数据本身。理解这三层关系是看懂地形相关API的关键。TerrainProvider负责提供地形瓦片Cesium支持多种地形格式包括Cesium Terrain Format.terrain、quantized-mesh、以及带法线的STK terrain。使用Cesium官方Ion地形时项目里这样写const viewer new Cesium.Viewer(cesiumContainer, { terrainProvider: await Cesium.createWorldTerrainAsync() });如果你的地形数据是自建的用CesiumTerrainProvider加载const terrainProvider await Cesium.CesiumTerrainProvider.fromUrl(/terrain); viewer.terrainProvider terrainProvider;6.2 高程数据的获取与使用热门搜索词里高程数据与WebGL的高频组合说明大家经常在浏览器端处理地形。获取高程数据的前置步骤是先有原始高程源。常用的公共数据源是SRTM和ALOS等高程数据集下载后有对应的GeoTIFF文件。如果要发布为Cesium可用的地形需要转换成Cesium的地形瓦片格式。用CesiumLab这个工具最方便加载GeoTIFF后直接输出成terrain文件放到服务器上就能用。浏览器端获取某个点的高度可以用Cesium自带的工具函数const positions Cesium.Cartesian3.fromDegrees(116.39, 39.9); const height viewer.scene.globe.getHeight( viewer.scene.globe.ellipsoid, Cesium.Cartographic.fromCartesian(positions) );这里getHeight返回的是当前地形的高程前提是已经加载了地形数据。还有一个高频需求是构建3D地形模型进行裁剪分析比如做一个地形剖切面。这个就需要拿到地形的顶点坐标用webgl或者cesium的sampleTerrainMostDetailed方法从地形服务获取指定经纬度的高程再生成Mesh或线。官网有一个地形剖面示例代码逻辑是对一条路径做密集采样再绘制折线道理很简单。6.3 地形数据的常见坑自建地形数据踩坑最多的地方有两个一个是坐标系不一致。从SRTM下载的数据一般是WGS84但如果你的底图用了墨卡托投影的切片高程叠加后会有偏移。解决办法是确保地形数据和底图在同一个坐标系下。第二个是地形精度和性能的平衡。地形数据越精细瓦片数量就越多加载和渲染的负载越大。实际部署时建议把地形服务用专门的静态服务器或CDN托管同时根据应用场景设置地形LOD策略而不是一股脑加载最精细的地形。官网文档里面还提到一个细节sampleTerrainMostDetailed会在必要时请求地形数据因此会消耗较多网络资源使用时要注意频率不要在一帧里对多个点循环调用否则会卡住页面。正确的做法是用Promise.all批量采样const positions [ Cesium.Cartographic.fromDegrees(116.3, 39.8), Cesium.Cartographic.fromDegrees(116.4, 39.9), Cesium.Cartographic.fromDegrees(116.5, 40.0) ]; const updatedPositions await Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, positions);7. 3D Tiles单体化与模型交互7.1 单体化到底解决什么问题很多智慧城市项目要点击倾斜摄影模型里的某一栋建筑弹出这个建筑的属性信息这就是单体化。3D Tiles加载的倾斜摄影模型本质上是一整片mesh没有建筑边界的概念。单体化就是把这片mesh中对应某栋建筑的部分通过某种方式标记和分离出来使得点击它能被正确识别。目前流行的单体化思路有以下几种第一种是ID单体化。在建模阶段每个建筑就指定唯一的ID用属性字段关联。加载3D Tiles后通过scene.pick拾取的feature直接读取它的属性字段判断属于哪栋建筑。这个方案最干净但要求原始数据在建模时就规范化很多倾斜摄影数据并不满足。第二种是动态单体化。在计算阶段生成建筑的边界多边形拾取到mesh后判断拾取的点是否落在某个建筑的多边形内如果是就认为点击的是这栋建筑。这个方案不需要重建模型是热门的“伪单体化”做法。具体代码就是在pick的回调里遍历建筑多边形集合用射线法或空间索引判断包含关系。第三种是3D Tiles的分类Classification功能。用Cesium3DTileset结合Cesium3DTileClassification把建筑轮廓作为额外的3D Tiles覆盖在倾斜摄影上通过样式让当前拾取的建筑高亮。实际项目里我见过做得最稳定的还是ID单体化加动态高亮结合的方式。拾取后如果feature有建筑ID直接高亮对应实体如果没有就用动态单体化判断。两步走覆盖率高开发量也可控。7.2 选中模型的交互高亮倾斜摄影选中高亮除了用tileset.style改颜色还可以叠加一个轮廓线。轮廓线其实就是一个ExtrudedPolygon从建筑底面拉伸一定高度这样点击时先判断属于哪个建筑然后创建或更新这个半透明的拉伸多边形。官网提到的Cesium3DTileStyle用法tileset.style new Cesium.Cesium3DTileStyle({ color: { conditions: [ [${feature.myAttribute} highlight, color(#00FF00)], [true, color(#FFFFFF)] ] } });但用style改颜色的缺陷是它改变的是整个瓦片的颜色没法做表面纹理高亮的光泽效果。所以很多时候更推荐用叠加多边形的方式代码虽然多一点但视觉效果更好。7.3 模型加载的坐标与缩放问题加载glTF模型时最常见的两个问题位置不对和大小不对。位置不对通常是经纬度坐标给的没问题但模型的中心点不在模型几何中心。比如一个建筑模型的锚点在楼底中心但你把它放在了楼顶的高程上模型就会悬空或陷地。处理办法是建模时就约定好锚点或者在代码里使用model.heightReference和模型自身的坐标修正偏移。大小不对特别是SU模型转成glTF后尺寸变了这大多是因为导出时的单位设置不对。SketchUp里默认是英寸还是英尺导出成米为单位的glTF时容易差一个比例系数。解决办法是在加载时用scale属性修正model: { uri: /models/building.glb, scale: 0.01 // 如果导出尺寸大了100倍 }还有一个问题是模型朝向模型默认是向北的如果实际建筑朝向不同需要旋转模型。Entity的model属性有heading、pitch、roll参数model: { uri: /models/building.glb, heading: Cesium.Math.toRadians(45) }8. 渲染优化的真实场景8.1 大场景卡顿的排查思路很多项目跑到后期发现三维场景转起来很卡热词里提到的cesium 3d地球滚动出现崩溃大概率就是性能问题导致的浏览器崩溃。我的排查顺序是这样先看是不是3D Tiles加载的瓦片太多了瓦片多了会导致显存占用飙升再看是不是Entity数量太多比如几千个Entity同时渲染再看是不是有复杂的动画一直在执行比如每帧都更新的雷达波。判断工具方面官网的scene.debugShowFramesPerSecond是最直观的帧率检查工具把它打开后左上角会显示当前FPSviewer.scene.debugShowFramesPerSecond true;如果FPS常年低于30就要考虑优化了。优先降低3D Tiles的maximumScreenSpaceError从16调到32画面会稍微糊一点但渲染压力会明显下降。然后考虑把远距离的Entity做LOD切换很远的地方只显示一个点近了才显示细节模型。8.2 用primitive代替entity做批量渲染当你需要渲染几百上千个点时Entity的创建开销和事件绑定就很拖后腿了。批量渲染更适合用PointPrimitiveconst collection new Cesium.PointPrimitiveCollection(); viewer.scene.primitives.add(collection); for (let i 0; i data.length; i) { collection.add({ position: Cesium.Cartesian3.fromDegrees(data[i].lon, data[i].lat), color: Cesium.Color.fromCssColorString(data[i].color), pixelSize: 8 }); }PointPrimitiveCollection的实现是WebGL的顶点批量绘制一次性把所有点传给GPU性能远好于循环创建Entity。8.3 Cesium在移动端的注意事项移动端跑Cesium对显存和带宽的要求很高。如果项目需要适配移动端我的建议是把默认的requestRenderMode打开只在需要渲染时才渲染否则Cesium会不断重绘手机很快就会发烫const viewer new Cesium.Viewer(cesiumContainer, { requestRenderMode: true, maximumRenderTimeChange: Infinity });打开这个模式后如果遇到模型拖拽后画面不更新是因为没有触发渲染请求需要在相机变化时手动请求渲染viewer.camera.changed.addEventListener(() { viewer.scene.requestRender(); });9. 常见问题与面试题复盘9.1 官方示例跑不起来的高频原因我在带新人的过程中发现他们报出来的问题高度雷同整理成一张速查表问题原因解决办法页面白屏容器没有高度给容器div加明确的height样式地球是黑乎乎的没有配置Ion token或网络无法访问Ion配置有效token或换用天地图/高德底图Cesium.js加载报404CDN地址失效或版本路径错误改为npm安装或更新CDN链接加载GeoJSON没反应数据源URL跨域或GeoJSON格式有误检查网络请求用数据验证工具检查GeoJSON3D Tiles白屏Ion AssetId没有在账号下关联用自己账号下的assetId或改用fromUrl加载自建数据鼠标点击拾取不到模型没有开启深度拾取设置viewer.scene.pickTranslucentDepth或检查模型材质是否半透明初始化报Cesium is not defined引入顺序错误或模块加载失败确认script标签在代码之前且网络正常加载9.2 面试中经常被问到的Cesium问题根据我自己面试和被面试的经验Cesium相关的高频问题大概是这些Cesium和Leaflet有什么区别。这个问题的坑在于不要说成一个是三维一个是二维Leaflet是二维WebGIS渲染引擎Cesium的核心是三维地球二者的数据模型和渲染管线完全不同。如果你说Leaflet只能画2DCesium能画3D面试官会觉得你理解太浅。更好的说法是Cesium有原生全球瓦片调度和三维场景管理能力而Leaflet更适合二维切片与业务图层叠加两者也有结合使用的情况。Cesium的坐标系有哪些。Cesium里有经纬度坐标系Cartographic、地心笛卡尔坐标Cartesian3、屏幕坐标Cartesian2。实际处理时你要频繁在这三者间切换尤其是从经纬度到Cartesian3要用fromDegrees或fromRadians这个过程背后是椭球体投影如果不了解会莫名觉得坐标不对。什么是3D Tiles。3D Tiles是Cesium为海量三维模型设计的开放规范它把模型数据分成了瓦片金字塔每层瓦片是glTF的子集支持LOD加载。回答时能提到它如何做视锥裁剪、屏幕空间误差、父子瓦片切换这些机制才算真正理解。如何实现单体化。这个问题的标准答案就是ID单体化、动态单体化和分类单体化三种思路。能结合代码说清楚其中一种基本上就过关了。Cesium中的Entity和Primitive的区别。Entity是高层抽象方便开发、提供属性和事件机制Primitive是底层绘制API性能更好。场景数据量大的时候选择Primitive是常见优化手段。9.3 版本升级带来的坑Cesium的版本更新节奏很快每个大版本之间API和默认行为都有变化。我在项目中遇到的一个记忆深刻的坑是从1.90升级到1.100之后之前的Cesium.viewerCesium3DTilesInspectorMixin这类调试混入方法被废弃换成新的Cesium3DTilesInspector控件导致老代码直接报错。另一个常见的升级坑是new Cesium.Viewer时原来可以传imageryProvider参数新版本虽然还支持但更推荐用baseLayer的方式管理图层。如果你在升级后遇到影像图层不显示优先检查是不是这样。所以项目里锁定Cesium版本是一个非常必要的操作。如果不需要新特性就固定在一个稳定版本不要频繁升级。真要升级先在测试环境跑一遍官方的升级指南和示例别直接上生产。10. 一点个人的实操体会最后聊点不太容易被技术文档记录的东西。Cesium这个库最迷人的地方在于它是一个完整的GIS三维引擎不是简单的地图组件。所以你学它的过程中实际上会接触到很多传统前端之外的知识例如WGS84坐标系、瓦片金字塔、LOD策略、WebGL渲染管线、glTF格式、地形生成算法。这些知识在以后接触数字孪生、智慧城市、自动驾驶仿真等领域时都能复用。我的建议是跟着官网把基础概念过一遍后一定要自己做一个相对完整的Demo比如加载一个城市范围内的倾斜摄影、做一个点击楼栋弹出信息的交互、再加一个动态雷达效果。做完这个Demo你对Cesium的认知会上一个台阶再去接真实项目会踏实很多因为很多坑你已经提前踩过了。写这篇内容的时候我回头看自己最早做Cesium项目时写的代码发现很多写法现在看已经很不合理了。这也是一个学习过程的自然规律刚开始会用就行了用多了、踩了坑才会理解官网文档里那些简短句子背后的深意。希望这篇内容能帮你把路走得更顺一些。