三維技術(shù)在前端的應(yīng)用,不僅局限于“靜態(tài)”模型的呈現(xiàn),其在模型動畫方面的應(yīng)用,也有巨大的前景和作用。三維技術(shù)結(jié)合監(jiān)控設(shè)備、傳感設(shè)備,能夠?qū)崟r的將某一區(qū)域內(nèi)所有目標(biāo)物的運動狀況和軌跡呈現(xiàn)在三維場景中,實現(xiàn)三維可視化的監(jiān)控、管理和預(yù)測,對于大型停車場、交通仿真、港口船舶等場景將帶來極大的便利。
參考資料:
交通仿真在線范例:http://support.supermap.com.cn:8090/webgl/examples/editor.html#trafficSimulation
一、使用動態(tài)圖層(Dynamic Layer)方法實現(xiàn)三維動畫
1、實現(xiàn)三維動畫的兩種方式
(1)KML 模型節(jié)點動畫
這種方式向 viewer對象 的 dataSources 合集里面添加 KML 格式的模型文件,將其變成一個 entity 實體,再通過改變 entities 實體的 position、orientation 等屬性實現(xiàn)動畫,也可以建模的時候在模型上先預(yù)置好動畫。這種方式適合呈現(xiàn)預(yù)置的、不需要進(jìn)行代碼控制的動畫,直接改變 position 等屬性實現(xiàn)的動畫效果較為粗糙,詳細(xì)代碼可查看超圖的 KML模型節(jié)點動畫在線范例
(2)S3M 模型動態(tài)圖層動畫
該方式的原理和上述有所不同,是通過新建一個 DynamicLayer3D 的圖層,然后把 s3m 模型放置到這個圖層中,再使用 updateObjectWithModel 方法讓模型根據(jù)指定的路徑信息(一般由后臺生成,包括經(jīng)緯度、高呈等)不斷進(jìn)行更新從而實現(xiàn)模型動畫效果。由于 supermap 對動態(tài)圖層上的模型變化做了平緩過渡的優(yōu)化,這種方式呈現(xiàn)出來的動畫質(zhì)量非常好,而且同一個模型可以根據(jù)生成的不同路線進(jìn)行復(fù)用,非常適合于大型仿真和實況場景的可視化展示。
2、動態(tài)圖層代碼實現(xiàn)動畫
(1)創(chuàng)建 s3m 動態(tài)圖層
// s3m模型引入路徑集合
var urls = [
'./models/ship1.s3m',
'./models/ship2.s3m',
'./models/ship4.s3m',
];
// 創(chuàng)造s3m動態(tài)圖層
var layerS3m = new Cesium.DynamicLayer3D(viewer.scene.context, urls);
viewer.scene.primitives.add(layerS3m);
(2)s3m 模型初始化
首先定義 s3m 模型的初始路徑點數(shù)據(jù),主要是定義路徑的起點、轉(zhuǎn)折點及終點這些關(guān)鍵點的經(jīng)緯度,url 指定使用的模型類型,這些數(shù)據(jù)一般都由后臺提供
// s3m模型初始點路徑數(shù)據(jù)
var s3mRoute = [
{
id: 1, // 模型 id
url: './models/ship1.s3m', // 指定模型的類型
longitude: [113.429257753643, 113.432889398072, 113.435648587521], // 模型關(guān)鍵點的經(jīng)度數(shù)據(jù)
latitude: [22.179584024493, 22.1819691979702, 22.1822381312213] // 模型關(guān)鍵點的緯度數(shù)據(jù)
},
{
id: 2,
url: './models/ship1.s3m', // 注意,模型是可以復(fù)用的
longitude: [113.429257753643, 113.431963627038, 113.437962937247],
latitude: [22.179584024493, 22.1810110654651, 22.1800193185396]
},
{
id: 3,
url: './models/ship2.s3m',
longitude: [113.429257753643, 113.431881938968, 113.43604789432, 113.435539632918],
latitude: [22.179584024493, 22.1810614918177, 22.1788091037995, 22.178002280521]
},
{
id: 4,
url: './models/ship4.s3m',
longitude: [113.430553206872, 113.432291642287],
latitude: [22.1802826334056, 22.1823216226192]
},
];
下面是s3m 模型初始化
// 初始化s3m模型
var s3mKeymap = initS3mShip(s3mRoute, urls)
// 處理s3m模型初始化函數(shù)
function initS3mShip (data, urls) {
var keymap = {};
for (var i = 0; i < urls.length; i++) {
keymap[urls[i]] = [];
}
var state;
for (var i = 0; i < data.length; i++) {
var length = data[i].longitude.length - 1;
switch(data[i].url) { // 對每種模型進(jìn)行配置
case './models/ship1.s3m': {
state = new Cesium.DynamicObjectState({
longitude: data[i].longitude[length], // 經(jīng)度
latitude: data[i].latitude[length], // 緯度
altitude: -4, // 海拔
scale: new Cesium.Cartesian3(1, 1, 1), // 模型大小
heading: Cesium.Math.toRadians(230), // 方位角
id: data[i].id // 模型 id
});
break;
};
case './models/ship2.s3m': {
state = new Cesium.DynamicObjectState({
longitude: data[i].longitude[length],
latitude: data[i].latitude[length],
altitude: 0,
scale: new Cesium.Cartesian3(1, 1, 1),
heading: Cesium.Math.toRadians(50),
id: data[i].id
});
break;
};
case './models/ship4.s3m': {
state = new Cesium.DynamicObjectState({
longitude: data[i].longitude[length],
latitude: data[i].latitude[length],
altitude: 0,
scale: new Cesium.Cartesian3(1, 1, 1),
id: data[i].id
});
break;
}
}
keymap[data[i].url].push(state);
}
for (var key in keymap) {
layerS3m.updateObjectWithModel(key, keymap[key]); //更新模型空間屬性
}
return keymap; // 返回模型空間屬性配置數(shù)據(jù)
}
(3)處理路徑數(shù)據(jù)
一般后臺只提供一些關(guān)鍵點的經(jīng)緯度信息,需要前端將這些數(shù)據(jù)根據(jù)指定步長分解為更準(zhǔn)確的一個個路徑點,再提供給動態(tài)圖層進(jìn)行渲染。
// 根據(jù)所給的步長和關(guān)鍵點數(shù)據(jù)生成完整路徑點集合
function generateRoutePoints (step, data) {
var longStep = step;
var pointsList = [];
var points;
for (var i = 0; i < data.length; i++) {
points = [];
for (var j = 0; j < data[i].longitude.length - 1; j++) {
// 根據(jù)給出的經(jīng)度步長計算出每2個關(guān)鍵點之間的移動次數(shù)
var moveTimes = Math.abs(parseInt((data[i].longitude[j] - data[i].longitude[j + 1]) / longStep));
// 根據(jù)經(jīng)度算出的移動次數(shù)計算出每2個關(guān)鍵點之間緯度的移動步長
var latStep = parseFloat((data[i].latitude[j] - data[i].latitude[j + 1]) / moveTimes).toFixed(8);
if (data[i].longitude[j] > data[i].longitude[j + 1]) { // 向西運動將經(jīng)度步長設(shè)為負(fù)值,否則相反
longStep = Math.abs(longStep);
} else {
longStep = -Math.abs(longStep);
}
for (var k = 0; k < moveTimes; k++) { // 將每一次根據(jù)指定步長移動后的經(jīng)緯度數(shù)據(jù)存放在 points 中
points.push({
longitude: data[i].longitude[j] - k * longStep,
latitude: data[i].latitude[j] - k * latStep
})
}
}
pointsList.push({ // 計算完所有關(guān)鍵點后,將模型的完整路徑數(shù)據(jù)存放在 pointsList 中
id: data[i].id,
url: data[i].url,
points: points
})
}
return pointsList; // 返回生成的完整數(shù)據(jù)
}
// 生成漁船路徑數(shù)據(jù)
var s3mLonStep = 0.00008966;
var s3mRoutePoints = generateRoutePoints (s3mLonStep, s3mRoute);
(4)加載路徑數(shù)據(jù)
點擊模型加載指定模型的路徑數(shù)據(jù),也可以一次性加載全部數(shù)據(jù),視業(yè)務(wù)需求而定
// 渲染模型路徑動畫,傳入模型 id、完整路徑數(shù)據(jù)、模型空間屬性數(shù)據(jù)
function generateActionShip (id, data, keymap) {
var url = data[id - 1].url; // 獲取點擊模型的模型存放位置
var updateTimes = data[id - 1].points.length; // 模型更新次數(shù)
var updateIndex = -1;
var flag = 0;
for (var i = 0; i < data.length; i++) { // 由于復(fù)用了相同的模型,要根據(jù) id 來判斷更新的是模型空間中的第幾個模型
if (data[i].url === url) {
updateIndex++;
}
if (i === id - 1) { // 因為上面的 id 是從1 開始賦值的,所以這里判斷的時候是對比 id - 1
break;
}
}
// 更新s3m圖層信息
_s3mUpdateInterval = setInterval(function(){
layerS3m.updateObjectWithModel(url, keymap[url]); // 循環(huán)調(diào)用 updateObjectWithModel 更新模型空間數(shù)據(jù)
keymap[url][updateIndex].longitude = data[id - 1].points[flag].longitude; // 更新模型空間的經(jīng)緯度信息
keymap[url][updateIndex].latitude = data[id - 1].points[flag].latitude;
flag++;
if (flag === updateTimes - 1) {
clearInterval(_s3mUpdateInterval); // 渲染完畢清除定時器
}
}, 200);
}
layerS3m.updateInterval = 200; // 設(shè)置動態(tài)圖層更新頻率
// 點擊模型顯示動畫
handleS3mId = new Cesium.ScreenSpaceEventHandler(scene.canvas);
handleS3mId.setInputAction(function(click) { // 監(jiān)聽點擊操作
var pickedObject = scene.pick(click.position);
if (Cesium.defined(pickedObject) && pickedObject.id) { // 獲取模型的 id 并顯示動畫
var s3mPickedId = pickedObject.id
generateActionShip(s3mPickedId, s3mRoutePoints, s3mKeymap);
} else {
console.log(pickedObject);
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
二、動畫軌跡繪制
我們的業(yè)務(wù)需求是實現(xiàn)點擊某個模型,能讓模型移動并顯示出移動軌跡,如果僅需實現(xiàn)動畫可忽略這部分。
// 繪制模型路徑,傳入當(dāng)前點擊模型的關(guān)鍵點路徑數(shù)據(jù)
function handlePolyline(data){
var length = data.longitude.length // 獲取關(guān)鍵點的個數(shù)
var blueLine = [];
for (var i = 0; i < length - 1; i++) {
blueLine[i] = viewer.entities.add({ // 添加路徑實體
id: 100 + i, // 路徑 id
name : 'line on the surface', // 名稱
polyline : {
positions : Cesium.Cartesian3.fromDegreesArrayHeights([data.longitude[i] , data.latitude[i], 2, data.longitude[i + 1], data.latitude[i + 1], 2]), // 傳入路徑兩端的經(jīng)緯度數(shù)據(jù)
width : 2, // 路徑寬度
material : new Cesium.PolylineDashMaterialProperty({
color: Cesium.Color.CYAN // 路徑顏色
})
}
});
}
}
// 繪制路徑,可以在點擊模型之后調(diào)用
handlePolyline(s3mPickedId - 1)
// 移除繪制的路徑,在模型路徑渲染完畢時調(diào)用
for (var k = 0; k < s3mRoute[s3mPickedId - 1].longitude.length - 1; k++) {
viewer.entities.removeById(100 + k);
}
三、動態(tài)模型跟蹤
可以通過 viewer 對象的 trackedEntity 實現(xiàn)對模型的跟蹤,使用后視角將切換到模型上,能增強沉浸式體驗,但是在這個視角下模型運動的過渡緩動效果還有待優(yōu)化,需斟酌使用。
// 將以下代碼復(fù)制到點擊模型獲取到 id 的后面即可
var selectedPrimitive = pickedObject.primitive; // 選中的圖元
var ownerGroup = selectedPrimitive._ownerGroup; // 圖元所在的組信息
var stateList = ownerGroup.stateList; // 狀態(tài)信息列表
var state = stateList.get(pickedObject.id);
if(!trackedEntity) {
trackedEntity = viewer.entities.add({
id: 'tracked-entity',
position: state.position,
point: {
pixelSize: 1,
show: true // 不能設(shè)為false
},
viewFrom: new Cesium.Cartesian3(-100, -150, 100) // 觀察位置的偏移量
});
} else {
trackedEntity.position = state.position;
}
viewer.trackedEntity = trackedEntity;