从发布服务到原生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 一起交流技术

相关推荐
GIS之家2 天前
vue+cesium示例:3D热力图(附源码下载)
前端·vue.js·3d·cesium·webgis·3d热力图
不浪brown3 天前
开源!矢量建筑白模泛光特效以及全国77个大中城市的矢量shp数据获取!
前端·cesium
在下胡三汉5 天前
怎么解决cesium加载模型太黑,程序崩溃,不显示,位置不对模型太大,Cesium加载gltf/glb模型后变暗
3dmax·cesium
GIS之家8 天前
vue+cesium示例:地形开挖(附源码下载)
前端·javascript·vue.js·gis·cesium·webgis
duansamve9 天前
Cesium快速入门到精通系列教程二:添加地形与添加自定义地形、相机控制
cesium
duansamve10 天前
Cesium快速入门到精通系列教程三:添加物体与3D建筑物
cesium
GIS之家16 天前
vue+cesium示例:3Dtiles三维模型高度调整(附源码下载)
前端·vue.js·3d·cesium·webgis
小野猫子25 天前
在vue3中使用Cesium的保姆教程
cesium
AllBlue1 个月前
常见三维引擎坐标轴 webgl threejs cesium blender unity ue 左手坐标系、右手坐标系、坐标轴方向
blender·webgl·cesium
前端熊猫1 个月前
Cesium 3D Tiles
3d·cesium·tiles