cesium 通过websocket接收轮船实时位置,通过粒子系统添加轮船尾浪效果
cesium 通过websocket接收轮船实时位置,通过粒子系统添加轮船尾浪效果
啊~ 宝宝儿们,我是一个前端小白白,最近呢,在尝试做一个GIS效果,就是webscoket接收后台传递过来的轮船实时位置,一秒钟推送一次,传递过来的信息包括了轮船的id、轮船经度和纬度、以及轮船的方向角度。要求使用cesium实时展示轮船的位置和朝向。但是我之前没有接触过cesium,根本不会,文档都是英文的,我都不会看,然后我连基本的操作和概念都不会,不知道正经应该怎么整,所以就瞎搞,好在实现了一些效果,然后呢就记录一下。首先呢,我的效果实现的有点瑕疵,因为不知道正经怎么搞,所以说代码写的并不是很规范或者是使用的方法并不一定就是对的,仅供参考,不可尽信哈!交流学习嘛~~~
参考资料
我就是一个代码裁缝。
首先就两个参考文档给大家分享一下,一个是cesium的中文文档,一个是案例地址。
cesium中文文档 【传送门】
cesium官方案例【传送门】
实现代码
创建cesium
创建cesium、创建模型、使用个性化图层影像啥的,这些我之前的博客已经写过来,就不再重复了,然后我本地址在这个地方
但是有一个地方更新一下,创建的时候地形影像可以用这个,效果更好一点。
terrainProvider: Cesium.createWorldTerrain({
requestWaterMask: true, // required for water effects
requestVertexNormals: true // required for terrain lighting
})
使用时间轴
我是这么想的,因为后端使用websocket接受实时数据嘛不是,一秒钟收发一条船的当前位置,更新位置的时候需要船模型缓慢的移动过去而不是直接从一个闪现到另一个点,这样看上去更圆滑,所以说我搜了半天,都是一跳一跳的,起码我的那点本事发现就只有时间轴是可以缓慢的移动过去而不是跳过去,所以说就用了时间轴。
时间轴的原理是啥哈,简单说一下我的理解,方便没有用过的人不至于一脸懵逼。
我的理解是哈~~ 就是在这个 cesium示例上创建一个时间线,需要设置这个时间线的开始时间和结束时间,然后我的船模型通过位置信息与时间线绑定,就是在那个时间,船模型的位置在哪一个时间点,我不知道我说清楚了没有,然后播放时间线的时候,船模型会根据你的时间线出现在你绑定的位置上。
就是假设哈!你的时间轴开始时间是今天的00:00:00,结束时间是今天的23:59:59,然后你又绑定了船模型的位置,比如今天00:00:00秒的时候,船在A点,这个A点是经纬度坐标哈!然后今天的01:00:00秒船模型在B点,然后运行的时候,模型就会在今天00:00:00秒的时候出现在A点,01:00:00的时候出现在B点,两点创建一条直线嘛,然后中间隔得这一个小时,cesium会自动计算从A点到B点走过的位置,会让船匀速的用一个小时的时间,从A点走到B点,我应该说清楚了吧宝宝们?如果不用时间轴而是接受数据之后直接更新船模型的位置信息,船是会直接闪现到更新后的位置,一秒钟一次,放近看船模型就是跳着走的,这个效果不好,需要其他的处理船才会平移过去,但是我不会!所以我用了时间轴,她帮我缓慢的移动过去。 OK,就是这个意思哈。如果有更好的方式就用其他的,我菜,我不大会。我现在的大脑袋瓜子就只允许我这么做了。
创建时间轴
我们不是创建了实例了嘛,然后使用时间轴的时候我们需要改点东西。就是开启时间轴,就是下面这个代码。
viewer = new Cesium.Viewer('map', {
baseLayerPicker: false, // 影像切换
animation: true, //是否显示动画控件
timeline: false, //是否显示时间线控件
infoBox: false, //是否显示点击要素之后显示的信息
geocoder: false, //是否显示地名查找控件
timeline: true, //是否启用时间线控件
fullscreenButton: false,
shouldAnimate: true,
navigationHelpButton: false, //是否显示帮助信息控件
terrainProvider: Cesium.createWorldTerrain({
requestWaterMask: true, // required for water effects
requestVertexNormals: true // required for terrain lighting
})
})
其实就是设置 timeline
为 true
,这个是必须的哈,不然时间轴不管用。设置了之后呢,可能就会出现下面这个东西。
然后界面如果不想显示左边的控制台和下面的时间线也没有关系,我们关闭就可以,怎么关闭呢,两个参数设置一下
animation 设置成 false
,左边的控制台就没有了,时间线添加下面这行代码隐藏
viewer.timeline.container.style.display = 'none';
通过上面上面两个步骤,控制台和时间轴都不显示了就,很棒!
时间轴设置
上面我们启用的时间轴时间是默认的哈,就是系统默认的,我们可以根据自己的需要自己设置修改时间轴的开始时间结束时间啥的,然后就是下面这个样子哈宝宝们~
start = Cesium.JulianDate.fromDate(new Date()); // 设置时间轴当前时间为开始时间
start = Cesium.JulianDate.addHours(start, 8, new Cesium.JulianDate()); // 开始时间加8小时改为北京时间
stop = Cesium.JulianDate.addSeconds(start, 400, new Cesium.JulianDate()); // 设置结束时间为开始时间加400秒
// 设置时钟开始时间
viewer.clock.startTime = start.clone();
// 设置时钟当前时间
viewer.clock.currentTime = start.clone();
// 设置时钟结束时间
viewer.clock.stopTime = stop.clone();
// 时间速率,数字越大时间过的越快,设置1好像是和实际时间一样
viewer.clock.multiplier = 1 ;
// 时间轴绑定到viewer上去
viewer.timeline.zoomTo(start, stop);
// 循环执行,到达终止时间,重新从起点时间开始
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP;
然后通过上面的代码,我们就自定义了时间轴的开始时间和结束时间。
添加模型绑定时间轴
然后我们可以添加一个模型实验一下子哈,我们先写死这个轮船的位置信息,先不从websocket获取哈,然后呢,就是单纯的测试一下时间轴怎么用哈。
首先创建一个船模型。
let property = computeFlight(data) // 这是通过一个方法把时间轴和船的位置信息绑定了
entity = viewer.entities.add({
availability: new Cesium.TimeIntervalCollection([new Cesium.TimeInterval({
start: start,
stop: stop
})]),
position: property,
orientation: new Cesium.VelocityOrientationProperty(property), // 根据速度计算方向角
model: {
uri: './models/boat2.gltf', //gltf文件的URL
scale: 0.05, //放大倍数
color: Cesium.Color.fromCssColorString('rgba(0, 253, 239, 0.6)'), // 船模型颜色
silhouetteColor: Cesium.Color.fromCssColorString('rgba(0, 255, 0, 1)'), // 船模型边框颜色
silhouetteSize: 1 // 船模型边框宽度
},
path: { // 船路径
resolution: 1, // 这个不知道是啥
material: new Cesium.PolylineGlowMaterialProperty({
glowPower: 0.1, // 颜色透明度
color: Cesium.Color.fromCssColorString('rgba(0, 253, 239, 0.5)') // 路线颜色
}),
width: 2 // 路线的显示宽度
}
});
viewer.trackedEntity = entity; // 视角跟随模型
然后是时间轴与位置绑定的函数
function computeFlight(source) {
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].longitude, source[i].dimension, source[i].height);
// 添加位置,和时间对应
property.addSample(time, position);
}
return property;
}
他其实是有一个船的位置信息的列表哈,是这个样子的
data = [{ longitude: 120.998994, dimension: 32.66277, height: 0, time: 0 }, { longitude: 122.507572, dimension: 31.570649, height: 0, time: 180 }, { longitude: 122.158023, dimension: 30.493342, height: 0, time: 400 }]
OK,兄弟们,奇迹诞生了~ 船走起来了~~~~
OK,到这里嘞,时间轴已经用起来了哈!开心!如果是轨迹回放的话,就可以了其实。
但是上面有一个点需要注意一下子哈!就是模型的方向角,可能会遇到这个问题哈,啥问题嘞,就是你的模型放上去就发现车头或者是船头不指向前进的方向,就是模型偏着跑,这是啥子问题嘞,我遇到过,原因就是这行代码 orientation: new Cesium.VelocityOrientationProperty(property), // 根据速度计算方向角
。 他是自动给你计算的方向角,他要求你的模型,就比如说我的船头,必须是朝向正东,如果不是正东就会偏,比如我最开始模型设计的船头朝向正北,所以说船直接横着跑,就像是螃蟹一样,这个需要模型设计的设计师,把船头朝向正东。
OK,到这里都已经完成了。
添加尾浪效果
船模型尽管添加进来了,上边也看出来船可以在海上跑了,但是呢,总是感觉船少点,有点不大和谐,比较僵硬,OK,找到问题了,船尾如果添加上尾浪效果,就是船推开海水的动画效果就好很多,这个要咋个添加呢,最简单的方式就是模型自带,这个就比较考验人了,因为我不会修改设计3D模型,真让人焦灼,没办法,只能通过代码实现了,怎么实现呢,我也不小的,一点思路都没有,搜了好半天,发现有一个叫做“粒子系统”的玩意儿可以实现这个效果。
在 cesium 官方案例上面有一个案例,可以瞅一眼。
他这个案例是小车后面的尾气效果,然后我想,我也可以根据这个粒子系统改一下,改成船后的尾浪效果,应该是差不多的东西吧,于是就开始研究“粒子系统”,啊,加进去很简单,但是要调整成自己需要的样子还真的不容易。
直接上代码吧!
// 在创建玩模型之后创建一个例子系统
let particleSystem = viewer.scene.primitives.add(new Cesium.ParticleSystem({
image: './image/bl.png', // 波浪图片
startColor: Cesium.Color.fromCssColorString('rgba(75, 125, 172, 0.6)'),
endColor: Cesium.Color.WHITE.withAlpha(0.0),
startScale: 1.0,
endScale: 15.0,
minimumParticleLife: 1,
maximumParticleLife: 6,
speed: 20.0,
emissionRate: 50.0,
emitter: new Cesium.CircleEmitter(2),
// imageSize: new Cesium.Cartesian2(2, 2),
minimumImageSize: new Cesium.Cartesian2(0, 0),
maximumImageSize: new Cesium.Cartesian2(2, 2),
//主模型参数(位置)
modelMatrix: computeModelMatrix(entity, Cesium.JulianDate.now()),
emitterModelMatrix: computeEmitterModelMatrix(),
updateCallback: applyGravity,
}));
viewer.scene.preRender.addEventListener((scene, time) => {
particleSystem.modelMatrix = computeModelMatrix(entity, time); // 粒子系统和模型绑定,让他跟着模型跑
});
然后需要三个函数。
function applyGravity(p, dt) {
const position = p.position;
Cesium.Cartesian3.normalize(position, gravityScratch);
Cesium.Cartesian3.multiplyByScalar(
gravityScratch,
0 * dt,
gravityScratch
);
p.velocity = Cesium.Cartesian3.add(
p.velocity,
gravityScratch,
p.velocity
);
}
function computeEmitterModelMatrix() {
//方向
let hpr = Cesium.HeadingPitchRoll.fromDegrees(80, 80, 80, new Cesium.HeadingPitchRoll());
var trs = new Cesium.TranslationRotationScale();
//以modelMatrix(飞机)中心为原点的坐标系的xyz轴位置偏移
trs.translation = Cesium.Cartesian3.fromElements(-30, 0, 0, new Cesium.Cartesian3());
trs.rotation = Cesium.Quaternion.fromHeadingPitchRoll(hpr, new Cesium.Quaternion());
return Cesium.Matrix4.fromTranslationRotationScale(trs, new Cesium.Matrix4());
}
// 计算当前时间点飞机模型的位置矩阵
function computeModelMatrix(entity, time) {
// //获取位置
let position = Cesium.Property.getValueOrUndefined(entity.position, time, new Cesium.Cartesian3());
if (!Cesium.defined(position)) {
return undefined;
}
//获取方向
let modelMatrix;
let orientation = Cesium.Property.getValueOrUndefined(entity.orientation, time, new Cesium.Quaternion());
if (!Cesium.defined(orientation)) {
modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(position, undefined, new Cesium.Matrix4());
} else {
modelMatrix = Cesium.Matrix4.fromRotationTranslation(Cesium.Matrix3.fromQuaternion(orientation, new Cesium.Matrix3()), position, new Cesium.Matrix4());
}
return modelMatrix;
}
然后具体每个参数是啥意思呢,可以自行百度,或者是查看官方文档,主要就是配置成自己的样子不好整,单纯的加进去倒是挺容易的,关于粒子系统的使用百度方法博文还是蛮多的,讲的都很详细,我就不瞎逼逼赖赖了,毕竟我会的也不多,就不瞎误导别人了。
推荐两篇博文可以看一下子关于粒子系统讲解的(我就是参考他们两个大佬博文改的呀):
https://blog.csdn.net/weixin_43889028/article/details/111405420
https://blog.csdn.net/UmGsoil/article/details/76542720
好了,这样的话,就有尾浪效果了,动态的哟~
完美,效果就比以前好了很多。
通过websocket实时显示船的位置
OK,到最重要,也是我做的最烂的一部分了,我不建议大家看,因为我觉得我这样有问题,但是做都做了,详细代码就不说了,分享一下思路吧就,大佬们如果有更好的还请教教我呀!
我是这样想的:
哦,不对,是这样考虑:
因为他每秒钟返回一个轮船信息嘛不是,所以说我websocket收到轮船消息了我就处理这个数据,这个数据包含着 轮船唯一标识符id,轮船经度、轮船纬度、轮船偏向角度。
所以说我就设置了当前的时间为时间轴的开始时间,结束时间超级超级长,就是在开始时间加上了999999999之类的,我说清楚了吧?
然后每当接收到新数据的时候,我就动态绑定最新的经纬度和时间。
let time = Cesium.JulianDate.addSeconds(start, 这是秒数, new Cesium.JulianDate);
let selfPosition = Cesium.Cartesian3.fromDegrees(longitude, latitude, 0);
property.addSample(time, selfPosition)
但是有一个问题哈,就是我处理逻辑需要时间嘛不是,所以说前端的时间轴可能会追上我设置的船的事实位置,所以说呢,我监听了一下时间轴的事件,如果时间轴快追上来就先暂停,等新的位置信息更新了在继续。
// 计算时间差距看是否追上,追上暂停一秒钟 这个second其实设计的是第几次发我,理论上一秒钟一次嘛不是,但是感觉有坑会
viewer.clock.onTick.addEventListener(function (clock) {
let s = Cesium.JulianDate.secondsDifference(viewer.clock.currentTime, start)
if (s >= second - 1) {
viewer.clock.shouldAnimate = false
} else {
setTimeout(() => {
viewer.clock.shouldAnimate = true
}, 1000);
}
if (a < 1) {
return
}
});
其实就是这样,然后我的船就可以正常跑了,测试一段时间感觉暂时没问题,但是我觉得是有坑的。但是我菜我又发现不了新的合适的方式,嗨!技能不行啊!毕竟刚开始接触,就这样,自己想要的效果实现了。
然后偏转角,这个有点瑕疵,什么瑕疵呢,就是船可能会停下呀,上边时间轴船运动的时候,我们是根据一个提供的自带函数计算的船的方向,根据起点和终点的坐标,船头方向指向终点,但是,当船停下,发过来的几个时间都是相同的位置,这个时候就出问题了呀!两点确定一条直线,但是两个点在一个位置,他就计算不出这个小船船改朝向哪里了,只能显示模型的默认朝向,就是正东,这样应该是不可以的吧,当然不介意的话是没问题的,但是我不想!
我的这个会传给我一个船的角度,然后我就记录下来,然后在时间轴变化的时候,不用模型自带的方法计算船的偏转角度了,我自己计算然后动态赋值!
let m = viewer.entities.getById(item) // 根据id查询模型
// 动态设置模型的方向 position是当前模型坐标 heading是这个模型当前时间偏转角
m.orientation = Cesium.Transforms.headingPitchRollQuaternion(position, new Cesium.HeadingPitchRoll(Cesium.Math.toRadians(heading), 0, 0))
然后就可以了,整体就是这个样子实现的,但是我老是觉得最后实时更新坐标有问题。
菜鸡炫技术!好了就这样,完美~~
【重要说明】博文仅作为本人的学习记录,论点和观点仅代表个人而不代表技术的真理,目的是自我学习和有幸成为可以向他人分享的经验,因此有错误会虚心接受改正,但不代表此刻博文无误!
【博客园地址】叫我+V : http://www.cnblogs.com/wjw1014
【CSDN地址】叫我+V : https://wjw1014.blog.csdn.net/
【Gitee地址】叫我+V :https://gitee.com/wjw1014