背景

在运维场景中,电梯作为运维环节重要的一部分是不可获缺的,如果能够在三维场景中,将逼真的电梯效果,包括外观、运行状态等表现出来,无疑是产品的一大亮点。本文将从无到有介绍如何在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表示楼层高度:

<div id="levels" style="position: absolute;left:125px;top:25px;width: 60%;height: 30px;">
<button class="fl" data-level="-1" data-high=-5200>B01</button>
<button class="fl" data-level="1" data-high=0>F01</button>
<button class="fl" data-level="2" data-high=4500>F02</button>
<button class="fl" data-level="3" data-high=8300>F03</button>
<button class="fl" data-level="4" data-high=12100>F04</button>
<button class="fl" data-level="5" data-high=15900>F05</button>
<button class="fl" data-level="6" data-high=19700>F06</button>
</div>
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数据实现驱动电梯效果。

总结

整个模拟真实电梯场景的过程主要由三个部分构成,首先通过形状BoxBufferGeometryPlaneBufferGeometry和材质MeshPhongMaterialMeshBasicMaterial创建出电梯并初始化在正确的位置;其次将动画应用于电梯的各个组成部分,主要是应用了Tween.js以及requestAnimationFrame;最后将电梯的物联网数据通过websocket方式接入进来以便于驱动电梯运行。

作者:悠扬的牧笛
地址:https://www.cnblogs.com/xhb-bky-blog/p/12819796.html
声明:本博客原创文字只代表本人工作中在某一时间内总结的观点或结论,与本人所在单位没有直接利益关系。非商业,未授权贴子请以现状保留,转载时必须保留此段声明,且在文章页面明显位置给出原文连接。

【BIM】BIMFACE中实现电梯实时动效的更多相关文章

  1. 新版MATERIAL DESIGN 官方动效指南(一)

    Google 刚发布了新版Material Design 官方动效指南,全文包括三个部分:为什么说动效很重要?如何制作优秀的Material Design动效及转场动画,动效的意义.新鲜热辣收好不谢! ...

  2. 前端读者 | 前端用户体验-UI动效设计

    本文来自互联网 @羯瑞 整理 UI动效现如今在 APP 和网页中几乎已经成为了基本的组成部分,经过仔细打磨的 UI动效对于整个界面的提升是显著的. 动效呈现出状态切换的过程,展现了元素之间的逻辑关系, ...

  3. Principle如何制作动效设计?简单易学的Principle动效设计教程

    Principle for Mac是一款新开发的交互设计软件.相比 Pixate 更容易上手,界面类似 Sketch 等做图软件,思路有点像用 Keynote 做动画,更「可视化」一些. 如果您还没有 ...

  4. Google I/O 官方应用中的动效设计

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/jILRvRTrc/article/details/82881743 作者:Nick Butcher, ...

  5. angular2中的路由转场动效

    1.为什么有的人路由转动效离场动效不生效? 自己研究发现是加动效的位置放错了  如下: <---! animate-state.component.html --> <div sty ...

  6. 【BIM】BIMFACE中创建疏散效果

    背景 在BIM运维中,消防疏散是不可或缺的一环,当发生火警的时候,触发烟感器发生报警,同时启动消防疏散,指导现场工作人员进行疏散,及时准确地显示出疏散路线对争取疏散时间尤为重要.我将介绍如何在bimf ...

  7. 【BIM】BIMFACE中创建雾化效果

    背景 在BIM运维场景初始化的时候,一般都是首先将整个运维对象呈现在用户面前,例如一座大厦.一座桥梁.一个园区等等,以便于用户进行总览,总体把握运维对象,如果这个宏大的场景边界过于清晰,与背景融合也不 ...

  8. 【BIM】BIMFACE中创建矢量文本[下篇]

    背景 在上一篇文章中,我们通过THREEJS创建了矢量文本,并添加到了BIMFACE场景中,但是仅仅加入到场景中并不是我们的目的,我们的目的是把这种矢量文本加到指定的构件或者空间上,以此标识该构件或空 ...

  9. Android 一个绚丽的loading动效分析与实现!

    http://blog.csdn.net/tianjian4592/article/details/44538605 前两天我们这边的头儿给我说,有个 gif 动效很不错,可以考虑用来做项目里的loa ...

随机推荐

  1. 使用 Chrome 插件 Vimium 打造黑客浏览器

    之前一直用 cVim,与 Vimium 功能类似,但是之后不在更新了,故转战到 Vimium. 简介 官网:http://vimium.github.io/ Vimium 是 Google Chrom ...

  2. GeoGebra案例(傅里叶级数的方波)

    傅里叶级数介绍:请移步参见这位马大佬的博文 https://www.matongxue.com/madocs/619.html 马大佬中的gif图在我看来是非常直观且有趣的,奈何本人本领实在有限,学习 ...

  3. 干货:python面对对象类继承简介

    前言 文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:python视觉算法 PS:如有需要Python学习资料的小伙伴可以加 ...

  4. stand up meeting 12-14

    今日更新: 项目的refactor部分均已经基本完成.答题界面和结果展示界面与code hunters team项目的merge部分也已经完成. 当然在这其中我们也遇到了一个小问题,在背单词模块中的词 ...

  5. .NET Core 发布时去掉多余的语言包文件夹

    用 .NET Core 3.x 作为目标框架时发布完之后,会发现多了很多语言包文件夹,类似于: 有时候,不想要生成这些语言包文件夹,需要稍微配置一下. 在 PropertyGroup 节点中添加如下的 ...

  6. 详解 通道 (Channel 接口)

    在本篇博文中,本人主要讲解NIO 的两个核心点 -- 缓冲区(Buffer) 和 通道 (Channel)之一的 缓冲区(Buffer), 有关NIO流的其他知识点请观看本人博文<详解 NIO流 ...

  7. 参数化parameterized

    pip install parameterized 注意:之前的nose-parameterized已经更新为parameterized库了 模块下测试方法直接使用parameterized impo ...

  8. [YII2] 展示页面显示图片 以及手机号隐藏为*和姓名隐藏姓为*,

  9. Java 网络编程 -- 基于TCP 模拟多用户登录

    Java TCP的基本操作参考前一篇:Java 网络编程 – 基于TCP实现文件上传 实现多用户操作之前先实现以下单用户操作,假设目前有一个用户: 账号:zs 密码:123 服务端: public c ...

  10. 架构设计 | 分布式业务系统中,全局ID生成策略

    本文源码:GitHub·点这里 || GitEE·点这里 一.全局ID简介 在实际的开发中,几乎所有的业务场景产生的数据,都需要一个唯一ID作为核心标识,用来流程化管理.比如常见的: 订单:order ...