简介:这是一份面向Web GIS开发者与三维可视化初学者的Cesium入门实战资料包,围绕三维地球场景搭建,系统演示了如何利用Cesium实现地形、影像、数据图层与3D模型的综合展示。压缩包整体35.82MB,内含873个文件,以JavaScript示例代码(135个)、样式表(43个CSS)为主体,并配套442个PNG、103个JPG、101个GIF等可视化素材,以及少量JSON、XML、HTML等配置文件,结构清晰,便于对照学习。目前已有303人学习下载。资料中涉及Cesium.Viewer初始化、KML/GeoJSON数据加载、glTF模型添加、相机飞行控制等关键操作,同时提供地图控件、弹窗样式等前端界面相关文件(如map-index.css、cesiumviewer.css),可帮助读者快速搭建交互式三维场景,适合用于智慧城市、数字孪生等项目原型验证。
1. 三维场景展示:Cesium 项目最该先解决的不是 API,而是场景组织
做 GIS 项目这么久,我拆过不少 Cesium 相关的资源包,一个很深的感受是:很多人拿到 Cesium 第一反应是去翻 API 文档,但真正让项目拉开差距的,往往是场景怎么组织、数据怎么分层、控件怎么收敛。这份「cesium之三维场景展示篇.zip」就是典型——解压后一堆 CSS 文件(map-index.css、bxmap.css、cesiumviewer.css、jDialog.css、popupoverlay.css 等),看起来是样式文件,实际上是别人已经把一套三维场景的壳子搭好了。你要做的不是重新发明轮子,而是看懂这套壳子怎么跟 Cesium 的初始化逻辑配合。
这套资源的适用人群很明确:已经能跑通 Cesium 官方 Hello World,但不知道真实项目里三维场景该怎么收敛的开发者。它解决的核心问题是「场景展示」——从 Viewer 初始化、影像地形加载、数据源接入,到业务图层和弹窗样式的整合。接下来我把这套场景的搭建逻辑拆开,从初始化到数据接入、再到图形绘制和动态效果,按真实项目的推进顺序走一遍。
2. Viewer 初始化:配置项决定场景基座,也决定后期维护成本
2.1 先分清 CesiumWidget 和 Viewer,再决定用哪个
很多新手上来就new Cesium.Viewer,但如果你只需要一个纯粹的三维地球、不要时间轴和动画控件,直接用CesiumWidget更轻。Viewer 是 CesiumWidget 的超集,内部封装了时间线、动画控件、信息框等默认 UI。但真实业务中,这些默认控件往往要么被隐藏、要么被替换成自己的 UI——这正是资源包里那些 CSS 文件发挥作用的地方。
// 轻量方案:直接用 CesiumWidget const widget = new Cesium.CesiumWidget('cesiumContainer', { baseLayer: Cesium.ImageryLayer.fromProviderAsync( Cesium.TileMapServiceImageryProvider.fromUrl( Cesium.buildModuleUrl('Assets/Textures/NaturalEarthII') ) ), skyBox: false, terrainProvider: new Cesium.EllipsoidTerrainProvider(), sceneMode: Cesium.SceneMode.SCENE3D });CesiumWidget 不创建时间线、动画控件和 infoBox,适合做纯展示型场景。baseLayer在这里指定了默认影像源,buildModuleUrl指向 Cesium 静态资源目录下的 NaturalEarthII 纹理。terrainProvider用 EllipsoidTerrainProvider 表示无真实地形数据时的椭球体表面,加载速度快,适合先跑通流程。
Viewer 的优势在于它自带数据源管理器(dataSources)、实体集合(entities)和默认的鼠标交互事件。如果项目后续要加 KML、GeoJSON 或 CZML 动态数据,直接用 Viewer 能省掉不少手动绑定工作。
2.2 默认控件收敛:资源包里那些 CSS 在管什么事
真实项目里几乎不会让 Cesium 默认 UI 直接裸露在页面上,因为默认的 infoBox 样式和业务系统的弹窗风格很难统一。资源包里的 cesiumviewer.css、jDialog.css、popupoverlay.css 就是干这个的——把 Cesium 的信息框、弹窗样式重写成符合业务侧 UI 规范的样子。
const viewer = new Cesium.Viewer('cesiumContainer', { animation: false, // 关闭动画控件 timeline: false, // 关闭时间线 baseLayerPicker: false, // 关闭底图切换器 geocoder: false, // 关闭搜索框 homeButton: false, // 关闭 Home 按钮 sceneModePicker: false, // 关闭场景模式切换 navigationHelpButton: false, // 关闭帮助按钮 infoBox: false, // 关闭默认信息框 selectionIndicator: false, // 关闭选中指示器 shouldAnimate: true // 允许时间轴驱动动画 });这些配置项的取舍逻辑是:展示型场景保留场景操作能力(鼠标旋转、缩放),去掉与业务无关的 UI 控件。shouldAnimate: true很关键——后面加载 CZML 动态数据时,如果这个值不是 true,动态实体的时间驱动不会生效。
我一般还会在初始化后手动设置相机初始视角,避免每次加载都从太空俯视开始:
viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 200000), orientation: { heading: Cesium.Math.toRadians(0), pitch: Cesium.Math.toRadians(-90), roll: 0 } });setView是瞬间定位,没有过渡动画。heading为 0 表示正北方向,pitch为 -90 度表示垂直向下看。开发阶段用setView快速定位,交付阶段一般改用flyTo给用户一个平滑的进入动画。
2.3 容器尺寸与高 DPI 适配
cesiumContainer这个 div 的尺寸直接决定 WebGL 渲染缓冲区大小。很多项目出现地球只显示一部分、或者拖动有明显延迟,第一步应该看容器 CSS:
#cesiumContainer { width: 100%; height: 100%; position: absolute; top: 0; left: 0; margin: 0; padding: 0; overflow: hidden; }这里有个高 DPI 屏幕的坑。Cesium 默认按 CSS 像素渲染,在 retina 屏上会发虚。解决方案是启用resolutionScale:
viewer.resolutionScale = Math.max(window.devicePixelRatio, 2.0);resolutionScale是渲染分辨率的倍率因子,默认 1.0。设成devicePixelRatio能让文字和线宽在高清屏上更锐利,但代价是 GPU 负载翻倍。2.0 是一个相对均衡的值,地图文字较多的场景不建议超过这个值。
3. 数据接入:影像、地形、矢量数据的分层加载策略
3.1 影像服务:在线服务与本地瓦片的切换逻辑
Cesium 默认的影像服务是createWorldImageryAsync,依赖网络。但政企项目往往要求内网部署,影像源必须换成内部发布的 WMTS 或 TMS 服务。这是场景展示类项目最常见的改造点。
// 内网 WMTS 影像服务示例 const wmtsProvider = await Cesium.ArcGisMapServerImageryProvider.fromUrl( 'http://内部IP:端口/arcgis/rest/services/影像服务/MapServer', { enablePickFeatures: false } ); viewer.imageryLayers.removeAll(); viewer.imageryLayers.addImageryProvider(wmtsProvider);直接用ArcGisMapServerImageryProvider加载内网 ArcGIS 影像服务,enablePickFeatures设为 false 可以避免鼠标点击时向服务端发送 identify 请求,减少不必要的网络开销。如果内网是标准 WMTS,用WebMapTileServiceImageryProvider就行,需要关注layer、style、tileMatrixSetID跟服务端配置严格一致,这三个参数任何一个不匹配都出不来图。
3.2 地形数据:高程对三维场景是质变
没有地形的三维场景只能叫「三维地图」,加上真实地形后,山体起伏、坡度分析、飞行动画才有意义。Cesium 官方之前提供了全球地形服务,但生产环境还是自己用 CTB(Cesium Terrain Builder)切地形更可控。
const terrainProvider = await Cesium.CesiumTerrainProvider.fromUrl( 'http://内网IP:端口/terrain/tiles', { requestVertexNormals: true, requestWaterMask: true } ); viewer.terrainProvider = terrainProvider;requestVertexNormals: true可以让地形在光照下显示明暗变化,否则地形是一片平色。requestWaterMask: true是水面效果开关,只有地形数据里带 watermask 图层时才生效。这两个选项都会增加地形瓦片的数据量,移动端项目建议只保留requestVertexNormals。
3.3 矢量数据源:KML、GeoJSON、CZML 的适用场景
数据源接入是场景展示的核心。Cesium 的dataSources层统一管理各类矢量数据,但它对 KML 的支持并不算好——复杂的 KML 符号、网络链接、TimeSpan 解析经常出问题。GeoJSON 则是最稳定的选择。
const geoJsonDataSource = await Cesium.GeoJsonDataSource.load( './data/北京市边界.geojson', { stroke: Cesium.Color.fromCssColorString('#00ffff'), fill: Cesium.Color.fromCssColorString('rgba(0, 255, 255, 0.2)'), strokeWidth: 2, clampToGround: true } ); viewer.dataSources.add(geoJsonDataSource);clampToGround: true会把几何体贴到地表,这是行政区划、道路、宗地这类数据的标准配置。不设置的话,面数据会浮在椭球面上,看起来像是飘在空中。
KML 数据源加载路径稍有不同,需要先创建数据源对象再加载:
const kmlDataSource = new Cesium.KmlDataSource(); await kmlDataSource.load('./data/标注点.kml', { camera: viewer.scene.camera, canvas: viewer.scene.canvas }); viewer.dataSources.add(kmlDataSource);KmlDataSource.load的第二个参数需要显式传递 camera 和 canvas,因为部分 KML 特性(比如 LookAt 视角、屏幕叠加层)需要依赖它们解析。漏传这两个参数的表现很隐蔽——数据加载成功但视角不动、叠加层不显示,排查起来费时间。
3.4 图层顺序:先地形,再影像,后矢量
数据加载的先后顺序直接影响渲染效果。正确顺序是:先设terrainProvider,再加载影像图层,最后添加数据源。如果先加载数据源再设地形,已经创建的 entity 不会自动贴地,需要手动更新高度。
另外要注意imageryLayers的顺序关系:索引 0 在最底层,新添加的图层默认在顶层。底图、影像叠加层、业务图层按业务需要调整imageryLayers.indexOf(layer)来控制层级。
4. Entity 与 Primitive:两个 API 层级,两条技术路线
4.1 先想清楚:业务数据用 Entity,海量数据用 Primitive
Cesium 提供两套绘制 API:Entity(面向对象封装)和 Primitive(底层渲染)。同样画一个矩形,Entity 几行代码搞定,Primitive 需要手动管理 Geometry 和 Appearance。但 Entity 的封装是有性能代价的——它内部会创建对应的 Primitive,且每个 Entity 独立管理状态更新。
一个直观的参考阈值:场景内同类型对象在 500 个以内,Entity 的开发效率和维护性远胜 Primitive;超过 1000 个,页面开始明显掉帧,这时候要么改成 Primitive 批量渲染,要么用InstancedCollection做实例化绘制。
// Entity 方式:业务数据结构化,属性可追踪 const rectangleEntity = viewer.entities.add({ name: '重点区域', rectangle: { coordinates: Cesium.Rectangle.fromDegrees(116.35, 39.85, 116.45, 39.95), material: Cesium.Color.RED.withAlpha(0.3), outline: true, outlineColor: Cesium.Color.RED } });fromDegrees接收西、南、东、北四个经度纬度值。withAlpha(0.3)生成半透明材质,outline 和 outlineColor 控制边界线。这个矩形虽然创建了 entity,但实际是 Cesium 内部封装的RectangleGeometry在渲染。
4.2 Primitive 路径:Geometry + Appearance 的自由度
Primitive 的好处不只是性能,更重要的是它可以脱离 Entity 的「属性更新」机制做静态渲染。对于运行时不变的海量数据,静态 Primitive 的渲染效率是 Entity 的数值级差距。
// Primitive 方式:批量绘制 1000 个广告牌点位 const positions = []; for (let i = 0; i < 1000; i++) { positions.push(Cesium.Cartesian3.fromDegrees( 116 + Math.random() * 2, 39 + Math.random() * 2, 100 + Math.random() * 500 )); } const primitive = new Cesium.GroundPrimitive({ geometryInstances: new Cesium.GeometryInstance({ geometry: new Cesium.CircleGeometry({ center: Cesium.Cartesian3.fromDegrees(116.39, 39.9), radius: 500000 }) }), appearance: new Cesium.MaterialAppearance({ material: Cesium.Material.fromType('Dot') }) }); viewer.scene.primitives.add(primitive);GroundPrimitive把几何体贴到地表,适合范围标示类数据。MaterialAppearance的材质可以根据业务需求替换成PolylineGlowMaterialProperty(发光线)、StripeMaterialProperty(条纹线)等。注意CircleGeometry创建的是单一几何体,要绘制多个独立圆形得用GeometryInstance数组给每个实例传入独立的modelMatrix。
4.3 动态图形:箭头、流动线、雷达波纹
场景展示里除了静态数据,还有一类「示意性标注」——轨迹箭头、管线流动方向、雷达覆盖范围。答案简单,用CallbackProperty或者CZML驱动。箭头可以用PolylineArrowMaterialProperty:
viewer.entities.add({ polyline: { positions: Cesium.Cartesian3.fromDegreesArray([ 116.39, 39.9, 116.42, 39.92, 116.45, 39.94 ]), width: 8, material: new Cesium.PolylineArrowMaterialProperty( Cesium.Color.fromCssColorString('#ffaa00') ) } });fromDegreesArray接收经纬度交替排列的一维数组。箭头方向是沿线走向的,不需要额外配置。流动线的做法稍有不同,它依赖材质贴图的 UV 偏移:
const flowLineMaterial = new Cesium.Material({ fabric: { type: 'FlowLine', uniforms: { color: Cesium.Color.fromCssColorString('#00ffff'), speed: 10.0 }, source: ` czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material = czm_getDefaultMaterial(materialInput); vec2 st = materialInput.st; float alpha = fract(st.s + czm_frameNumber * 0.01 * speed); material.diffuse = color.rgb; material.alpha = alpha * 0.8; return material; } ` } });czm_frameNumber是 Cesium 内置的帧计数器,每渲染一帧自动累加。材质里用它驱动 UV 偏移就实现了流动效果。speed控制流速,值越大动画越快。这种方式不消耗 CPU 逐帧更新属性,性能开销几乎可以忽略。
雷达波纹效果最常见的实现也是基于CallbackProperty,根据时间戳动态缩放半径:
let startTime = Cesium.JulianDate.now(); viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.39, 39.9), ellipse: { semiMajorAxis: new Cesium.CallbackProperty(function(time, result) { const elapsed = Cesium.JulianDate.secondsDifference(time, startTime); const radius = 100000 + (elapsed % 5) / 5 * 500000; return radius; }, false), semiMinorAxis: new Cesium.CallbackProperty(function(time, result) { const elapsed = Cesium.JulianDate.secondsDifference(time, startTime); const radius = 100000 + (elapsed % 5) / 5 * 500000; return radius; }, false), material: Cesium.Color.RED.withAlpha(0.2) } });CallbackProperty的第二个参数false表示结果不缓存,每帧重新计算。secondsDifference计算两个 JulianDate 的时间差,elapsed % 5让半径在 5 秒周期内从 100 公里膨胀到 600 公里再归零。
5. Cesium 避坑指南:六个高频问题的排查路径
5.1viewer.scene.rendererror大量报错,控制台刷屏
现象:页面加载后控制台持续输出RenderError,场景黑屏或部分瓦片不显示。
原因:最常见的有三类。一是显卡驱动对 WebGL 支持不完整;二是影像瓦片服务返回了损坏的图片数据;三是地形数据精度过高导致着色器编译失败。
解决:先做排除法。把terrainProvider换回EllipsoidTerrainProvider测试,如果报错消失就是地形数据问题。如果还报,打开window.cesiumDebug看一下是哪个资源加载失败。代码层面可以加错误拦截:
viewer.scene.renderError.addEventListener(function(scene, error) { console.warn('渲染错误', error); // 不阻断后续交互,记录错误后继续渲染 scene.requestRender(); });requestRender()强制 Cesium 重新渲染一帧,配合requestRenderMode: true使用时,能避免错误发生后场景永久静止。
5.2 加载 KML 后浏览器卡死
现象:KML 文件不大(几 MB),但加载后页面无响应,CPU 飙到 100%。
原因:KML 里包含了大量Style标签,Cesium 的 KML 解析器会为每个样式创建对应的 Cesium 材质对象,样式数量上千时主线程阻塞。
解决:先离线精简 KML,把用不到的样式标签删掉。更稳的做法是让后端把 KML 转成 GeoJSON 再交给前端。如果必须用 KML,可以考虑在 Worker 线程里解析:
// 用 Web Worker 避免阻塞主线程 const worker = new Worker('./kml-worker.js'); worker.postMessage({ url: './data/标注点.kml' }); worker.onmessage = function(e) { const dataSource = Cesium.KmlDataSource.process(e.data, { camera: viewer.scene.camera, canvas: viewer.scene.canvas }); viewer.dataSources.add(dataSource); };把 KML 文件读取交给 Worker,主线程只做KmlDataSource.process的转换工作。数据量大时这一改能直接决定页面是加载 3 秒还是直接崩溃。
5.3 entity 无法点击选中
现象:实体在场景中能看到,但viewer.pick返回 undefined。
原因:实体的show属性为 true 但材质透明度过低(alpha 接近 0),Cesium 的拾取机制基于颜色编码,全透明对象没有可拾取的颜色缓冲。另一个常见原因是实体被其他图元遮挡。
解决:检查实体的color是否设置了过低的 alpha。如果业务上必须显示半透明,可以给实体加一个不可见的填充层用于拾取:
const pickEntity = viewer.entities.add({ position: position, point: { pixelSize: 8, color: Cesium.Color.WHITE.withAlpha(0.01) }, // ... 实际的显示属性 });alpha 0.01 的白色点几乎不可见,但足够写入拾取缓冲。
5.4 地形加载后影像瓦片偏移
现象:设置terrainProvider后,影像图层和高程明显不重合,山脊线和道路错开几百米。
原因:影像服务和地形服务使用的坐标系不一致,或者地形服务本身没有包含正确的tilingScheme元数据。
解决:确认影像服务是 WebMercator(EPSG:3857)还是地理坐标系(EPSG:4326),用WebMapTileServiceImageryProvider时tilingScheme必须跟服务端一致:
const provider = await Cesium.WebMapTileServiceImageryProvider.fromUrl(url, { layer: 'image', style: 'default', format: 'image/png', tileMatrixSetID: 'EPSG:3857', tilingScheme: new Cesium.WebMercatorTilingScheme() // 关键 });tilingScheme不一致时影像不会整体崩溃,而是逐瓦片错位,看起来像「地砖没对齐」,很难一眼看出原因。
5.5Cesium.Math.toRadians(-heading)的负值问题
现象:flyTo飞过去之后相机朝向反了,或者倾斜角度不对。
原因:heading的角度含义是绕 Z 轴旋转,正值代表顺时针。如果业务系统里习惯用「方位角 = 正北为 0 顺时针」,传给 Cesium 时取负号确实能对上部分场景,但反过来就会朝向相反方向。
解决:正确理解角度约定,不要盲目取负。相机朝向正北 =heading: 0,朝东 =heading: 90,朝南 =180,朝西 =270。pitch 朝下是负值:pitch: -90是垂直俯视。如果从业务系统拿到的角度定义不同,先做一次角度基准换算再传值。
5.6 场景部分区域瓦片永远灰白色
现象:飞到某个区域,地表显示出灰白色的无影像区域,只有地形起伏。
原因:影像服务覆盖范围有限,或者当前层级的瓦片在服务端不存在。Cesium 不会报错,只会渲染空白瓦片。
解决:判断是覆盖范围还是层级策略问题:用viewer.imageryLayers复制一份当前图层,手动传入一个可用的 fallback 影像源:
const fallbackLayer = viewer.imageryLayers.addImageryProvider( await Cesium.TileMapServiceImageryProvider.fromUrl( Cesium.buildModuleUrl('Assets/Textures/NaturalEarthII') ) ); fallbackLayer.alpha = 0.5; // 让 fallback 半透明叠在地图下把 fallback 层插到业务影像层下面,业务瓦片没有的地方就露出底图,至少不会灰秃秃一片。
6. CZML 与 glTF:让场景「动起来」的两个进阶技巧
6.1 CZML 驱动动态实体:时间轴是它的灵魂
CZML 是 Cesium 原生的动态数据格式,JSON 结构描述一个对象的位置、姿态、样式随时间的变化。它比CallbackProperty的优势在于数据驱动——后端只需要生成 CZML 文件,前端不需要写任何动画逻辑。
构造一段简化的路径动画 CZML:
[ { "id": "document", "version": "1.0", "clock": { "interval": "2024-01-01T00:00:00Z/2024-01-01T01:00:00Z", "currentTime": "2024-01-01T00:00:00Z", "multiplier": 10, "range": "LOOP_STOP" } }, { "id": "drone", "name": "巡查无人机", "availability": "2024-01-01T00:00:00Z/2024-01-01T01:00:00Z", "position": { "epoch": "2024-01-01T00:00:00Z", "cartographicDegrees": [ 0, 116.39, 39.9, 500, 300, 116.40, 39.91, 600, 600, 116.42, 39.92, 550 ] }, "orientation": { "epoch": "2024-01-01T00:00:00Z", "velocityReference": "drone#position" }, "model": { "gltf": "./models/drone.gltf", "minimumPixelSize": 64 }, "path": { "width": 2, "material": { "solidColor": { "color": { "rgba": [0, 255, 255, 255] } } } } } ]velocityReference让物体的朝向自动对齐运动方向,不需要手动计算姿态角。cartographicDegrees数组的排列是:时刻、经度、纬度、高度,然后下一组:时刻、经度、纬度、高度。加载方式跟 KML 一样用CzmlDataSource.load。
这里有一个容易忽略的坑:viewer.clock.shouldAnimate必须设为true,否则数据加载了但场景一动不动。这个属性控制时钟是否自动前进,shouldAnimate在 Viewer 初始化配置里的animation: false时默认为 false。
6.2 glTF 模型节点改造:批量替换附着物
glTF 是 Cesium 加载 3D 模型的推荐格式。场景展示中经常要把模型加在地物上——比如把设备模型摆到楼顶、把树木模型随机撒到绿地上。
const position = Cesium.Cartesian3.fromDegrees(116.39, 39.9, 20); const heading = Cesium.Math.toRadians(45); const pitch = 0; const roll = 0; const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll); const fixedFrame = Cesium.Transforms.headingPitchRollToFixedFrame(position, hpr); const modelPrimitive = await Cesium.Model.fromGltfAsync({ url: './models/building.gltf', modelMatrix: fixedFrame, scale: 1.0, minimumPixelSize: 128 }); viewer.scene.primitives.add(modelPrimitive);headingPitchRollToFixedFrame生成一个模型在指定位置和姿态下的模型矩阵。scale是整体缩放系数,minimumPixelSize是模型在屏幕上显示的最小像素大小——相机拉远时模型不会缩成一个小点消失,对场景标注类模型很实用。
模型节点的动态修改要用modelPrimitive.model.getNode('nodeName')拿到节点后再改矩阵。批量生成大量树木、路灯这类重复模型时,用ModelInstance复用同一个 glTF 资源,不同实例只传不同的modelMatrix,性能开销远小于每个模型单独加载一份 glTF。
6.3 卫星波束与视锥效果:可视化最后一步
卫星波束这类「从轨道指向地面」的展示效果,本质是一个锥体几何。Cesium 里可以直接用CylinderGeometry搭配矩阵变换实现,更灵活的方案是手写视锥:
const beamEntity = viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0), cylinder: { length: 800000, topRadius: 1000, bottomRadius: 200000, material: Cesium.Color.CYAN.withAlpha(0.3), outline: true, outlineColor: Cesium.Color.CYAN } });cylinder的length是锥体总长度,topRadius和bottomRadius分别是两端半径。把topRadius设小、bottomRadius设大,就是一个锥体,再叠加上半透明材质和描边,视觉上就是波束从卫星指向地面。要模拟真实方位角,用viewer.entities.add后修改实体位置和朝向,或者直接操作cylinder对应的Primitive的模型矩阵。
从那以后我每次做场景展示类项目,都强制自己先走一遍「Viewer 初始化配置收敛 → 影像地形分层 → 数据源类型选型 → 动效方案定型」这套流程,而不是一上来就往场景里怼数据。这个习惯帮我规避了至少一半的后期返工。资源包里的 CSS 文件正好对应这套流程里的 UI 收敛环节,配合使用能省不少事。希望这些拆解能帮你在自己的 Cesium 项目里少踩几个坑。
本文还有配套的精品资源,点击获取