背景
在运维场景中,电梯作为运维环节重要的一部分是不可或缺的,如果能够在三维场景中,将逼真的电梯效果,包括外观、运行状态等表现出来,无疑是产品的一大亮点。本文将从无到有介绍如何在bimface中实现逼真的电梯运行效果,主要包括电梯模型的创建、电梯上下行和停靠楼层动画的实现以及如何对接实时物联网数据来驱动电梯模型运行。
实践
创建电梯模型
首先创建一个立方体模型作为电梯,因为该电梯是外部构件,姑且称之为外部电梯,运维场景中已经包含了电梯模型,这个电梯是建模期间就已经完成的,暂时称之为内部电梯,用来为外部电梯提供起始位置信息。基于以上前提来说说大概的思路方法,简化的电梯实际上就是一个有宽高深的立方体,然后为立方体的每一个面贴上相应的材质,以便于区分电梯的顶部、正面和其他侧面,然后把创建好的电梯作为外部构件加入到场景中正确的位置,那如何获取正确的位置呢?可以获取模型中的内部电梯的包围盒数据,通过包围盒数据计算出外部电梯的位置即可。
let width = 1200, height = 2600, depth = 1000;
let elevatorGeometry = new THREE.BoxBufferGeometry(width, height, depth);
let group = new THREE.Group();
// 电梯侧面材质
let othersMaterial = new THREE.MeshPhongMaterial();
// 电梯顶部材质
let topMaterial = new THREE.MeshPhongMaterial();
// 电梯正面材质
let frontMaterial = new THREE.MeshPhongMaterial();
let loader = new THREE.TextureLoader();
loader.setCrossOrigin("Anonymous");
let others = loader.load('images/basic.png', function (map) {
othersMaterial.map = map;
othersMaterial.wireframe = false;
othersMaterial.needsUpdate = true;
});
let top = loader.load('images/top.png', function (map) {
topMaterial.map = map;
topMaterial.wireframe = false;
topMaterial.needsUpdate = true;
});
let front = loader.load('images/front.png', function (map) {
frontMaterial.map = map;
frontMaterial.wireframe = false;
frontMaterial.needsUpdate = true;
});
let elevatorMaterials = [othersMaterial, othersMaterial, topMaterial, othersMaterial, frontMaterial, othersMaterial];
let elevatorMesh = new THREE.Mesh(elevatorGeometry, elevatorMaterials);
// 调整位置,使模型中电梯构件包含外部电梯Mesh
group.add(elevatorMesh);
group.rotation.x = Math.PI / 2;
// _position是根据模型中的电梯计算得出,从外部传入
group.position.set(_position);
group.updateMatrixWorld();
_viewer_.addExternalObject(_name_, group);
_viewer_.render();
经过上述代码的处理,就可以在场景中看见新创建的电梯的大概样子了,效果如下:
目前电梯模型有了,但是为了能够实时显示电梯数据,我把电梯轿厢内的楼层指示牌放在电梯的外表面,以便于观察当前电梯的状态,如目前所在楼层、上下行等信息。
let panelWidth = 200, panelHeight = 200, segments = 100;
// 指示上下行的箭头
let panel = new THREE.PlaneBufferGeometry(panelWidth, panelHeight, segments, segments);
// 指示楼层
let panelFloor = new THREE.PlaneBufferGeometry(panelWidth, panelHeight, segments, segments);
// 定义各个楼层的材质
let belowOneFloorMaterial = new THREE.MeshBasicMaterial();
let OneFloorMaterial = new THREE.MeshBasicMaterial();
let TwoFloorMaterial = new THREE.MeshBasicMaterial();
let ThreeFloorMaterial = new THREE.MeshBasicMaterial();
let FourFloorMaterial = new THREE.MeshBasicMaterial();
let FiveFloorMaterial = new THREE.MeshBasicMaterial();
let SixFloorMaterial = new THREE.MeshBasicMaterial();
// 加载材质
let up = loader.load('images/ele_up.png', function (map) {
upMaterial.map = map;
upMaterial.wireframe = false;
upMaterial.needsUpdate = true;
});
up.wrapS = THREE.RepeatWrapping;
up.wrapT = THREE.RepeatWrapping;
up.repeat.y = 1;
window[_name_] = up;
let down = loader.load('images/ele_down.png', function (map) {
downMaterial.map = map;
downMaterial.wireframe = false;
downMaterial.needsUpdate = true;
});
down.wrapS = THREE.RepeatWrapping;
down.wrapT = THREE.RepeatWrapping;
down.repeat.y = 1;
let pathList = [];
pathList.push({ role: belowOneFloorMaterial, path: 'images/Digit/-1F.png' });
pathList.push({ role: OneFloorMaterial, path: 'images/Digit/1F.png' });
pathList.push({ role: TwoFloorMaterial, path: 'images/Digit/2F.png' });
pathList.push({ role: ThreeFloorMaterial, path: 'images/Digit/3F.png' });
pathList.push({ role: FourFloorMaterial, path: 'images/Digit/4F.png' });
pathList.push({ role: FiveFloorMaterial, path: 'images/Digit/5F.png' });
pathList.push({ role: SixFloorMaterial, path: 'images/Digit/6F.png' });
const buildMaterials = (item) => {
return new Promise((resolve, reject) => {
loader.load(item.path, function (map) {
item.role.map = map;
item.role.wireframe = false;
item.role.needsUpdate = true;
});
});
}
for (let i = 0; i < pathList.length; i++) {
buildMaterials(pathList[i]);
}
// 创建楼层信息面板(上下行指示箭头以及楼层)
let planeUpDownMesh = new THREE.Mesh(panel, upMaterial);
planeUpDownMesh.position.z = 505;
planeUpDownMesh.position.x = 210;
let planeFloorMesh = new THREE.Mesh(panelFloor, OneFloorMaterial);
planeFloorMesh.position.z = 505;
planeFloorMesh.position.x = 210;
planeFloorMesh.position.y = planeUpDownMesh.position.y - 200;
group.add(planeUpDownMesh);
group.add(planeFloorMesh);
_viewer_.addExternalObject(_name_, group);
_viewer_.render();
电梯指示牌由两个尺寸相同的PlaneBufferGeometry
作为基底,一个用于指示上下行,采用了两个箭头图片作为材质;另一个指示楼层信息,一共有七个楼层,采用七个数字图片作为材质,以便于切换楼层。
至此,组成电梯模型的各个部分均已经加入到场景中,下一步让电梯、上下行指示箭头和楼层信息动起来!
创建电梯动画
首先先从指示箭头入手,指示箭头指示电梯的上下行状态,默认是向上移动,它是由PlaneBufferGeometry
贴上材质得到的,如果想获取动画效果,就要不停地改变材质的offset
参数并同时渲染。在上一部分有这样一行代码window[name] = up;
作用是将箭头的材质存储到全局变量中,以便于外部修改它的offset
参数来实现动画。
// 定义移动速度
const SPEED = 0.04;
let mgr = viewer.getExternalComponentManager();
function animation() {
if (!window[_name_]) {
window[_name_] = up;
}
window[_name_].offset.y += SPEED * _direction_;
mgr.setTransform(_name_, _position_);
requestAnimationFrame(animation.bind(this));
viewer.render();
}
animation();
现在指示箭头可以向上移动了,但是电梯不是单向运行,下行时就要改变箭头的指向以及移动方向,这里就涉及到材质的动态替换了。为了实现更逼真的物理效果,这里引入了Tween.js
组件进行动画过渡。
import TWEEN from '../Tween.js'
let tween = new TWEEN.Tween(_position_)
.to({ z: height / 2 }, 10)
.onUpdate(onUpdate)
.onStart(onStart)
.onComplete(onComplete)
.start();
function onStart(object) {
console.log("start");
if (_target_floor_ - _current_floor_ < 0) {
// 下行时替换为向下的箭头并改变材质移动方向
_direction_ = GO_DOWN;
window[_name_] = downMaterial.map;
planeUpDownMesh.material = downMaterial;
} else {
_direction_ = GO_UP;
window[_name_] = upMaterial.map;
planeUpDownMesh.material = upMaterial;
}
};
电梯上下行动画已经解决,下一步让电梯的轿厢动起来,首先获取电梯的起始位置和到达位置,再通过Tween.js
实现过渡动画,模拟电梯平稳升降的过程。起始和到达位置可以通过按钮来模拟,以下代码是用于模拟电梯运动的数据,其中data-level
表示目标楼层,data-high
表示楼层高度:
let INTERVAL = 2000;
let list = document.getElementsByClassName(_domClass_);
for (let b = 0, len = list.length; b < len; b++) {
list[b].addEventListener("click", (e) => {
let val = list[b].getAttribute('data-high');
_target_floor_ = list[b].getAttribute('data-level');
// 根据电梯跨越的层数计算运行时间
_time_ = Math.abs(_target_floor_ - _current_floor_) * INTERVAL;
let _height = Number(val) + (height / 2);
tween = null;
tween = new TWEEN.Tween(_position_)
.to({ z: _height }, _time_)
.easing(TWEEN.Easing.Cubic.Out)
.onUpdate(onUpdate)
.onStart(onStart)
.onComplete(onComplete)
.start();
});
}
完成上述代码后,我们就可以通过按钮模拟电梯上下行的动画,同时箭头会根据电梯上下行自行调整到正确的指示和移动方向,但是还缺少切换楼层的步骤,当电梯从起始位置出发后,到达目标位置时,应该讲楼层展示为目标楼层,这一步和切换指示箭头方向的逻辑是一致的,通过动态修改材质实现,我们将这一步写在Tween.js
完成动画后的complete
事件回调函数中,当电梯停止后将材质修改为目标楼层的材质。
function onComplete(object) {
// 完成动画后,切换楼层文本
if (_direction_ < 0) {
_direction_ = -1;
window[_name_] = downMaterial.map;
planeUpDownMesh.material = downMaterial;
} else {
_direction_ = 1;
window[_name_] = upMaterial.map;
planeUpDownMesh.material = upMaterial;
}
_current_floor_ = _target_floor_;
//切换当前坐标
_position_.z = object.z;
//切换楼层
switch (_current_floor_) {
case 1:
planeFloorMesh.material = OneFloorMaterial;
break;
case 2:
planeFloorMesh.material = TwoFloorMaterial;
break;
case 3:
planeFloorMesh.material = ThreeFloorMaterial;
break;
case 4:
planeFloorMesh.material = FourFloorMaterial;
break;
case 5:
planeFloorMesh.material = FiveFloorMaterial;
break;
case 6:
planeFloorMesh.material = SixFloorMaterial;
break;
case -1:
planeFloorMesh.material = belowOneFloorMaterial;
break;
}
};
到这一步,关于电梯模型的创建以及动画的创建就完成了,但是驱动电梯运行的方式还是通过按钮来模拟的,下一步采用接入电梯物联网数据来代替按钮的方式,让IoT
实时数据驱动电梯运行。
物联网数据驱动电梯运行
这一部分依赖于websocket
连接实现,大概的思路就是后端微服务会提供socket
连接池,通过匹配ServerEndpoint
进行连接,每当有IoT
数据上报时,socket
连接就会向前端页面推送电梯运行数据,拿到这些数据后,在websocket
的接收消息的回调中处理数据,从而实现整个的数据驱动电梯的过程。下面调整一下代码,将按钮模拟电梯运行的代码重构下,放在websocket
的接收消息的回调中。
// 引入websocket代替上面的按钮事件
var socket;
socket = new WebSocket("ws://localhost:8087/websocket/0004/" + _id_);
socket.onopen = () => {
console.log("socket opened!");
}
// msg中包含电梯的IoT运行数据
socket.onmessage = (msg) => {
let _data = JSON.parse(msg.data);
let val = 0;
if (_data.data) {
let _iot_data = JSON.parse(_data.data);
if (_iot_data.hight >= 0 && _iot_data.direction >= 0) {
val = _iot_data.hight;
_target_floor_ = _iot_data.floor;
_time_ = Math.abs(_target_floor_ - _current_floor_) * INTERVAL;
let _height = Number(val) + (height / 2);
tween = null;
tween = new TWEEN.Tween(_position_)
.to({ z: _height }, _time_)
.easing(TWEEN.Easing.Cubic.Out)
.onUpdate(onUpdate)
.onStart(onStart)
.onComplete(onComplete)
.start();
}
}
}
socket.onclose = () => {
console.log("socket closed!");
}
socket.onerror = () => {
console.error("socket error!");
}
效果
在场景中创建两部电梯,一部位于一层,另一部位于二层,通过向websocket
后台微服务发送电梯实时IoT
数据实现驱动电梯效果。
总结
整个模拟真实电梯场景的过程主要由三个部分构成,首先通过形状BoxBufferGeometry
、PlaneBufferGeometry
和材质MeshPhongMaterial
、MeshBasicMaterial
创建出电梯并初始化在正确的位置;其次将动画应用于电梯的各个组成部分,主要是应用了Tween.js
以及requestAnimationFrame
;最后将电梯的物联网数据通过websocket
方式接入进来以便于驱动电梯运行。
地址: https://www.cnblogs.com/xhb-bky-blog/p/12819796.html
声明:本博客原创文字只代表本人工作中在某一时间内总结的观点或结论,与本人所在单位没有直接利益关系。非商业,未授权贴子请以现状保留,转载时必须保留此段声明,且在文章页面明显位置给出原文连接。