Cesium 轨迹漫游

Cesium中,轨迹漫游的核心是借助CZML格式,CZML是Cesium团队制定的一种用来描述动态场景的JSON架构语言,可以用来描述点、线、多边形、体、模型及其他图元,同时定义它们是怎样随时间变化的,参考CZML Structure · AnalyticalGraphicsInc/czml-writer Wiki (github.com)

我这里放一个简单的模板吧

[
  {
    "id": "document",
    "version": "1.0"
  },
  {
    "id": "pathRoamingEntity",
    "availability": "2012-08-04T10:00:00Z/2012-08-04T20:00:00Z",
    "model": {
      "scale": 1,
      "minimumPixelSize": 100,
      "maximumScale": 20
    },
    "path": {
      "material": {"solidColor": {"color": {"rgba": [255, 255, 0, 255]}}},
      "width": [{"number": 5.0}],
      "show": [{"boolean": false}],
      "resolution": 5.0
    },
    "orientation": {
      "velocityReference": "#position"
    },
    "viewFrom": {
      "cartesian": [-2080, -1715, 779]
    },
    "position": {
      "interpolationAlgorithm": "LINEAR",
      "epoch": "2012-08-04T10:00:00Z",
      "forwardExtrapolationType": "HOLD",
      "cartographicDegrees": []
    }
  }
]

CZML 不过多赘述,我们先说思路,其实思路不难,配置好 CZML,加入到 dataSoures 中,开启动画即可

我们先定义一段路径:

let ps = [
    [119.44037341293323, 35.34197106899855, 5.872732096309598],
    [119.44252948098223, 35.34223901339689, 6.31711015359973],
    [119.4560550425358, 35.34202148007459, 22.906707659456394],
    [119.45610614546445, 35.32762691608659, 3.0852594116911622],  
]

将线添加到场景中,注意 地理坐标 转 投影坐标

let car3 = formCartographicArrS(ps)
// 添加线
let pathEntity = new Cesium.Entity({
    polyline: {
        // 注意,此处position为笛卡尔坐标系
        positions: car3,
        width: 4,
        clampToGround: true,
        arcType: Cesium.ArcType.RHUMB,
    }
})
viewer.entities.add(pathEntity)

此时,若我们的场景中有地形,则要修改一下高度,比如我们要模型离地100米飞行

// 清洗height,改为相对高度,配合 heightReference
let height = 0
ps.forEach((v, k ,arr)=>{
    arr[k][2] = 0 + height
})

在上述的CZML中,有很多必要属性我们没有添加,比如模型,比如路径

此处主要干了三个事:

1.添加模型信息

2.添加路径信息

3.计算速度,修改时间

//添加模型
czml[1].model.gltf = "./CesiumMilkTruck/CesiumMilkTruck.glb"
czml[1].model.scale = 0.01

// CZML中通过时间来控制速度,我们先定义一个起始时间并复制一份当前时间,并记录一份结束时间
const startTime = Cesium.JulianDate.fromIso8601('2012-08-04T10:00:00Z');
let currentTime = startTime.clone();
let lastPosition = null;

// 为了让模型保持匀速运动,我们需要手动计算时间,此处借助 turfJS 库
let speed = 50
ps.forEach(v => {
    if(lastPosition){
        // 对于起始点,我们直接传入起始时间即可,此处为非起始点逻辑
        let from = turf.point(lastPosition);
        let to = turf.point(v);
        // 计算两点间长度
        let distance = turf.distance(from, to, {units: 'meters'});
        // 计算新时间
        currentTime = Cesium.JulianDate.addSeconds(currentTime, Math.ceil(distance / speed), currentTime);

    }
    // 添加路径,注意坐标为经纬度,且格式为 [时间节点,经度, 维度, 高],此处时间节点就是用来计算速度的,每一个线段起始时间节点与终止时间节点定义了当前线段的速度
    czml[1].position.cartographicDegrees.push(Cesium.JulianDate.toIso8601(currentTime))
    czml[1].position.cartographicDegrees.push(v[0])
    czml[1].position.cartographicDegrees.push(v[1])
    czml[1].position.cartographicDegrees.push(v[2])

    lastPosition = v
})
// 根据上述计算的时间修改 availability
czml[1].availability = `${startTime}/${currentTime}`

添加 dataSourecs,是一个异步Promise,回调参数为我们传入的dataSource

同时注意参数,该方法允许我们传入 Promise<DataSource>,所以对于下述静态方法定义的 CZML 数据源无需回调,直接将 Promise 作为参数传入即可

// 添加 CZML
viewer.dataSources.add(
    Cesium.CzmlDataSource.load(czml)
).then(c=>{
    ...
})

到此时其实已经完成大部分了,但此时我们的模型可能还没有动起来(受 clock 影响),所以在回调中我们要做一些工作

// c为回调参数
// 获取Entity
let e = c.entities.getById("pathRoamingEntity")
// 设置高度为贴地相对高度
e.model.heightReference = Cesium.HeightReference.RELATIVE_TO_GROUND

viewer.clock.multiplier = 1
// 让时间动起来
viewer.clock.shouldAnimate = true;

接下来我们希望视角随着模型移动,视角随着模型移动有两种方法

1.使用 trackedEntity

在 viewer 中,提供了一个非常便捷的方法,有一个属性 trackedEntity,可以使当前的相机锁定一个Entity

配合 CZML 中的 viewFrom,允许我们设置一个相对的投影坐标(笛卡尔)作为初始视角,是以当前 Entity 做一个偏移

即可配置初始相机方向同时跟踪

viewer.trackedEntity = c.entities.getById("pathRoamingEntity")

2.上述方法虽然完成了跟踪,但是实际我们并没有能够使相机随着模型运动的方向随时改变,所以第二种方法是使用 addEventListener

如下所示,其实实现方法有很多种,大同小异,此处采用的是 Camera 中的 lookAt 方法

我们先看一下lookAt,lookAt 要求我们提供两个参数,目标位置 和 距离目标的偏移,目标位置我们可以直接记录,目标偏移量就需要我们手动算一下了,

大致思路就是,根据前一个点和后一个点算出 heading 朝向,因为 heading 代表 Z 轴旋转,所以比较重要,pitch 代表 Y 轴朝向,我们可以自己选择一个合适的角度,注意,俯角是负数

// 前一个点
let prePoint = null
viewer.scene.postRender.addEventListener(() => {
    if (e && viewer.clock.shouldAnimate) {
        // 获取当前时间的位置
        let curPoint = e.position.getValue(viewer.clock.currentTime)
        if(prePoint){
            // 计算 heading
            let heading = getHeading(prePoint, curPoint)
            // 计算 pitch
            let pitch = Cesium.Math.toRadians(-30.0);
            let range = 100;
            viewer.camera.lookAt (
                curPoint, 
                new Cesium.HeadingPitchRange(heading, pitch, range)
            );
        }
        // 当前点在下一次渲染时为前一个点
        prePoint = Cesium.Cartesian3.clone(curPoint)
    }
});
function getHeading(pointA, pointB){
    //建立以点A为原点,X轴为east,Y轴为north,Z轴朝上的坐标系
    const transform = Cesium.Transforms.eastNorthUpToFixedFrame(pointA);
    //向量AB
    const positionvector = Cesium.Cartesian3.subtract(pointB, pointA, new Cesium.Cartesian3());
    //因transform是将A为原点的eastNorthUp坐标系中的点转换到世界坐标系的矩阵
    //AB为世界坐标中的向量
    //因此将AB向量转换为A原点坐标系中的向量,需乘以transform的逆矩阵。
    const vector = Cesium.Matrix4.multiplyByPointAsVector(Cesium.Matrix4.inverse(transform, new Cesium.Matrix4()), positionvector, new Cesium.Cartesian3());
    //归一化
    const direction = Cesium.Cartesian3.normalize(vector, new Cesium.Cartesian3());
    //heading
    const heading = Math.atan2(direction.y, direction.x) - Cesium.Math.PI_OVER_TWO;
    return Cesium.Math.TWO_PI - Cesium.Math.zeroToTwoPi(heading);
}

注意:视角追踪有一个问题,当地形起伏过大时,相机可能飞入地形下面!

到此,轨迹漫游算是结束了!后续我还会写一些之前做 SDK 时的一些功能,感兴趣的朋友可以移步:LiZzhi/cesium-plugin (github.com),如果对您有帮助,请给我一颗star,谢谢。

小弟目前在读GIS研究生一枚,代码中不足之处,欢迎各位大佬指正!

posted @ 2023-01-09 12:11  邢韬  阅读(2601)  评论(2编辑  收藏  举报