十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Cesium航线规划从零实现:航点编辑、飞行模拟与交互式演示

Cesium航线规划从零实现:航点编辑、飞行模拟与交互式演示 简介这是一份基于CesiumJS的无人机航线规划可运行源码面向前端GIS开发者与无人机任务规划相关项目人员完整复现大疆司空2平台中航线设计与动态渲染能力。项目核心亮点在于航线渲染中的箭头呈现作者在尝试着色器与图片方案后采用离屏Canvas动态生成箭头位图并注入GPU实现随地图缩放自适应调整、角度严格匹配航段切线方向的定向箭头效果。源码采用航点—航段—航线三层结构支持WGS84坐标转换、直线/圆弧/贝塞尔插值、多架次协同、禁飞区规避等功能每个航点含12项结构化属性并集成了基于React的交互控制面板。压缩包仅3个文件以html、inscode及gitignore为主整体仅8KB轻量精简便于直接运行调试。目前已有44人学习下载对想快速掌握Cesium三维航线绘制的开发者具有扎实的参考价值。 看到《Ceisum航线规划实现[可运行源码]》这个标题我第一反应是又一个把Cesium拼错的老哥。不过这不重要重要的是“航线规划”这个需求最近问的人确实多——无人机巡检要扫一条折线航迹飞行训练要预演航线园区安防要规划巡逻路径。本质都是同一件事在一张三维地图上把离散航点变成一条连续、可编辑、能回放模拟的航线。这篇文章就把我从零搭一个可运行的Cesium航线规划Demo的完整过程拆开讲核心代码会全部贴出来完整可运行工程文件我放在文末说明获取方式。适合刚接触Cesium、想快速跑通一个航线规划原型或者正在做无人机/车辆轨迹可视化方案评估的同学。1. 先把环境弄对Cesium项目初始化与底图选型1.1 为什么是Cesium而不是Three.js或OpenLayers很多人一开始都在纠结选型我直接说结论做航线规划这类“地理空间可视化”需求Cesium是目前最省事的。Three.js确实能做出炫酷的3D场景但经纬度投影、地形高程、坐标系转换、多图层叠加这些GIS基本功全都得自己造轮子一个坐标系写错航线就不知道飘到哪个星球去了。而OpenLayers、Leaflet虽然是老牌GIS库但它们是二维地图引擎做不出“飞机绕山飞”这种带高程和视角的直观效果。Cesium的好处在于它内置了WGS84坐标系、地球椭球体、3D Tiles地形、时间轴动画这一整套体系。航线规划里最核心的“路径可视化 按时间运动”它原生就支持不需要额外引入数学库做轨迹插值。代价就是概念多上手门槛比2D地图高一点但你要知道这些概念恰恰是项目后期无论如何都要面对的。1.2 工程初始化与基础Viewer配置我建议直接用Vite npm包的方式不要用CDN引入旧的全局版Cesium。原因很简单后来的Cesium版本模块化程度越来越高你要用构造器、插值算法、实体属性按需import比挂一个全局Cesium.xxx舒服得多也方便后续扩展自定义模块。npm create vitelatest cesium-route-planning cd cesium-route-planning npm install cesium初始化Viewer时我习惯把用不到的控件关掉避免干扰航线编辑操作import * as Cesium from cesium; const viewer new Cesium.Viewer(cesiumContainer, { animation: true, timeline: true, baseLayerPicker: false, geocoder: false, homeButton: true, sceneModePicker: false, navigationHelpButton: false, infoBox: false, selectionIndicator: false }); viewer.scene.globe.enableLighting false;这里有一个常见误解新手会把animation和timeline关掉结果后面做飞行模拟时飞机不动调了半天才发现是时钟控件被关了。时间线控件不只是UI它直接关联Viewer内部的Clock对象。飞行模拟的核心逻辑依赖这个Clock所以这两个控件建议保持开启至少要保留Clock的运转。1.3 底图加载绕不开的Token问题先给结论新版本Cesium如果不配置Ion Token默认底图会直接黑屏控制台报401。所以你必须有一个自己的Token去Cesium官网注册后就能拿到。注册免费个人学习完全够用。Cesium.Ion.defaultAccessToken 你的_token;如果你不想注册或者公司网络访问Ion不稳定可以绕开Ion直接加载第三方WMTS服务。最省事的方案是用ArcGIS World Imagery的在线影像不需要Token只要网络能访问就行const viewer new Cesium.Viewer(cesiumContainer, { baseLayer: false }); const provider await Cesium.ArcGisMapServerImageryProvider.fromUrl( https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer ); viewer.imageryLayers.addImageryProvider(provider);我个人建议初学者直接用ArcGIS这个方案先把核心功能跑通后面再统一换正式底图。底图这关过了接下来才是航线规划的重头戏。注意baseLayer: false和imageryProvider: false在不同版本里有差异。Cesium 1.107之后推荐用baseLayer: false旧版本用imageryProvider: false。遇到报错先查你的版本号别一上来就复制代码。2. 航线的数据模型航点设计、坐标转换与平滑画线2.1 航点数据怎么组织才不容易乱航线规划的第一步不是画线而是设计数据模型。我见过很多demo把航点写死成一个个变量后面要加交互和飞行模拟时代码直接变成面条。我的经验是航点必须是一个纯数组每个航点包含经纬度、高度、名称这三个最基本的字段。const waypoints [ { lon: 121.4737, lat: 31.2304, height: 500, name: T1 }, { lon: 121.4902, lat: 31.2401, height: 800, name: T2 }, { lon: 121.5139, lat: 31.2278, height: 1200, name: T3 }, { lon: 121.5301, lat: 31.2156, height: 600, name: T4 } ];这个数组是后续所有功能的数据源。画线读取它标注读取它飞行模拟读取它交互编辑也是修改它。所以代码逻辑的顺序应该是先确定数据模型再写渲染逻辑。如果你把航点数据散落到各个entity里后期维护就是一场灾难。2.2 坐标转换经纬度到笛卡尔坐标的背后逻辑Cesium内部使用的是ECEF地心地固坐标系也就是说所有实体的位置最终都得是Cartesian3而不是经纬度。但人脑习惯的肯定是经纬度所以需要Cesium.Cartesian3.fromDegrees()做转换。const positions waypoints.map(w Cesium.Cartesian3.fromDegrees(w.lon, w.lat, w.height) );这里有个特别容易踩的坑fromDegrees的第三个参数是高度单位是米。很多新手只传经纬度高高度省略不写默认当成0结果航线直接贴在地上地形一起伏航线就钻到山体里。所以定高航线一定要显式传高度值。如果你需要让航点贴地也就是从地形上获取高度可以用下面的方式const carto Cesium.Cartographic.fromDegrees(w.lon, w.lat); const height viewer.scene.globe.getHeight(carto); const position Cesium.Cartesian3.fromDegrees(w.lon, w.lat, height);但注意这依赖地形数据已经加载完成没加载出来时getHeight可能返回undefined。实际项目里我一般会加个兜底查不到高度就用默认值500米至少保证航线能画出来。2.3 画线、标注和航点符号有了positions数组画线就很简单了。我用PolylineGlowMaterialProperty做发光材质视觉效果比纯色线好很多尤其适合演示场景viewer.entities.add({ polyline: { positions: positions, width: 4, material: new Cesium.PolylineGlowMaterialProperty({ color: Cesium.Color.CYAN, glowPower: 0.2 }), clampToGround: false } });航点标注我一般分成两个实体一个点符号一个文字标签这样便于后面单独控制waypoints.forEach((w, i) { viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(w.lon, w.lat, w.height), point: { pixelSize: 10, color: Cesium.Color.ORANGE, outlineColor: Cesium.Color.WHITE, outlineWidth: 2 }, label: { text: ${w.name} (${i 1}), font: 14px sans-serif, pixelOffset: new Cesium.Cartesian2(0, -20), disableDepthTestDistance: Number.POSITIVE_INFINITY } }); });disableDepthTestDistance这个参数很实用。如果航点被地形或建筑挡住默认标签会被遮挡设置成无穷大后标签永远显示在最上层方便交互操作。2.4 让航线转弯更自然Catmull-Rom插值直接用折线连接航点出来的效果是直的折角。真实飞行中转弯有半径航线看起来应该有平滑过渡。这个需求可以用Catmull-Rom样条曲线解决const spline new Cesium.CatmullRomSpline({ times: waypoints.map((_, i) i), points: positions }); const smoothPositions []; for (let t 0; t waypoints.length - 1; t 0.02) { smoothPositions.push(spline.evaluate(t)); }这样画出来的航线就会在航点处平滑转弯。要注意的是样条曲线不一定会精确经过所有航点它是“靠近”航点而不是“穿过”。如果你需要严格经过每个航点那就要用Hermite插值或者分段线性插值在“严格经过”和“平滑过渡”之间找一个平衡点。3. 让飞机跟航线动起来Clock时间轴驱动的姿态与位置3.1 为什么必须用时间轴驱动很多人第一次做飞行模拟第一反应是写个requestAnimationFrame循环每帧手动插值改飞机位置。这个思路能做出来但后患很大暂停、倍速、倒放、对齐航点时间全得自己实现而且一旦场景里还有别的动画帧率波动会直接影响飞行速度。Cesium的Clock机制解决的就是这个问题。它把“场景时间”作为唯一基准位置采样、轨迹尾迹、相机跟踪全都跟时间绑定。你只需要把“什么时间在什么位置”这个对应表提供给Cesium剩下每帧该显示在哪Cesium自己算。这等于把动画控制权交还给了引擎省心太多。3.2 配置Clock和SampledPositionProperty首先设定飞行时间范围。我一般设定整个航线飞行180秒但倍速调到2实际观看就是90秒const start Cesium.JulianDate.now(); const duration 180; const stop Cesium.JulianDate.addSeconds(start, duration, new Cesium.JulianDate()); viewer.clock.startTime start.clone(); viewer.clock.stopTime stop.clone(); viewer.clock.currentTime start.clone(); viewer.clock.clockRange Cesium.ClockRange.LOOP_STOP; viewer.clock.multiplier 2; viewer.timeline.zoomTo(start, stop);然后创建一个SampledPositionProperty把航点按时间均匀采样进去const positionProperty new Cesium.SampledPositionProperty(); const times []; const values []; waypoints.forEach((w, i) { const time Cesium.JulianDate.addSeconds( start, (duration / (waypoints.length - 1)) * i, new Cesium.JulianDate() ); times.push(time); values.push(Cesium.Cartesian3.fromDegrees(w.lon, w.lat, w.height)); }); positionProperty.addSamples(times, values); positionProperty.setInterpolationOptions({ interpolationDegree: 2, interpolationAlgorithm: Cesium.HermitePolynomialApproximation });这里interpolationDegree: 2表示二阶插值加上HermitePolynomialApproximation算法后飞机会在航点处平滑转向不会出现瞬间折角。3.3 创建飞机实体位置、朝向、尾迹飞机实体绑定这个positionProperty再配一个VelocityOrientationProperty让它自动朝向运动方向const aircraft viewer.entities.add({ position: positionProperty, orientation: new Cesium.VelocityOrientationProperty(positionProperty), model: { uri: /models/aircraft.glb, minimumPixelSize: 64 }, path: new Cesium.PathGraphics({ leadTime: 0, trailTime: 60, width: 3, material: Cesium.Color.CYAN }) }); viewer.trackedEntity aircraft;VelocityOrientationProperty会自动根据速度矢量计算飞机朝向这对大多数航线演示够用了。但如果你要做“机头始终朝某个方向”或者模拟盘旋机动就需要自己算HeadingPitchRoll用Cesium.Transforms.headingPitchRollQuaternion生成四元数。关于模型我先用Cesium官方提供的CesiumMilkTruck.glb测试跑通后再换自己的模型。如果官方模型能显示你自己的模型不显示那基本就是模型跨域或格式问题不是代码问题。关于尾迹PathGraphics要注意trailTime的单位是秒不是距离。倍速调到4倍时同样60秒的尾迹会拉得特别长因为它按时间算不按实际路径长度算。这是特性不是bug理解了这个逻辑调参就有方向了。4. 交互式航线编辑点选、拖拽与实时更新4.1 用ScreenSpaceEventHandler监听鼠标航线规划不能只是把死数据画出来得能在地图上自由添加、移动航点。Cesium的交互集中在ScreenSpaceEventHandler里它可以把鼠标事件和屏幕坐标绑定起来。const handler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);这里有个判断优先级的问题点击时先要判断点到了什么如果点到了已有的航点实体就进入拖拽如果点到了空白地图就添加新航点。4.2 点击添加航点、右键删除航点添加航点的核心是把屏幕坐标转换为地球上的经纬度坐标handler.setInputAction((movement) { if (isDragging) return; const picked viewer.scene.pick(movement.position); if (Cesium.defined(picked) picked.id picked.id.waypointIndex ! undefined) { return; // 点到了已有航点交给拖拽逻辑处理 } const cartesian viewer.camera.pickEllipsoid(movement.position, viewer.scene.globe.ellipsoid); if (cartesian) { addWaypoint(cartesian); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);右键删除航点handler.setInputAction((movement) { const picked viewer.scene.pick(movement.position); if (Cesium.defined(picked) picked.id picked.id.waypointIndex ! undefined) { removeWaypoint(picked.id.waypointIndex); } }, Cesium.ScreenSpaceEventType.RIGHT_CLICK);addWaypoint里需要把Cartesian3转换为经纬度然后push到waypoints数组再重新渲染。这里有一步很关键转换时用到了Cesium.Cartographic.fromCartesian拿到的高度是相对于椭球面的不是相对地形的。如果用户点在地形高处这个高度可能不准确生产环境需要额外处理。4.3 拖拽移动航点防止相机被带跑拖拽的实现要点是LEFT_DOWN开始拖、MOUSE_MOVE更新位置、LEFT_UP结束拖拽。最坑的点在于拖拽会触发默认的相机旋转逻辑你拖一个航点结果地图也跟着转体验很差。解决办法是在拖拽开始时关掉相机旋转控制let isDragging false; let dragEntity null; handler.setInputAction((movement) { const picked viewer.scene.pick(movement.position); if (Cesium.defined(picked) picked.id picked.id.waypointIndex ! undefined) { isDragging true; dragEntity picked.id; viewer.scene.screenSpaceCameraController.enableRotate false; } }, Cesium.ScreenSpaceEventType.LEFT_DOWN); handler.setInputAction((movement) { if (!isDragging || !dragEntity) return; const cartesian viewer.camera.pickEllipsoid(movement.endPosition, viewer.scene.globe.ellipsoid); if (cartesian) { dragEntity.position cartesian; } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); handler.setInputAction(() { isDragging false; dragEntity null; viewer.scene.screenSpaceCameraController.enableRotate true; rebuildRoute(); }, Cesium.ScreenSpaceEventType.LEFT_UP);拖拽时直接用pickEllipsoid会把高度变成0这是个大坑。我处理的方式是拖拽开始时记录航点原始高度更新时只改经纬度高度保持不变const carto Cesium.Cartographic.fromCartesian(cartesian); const newPosition Cesium.Cartesian3.fromDegrees( Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude), dragEntity.originalHeight ); dragEntity.position newPosition;这样用户拖的是航点的平面位置高度不丢失飞机不会突然穿地。4.4 重算航线与飞行采样拖拽或增删航点后关键的一步是rebuildRoute()把当前所有航点实体的最新位置重新读出来更新waypoints数组然后重新生成折线实体和SampledPositionProperty的采样数据。SampledPositionProperty可以清空后重新addSamples但我建议直接销毁旧的飞机实体创建新的逻辑更清晰避免时间采样残留。function rebuildRoute() { waypoints.length 0; waypointEntities.forEach(ent { const carto Cesium.Cartographic.fromCartesian(ent.position.getValue()); waypoints.push({ lon: Cesium.Math.toDegrees(carto.longitude), lat: Cesium.Math.toDegrees(carto.latitude), height: carto.height, name: ent.name }); }); // 重画航线 viewAllEntities(); // 重置飞行模拟 startFlightSimulation(); }5. 跑通之后才会遇到的坑现象、原因与排查方向5.1 地图黑屏不一定是Token的锅虽然前面我说Token问题是第一坑但还有一层坑是Token配了地图还是黑屏。这时不要急着怀疑网络先打开浏览器Network面板看瓦片请求到底成功没有。如果请求返回403那就是Token有效期或域名白名单问题如果请求压根没发出去那就是底图Provider配置错了。另外有些浏览器插件会拦截跨域请求航点数据存在前端时感觉不出来一加载在线瓦片就黑屏排查时要先把插件禁用。5.2 飞机不显示先换官方模型测试飞机模型加载不出来时我最常用的方法就是先替换成官方内置模型测试。官方模型能显示而自己的不显示99%是跨域或模型格式问题。GLB格式比glTF更省事一个文件搞定纹理也内嵌了建议导出成GLB。另外模型文件不要放在src目录下要放public目录Vite构建后路径才不会乱。5.3 尾迹长度看起来不对我用表格列出几个容易混淆的调节项方便你对照排查现象原因解决方向飞机不走、卡住Clock没启动或currentTime不在采样区间内检查viewer.clock配置timeline控件是否被关掉航线变成尖角没有设置插值算法或interpolationDegree太低SampledPositionProperty里配置HermitePolynomialApproximation倍速调快后尾迹特别长trailTime按时间计算不按距离倍速越高trailTime要调小拖拽后飞机穿地高度丢失变成0拖拽时保留原航点高度模型不显示跨域、格式、路径问题换官方模型测试用GLB格式放public目录5.4 多航点场景下的性能隐患当航点数量超过几百个时用Entity一个个画点和线浏览器会明显卡顿。原因在于Entity是高层封装每个都带独立的属性监听和渲染状态。这种情况建议改成Primitive方式或者先用GeoJSONDataSource批量导入数据。但如果你只是做演示DemoEntity完全够用没必要一开始就上Primitive除非你的航点真的多到拖拽都掉帧。最后说两句个人习惯我自己的开发习惯是先把“点-线-模型-时间轴”这一整套跑通确定功能方向再考虑换模型、换底图、接后端接口。很多同学一上来就纠结选哪个飞机模型、要不要加载地形、航线数据要不要走接口结果连最基本的航线都画不出来。航线规划这个需求核心是数据模型和时间轴两个东西其他都是外围包装。完整可运行工程我打包放在资源区了需要的话评论区吱一声我看到会回复。还有一个小技巧如果飞机飞行过程中机头方向不稳别急着改模型先把interpolationDegree调高到3试试大部分转向抖动都会好很多。本文还有配套的精品资源点击获取
返回列表