三维技术在前端的应用,不仅局限于“静态”模型的呈现,其在模型动画方面的应用,也有巨大的前景和作用。三维技术结合监控设备、传感设备,能够实时的将某一区域内所有目标物的运动状况和轨迹呈现在三维场景中,实现三维可视化的监控、管理和预测,对于大型停车场、交通仿真、港口船舶等场景将带来极大的便利。
参考资料:
交通仿真在线范例:http://support.supermap.com.cn:8090/webgl/examples/editor.html#trafficSimulation
一、使用动态图层(Dynamic Layer)方法实现三维动画
1、实现三维动画的两种方式
(1)KML 模型节点动画
这种方式向 viewer对象 的 dataSources 合集里面添加 KML 格式的模型文件,将其变成一个 entity 实体,再通过改变 entities 实体的 position、orientation 等属性实现动画,也可以建模的时候在模型上先预置好动画。这种方式适合呈现预置的、不需要进行代码控制的动画,直接改变 position 等属性实现的动画效果较为粗糙,详细代码可查看超图的 KML模型节点动画在线范例
(2)S3M 模型动态图层动画
该方式的原理和上述有所不同,是通过新建一个 DynamicLayer3D 的图层,然后把 s3m 模型放置到这个图层中,再使用 updateObjectWithModel 方法让模型根据指定的路径信息(一般由后台生成,包括经纬度、高呈等)不断进行更新从而实现模型动画效果。由于 supermap 对动态图层上的模型变化做了平缓过渡的优化,这种方式呈现出来的动画质量非常好,而且同一个模型可以根据生成的不同路线进行复用,非常适合于大型仿真和实况场景的可视化展示。
2、动态图层代码实现动画
(1)创建 s3m 动态图层
// s3m模型引入路径集合
var urls = [
'./models/ship1.s3m',
'./models/ship2.s3m',
'./models/ship4.s3m',
];
// 创造s3m动态图层
var layerS3m = new Cesium.DynamicLayer3D(viewer.scene.context, urls);
viewer.scene.primitives.add(layerS3m);
(2)s3m 模型初始化
首先定义 s3m 模型的初始路径点数据,主要是定义路径的起点、转折点及终点这些关键点的经纬度,url 指定使用的模型类型,这些数据一般都由后台提供
// s3m模型初始点路径数据
var s3mRoute = [
{
id: 1, // 模型 id
url: './models/ship1.s3m', // 指定模型的类型
longitude: [113.429257753643, 113.432889398072, 113.435648587521], // 模型关键点的经度数据
latitude: [22.179584024493, 22.1819691979702, 22.1822381312213] // 模型关键点的纬度数据
},
{
id: 2,
url: './models/ship1.s3m', // 注意,模型是可以复用的
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模型初始化函数
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) { // 对每种模型进行配置
case './models/ship1.s3m': {
state = new Cesium.DynamicObjectState({
longitude: data[i].longitude[length], // 经度
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; // 返回模型空间属性配置数据
}
(3)处理路径数据
一般后台只提供一些关键点的经纬度信息,需要前端将这些数据根据指定步长分解为更准确的一个个路径点,再提供给动态图层进行渲染。
// 根据所给的步长和关键点数据生成完整路径点集合
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++) {
// 根据给出的经度步长计算出每2个关键点之间的移动次数
var moveTimes = Math.abs(parseInt((data[i].longitude[j] - data[i].longitude[j + 1]) / longStep));
// 根据经度算出的移动次数计算出每2个关键点之间纬度的移动步长
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]) { // 向西运动将经度步长设为负值,否则相反
longStep = Math.abs(longStep);
} else {
longStep = -Math.abs(longStep);
}
for (var k = 0; k < moveTimes; k++) { // 将每一次根据指定步长移动后的经纬度数据存放在 points 中
points.push({
longitude: data[i].longitude[j] - k * longStep,
latitude: data[i].latitude[j] - k * latStep
})
}
}
pointsList.push({ // 计算完所有关键点后,将模型的完整路径数据存放在 pointsList 中
id: data[i].id,
url: data[i].url,
points: points
})
}
return pointsList; // 返回生成的完整数据
}
// 生成渔船路径数据
var s3mLonStep = 0.00008966;
var s3mRoutePoints = generateRoutePoints (s3mLonStep, s3mRoute);
(4)加载路径数据
点击模型加载指定模型的路径数据,也可以一次性加载全部数据,视业务需求而定
// 渲染模型路径动画,传入模型 id、完整路径数据、模型空间属性数据
function generateActionShip (id, data, keymap) {
var url = data[id - 1].url; // 获取点击模型的模型存放位置
var updateTimes = data[id - 1].points.length; // 模型更新次数
var updateIndex = -1;
var flag = 0;
for (var i = 0; i < data.length; i++) { // 由于复用了相同的模型,要根据 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]); // 循环调用 updateObjectWithModel 更新模型空间数据
keymap[url][updateIndex].longitude = data[id - 1].points[flag].longitude; // 更新模型空间的经纬度信息
keymap[url][updateIndex].latitude = data[id - 1].points[flag].latitude;
flag++;
if (flag === updateTimes - 1) {
clearInterval(_s3mUpdateInterval); // 渲染完毕清除定时器
}
}, 200);
}
layerS3m.updateInterval = 200; // 设置动态图层更新频率
// 点击模型显示动画
handleS3mId = new Cesium.ScreenSpaceEventHandler(scene.canvas);
handleS3mId.setInputAction(function(click) { // 监听点击操作
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);
二、动画轨迹绘制
我们的业务需求是实现点击某个模型,能让模型移动并显示出移动轨迹,如果仅需实现动画可忽略这部分。
// 绘制模型路径,传入当前点击模型的关键点路径数据
function handlePolyline(data){
var length = data.longitude.length // 获取关键点的个数
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]), // 传入路径两端的经纬度数据
width : 2, // 路径宽度
material : new Cesium.PolylineDashMaterialProperty({
color: Cesium.Color.CYAN // 路径颜色
})
}
});
}
}
// 绘制路径,可以在点击模型之后调用
handlePolyline(s3mPickedId - 1)
// 移除绘制的路径,在模型路径渲染完毕时调用
for (var k = 0; k < s3mRoute[s3mPickedId - 1].longitude.length - 1; k++) {
viewer.entities.removeById(100 + k);
}
三、动态模型跟踪
可以通过 viewer 对象的 trackedEntity 实现对模型的跟踪,使用后视角将切换到模型上,能增强沉浸式体验,但是在这个视角下模型运动的过渡缓动效果还有待优化,需斟酌使用。
// 将以下代码复制到点击模型获取到 id 的后面即可
var selectedPrimitive = pickedObject.primitive; // 选中的图元
var ownerGroup = selectedPrimitive._ownerGroup; // 图元所在的组信息
var stateList = ownerGroup.stateList; // 状态信息列表
var state = stateList.get(pickedObject.id);
if(!trackedEntity) {
trackedEntity = viewer.entities.add({
id: 'tracked-entity',
position: state.position,
point: {
pixelSize: 1,
show: true // 不能设为false
},
viewFrom: new Cesium.Cartesian3(-100, -150, 100) // 观察位置的偏移量
});
} else {
trackedEntity.position = state.position;
}
viewer.trackedEntity = trackedEntity;