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))
然后就可以了,整體就是這個樣子實現的,但是我老是覺得最后實時更新坐標有問題。
菜雞炫技術!好了就這樣,完美~~
