本文将从三个方面来记录此次从0到1的过程:
1. 调用影像和地形服务
初始化地图:
            
            
              php
              
              
            
          
          ```js
const viewer = new Cesium.Viewer('map', {
    baseLayer: false,// 图层控件显隐控制
    timeline: false, // 隐藏时间轴
    animation: false, // 隐藏动画控制器
    geocoder: false, // 隐藏地名查找控制器
    homeButton: false, // 隐藏Home按钮
    sceneModePicker: false, // 隐藏投影方式控制器
    baseLayerPicker: false, // 隐藏图层选择控制器
    navigationHelpButton: false, // 隐藏帮助按钮
    fullscreenButton: false, // 隐藏全屏按钮
    selectionIndicator: false, // 关闭双击选择entities实例
    orderIndependentTranslucency: false,
    contextOptions: {
        webgl: {
            alpha: true
        }
     },
     skyBox: false,
     sun: false,
     moon: false,
     skyAtmosphere: false
 })
```加载wmts影像服务:
            
            
              php
              
              
            
          
          ```js
const osmImageryProvider = new Cesium.WebMapTileServiceImageryProvider({
      url: '/geoserver/gwc/rest/wmts/ditu:w(6)/{style}/{TileMatrixSet}/{TileMatrixSet}:{TileMatrix}/{TileRow}/{TileCol}?format=image/png',
      layer: 'ditu:w(6)',
      format: 'image/png',
      style: 'raster',
      maximumLevel: 40,
      tileMatrixSetID: 'EPSG:900913'
    })
    viewer.imageryLayers.addImageryProvider(osmImageryProvider)
```加载地形服务
            
            
              js
              
              
            
          
          const cesiumTerrainProvider = await Cesium.CesiumTerrainProvider.fromUrl(
      '/terrain',
      {
        requestWaterMask: true, // 请求水体遮罩数据(可选)
        requestVertexNormals: true // 请求顶点法线用于光照效果(可选)
      }
    )
    viewer.scene.terrainProvider = cesiumTerrainProvider加载地形服务的时候要注意,cesium 只支持terrain 数据集
最开始我尝试着使用wmts发布的dem地形服务加载,试过很多方法都不彳亍,后面看到有一个python自动转化的方法,但是由于有学习成本就没使用
转换的方法我这边采用的是cesiumLab,官网直接下载,免费使用,登录之后选择地形处理,输入带dem信息的tif文件后导出即可,terrain 数据集一般内容比较大,建议放在服务器上,放在public里面会导致体积过大
3. 加装水流
这里的思路是加装geojson 之后给高度,这样高度以下的部分就会被dem遮挡住,这样就会有水流的效果,如果需要实现类似水波纹的效果网上可以直接搜索,有很多实例
            
            
              js
              
              
            
          
          // 核心属性:加载geojson后,用地形遮挡住下面看不见的部分
    viewer.scene.globe.depthTestAgainstTerrain = true
    
// 加载geojson水面效果
  function showWather (geojson, height) {
    Cesium.GeoJsonDataSource.load(
      URL.createObjectURL(new Blob([geojson], { type: 'application/json' }))
    ).then((dataSource) => {
      viewer.dataSources.add(dataSource)
      const entitys = dataSource.entities.values
      for (const entity of entitys) {
        // entity.polygon.material = Cesium.Color.SKYBLUE.withAlpha(0.25);
        entity.polygon.material = Cesium.Color.fromRandom({
          alpha: 0.5,
          blue: 0.8,
          green: 0.4,
          red: 0.2
        })
        entity.polygon.outline = false
        entity.polygon.extrudedHeight = height
      }
    })
  }水流逐渐上涨过程
第一步实验
我们有一个水流逐渐上涨的需求,比如从120米涨水到160米,需要有一个缓慢的过渡效果,我最开始的思路是,通过修改geojson的高度来实现
后面发现,每次修改后,它的图层从关闭到重新加载会有一段时间,导致图层会有零点几秒的闪烁时间,展示的效果不好
后续又采用叠加新的geojson 并且给不同的高度来实现
但是最开始测试后发现,图层会重复叠加,由于图层有透明度,图层叠加后会变得不透明 解决方案:每次叠加新图层之前后,清除掉上一个图层
最终虽然这样不会有图层叠加的问题了,但是零点几秒的闪烁时间依然存在,原因就是销毁到加载cesium需要时间
最终解决方案
思路:提前将所有图层全部预加载进入cesium,并且全部存入一个数组中,但是全部不显示,后续根据时间来控制图层的显示隐藏,这样效率比直接加载快很多,并且不会有图层叠加和零点几秒的闪烁时间的问题
代码:
            
            
              js
              
              
            
          
          dataSource.show = false//隐藏掉图层,dataSource看上面代码
//后续循环村图层的数组进行控制显隐藏就好了,其实这里每次显示时使用一个变量去记住会更好,
//下次直接去销毁就行,不需要循环,性能会更好
data.waterdataTop.forEach(v => {
    if (v.tm === tm) {//这里是我需要根据时间判断那个图层需要显示
          data.waterdataTop.forEach(res => {
             res.dataSource.show = false
           })
            v.dataSource.show = true
          }
      }
    })4. 模拟视角飞行
视频上传不了,只能看图了
飞行路径图: 
飞行效果图: 
这个经过很多很多查找,大部分都是视角跟着一个飞机或者其他物体去运动,或者视角是垂直于地面的,并没有我想要的像是人在走一样的效果
实验过程
思路:由于没有找到对应的飞翔api,所以决定采用将一段路分成多个点,每次定位到下一个点的方式来实现飞行效果,只要定位的速度够快,并且点够多,这样就不会有卡顿
最先想到的是flyto ,因为这个api是有一个过渡效果的,但是经过实验后发现不彳亍,因为它是一个曲线飞行,首先会先将视角跳转到高度后再会来,不能做到平滑过渡,并且它的飞行速度也是曲线,先慢后快,最后决定使用lookAt来实现
最终实现
需要实现的步骤:
- 绘制出飞行路径,第一次点击为起点,后面每一次点击是为一条直线,这条直线就是飞行的路径,根据首尾俩点再在这条直线上插补出很多我需要的经纬度信息
- 将所有的飞行路径放到一个数组里,后续控制飞机飞行就只需要从数组里取到数据就行
- 定义一个全局index,然后使用定时器实现开始和暂停
实现:
- 绘制飞行路径并且生成飞行路径数组
getFlyRoute就是返回飞行数组的函数,需要传入一个多个点的数组,是个二维数组,输出的也是二维数组,数组的第三位是与前一个点的角度
示例:: "routeArr":
\[113.03418811062859,30.96501808361106\],\[113.03418811062859,30.96501808361106\],\[113.04394567765505,30.96910239672927\],\[113.04394567765505,30.96910239672927\],\[113.05719347194503,30.962402300357656\],\[113.05719347194503,30.962402300357656\],\[113.07151377792708,30.95662679673569\],\[113.07151377792708,30.95662679673569\]
            
            
              js
              
              
            
          
          import * as Cesium from 'cesium'
import { reactive } from 'vue'
import angle from './mapAngle'
const { Calculate } = angle()
  
// 视角飞行函数
export default function fly () {
  // const routeArr=ref([])
  const data = reactive({
    // 点的arr
    routeArr: [],
    // 实例的id,用来销毁
    routeID: [],
    // 屏幕点击事件变量
    handler: null
  })
  /* const routeArr = [];
  const routeID = [];
  let handler=null
  // 屏幕双击事件变量
  let doubleHandler=null */
  // 获取飞行的路线
  /**
   * param:飞行路线数组、
   * 返回完整的飞行路线数组
   */
  function getFlyRoute (param) {
    let arr = []
    param.forEach((v, i) => {
      if (i === param.length - 1) {
        // 如果是最后一个就return 出去
        return
      }
      arr = [...arr, ...interpolateCoordinates(v, param[i + 1])]
    })
    arr.forEach((v, i) => {
      if (i === arr.length - 1) {
        // 如果是最后一个,使用前一个的数据的角度
        v.push(arr[i - 1][2])
        return
      }
      const angle1 = {
        lon: v[0],
        lat: v[1],
        elv: 200
      }
      const angle2 = {
        lon: arr[i + 1][0],
        lat: arr[i + 1][1],
        elv: 200
      }
      v.push(Calculate(angle1, angle2))
    })
    return arr
  }
  
  // 获取点的坐标以及画点
  function getPonitCoordinate (_viewer) {
    closeEntities(_viewer)
    // 注册屏幕点击事件
    data.handler = new Cesium.ScreenSpaceEventHandler(_viewer.scene.canvas)
    data.handler.setInputAction(function (event) {
      // 转换为不包含地形的笛卡尔坐标
      const clickPosition = _viewer.scene.camera.pickEllipsoid(event.position)
      // 转经纬度(弧度)坐标
      const radiansPos = Cesium.Cartographic.fromCartesian(clickPosition)
      /* console.log(radiansPos, "radiansPos");
      console.log(clickPosition, "clickPosition"); */
      // 转角度
      // console.log("经度:" + Cesium.Math.toDegrees(radiansPos.longitude) + ", 纬度:" + Cesium.Math.toDegrees(radiansPos.latitude));
      // 添加点
      const position = [
        Cesium.Math.toDegrees(radiansPos.longitude),
        Cesium.Math.toDegrees(radiansPos.latitude)
      ]
      addPoint(_viewer, position)
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
    // 双击事件关闭画点
    data.handler.setInputAction(function (event) {
      _viewer.trackedEntity = undefined
      closeHandler()
    }, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK)
  }
  // 添加点
  function addPoint (_viewer, param = []) {
    const id = data.routeArr.length + 1 + 'point'
    const point = {
      id,
      position: Cesium.Cartesian3.fromDegrees(param[0], param[1], 200),
      point: {
        color: Cesium.Color.YELLOW,
        pixelSize: 20
      }
    }
    if (data.routeArr.length === 0) {
      // 第一个点为开始点
      point.label = {
        text: '开始点',
        font: '14pt monospace',
        style: Cesium.LabelStyle.FILL_AND_OUTLINE,
        outlineWidth: 2,
        verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
        pixelOffset: new Cesium.Cartesian2(0, -9)
      }
    }
    _viewer.entities.add(point)
    data.routeArr.push(param)
    data.routeID.push(id)
    drawLine(_viewer, param)
  }
  // 回显点和线
  function addMorePoint (_viewer, params = {}) {
    clearRouteArr()
    closeEntities(_viewer)
    data.routeID = []
    // console.log(params);
    params.value.forEach((v, i) => {
      addPoint(_viewer, v)
    })
  }
  
  /**
   * 俩点之间添加线
   * @param {positions:[lastPoint,nextPoint]}
   * @returns
   */
  
  function AddPolyline (_viewer, params) {
    const id = data.routeArr.length + 'line'
    const entity = new Cesium.Entity({
      id,
      show: true,
      polyline: new Cesium.PolylineGraphics({
        show: true,
        // fromDegrees返回给定的经度和纬度值数组(以度为单位),该数组由Cartesian3位置组成。
        // Cesium.Cartesian3.fromDegreesArray([经度1, 纬度1, 经度2, 纬度2,])
        // Cesium.Cartesian3.fromDegreesArrayHeights([经度1, 纬度1, 高度1, 经度2, 纬度2, 高度2])
        positions: Cesium.Cartesian3.fromDegreesArrayHeights(params.positions),
        width: 5,
        material: Cesium.Color.RED
      })
    })
    _viewer.entities.add(entity)
    data.routeID.push(id)
    _viewer.trackedEntity = undefined
  }
  
  /**
   * 画路径线
   */
  
  function drawLine (_viewer, nextPoint) {
    if (data.routeArr.length <= 1) {
      return
    }
    const lastPoint = data.routeArr[data.routeArr.length - 2]
    const positions = {
      positions: [...lastPoint, 200, ...nextPoint, 200]
    }
    AddPolyline(_viewer, positions)
  }
  
  /**
   * 关闭画的点和线显示
   */
  
  function closeEntities (_viewer) {
    data.routeID.forEach((v) => {
      const entID = _viewer.entities.getById(v)
      _viewer.entities.remove(entID)
    })
  }
  
  /**
   * 获取每一次移动的步长
   */
  
  function getStep () {
    const lat1 = 113.05459941234724
    const lon1 = 30.96423174313923
    const lat2 = 113.05454766211562
    const lon2 = 30.964100607409584
    let coordinates = 0
  
    const latStep = lat2 - lat1 /* / 6 */ // 6个间隔,其中一个是起点
    const lonStep = lon2 - lon1 /* / 6 */ // 6个间隔,其中一个是起点
    coordinates = latStep * latStep + lonStep * lonStep
  
    return coordinates * 16 /* 数字越大,间隔越大 */
  }
  
  /**
   * 输入俩点,获取其中的插补
   * 插补函数,在俩个坐标点之间插补坐标,输出包含输入第一个点在内的数组
   */
  
  function interpolateCoordinates ([lat1, lon1], [lat2, lon2]) {
    // console.log(lat1, lon1, lat2, lon2);
    const coordinates = []
    const newLat = lat2 - lat1
    const newLon = lon2 - lon1
    // 俩点连线后的线长
    const z = newLat * newLat + newLon * newLon
    const step = parseInt(z / getStep())
    const latStep = (lat2 - lat1) / (step + 1) // 加一个间隔,其中一个是起点
    const lonStep = (lon2 - lon1) / (step + 1) // 加一个间隔,其中一个是起点
    for (let i = 0; i <= step; i++) {
      const interpolatedLat = lat1 + latStep * i
      const interpolatedLon = lon1 + lonStep * i
      coordinates.push([interpolatedLat, interpolatedLon])
    }
  
    return coordinates
  }
  
  // 关闭地图点击和双击事件
  function closeHandler () {
    data.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK)
    data.handler.removeInputAction(
      Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK
    )
  }
  
  // 清空point数组
  function clearRouteArr () {
    data.routeArr = []
  }
  
  return {
    data,
    getFlyRoute,
    getPonitCoordinate,
    closeEntities,
    closeHandler,
    addMorePoint,
    clearRouteArr
  }
}这里会有一个很重要的问题:每次定位的角度怎么计算,毕竟像人一样扭动头需要角度,垂直观看地面则是不需要
思路:将后面的点与前面一个点对比,可以获得经纬度,再根据二点的经纬度来计算角度,这里角度计算踩了很多坑,找chartgpt也写了几个角度也都不对,后面找到了一个在线转换的网站,趴了他的源代码后找到了计算方法,经过改造后就成了我的,很感谢这些旧时代网站,浏览器能直接访问源码,下面是计算角度代码:
这里就是上面引入的计算角度的Calculate方法
            
            
              js
              
              
            
          
          // 俩个金纬度求与北方的夹角,北方为0度
export default function angle(){
  function EarthRadiusInMeters(latitudeRadians) {
    var a = 6378137.0;
    var b = 6356752.3;
    var cos = Math.cos(latitudeRadians);
    var sin = Math.sin(latitudeRadians);
    var t1 = a * a * cos;
    var t2 = b * b * sin;
    var t3 = a * cos;
    var t4 = b * sin;
    return Math.sqrt((t1 * t1 + t2 * t2) / (t3 * t3 + t4 * t4));
  }
  function LocationToPoint(c) {
    var lat = (c.lat * Math.PI) / 180.0;
    var lon = (c.lon * Math.PI) / 180.0;
    var radius = c.elv + EarthRadiusInMeters(lat);
    var cosLon = Math.cos(lon);
    var sinLon = Math.sin(lon);
    var cosLat = Math.cos(lat);
    var sinLat = Math.sin(lat);
    var x = cosLon * cosLat * radius;
    var y = sinLon * cosLat * radius;
    var z = sinLat * radius;
    return { x: x, y: y, z: z, radius: radius };
  }
  function RotateGlobe(b, a, bradius, aradius) {
    var br = { lat: b.lat, lon: b.lon - a.lon, elv: b.elv };
    var brp = LocationToPoint(br);
    brp.x *= bradius / brp.radius;
    brp.y *= bradius / brp.radius;
    brp.z *= bradius / brp.radius;
    brp.radius = bradius;
    var alat = (-a.lat * Math.PI) / 180.0;
    var acos = Math.cos(alat);
    var asin = Math.sin(alat);
    var bx = brp.x * acos - brp.z * asin;
    var by = brp.y;
    var bz = brp.x * asin + brp.z * acos;
    return { x: bx, y: by, z: bz };
  }
  /**
   *
   * @param {*} a
   *  {
   *      lat:30.968046236506876,
          lon:113.04772063790635,
          elv:200 //高度(米)
      }
   * @param {*} b 同上
   * @returns 俩点与北方(角度为0度)之间的夹角
   */
  function Calculate(a,b) {
   var ap = LocationToPoint(a);
   var bp = LocationToPoint(b);
   var br = RotateGlobe(b, a, bp.radius, ap.radius);
   var theta = (Math.atan2(br.z, br.y) * 180.0) / Math.PI;
   var azimuth = 90.0 - theta;
   if (azimuth < 0.0) {
     azimuth += 360.0;
   }
   if (azimuth > 360.0) {
     azimuth -= 360.0;
   }
   return azimuth
  }
 
  return {
    Calculate
  }
}这样有了一整条路线的数组后,再配合定时器一直定位,就可以实现飞行的效果了,当然,这里转换角度的时候过渡不是很平滑,可以再优化转角度的时候,将角度再做插补
            
            
              js
              
              
            
          
          flying = setInterval(() => {
        flyIndex += 1
        if (flyIndex >= flyLength) {
          // 如果大于了飞行的数组长度,则暂停
          outFly()
          return
        }
        const position = jsonArr[flyIndex]
        const destination = Cesium.Cartesian3.fromDegrees(
          Number(position[0]),
          Number(position[1]),
          200
        )
        const orientation = new Cesium.HeadingPitchRange(
          Cesium.Math.toRadians(position[2]),
          Cesium.Math.toRadians(-15),
          200
        )
        data.viewer.scene.camera.lookAt(destination, orientation)
      }, 80)暂停或者跑完后,需要解锁相机
data.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY) // 解锁相机
这样一个飞行效果就完美实现了,然后查看浏览器性能,不经感慨,谷歌还是强大,v8NB
![D6$}RUN19KKE{(WQNO{1AK.png
总结
这是一次从0到1的全新尝试,有做得不好的地方,比如,不应该使用原生cesium,应该使用现成的库,毕竟这样能节约很多时间和成本,但是当时并不知道有这么多Mars3D这类库,还是年轻了
淹没的效果其实做得不算太好,这种做法有一个缺点,geojson的高度是固定的,也就是一个范围内的高度都是一样的,会导致有些不该有水的地方有水,有些有水的地方没水,地势高的地方也会没水,不够合理,没能够完美实现想要的效果
还有一些其他的效果也做得不是很好,但是在mars3D里面,使用别人封装好的方法却很好实现,下一步需要去看别人代码看看别人是怎么实现的
结束语:
有时候是真的能感受到说话的艺术,明明有很多话想说,但是的的确确自己很难逻辑清晰的,有条不紊的表达出来,以前偶尔会感慨纸短情长,但是技术的长度,永远不会写尽!
如果看到这里了,说明你是真的闲,欢迎与我交流,私信和留言都会回复,觉得我写的还可以的,或者感兴趣的可以加我vx: zz0254i 一起交流技术