Cesium.js实现无人机按轨迹飞行,并扫描地面

无人机在生活的很多方便,已经运用的越来越广泛,无人机巡检、无人机喷房、灾害查勘、摄影测量等等。在GIS开发中,我们经常也需要对接无人机的数据,倾斜摄影模型、多光谱影像等等。

最近需要开发一个无人机贴地飞行,扫描地面的效果。查找了很多资料,也遇到了一些困惑,比较麻烦,不过最终都解决了。这里分享一下遇到的一些问题和最终的解决方案。

最终效果

先看效果和读者期望的是否一致或相似。
无人机扫描
动态效果

遇到的问题

  1. 如何让无人机按预定轨迹移动

模型随着轨迹移动,有两种方式:

一种是CZML,制作特定的轨迹路线文件,Cesium加载后,模型根据路线移动。这种方式,制作CZML有点麻烦,我没有找到相关的工具。

二是使用SampledPositionProperty,进行路线的插值。我采用的是这种方法,直接 以经纬度作为参数就可以了。

  1. 模型姿态调整

刚刚加载无人机模型时,飞机机身是垂直于地面的。模型的姿态需要根据路径动态计算,一直没有找到,在路径计算的结果上便宜的方法。
垂直地面

最后直接,下载了一个开源工具,对GLTF模型进行编辑,将模型的旋转角度修改了。加载新的模型,无人机姿态就正常了。

这个编辑工具是blender,开源工具,安装起来也非常方便,支持对gltf格式的编辑与修改

  1. 扫描地面的效果

扫描地面的效果,原理上是添加了一个圆锥,半透明的颜色,看起像像是扫描一样。圆锥的高度比无人机略低。和无人机模型同时移动,形成了上图的效果。

完整代码

<template>
  <div id="cesiumContainer">

  </div>
</template>

<script>
import * as Cesium from 'cesium';

export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
  setup() {
    window.CESIUM_BASE_URL = '/cesium/';
    Cesium.Ion.defaultAccessToken = '你的token';
  },
  data(){
    return{
      height:3000
    }
  },
  mounted() {
    // "cesiumContainer"是需要渲染地图的dom的id.
    const viewer = new Cesium.Viewer('cesiumContainer', {
      terrainProvider: Cesium.createWorldTerrain()
    });
    window.viewer=viewer;

    this.prepareData()

  },
  methods:{
    prepareData(){
      let resArr = [
        { lon: 108.36707938843418, lat: 22.82040673160957, height: 0,   },
        { lon: 108.41378409564336, lat: 22.819920224242807, height: 0,   },
        { lon: 108.46048880285254, lat: 22.819433716876045, height: 0,   },
        { lon: 108.50719351006173, lat: 22.818947209509282, height: 0,   },
        { lon: 108.5538982172709, lat: 22.81846070214252, height: 0,   },
        { lon: 108.5684908551974, lat: 22.79975173193051, height: 0,   },
        { lon: 108.54682627836809, lat: 22.75866797098716, height: 0,   },
        { lon: 108.5364187466562, lat: 22.713135019747657, height: 0,   },
        { lon: 108.51012491242496, lat: 22.684817620988767, height: 0,   },
        { lon: 108.46390406008231, lat: 22.678094587920747, height: 0,   },
        { lon: 108.41768320773969, lat: 22.67137155485273, height: 0,   },
        { lon: 108.40521353712411, lat: 22.63231030624854, height: 0,   },
        { lon: 108.40047211282993, lat: 22.585844348165494, height: 0,   },
        { lon: 108.40076314236018, lat: 22.539429102792543, height: 0,   },
        { lon: 108.40643999603807, lat: 22.493068131089686, height: 0,   },
        { lon: 108.40927369012122, lat: 22.446956147780245, height: 0,   },
        { lon: 108.3956249685447, lat: 22.40228760443888, height: 0,   },
        { lon: 108.39365522094775, lat: 22.358234673324827, height: 0,   },
        { lon: 108.41192432111414, lat: 22.315248555286278, height: 0,   },
        { lon: 108.42500817357596, lat: 22.271118918346254, height: 0,   },
        { lon: 108.42651407546654, lat: 22.224435959738113, height: 0,   },
        { lon: 108.43518145756487, lat: 22.17864681103129, height: 0,   },
        { lon: 108.4451958307643, lat: 22.133025777567156, height: 0,   },
        { lon: 108.44898976012885, lat: 22.086544506394528, height: 0,   },
        { lon: 108.4569608382348, lat: 22.04154387903869, height: 0,   },
        { lon: 108.48114597278934, lat: 22.00158583064425, height: 0,   },
        { lon: 108.50729410478296, lat: 21.963447043352055, height: 0,   },
        { lon: 108.54530135015922, lat: 21.93629901094044, height: 0,   },
        { lon: 108.58330859553548, lat: 21.909150978528828, height: 0,   },
        { lon: 108.62717065532753, lat: 21.904893075811103, height: 0,   },
        { lon: 108.67098222685098, lat: 21.896572536781637, height: 0,   },
        { lon: 108.71231431555826, lat: 21.874818805883073, height: 0,  },
        { lon: 108.75609068622725, lat: 21.861157212821972, height: 0,   },
        { lon: 108.8025541826556, lat: 21.85639172600881, height: 0,   },
        { lon: 108.84746943394899, lat: 21.844801687465267, height: 0,   },
        { lon: 108.89144073261119, lat: 21.829050774511643, height: 0,   },
        { lon: 108.93541203127339, lat: 21.81329986155802, height: 0,   },
        { lon: 108.97980814203709, lat: 21.799033177178963, height: 0,   },
        { lon: 109.02545431177013, lat: 21.78913400783927, height: 0,   },
        { lon: 109.07110048150317, lat: 21.779234838499573, height: 0,   },
        { lon: 109.1167466512362, lat: 21.76933566915988, height: 0,   },
        { lon: 109.16245083841007, lat: 21.75972231270521, height: 0,   },
        { lon: 109.20834398370768, lat: 21.751039825757015, height: 0,   },
        { lon: 109.23344423638372, lat: 21.725129392815596, height: 0,   },
        { lon: 109.23344423638372, lat: 21.678422151782613, height: 0,   },
        { lon: 109.22763271225567, lat: 21.632430342063202, height: 0,   },
        { lon: 109.21630454235802, lat: 21.587117662472572, height: 0,  },
        { lon: 109.22595001238774, lat: 21.541659339628023, height: 0,   },
        { lon: 109.22053316667653, lat: 21.5044292752671, height: 0,   },
        { lon: 109.1774187903384, lat: 21.486464951792875, height: 0,   },
        { lon: 109.15388011769447, lat: 21.47665717152458, height: 0,   },
      ];
      resArr.map((item, index) => {
        item.time = index * 40;
      });
      //创建路线
      this.createLine(resArr);
      //模型移动
      this.modelMove(resArr);
    },
//模型移动功能
    modelMove(arr){
      //初始化位置

      window.viewer.scene.camera.setView({
        destination: Cesium.Cartesian3.fromDegrees(arr[0].lon + 0.7, arr[0].lat - 2.4, 100000),
        orientation: {
          heading: Cesium.Math.toRadians(0),
          pitch: Cesium.Math.toRadians(-30),//看地图的角度
          roll: Cesium.Math.toRadians(0),
        },
      });
      //获取最后一个经纬度点的时间
      let lastTime = arr[arr.length - 1].time;
      //起始时间
      let start = Cesium.JulianDate.fromDate(new Date());
      //结束时间
      let stop = Cesium.JulianDate.addSeconds(start, lastTime, new Cesium.JulianDate());
      //设置始时钟始时间

      window.viewer.clock.startTime = start.clone();
      //设置时钟当前时间
      window.viewer.clock.currentTime = start.clone();
      //设置始终停止时间
      window.viewer.clock.stopTime  = stop.clone();
      //时间速率,数字越大时间过的越快
      window.viewer.clock.multiplier = 10;
      //时间轴
      window.viewer.timeline.zoomTo(start,stop);
      //循环执行,即为2,到达终止时间,重新从起点时间开始
      window.viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP;
      //添加模型
      let property = this.computeFlight(start,arr);
      window.viewer.entities.add({
        //和时间轴关联
        availability: new Cesium.TimeIntervalCollection([new Cesium.TimeInterval({
          start: start,
          stop: stop
        })]),
        position: property,
        //根据所提供的速度计算模型的朝向
        orientation: new Cesium.VelocityOrientationProperty(property),
        //模型数据
        model: {
          uri: "./mq-9/scene2.gltf",
          minimumPixelSize: 200,
        },
        //文字标签
        label: {
          text: "无人机",
          font: '500 30px Helvetica',
          scale: 0.5,
          style: Cesium.LabelStyle.FILL,
          fillColor: Cesium.Color.fromCssColorString("#000000"),
          pixelOffset: new Cesium.Cartesian2(0, -50),//偏移量
          showBackground: true,
          backgroundColor: new Cesium.Color(230,129,36,0.7),
          backgroundPadding: new Cesium.Cartesian2(16, 9)
        },
      });

      let property2 = this.computeFlight2(start,arr);
      window.viewer.entities.add({
        //和时间轴关联
        availability: new Cesium.TimeIntervalCollection([new Cesium.TimeInterval({
          start: start,
          stop: stop
        })]),
        position: property2,
        //根据所提供的速度计算模型的朝向
        orientation: new Cesium.VelocityOrientationProperty(property2),
        //模型数据
        cylinder: {
          heightReference :Cesium.HeightReference.CLAMP_TO_GROUND,
          length:this.height,
          topRadius:0,
          bottomRadius:this.height,
          material: Cesium.Color.RED.withAlpha(.4),
          outline:!0,
          numberOfVerticalLines:0,
          outlineColor:Cesium.Color.RED.withAlpha(.8)
        },
      });
    },
    createLine(arr){
      let newArr1 = [], newArr2 = [];
      arr.map(item => {
        newArr1.push(item.lon);
        newArr1.push(item.lat);
      })
      newArr2 = newArr1;
      window.viewer.entities.add({
        name: 'line',
        polyline: {
          positions: Cesium.Cartesian3.fromDegreesArray(newArr2),
          width: 5,
          material: Cesium.Color.fromCssColorString('#883d01'),
          clampToGround: true
        }
      })
    },

    //数据转换
    computeFlight(start, source){
      //取样位置,相当于一个集合
      let property = new Cesium.SampledPositionProperty();
      for(let i = 0; i < source.length; i++){
        let time = Cesium.JulianDate.addSeconds(start, source[i].time, new Cesium.JulianDate);
        let position = Cesium.Cartesian3.fromDegrees(source[i].lon, source[i].lat, this.height);
        property.addSample(time, position);
      }
      return property;
    },
    //数据转换
    computeFlight2(start, source){
      //取样位置,相当于一个集合
      let property = new Cesium.SampledPositionProperty();
      for(let i = 0; i < source.length; i++){
        let time = Cesium.JulianDate.addSeconds(start, source[i].time, new Cesium.JulianDate);
        let position = Cesium.Cartesian3.fromDegrees(source[i].lon, source[i].lat, this.height-20);
        property.addSample(time, position);
      }
      return property;
    },
  }
}
</script>

<style scoped>
#cesiumContainer {
  width: 100%;
  height: 100%;
}
</style>