鐵路模擬仿真實現(xiàn)
實現(xiàn)效果

內容比較多,只講主要部分,詳細內容可以參考代碼,有不懂的歡迎討論
初始化變量
這些變量下面都會用到
// 運動車廂的速度
let velocity = 30 // 速度,根據(jù)他來計算到達各個點的時間
// 當前目標點的位置
// var currentIndex = 1
// 每節(jié)車廂相對上一節(jié)車廂延時一定時間到達同一個位置
var delayTime = 13
// 存儲所有運動中的實體對象
var dynamicEntities = []
// 運動模型數(shù)量
var dynamicNum = 5
// 每節(jié)鐵軌的長度,用于計算兩個點之前鋪設多少節(jié)鐵軌
var modelLength = 170
// 初始化dynamicEntitye
for (let i = 0; i < dynamicNum ; ++i) {
let obj = {
entity: null, // 實體對象
property: new Cesium.SampledPositionProperty(), // 動態(tài)位置屬性
timeAndOrientationList: [],
startTime: 0,
endTime: 0
}
dynamicEntities.push(obj)
}
加載線路并獲取位置
我們需要有一系列點路徑坐標(火車運行的路徑)。這里我從Google Eearth中繪制了一條線,然后導出為KML數(shù)據(jù)加載進來。

通過加載的這條路徑,我們需要獲取路徑中每個轉折點的坐標信息。通過這些轉折點,我們可以完成設置鐵軌位置和計算出模型實體每個時間點對應點位置
加載KML
// 初始化路徑 設置帶時間的路徑
viewer.dataSources.add(Cesium.KmlDataSource.load(routerUrl,
{
camera: viewer.scene.camera,
canvas: viewer.scene.canvas,
clampToGround: true
})
).then(dataSource => {
// ... 加載好后獲取改路徑點坐標數(shù)組
var router = dataSource.entities.getById('0129AA13ED12D2857AD0');
var positions = router.polyline.positions._value
viewer.flyTo(router)
// createDynamicPositions(positions) // 計算帶時間的路徑
// createDynamicEntity() // 根據(jù)動態(tài)路徑創(chuàng)建模型實體
})
首先我們加載好路線后,就要獲取改路線的坐標數(shù)組(每個轉折點或頂點的位置)。
// 獲取路徑對象
var router = dataSource.entities.getById('0129AA13ED12D2857AD0');
// 獲取對象中的坐標數(shù)組
var positions = router.polyline.positions._value
我們可以看一下這些數(shù)組的內容

在這里可以看出來,這些坐標全是笛卡爾類型。同時可以知道我們總共有13個轉折點
接下來兩章是重點
加載鐵軌
效果展示

實現(xiàn)上面效果,這里我們需要做下面幾步。
- 計算每段路(兩個點)之間的距離S
- 設置每個鐵軌的固定長度L
- 計算每段路可以鋪設多少個模型 num = S / L
- 通過每段路兩端的點的坐標,計算出這段中每個鐵軌模型的位置
// 這個是每個模型的長度,在一開始的時候就定義了
// var modelLength = 170
function repeateModel(posCart1, posCart2) {
// 需要擺放模型的數(shù)量
// 模型的數(shù)量 = 兩個點之間的長度 / 每個模型的長度
let modelNum = parseInt(computeDistance(posCart1, posCart2) / modelLength)
// 根據(jù)兩個點的經緯度調整每個模型的方向
let heading = computeOrientation(posCart1, posCart2)
// 開始計算每個模型的位置
for (var i = 1; i < modelNum; ++i) {
// 求第i個點的位置。下面有介紹為什么這樣寫
var mid = new Cesium.Cartesian3()
Cesium.Cartesian3.add(
Cesium.Cartesian3.multiplyByScalar(posCart1, i / modelNum, new Cesium.Cartesian3()),
Cesium.Cartesian3.multiplyByScalar(posCart2, (modelNum - i) / modelNum, new Cesium.Cartesian3()),
mid
)
// 計算出位置后,添加鐵軌模型到Viewer中。同時調整模型的方向
viewer.entities.add({
position: mid,
model: {
uri: modelRailwayUrl,
scale: 0.025
},
orientation: changeOrientation(mid, heading)
})
}
}
兩個坐標之前第i的位置如何求
先看一下下面的一道數(shù)學題

通過這道題,我們就可以寫出下面代碼,求出第i個點的位置了
Cesium.Cartesian3.add(
Cesium.Cartesian3.multiplyByScalar(posCart1, i / modelNum, new Cesium.Cartesian3()),
Cesium.Cartesian3.multiplyByScalar(posCart2, (modelNum - i) / modelNum, new Cesium.Cartesian3()),
mid
)
模型方向問題
在上面代碼中。我們經常要用到一個計算模型方向和改變模型方向的函數(shù),那么為什么要計算模型的方向呢?
我們打開鐵軌模型和系統(tǒng)自帶的一些模型。看看他們的方向
使用下面命令調出查看方向的小工具
viewer.extend(Cesium.viewerCesiumInspectorMixin);

可以看到,我們的模型的方向默認位置是朝向南方(紅色是東方,綠色是北方)。而官網的模型方向默認是東方。根據(jù)官方對模型的描述
By default, the model is oriented upright and facing east. Control the orientation of the model by specifying a Quaternion for the Entity.orientation property. This controls the heading, pitch, and roll of the model.
可以看到,我們的模型方向是有問題。因此需要手動糾正。查閱很多方法,無法從模型本身入手。所以只能通過代碼的方式來糾正方向。大概的思路是先計算出兩個點的方向,然后在向東方偏移90度左右即可。
計算方向函數(shù)
function computeOrientation(posCart1, posCart2) {
let heading = bearing(
Cesium.Cartographic.fromCartesian(posCart1).latitude,
Cesium.Cartographic.fromCartesian(posCart1).longitude,
Cesium.Cartographic.fromCartesian(posCart2).latitude,
Cesium.Cartographic.fromCartesian(posCart2).longitude
)
return heading
}
// 計算兩點之間的方向
function bearing(startLat, startLng, destLat, destLng) {
startLat = Cesium.Math.toRadians(startLat);
startLng = Cesium.Math.toRadians(startLng);
destLat = Cesium.Math.toRadians(destLat);
destLng = Cesium.Math.toRadians(destLng);
let y = Math.sin(destLng - startLng) * Math.cos(destLat);
let x = Math.cos(startLat) * Math.sin(destLat) - Math.sin(startLat) * Math.cos(destLat) * Math.cos(destLng - startLng);
let brng = Math.atan2(y, x);
let brngDgr = Cesium.Math.toDegrees(brng);
return (brngDgr + 360) % 360;
}
改變模型的位置
function changeOrientation(position, degree) {
var heading = Cesium.Math.toRadians(degree);
var pitch = Cesium.Math.toRadians(0.0);
var roll = Cesium.Math.toRadians(0.0);
var orientation = Cesium.Transforms.headingPitchRollQuaternion(position, new Cesium.HeadingPitchRoll(heading, pitch, roll));
return orientation
}
加載運動的車頭和車廂
這里我們需要了解一個知識。Cesium的Property機制總結.這篇文章中,我們可以看到一個屬性SampledPositionProperty,它可以使用物體的運動。

它的實現(xiàn)代碼如下
var property = new Cesium.SampledPositionProperty();
property.addSample(Cesium.JulianDate.fromIso8601('2019-01-01T00:00:00.00Z'),
Cesium.Cartesian3.fromDegrees(-114.0, 40.0, 300000.0));
property.addSample(Cesium.JulianDate.fromIso8601('2019-01-03T00:00:00.00Z'),
Cesium.Cartesian3.fromDegrees(-114.0, 45.0, 300000.0));
blueBox.position = property;
它的原理是,Entity在不同的時間運動到不同的位置。因此我們的火車運動也是一樣,在不同的時候運動到不同的位置即可。那么如何實現(xiàn)呢?
還是通過之前獲取的鐵軌路徑數(shù)組,再計算到達每個轉折點的時間。構成一個如下圖所示的數(shù)據(jù)結構。

如何讓模型運動起來也可以總結為下面這張圖

比如4點的時候在position1位置,4.30的時候在position2位置。4.50的時候在position3位置。
那么現(xiàn)在時間點應該如何計算
我們設置一個速度變量V,然后計算兩點的距離S。那么到達下一個的時間就是
time = S / V
因此實現(xiàn)代碼如下(偽代碼)
// 計算到下一個坐標所花費的時間
let time2Next = computeTime(datas[index], datas[index + 1])
// 計算到達改點的時刻
let time = totalTime + time2Next
// 將時刻+位置信息寫入到模型的位置變量中
dynamicEntity.property.addSample(
Cesium.JulianDate.addSeconds(start, time, new Cesium.JulianDate()),
position
)
// 計算總花費時間
totalTime += time2Next
這里又有新的問題。
我們需要好幾節(jié)車廂一起運動,如何實現(xiàn)呢?
使用延時啟動,就是每一個車廂到達一個轉折點的時間都比上一節(jié)車廂晚一段時間。如下圖所示,不同的車廂在不同的時間點的位置不一樣。

let time = totalTime + delayTime * i
我們看一下實現(xiàn)效果,車廂是一節(jié)在一節(jié)的后面出現(xiàn)的

通過代碼實現(xiàn)
function createDynamicPositions(positions) {
var length = positions.length
var totalTime = 0// 跑完全部路程的時間
// 遍歷鐵軌路徑的每個轉折點
positions.forEach((position, index, datas) => {
// 在每個路徑轉角處創(chuàng)建一個Point對象
CreatePoint(position, index)
if (index + 1 < length) {
// 計算一個點到另一點需要到時間
let time2Next = computeTime(datas[index], datas[index + 1])
// 計算兩個轉角點的方向
let orientation = computeOrientation(datas[index], datas[index + 1])
// 為每個車廂模型設置 時間+位置
dynamicEntities.forEach((dynamicEntity, i) => {
// 這里實現(xiàn)了 每個模型都相對于之前都個模型延時一定時間進行啟動
let time = totalTime + delayTime * i
dynamicEntity.property.addSample(
Cesium.JulianDate.addSeconds(start, time, new Cesium.JulianDate()),
position
)
// 記錄每個模型分別達到一個點的時間、方向、位置
let obj = {
time: totalTime, // 到達下一個點需要耗費的時間,它是一個數(shù)值,不是一個時間點
position: position,
orientation: orientation
}
// 計算開始時間
if (index === 0) {
dynamicEntity.startTime = Cesium.JulianDate.addSeconds(start, time, new Cesium.JulianDate())
}
// 計算最后一個時間
if (index + 2 === length) {
dynamicEntity.endTime = Cesium.JulianDate.addSeconds(start, time, new Cesium.JulianDate())
}
// 將計算得到的 時間+位置 屬性存儲到每個實體中
dynamicEntity.timeAndOrientationList.push(obj)
})
totalTime += time2Next
}
});
這里我們發(fā)現(xiàn)我們也計算了每個模型的方向,為什么要計算方向呢?在上面設置鐵軌的時候講到了,因為我們的模型方向默認是有點問題的。默認朝向南方,因此需要手動調整方向,我們需要自己寫一個方法,判斷到了轉角處進行轉向。(如果是模型默認朝向東方的話,則不需要使用該方法,直接使用自帶的一種方法,具體方法后面再談)
如何實現(xiàn)到了轉角處自動轉向呢?我們在剛剛上一步的時候記錄了每個模型到達某個位置的時候是在是什么時間點。因此只需要判斷,當前模型運行的時間是否到了轉角的時間點,到了的話就開始轉向,而這個方向我們同時也在上一步的時候存儲到每個實體中
let obj = {
time: totalTime, // 到達下一個點需要耗費的時間,它是一個數(shù)值,不是一個時間點
position: position,
orientation: orientation
}
監(jiān)聽當前時間點并轉向的代碼如下
viewer.clock.onTick.addEventListener((clock) => {
// 判斷每個運動的模型當前是否到了轉向時間
dynamicEntities.forEach(dynamicEntity => {
// 計算每個運動的模型與模型的開始時間差
let timeOffset = Cesium.JulianDate.secondsDifference(clock.currentTime, dynamicEntity.startTime);
// 判斷是否達到轉向的時間點
dynamicEntity.timeAndOrientationList.forEach((obj, index, array) => {
if (timeOffset >= obj.time && timeOffset <= array[index + 1].time) {
// 177在第一條鐵軌是一個好的角度
dynamicEntity.entity.orientation = changeOrientation(obj.position, obj.orientation + 180)
}
})
})
如果模型的方向是正確的,只需要在創(chuàng)建模型實體對象的時候,指定該屬性即可
orientation: new Cesium.VelocityOrientationProperty(dynamicEntity.property),
目前還有下面問題暫時無法解決
- 各個模型之間的銜接不好
經過測試如果模型的方向是正確的話,那么就可以解決這個問題。所以可以從模型入手,更改模型的默認方向,使它默認朝向東方,但是自己一直沒有找到如何編輯GLB模型。所以暫時無解。