cesium在进行动画展示这一块的功能比较完善。最近有一个需求,需要进行模拟卫星的飞行轨迹,如果可以实现,针对扫描卫星需要添加模拟扫描光波。

当我首先针对需求进行卫星,将卫星运行与扫描进行拆分,当利用cesium的api实现卫星轨道后,能否针对卫星模型绑定一个固定的扫描的模型上去。后期这个想法被我抛弃了,因为我之前二维做的多,三维这一块有部分经验,但是都没有深入了解过,所以我将二维的思想带入了cesium的体系中,也走了不少绕路,cesium针对模拟动画其实有更完美的解决方案,下面我简单介绍一下自己的思维历程。

模型法

首先,我认为既然要实现卫星扫描轨迹,那么肯定需要两个模型:卫星模型和扫描模型。卫星模型网上有,glb或者gltf的都行,而扫描模型我直接使用Cesium的几何模型去实现圆锥即可。

1.初始化Vierer
1
2
3
4
5
6
7
8
9
10
11
12
viewer = new Cesium.Viewer('cesiumContainer', {
shouldAnimate: true, // 这个值比较特殊,如果为true页面渲染后自动执行动画,反之需要自己手动去点击开始
geocoder: false, // 隐藏查找位置
homeButton: false, // 隐藏返回视角到初始位置
sceneModePicker: false, // 隐藏视角模式的选择
baseLayerPicker: false, // 隐藏图层选择器
navigationHelpButton: false, // 隐藏帮助
timeline: true, // 隐藏时间轴
fullscreenButton: false, // 隐藏全屏按钮
animation: true, // 隐藏动画速度控制器
infoBox: false, // 点击的详情弹窗entity的description可以描述html显示在弹窗中,也可以通过viewer.infoBox.frame来接入访问
});
2. 时间定义

动画的执行都是与时间有关的,从什么时间开始,到什么时间结束。而Cesium使用了不同于我们js常用的时间规范,需要用到Cesium.JulianDateCesium.JulianDate 是指示在 Cesium 中添加或描述对象动画时使用的 Julian Date 格式的时间标签。Julian Date 是以天为单位计数的相对格林尼治标准时间的一种计时法。

所以以下代码意思我们以当前时间为基准,6分钟内执行动画。但是速率是10.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义开始时间
start = Cesium.JulianDate.addHours(new Cesium.JulianDate.fromDate(new Date()), 8, new Cesium.JulianDate());////把js中的时间转换为JulianDate时间,东八区时间

// 定义结束时间,360秒后
stop = Cesium.JulianDate.addSeconds(start, 360, new Cesium.JulianDate());

//确保查看器处于预期的时间
viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = start.clone();
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP; //循环结束时

//时间变化来控制速度 // 时间速率,数字越大时间过的越快
viewer.clock.multiplier = 10;

//给时间线设置边界
viewer.timeline.zoomTo(start, stop);

Cesium.JulianDate.addHours() :是一种可以在 Julian 时间戳上增加指定小时数的方法。Cesium.JulianDate.addSeconds()同理增加秒。

  • julianDate - Julian 日期时刻,可以是 JulianDate 对象或 secondsSinceEpoch 值。

  • hours - 要添加的小时数,可以为负值表示减小。

  • result - 可选,用于存储结果的 JulianDate 对象。

Cesium.JulianDate.clone():用于克隆一个 JulianDate 对象。

viewer.timeline.zoomTo(start, stop)方法可以用来在时间轴控件中设置一个时间窗口并自动居中和缩放。

  • start - 时间窗口起始时间,可以是Date对象或JulianDate对象
  • stop - 时间窗口结束时间
3.轨迹处理方法

上面定义好了播放时间以及时间间隔等等,那么准备工作做完了,可以开始构建模型了。

先定义一些存储路径变量的方法:

1
2
3
4
5
6
7
function mySatePosition(hen) {
this.lon = 0; // 经度初始化
this.lat = 0; // 纬度初始化
this.satelliteHeight = hen; // 卫星高度
this.orbitHeight = hen / 2; // 轨道高度
this.time = 0; //
}

定义轨迹方法:

可以看出这个方法利用传进去的是否纬度标识,如果是就构造了一个固定纬度degree,经度360度分段的循环数组,反之一样。所以这些变量其实可以根据自己需要定义初始化或者调整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getRandState(ifLat, degree, hen) {
let arr = [];
let lat = Math.floor(Math.random() * 360);
for (let i = lat; i <= 360 + lat; i += 30) {
let sateP = new mySatePosition(hen); // new一个路径变量类
if (ifLat == 'lon') {
sateP.lon = degree;
sateP.lat = i;
} else {
sateP.lon = i
sateP.lat = degree;
}
sateP.time = i - lat;
arr.push(sateP);
}
return arr
}

经纬度转换为Cesium模型使用的position

1
2
3
4
5
6
7
8
9
function computePosition(source, panduan) {
let property = new Cesium.SampledPositionProperty();
for (let i = 0; i < source.length; i++) {
let time = Cesium.JulianDate.addSeconds(start, source[i].time, new Cesium.JulianDate());
let position = Cesium.Cartesian3.fromDegrees(source[i].lon, source[i].lat, panduan === 1 ? source[i].satelliteHeight : source[i].orbitHeight);
property.addSample(time, position);
}
return property;
}
4. 模型构建

定义好上一步方法,定义模型即可。这边三个参数ifLat, degree, hen含义上面方法也需要使用,含义分别就是是否纬度,角度以及卫星高度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
 //获取路径
let path = getRandState(ifLat, degree, hen);

/**
* 扫描圆锥
*/
let entityPath = computePosition(path, 2);
let entity = viewer.entities.add({
//关联时间轴 TimeIntervalCollection管理时间间隔数据的集合 把时间轴的起止时间同步为实体的
availability: new Cesium.TimeIntervalCollection([new Cesium.TimeInterval({
start: start,
stop: stop
})]),
position: entityPath,
orientation: new Cesium.VelocityOrientationProperty(entityPath),
cylinder: {
HeightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
length: hen,
topRadius: 0,
bottomRadius: hen / 2,
material: Cesium.Color.RED.withAlpha(0.4),
outline: true,
numberOfVerticalLines: 0,
outlineColor: Cesium.Color.RED.withAlpha(0.8)
},
});
//插值器,两个点位之间的模拟插值
entity.position.setInterpolationOptions({
interpolationDegree: 5,
interpolationAlgorithm: Cesium.LagrangePolynomialApproximation
});


/**
* 卫星
*/
let satellitePath = computePosition(path, 1);
let satelliteEntity = viewer.entities.add({
// 将实体availability设置为与模拟时间相同的时间间隔。
availability: new Cesium.TimeIntervalCollection([new Cesium.TimeInterval({
start: start,
stop: stop
})]),
//计算实体位置属性
position: satellitePath,
//基于位置移动自动计算方向.
orientation: new Cesium.VelocityOrientationProperty(satellitePath),
//加载飞机模型
model: {
uri: './sources/kml.glb',
minimumPixelSize: 68,
scale: 2000.0,
},
//路径
path: {
resolution: 1,
material: new Cesium.PolylineGlowMaterialProperty({
glowPower: 0.1,
color: Cesium.Color.GREEN
}),
width: 5
}
});
//插值器,两个点位之间的模拟插值
satelliteEntity.position.setInterpolationOptions({
interpolationDegree: 5,
interpolationAlgorithm: Cesium.LagrangePolynomialApproximation
});
5. 开始执行

我们给第四步添加好ifLat, degree, hen三个参数就可以看到一颗卫星运行起来。我们可以把上一步继续封装来实现多个卫星运行。

Cesium Demo01

CZML法

当我参考网上案例完成上一步卫星轨道的过程中,就发现了其实Cesium对于动画类的模型,其实推荐使用CZML数据驱动的模式,并且上一步实现的卫星轨道有一个问题,轨道高度固定,而往往真实的卫星轨道可能会随时改变。如果大家想要了解CZML是什么,可以访问Cesium官网或者看一下我对于CZML的简单理解Cesium之CZML。简单来说,CZML将动画运行的轨道,模型,渲染样式等等全部以json数据格式的方式写在了czml文档里面,用数据驱动场景渲染。

1.czml定义

我们按照规范可以自己完整的构建一个CZML文档,但是卫星一般网上有能下载到,然后也有转换工具,可以直接生成CZML。下面是一个示例:

注意:因为czml里面包含大量坐标导致过长,填入了下载地址。

test.czml

2. 调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
viewer = new Cesium.Viewer('cesiumContainer', {
shouldAnimate: true,
geocoder: false, // 隐藏查找位置
homeButton: false, // 隐藏返回视角到初始位置
sceneModePicker: false, // 隐藏视角模式的选择
baseLayerPicker: false, // 隐藏图层选择器
navigationHelpButton: false, // 隐藏帮助
timeline: true, // 隐藏时间轴
fullscreenButton: false, // 隐藏全屏按钮
animation: true, // 隐藏动画速度控制器
infoBox: false, // 点击的详情弹窗entity的description可以描述html显示在弹窗中,也可以通过viewer.infoBox.frame来接入访问
});


var clock = viewer.clock;
var cylinderEntity = viewer.entities.add({
cylinder: {
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
length: 600000,
topRadius: 0,
bottomRadius: 600000/2,
material: Cesium.Color.RED.withAlpha(.4),
outline: !0,
numberOfVerticalLines: 0,
outlineColor: Cesium.Color.RED.withAlpha(.4)
}
});



// 卫星带扫描
var property;
var satellite = null;
viewer.dataSources.add(Cesium.CzmlDataSource.load("xxx/test.czml")).then(function (dataSource) {
// 添加扫描
satellite = dataSource.entities.getById("Satellite/GAOFEN 1");

property = new Cesium.SampledPositionProperty();
var date = Cesium.JulianDate.toDate(clock.startTime);
for (var ind = 0; ind < 292; ind++) {
var time = Cesium.JulianDate.addSeconds(clock.startTime, 300 * ind, new Cesium.JulianDate());
var position = satellite.position.getValue(time);

var cartographic = viewer.scene.globe.ellipsoid.cartesianToCartographic(position);
var lat = Cesium.Math.toDegrees(cartographic.latitude),
lng = Cesium.Math.toDegrees(cartographic.longitude),
hei = cartographic.height;
// console.log(position)
property.addSample(time, Cesium.Cartesian3.fromDegrees(lng, lat, hei));
}
cylinderEntity.position = property;
cylinderEntity.position.setInterpolationOptions({ //设定位置的插值算法
interpolationDegree: 5,
interpolationAlgorithm: Cesium.LagrangePolynomialApproximation
});

});

代码部分有点难以理解,其实我们viewer.dataSources.add(Cesium.CzmlDataSource.load("xxx/test.czml"))就可以加载卫星轨道以及模拟,但是,因为czml驱动的动画,我们如果想要获取模型entity,那么可能就得通过xxx.then函数链来获取具体内容了。里面的操作逻辑其实就是分段获取每个时刻卫星的状态,然后又通过转换对应赋值给圆锥实体,这样可以实现效果,但是比较麻烦。能不能进一步优化?

3. 优化

既然czml可以定义模型,是否可以同时定义圆锥和卫星模型,然后两个使用同一份动画数据呢?答案是可以。

我们在第一步czml文件中直接添加圆锥模型,省略了其它没变的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[
{
...
},
{
...,
"model": {
"gltf": "../sources/kml.glb",
"scale": 1,
"minimumPixelSize": 128
},
"cylinder": {
"length": 650000,
"topRadius": 0,
"bottomRadius": 300000,
"heightReference": "CLAMP_TO_GROUND",
"outline": true,
"numberOfVerticalLines": 0,
"material": [214,88,148, 0.4],
"outlineColor": [214,88,148, 0.4]

},
...
}
]

注意:czml定义和代码定义时有一些变量是无法使用的,只能用基本数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
viewer = new Cesium.Viewer('cesiumContainer', {
shouldAnimate: true,
geocoder: false, // 隐藏查找位置
homeButton: false, // 隐藏返回视角到初始位置
sceneModePicker: false, // 隐藏视角模式的选择
baseLayerPicker: false, // 隐藏图层选择器
navigationHelpButton: false, // 隐藏帮助
timeline: true, // 隐藏时间轴
fullscreenButton: false, // 隐藏全屏按钮
animation: true, // 隐藏动画速度控制器
infoBox: false, // 点击的详情弹窗entity的description可以描述html显示在弹窗中,也可以通过viewer.infoBox.frame来接入访问
});

// 高分一号,带扫描
var property;
var satellite = null;
viewer.dataSources.add(Cesium.CzmlDataSource.load("xxx/gf.czml"))

代码清晰简单多了,将所有的数据定义全部放在了czml文件中。当然我们可以多找几份czml文件,然后添加渲染。

image-20240127113702769

Cesium Demo02

4. 存在的问题

因为卫星轨道不再如方法一一样是固定高度,那么存在了一个问题就是扫描高度没法动态跟着联动,后边我需要再研究研究czml文档规范去优化。

参考资料

cesium 卫星环绕扫描_cesium 卫星扫描-CSDN博客