从发布服务到原生cesium实现的水流涨水加视角飞行效果

本文将从三个方面来记录此次从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来实现

最终实现

需要实现的步骤:

  1. 绘制出飞行路径,第一次点击为起点,后面每一次点击是为一条直线,这条直线就是飞行的路径,根据首尾俩点再在这条直线上插补出很多我需要的经纬度信息
  2. 将所有的飞行路径放到一个数组里,后续控制飞机飞行就只需要从数组里取到数据就行
  3. 定义一个全局index,然后使用定时器实现开始和暂停

实现:

  1. 绘制飞行路径并且生成飞行路径数组

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 一起交流技术

相关推荐
不浪brown6 天前
Cesium的新武器!Reality Tiler V2的发布,让三维瓦片的构建性能迈上一个新台阶!
前端·cesium
supermapsupport9 天前
iClient3D for Cesium 加载shp数据并拉伸为白模
3d·cesium·supermap·webgis
Jiude10 天前
调试Cesium源码分析并解决在Vite中使用遇到的问题
前端·架构·cesium
哈哈地图13 天前
Cesium材质——Material
材质·cesium
supermapsupport13 天前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
汪洪墩15 天前
【Mars3d】设置backgroundImage、map.scene.skyBox、backgroundImage来回切换
开发语言·javascript·python·ecmascript·webgl·cesium
supermapsupport16 天前
iClent3D for Cesium 实现无人机巡检飞行效果
gis·cesium·supermap·webgis
不浪brown16 天前
仅需3行代码!带你做一个3D卫星轨道飞行动画
cesium
白嫖叫上我18 天前
Cesium 无人机航线规划(航点航线)
无人机·cesium
BJ-Giser20 天前
前端解析超图的iserver xml
前端·可视化·cesium