简介:这是一份基于Cesium的无人机飞行模拟器完整源码,面向三维GIS、Web前端及无人机仿真方向的学习者与开发者,适合用于搭建可交互的飞行训练环境,也可作为Cesium二次开发与WebGL图形编程的实战范例。压缩包共1555个文件、约83MB,以1015个JavaScript源文件与210个GLSL着色器文件为核心,配合CSS/HTML界面、JSON配置、PNG/JPG/SVG图像资源及GIF动图,覆盖三维地图渲染、着色器效果、前端交互与静态资源优化的完整实现。目前已有859人学习下载。源码目录结构清晰,除三维地球渲染与无人机航线控制等核心逻辑外,还包含gulpfile构建脚本、package.json依赖管理等工程化内容,便于系统研究Cesium API调用、GLSL着色器编写、图像资源取舍以及现代前端项目的组织方式。项目采用CSS与HTML分离的页面结构,并结合SVG矢量图保证界面在不同分辨率下的清晰度,这一设计思路也值得前端开发者借鉴。 做一个能跑起来的基于Cesium的无人机飞行模拟器,最难的从来不是起飞,而是怎么把“飞行”这件事在三维地球里变得可信。我最早用Cesium做模拟器时,先在官方示例里铺了OSM建筑物,然后画了一条航线,把一个glTF的无人机模型挂到viewer.entities上,结果模型是能动了,但姿态僵硬得像在传送带上平移,相机也跟不上,更别提雷达波、动态光照这些效果了。后来花了几周把整个源码骨架重写了一遍,才真正摸清这套体系的门道。这篇文章就围绕“基于Cesium技术的无人机飞行模拟器设计源码”这条主线,把从数据结构、仿真循环到特效落地、排错优化的完整思路讲一遍。适合那种不想只用现成飞行模拟软件、想在Web端自己搭一套可视化仿真原型,或者准备把Cesium塞进数字孪生项目里的开发者。
1. 为什么选Cesium而不是专用飞行模拟引擎:一个偏“数据可视化”的定位
1.1 无人机模拟器对渲染引擎的硬性要求
先明确一件事:这里说的“无人机飞行模拟器”不是那种带全套气动模型、遥控器输入、物理碰撞的飞行训练软件,而是一个偏数据可视化、航线预演、态势显示的技术原型。它要满足几个硬性条件:能加载真实地形和影像,能显示大范围地理环境,能按经纬度和高度飞行,能接入实时仿真数据,还要能在浏览器里跑。
专用飞行模拟引擎在这个场景下有两个问题:一是全球地形和影像的加载能力弱,二是和WebGIS生态打通成本高。而Cesium本身就是一个虚拟地球平台,自带全球高程、影像服务、3D Tiles、glTF模型、时间轴动画,天然适合“让无人机按坐标飞到某个位置”这件事。所以我最终选择了Cesium作为渲染核心,同时自己写一套仿真调度逻辑,而不是直接用现成的无人机模拟软件。
1.2 Cesium现有API能覆盖哪些,缺的又是什么
Cesium能直接覆盖的部分很多:地球场景、相机控制、entity动画、3D Tiles加载、粒子、广告牌、聚光灯等。其中对我这个项目最关键的几个API是:
Cesium.SampledPositionProperty:用来按时间采样飞行位置,配合viewer.clock驱动。Cesium.VelocityOrientationProperty:根据速度向量自动计算模型朝向,配合SampledPositionProperty用。Cesium.headingPitchRollQuaternion:处理姿态四元数,实现转向和俯仰。viewer.clock.onTick:作为仿真循环入口,比自己去requestAnimationFrame更贴合Cesium的时间体系。
缺的部分也很明显:没有现成的航线编辑器、没有任务状态机、没有雷达波扫描的组件、没有无人机仪表盘UI。这些都需要在源码层面自己补。如果你只想快速搭原型,官方示例已经够用,但要做到“模拟器”而不是“动画播放器”,就必须把时间、状态、数据、渲染四层拆清楚。
2. 仿真循环与场景数据结构:模块怎么拆,状态怎么流转
2.1 模块清单与数据流
我最终把整个项目拆成了七个模块,每个模块只做一件事:
- SceneManager:负责初始化Viewer、影像、地形、3D Tiles。
- RouteManager:负责航点的解析、插值、生成航线。
- FlightSimulator:核心状态机,维护飞行任务阶段(待机、起飞、巡航、执行动作、降落)。
- UAVEntityManager:负责把无人机模型挂到场景里,并绑定位置、姿态、可见性。
- CameraRig:负责跟随相机、自由视角切换、视角平滑。
- TelemetryDispatcher:从仿真循环里订阅位置、姿态、速度,同时推送仪表盘和图层特效。
- FxLayerManager:负责动态光照、雷达波、矢量箭头、地形压平、热力图等扩展效果。
数据流的核心思想是“单向流动”:航点数据进入FlightSimulator,FlightSimulator根据clock时间计算当前状态,然后把结果写进UAVEntityManager,TelemetryDispatcher再从实体属性里读取数据分发出去。不要在UI回调里直接改无人机位置,否则时间轴一拖拽,所有状态就全乱了。
2.2 仿真循环设计:用Cesium的Clock串联所有状态
很多人一开始会在viewer.scene.postRender里做更新,我试过,能跑但很别扭。postRender每帧触发,但和Cesium的clock时间不同步,拖拽时间轴时容易跳变。更好的做法是用viewer.clock.onTick作为唯一驱动源,在这个事件里根据viewer.clock.currentTime计算任务进度。
viewer.clock.shouldAnimate = true; viewer.clock.multiplier = 2; // 两倍速飞行 viewer.clock.onTick.addEventListener((clock) => { const time = clock.currentTime; flightSimulator.tick(time); telemetryDispatcher.tick(time); fxLayerManager.tick(time); });这里有个重要的设计取舍:所有模块都通过tick(time)接收同一个时间参数,而不是各自去读取clock。这样测试的时候可以直接传入任意时间戳,不需要真的让时钟跑起来,单元测试会好写很多。
FlightSimulator内部维护了一个状态机,每个航点不仅有经纬度高度,还带有动作标记,比如到达该点后转弯、开启雷达、悬停5秒等。状态流转用一张表来描述:
| 状态 | 触发条件 | 动作 |
|---|---|---|
| TAKEOFF | 开始任务 | 高度从地面爬升到航线起点 |
| CRUISE | 到达航线起点 | 按航点顺序飞行 |
| HOLD | 航点动作为悬停 | 悬停指定时间 |
| EXECUTE | 航点动作为载荷操作 | 触发雷达波/光照变换 |
| LAND | 所有航点完成 | 下降到降落点 |
这个状态机是整个模拟器的核心,比Cesium的entity动画更可靠。因为entity动画只是“让模型动起来”,状态机负责“为什么动”。
3. 航线规划、相机追踪与仪表盘联动:核心源码逐段说
3.1 航点插值:经纬度坐标转成时间采样
航线数据最麻烦的是速度控制。只给航点坐标不行,还要给出到达每个航点的时间,然后反推速度。我的做法是先用大圆航线做空间插值,再根据预设速度生成采样点,最后把采样点塞进SampledPositionProperty。
function buildFlightPath(waypoints, speedMs) { const positions = new Cesium.SampledPositionProperty(); const start = Cesium.JulianDate.now(); let elapsed = 0; for (let i = 0; i < waypoints.length - 1; i++) { const p1 = Cesium.Cartesian3.fromDegrees(waypoints[i][0], waypoints[i][1], waypoints[i][2]); const p2 = Cesium.Cartesian3.fromDegrees(waypoints[i + 1][0], waypoints[i + 1][1], waypoints[i + 1][2]); const distance = Cesium.Cartesian3.distance(p1, p2); const duration = distance / speedMs; const interval = Math.max(2, Math.floor(duration * 10)); for (let j = 0; j < interval; j++) { const t = j / interval; const time = Cesium.JulianDate.addSeconds(start, elapsed + duration * t, new Cesium.JulianDate()); const lat = Cesium.Math.lerp(waypoints[i][1], waypoints[i + 1][1], t); const lon = Cesium.Math.lerp(waypoints[i][0], waypoints[i + 1][0], t); const alt = Cesium.Math.lerp(waypoints[i][2], waypoints[i + 1][2], t); positions.addSample(time, Cesium.Cartesian3.fromDegrees(lon, lat, alt)); } elapsed += duration; } return positions; }这段代码里用了一个小技巧:经纬度和高度的线性插值在绝大多数场景下够用,但如果航线跨度过大,经纬度线性插值会偏离大圆航线。真正严谨的做法是先做球面插值(Cesium.Cartesian3.lerp之后转制图坐标),再修正高度。我在项目里为了省事用了线性插值,结果500公里以上的航线在中高纬度会有肉眼可见的偏航,后来才换成球面插值。这是第一个值得记住的坑。
3.2 模型加载与姿态插值:不要只绑一个SampledPositionProperty
把无人机模型挂到entity上很简单,但要让姿态自然,必须把位置和朝向一起处理。位置用SampledPositionProperty,朝向用VelocityOrientationProperty就可以做到基本沿航线方向飞行。但如果航点转弯角度很急,模型会突然甩头,这时候需要给航向角额外做平滑处理。
const droneEntity = viewer.entities.add({ position: sampledPosition, orientation: new Cesium.VelocityOrientationProperty(sampledPosition), model: { uri: "/models/drone.gltf", minimumPixelSize: 64, maximumScale: 2000, }, });这里有个比较隐蔽的问题:VelocityOrientationProperty只根据速度方向算偏航角,不会补偿俯仰角。无人机爬升或俯冲时,模型机头仍然平着,看起来不像在爬升。我的解决方案是额外监听每个采样点的垂直速度,生成一个俯仰偏移角度,然后用矩阵乘法把俯仰角叠加到VelocityOrientationProperty的参考系上。
姿态插值部分我建议用Cesium.Transforms.headingPitchRollQuaternion在每一个采样点构造四元数,而不是让Cesium自动推断。代价是代码多一点,换来的是姿态完全可控,悬停、侧飞、倒飞都能表达。
3.3 相机跟随:平滑与碰撞如何平衡
相机跟随有很多种实现方式,最简单的就是每帧把viewer.camera.lookAt指向无人机。但这样体验很差,转向时镜头会剧烈晃动。我最后采用的是“环形缓冲跟随”方案:维护一个长度为10的相机目标位置历史,相机始终朝着历史目标插值,形成自然延迟效果。
function updateCamera(dronePosition, droneHeading) { cameraTargetHistory.push(dronePosition.clone()); if (cameraTargetHistory.length > 10) cameraTargetHistory.shift(); const target = cameraTargetHistory[0]; const offset = new Cesium.Cartesian3.fromDegrees( Cesium.Math.toDegrees(dronePosition.longitude) + 0.008, Cesium.Math.toDegrees(dronePosition.latitude) - 0.006, 260 ); viewer.camera.lookAt(target, offset); }偏移量需要根据飞行高度动态调整,否则无人机飞高后相机会离得太远。另一个必须处理的问题是相机碰撞:在山区飞行时相机可能钻到地形里,所以我在lookAt之前会检测相机位置是否低于该处地形高度,如果低于就抬升相机高度。这个检测用的是viewer.scene.globe.getHeight,性能开销不大,但能明显减少穿模。
3.4 从仿真循环到DOM仪表盘:状态数据怎么展示
仪表盘我用的是纯DOM元素,每秒更新10次,而不是每帧更新。因为高度、速度、剩余航程这些数据不需要60fps刷新,频繁操作DOM反而掉帧。TelemetryDispatcher在tick里把实体数据写入一个共享对象,仪表盘组件用setInterval读取并渲染。
class TelemetryDispatcher { tick(time) { if (!droneEntity.position) return; const cartographic = Cesium.Cartographic.fromCartesian( droneEntity.position.getValue(time) ); this.telemetry = { longitude: Cesium.Math.toDegrees(cartographic.longitude), latitude: Cesium.Math.toDegrees(cartographic.latitude), altitude: cartographic.height, heading: this.currentHeading, pitch: this.currentPitch, speed: this.currentSpeed, }; } }速度计算不直接读SampledPositionProperty,因为连续两个采样点之间差值算出的速度会抖动。我在FlightSimulator里维护了一个“最近5帧位移集合”,用滑动窗口平均得出平滑速度。这样仪表盘里的速度值不会一直在35和42之间跳动,观感好很多。
4. 动态光照、雷达波、地形压平这些“看得见”的细节是怎么加进去的
4.1 动态光照与阴影:让无人机周围的光线跟随时间变化
Cesium默认光照能把全球照亮,但阴影效果需要显式开启。动态光照不只是让太阳光跟着时间转,更关键的是阴影生成和衰减。我用的是viewer.scene.globe.enableLighting = true加方向光配置,同时为无人机加了一个聚光灯,模拟夜间搜索场景。
viewer.scene.enableLighting = true; viewer.scene.globe.enableLighting = true; viewer.scene.light = new Cesium.DirectionalLight({ direction: Cesium.Cartesian3.fromDegrees(-60, 40, 80), intensity: 2.5, });但要注意,Cesium的光照精度和实时阴影距离有限。无人机在大范围地图里飞行时,近地小物件的阴影很容易出现锯齿或闪动。我的做法是只让阴影覆盖无人机周围500米范围的区域,也就是把阴影贴图分辨率分配在局部,而不是整个场景。这需要自定义渲染逻辑,官方API没有直接暴露对应的控制,我是在场景回调里动态修改光源的direction,保持太阳光方向的同时,增加一个跟随无人机的局部光源来实现。
4.2 雷达波:用CallbackProperty画会呼吸的圆环
雷达波是模拟器里视觉效果最强、实现成本最低的一个特效。用CallbackProperty动态生成环形微波,配合透明度衰减,就能做出不断扩散的雷达波。
const radarEntity = viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(lon, lat, alt), ellipse: { semiMajorAxis: new Cesium.CallbackProperty(() => radarRadius, false), semiMinorAxis: new Cesium.CallbackProperty(() => radarRadius, false), material: new Cesium.ColorMaterialProperty( Cesium.Color.RED.withAlpha(0.3) ), height: alt, outline: true, outlineColor: Cesium.Color.RED, }, });雷达波扩散的核心是radius变量随着时间增长到最大值后归零,这样子看起来才像扫描周期。我在FxLayerManager里维护了一个scanPhase,每次tick里取模计算。如果要做波束状雷达,就改成多个扇形primitive,按角度偏移绘制。
更进阶的雷达波可以画“空心圆柱体”或者旋转的波束面。空心圆柱体用Cesium.PolylineVolumeGraphics或CorridorGraphics都能实现,关键是动态修改顶部和底部半径,让圆柱像从无人机向下发射的扫描光束。这部分我用的是CallbackProperty加Cesium.Math.lerp平滑变化,效果很接近真实雷达扫描。
4.3 地形压平与矢量箭头:不只是视觉效果,还影响航线规划
地形压平在无人机模拟里非常实用。很多飞行任务需要无人机在一个相对平坦的区域起降,但真实地形可能并不平。Cesium提供的地形压平API可以把指定多边形范围内的地形拉平,我基于它加了一个“起降点平整”功能。
const flatPolygon = new Cesium.PolygonHierarchy( Cesium.Cartesian3.fromDegreesArray([ 120.1, 30.1, 120.105, 30.1, 120.105, 30.105, 120.1, 30.105, ]) ); const area = await Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [120.1, 30.1]); const height = area[0].height; for (const p of flatPolygon.positions) { cartographic = Cesium.Cartographic.fromCartesian(p); p = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, height); }矢量箭头则是另一个容易翻车的点。很多人想实现“高德地图那种带方向的路线箭头”,但Cesium内置实体没有箭头样式。我的实现思路是用PolylineGeometry加箭头纹理贴图,或者用多个三角形手动画出箭头形状。如果想快速出效果,可以贴箭头纹理,但纹理方向需要和线方向对齐。项目里我最终用的是基于PolylineVolumeGraphics的箭头条,横截面宽度动态变化,飞行中看起来像流动的指示带。
4.4 哪些特效值得用,哪些不值
我整理了一张取舍表,按“对模拟器真实感的贡献/性能开销”排序:
| 特效 | 贡献 | 性能开销 | 建议 |
|---|---|---|---|
| 动态光照 | 高 | 中 | 必须开启 |
| 雷达波扩散环 | 极高 | 极低 | 强烈推荐 |
| 航线矢量箭头 | 高 | 低 | 推荐 |
| 热力图 | 中 | 中 | 根据场景决定 |
| 视频贴图 | 高 | 高 | 只在大屏展示时用 |
| 高斯泼溅模型 | 高 | 极高 | 谨慎使用 |
高斯泼溅模型是最近比较火的方向,Cesium有对应的扩展加载方式。但说实话,在无人机模拟器里它不是必需项,除非你要展示精细的城市实景扫描模型。热力图也不错,适合展示飞行覆盖范围或信号强度分布,但如果数据量很大,建议先聚合再绘制,不要几万个点直接上Cesium.Entity。
河流材质、天空盒这些属于锦上添花。河流材质可以用PolylineMaterialAppearance配合自定义shader做流动纹理,天空盒直接替换viewer.scene.skyBox就行。如果要做一个“看起来很像飞行模拟软件”的项目,天空盒和河流材质能提升质感,但优先级排在光照和雷达波之后。
5. 坐标偏移、模型漂移、编译分支:完整排错链路
5.1 模型“飘”了:Web墨卡托投影与CGCS2000的纠缠
我在开发过程中遇到的最难排查的问题,是无人机模型在某个区域飞行时会整体偏移几十米,甚至“飘”到路对面。最初我怀疑是模型锚点设置问题,检查了半天glTF没毛病。后来拿不同坐标系的底图数据做对比,才确认问题出在数据源的投影坐标不一致。
Cesium内部使用WGS84椭球和经纬度坐标,但很多二维GIS数据用的是EPSG:3857(Web墨卡托投影),直接把3857坐标当成经纬度传入Cesium,画出来的东西当然会“飘”。解决办法是必须在数据入口做坐标转换,把3857的x、y除以投影比例尺变成经纬度范围,再换算成弧度。我这里加了一个转换函数:
function webMercatorToCartesian3(x, y) { const lon = x / 20037508.34 * 180; const lat = (Math.atan(Math.exp(y / 20037508.34 * Math.PI)) * 360 / Math.PI) - 90; return Cesium.Cartesian3.fromDegrees(lon, lat); }如果是CGCS2000投影坐标系的数据,需要先拿到该分带的中央经线和假北值,做高斯投影反算,得到经纬度后再给Cesium。这一步没有现成的通用函数,我当时直接用proj4库配合自定义坐标系参数完成。另外,如果数据自带的是EPSG:4326经纬度,直接传给Cesium就行,千万别再转投影。
这个排查过程花了我差不多一个下午。现在项目里所有地物数据进来,第一件事就是“验证坐标系”,我会在调试面板上加载一个已知经纬度的点,和底图位置做比对,误差超过1米立刻报警。
5.2 相机周边加载低精度:为什么能看到“加载过程”
模拟器飞行速度很快,经常出现相机飞到哪里,哪里的模型才开始加载的情况。这个现象本质是3D Tiles的Level of Detail策略导致的:Cesium会优先加载相机附近的精细瓦片,但无人机速度如果达到每秒几百米,瓦片加载速度跟不上。
我当时的解决思路是“相机周边预加载”:设定一个比相机视角更广的预加载包围盒,并在飞行动线方向提前请求瓦片。具体操作是监听相机移动事件,动态调整viewer.scene.screenSpaceCameraController的视角范围,同时给3D Tiles设置一个偏大的maximumScreenSpaceError,让远处瓦片先以低精度形式加载出来。
const tileset = await Cesium.Cesium3DTileset.fromUrl("/data/tileset.json"); tileset.maximumScreenSpaceError = 16; // 适当调大,低精度先顶上来 viewer.scene.primitives.add(tileset);这里有个权衡:maximumScreenSpaceError调得越大,远处模型越粗糙,但加载也越流畅。无人机模拟器追求的是不穿帮的连续感,所以我把16作为默认值,进入目标区域后再动态降到2到4,触发精细加载。这个“动态LOD”的思路比单纯等Cesium自动调度靠谱得多。
5.3 强制GroundPrimitive更新与自定义编译分支
还有一个高频问题:某些地形压平、雷达波覆盖在GroundPrimitive上的贴图,移动相机后颜色不刷新,或者地形更新后旧的primitive还残留在画面上。这是Cesium的ground primitive缓存机制在作怪,它为了性能会缓存渲染结果,但动态变更时缓存不会自行失效。
遇到这种情况,可以在变更后强制更新相关汇合体。官方没有直接提供forceUpdate,但可以通过切换primitive的show属性强迫它重新创建,更稳的是viewer.scene.requestRender()配合把primitive从集合里移除再重新添加。我的经验是:不要依赖内置缓存,在每次地形压平改变后,销毁旧的ground primitive,创建新的。开销不大,但能彻底避免那种“改了数据但画面不变”的诡异问题。
自定义编译Cesium分支也是很多人会碰到的事。默认npm包是预编译好的,功能够用,但如果你想调整shader、增加自定义primitive类型,或者想改掉一些内置UI行为,就得自己编一个分支。编译流程不复杂:拉源码、跑npm install、执行npm run combineRelease,但要注意Node版本和Python版本匹配,我在编译时因为Python版本过高卡了很久。如果只是为了加一个功能,更推荐用扩展机制而不是改源码,比如自定义MaterialAppearance或写一个CustomShader,这样不用承担每次更新Cesium时合并代码的负担。
5.4 加载MVT、SVG等非标准数据源
无人机飞行模拟器除了地形和模型,还经常要叠加实时地理数据,比如MVT矢量切片、SVG标注、图片底图。Cesium原生不直接支持MVT,一般思路是先解码MVT的矢量瓦片,转换成GeoJSON,再通过GeoJsonDataSource加载。解码方案可以找现成的库,也可以用Mapbox的vector-tile-js。这个过程最大的坑是属性字段的精度丢失,MVT里存的是整数坐标,需要乘以extent换算成经纬度,如果换算系数错了,所有要素都会堆到同一个点。
SVG作为底图或标注层,Cesium也不直接支持。我的做法是把SVG转成Canvas画布,再作为SingleTileImageryProvider贴到底图上。注意透明度处理和缩放比,SVG默认的viewBox和Cesium的rectangle如果对不齐,标注会偏位。视频贴图相对简单,用VideoSynchronizer把video元素和Cesium时间同步,然后作为图像层贴在某个实体表面。这个功能很适合模拟“无人机实时回传画面”,但视频分辨率一定要控制,4K视频直接上贴图会把帧率拖到个位数。
6. 从模拟器到应用平台:后续扩展的几种可行路径
6.1 和Vue3、Unreal、Unity的联动
如果项目组已经有Vue3前端框架,可以直接用vue3-cesium组件库把Viewer封装成组件。但要注意,组件化之后不要在Vue的响应式数据里直接存Cesium对象,否则每次数据变化都会触发大量的Proxy重写,性能瞬间崩掉。正确做法是维护一个非响应式的Viewer实例单例,在Vue组件里只通过事件和命令通信。
Cesium还有一个比较特殊的方向是Cesium for Unity和Cesium for Unreal。这两个是把Cesium的地球能力嵌入游戏引擎,在一个相对传统的3D引擎里渲染全球地形。如果你有Unity或Unreal的团队,可以把这套模拟器的状态机逻辑移植过去,渲染和UI交给引擎做,数据调度继续用Cesium。我在一个城市孪生项目里试过Cesium for Unreal,地理围栏绘制很方便,但动态光照和雷达波这些特效要另外实现,并没有完全继承Web端的方便。
6.2 从飞行模拟器到数字化平台的扩展
做完基础模拟器后,可以往两个方向扩展。一是往“任务规划”方向走,加入地理围栏、禁飞区、走廊规划,让无人机只在合法区域内飞行。绘制地理围栏和绘制普通矩形的区别在于,围栏需要处理海拔高度和地形的关系,目前我用的是PolygonHierarchy加动态高度,数据实时写入后端。二是往“数据展示”方向走,接入热力图、实时视频、传感器数据图表,让模拟器变成一个态势感知平台。
这两个方向需要的底层能力其实都已经在之前的源码骨架里具备:状态机、坐标转换、特效图层管理。新增功能时不需要动核心模块,只要在FxLayerManager里加一个“围栏渲染器”,在TelemetryDispatcher里加一个数据字段,扩展成本很低。
顺带提一句,热力图的实现如果不想自己写插值,可以直接用Cesium.PointPrimitive加颜色渐变,或者用heatmap.js生成Canvas再做贴图。我在项目里选了后者,因为数据量到了一万以上时,实体图元的渲染压力太大,而Canvas贴图只需要一张图像。
6.3 还会继续踩的坑
结合这几周的使用体会,再补充几个常见坑:
- 不要过度依赖
SampledPositionProperty:它在长航线采样点很多时会占大量内存,优化方式是合并相邻近似点,或者分段加载采样数据。 - 雷达波和光照叠加时注意透明度混合模式:雷达波扩散环在明亮地表上看不清,需要把
blendOption调整成Cesium.BlendOption.OPAQUE或半透明。 - 实际项目里通常不需要加载“官方模型”,公司自己的无人机模型往往带复杂的PBR材质和动画骨骼,Cesium对glTF 2.0支持还可以,但遇到多个动画clip时的播放控制并不方便,要在模型导出时把动画烘焙掉。
- 如果要在手机上使用,不要开太多后处理特效,动态光照加雷达波加3D Tiles,中端手机帧率会掉到20帧以下。建议在低端设备上自动关闭阴影和热力图图层。
这套源码骨架真正值钱的地方在于状态机和流程控制,而不是某一条航线或者某一个视觉特效。把仿真循环、数据分发、图层管理层拆对了,后面接再多的功能也是往里添砖,不用推翻重来。
本文还有配套的精品资源,点击获取