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研究生一枚,代码中不足之处,欢迎各位大佬指正!