背景

在运维场景中,电梯作为运维环节重要的一部分是不可获缺的,如果能够在三维场景中,将逼真的电梯效果,包括外观、运行状态等表现出来,无疑是产品的一大亮点。本文将从无到有介绍如何在bimface中实现逼真的电梯运行效果,主要包括电梯模型的创建、电梯上下行和停靠楼层动画的实现以及如何对接实时物联网数据来驱动电梯模型运行。

实践

创建电梯模型

首先创建一个立方体模型作为电梯,因为该电梯是外部构件,姑且称之为外部电梯,运维场景中已经包含了电梯模型,这个电梯是建模期间就已经完成的,暂时称之为内部电梯,用来为外部电梯提供起始位置信息。基于以上前提来说说大概的思路方法,简化的电梯实际上就是一个有宽高深的立方体,然后为立方体的每一个面贴上相应的材质,以便于区分电梯的顶部、正面和其他侧面,然后把创建好的电梯作为外部构件加入到场景中正确的位置,那如何获取正确的位置呢?可以获取模型中的内部电梯的包围盒数据,通过包围盒数据计算出外部电梯的位置即可。

  1. let width = 1200, height = 2600, depth = 1000;
  2. let elevatorGeometry = new THREE.BoxBufferGeometry(width, height, depth);
  3. let group = new THREE.Group();
  4. // 电梯侧面材质
  5. let othersMaterial = new THREE.MeshPhongMaterial();
  6. // 电梯顶部材质
  7. let topMaterial = new THREE.MeshPhongMaterial();
  8. // 电梯正面材质
  9. let frontMaterial = new THREE.MeshPhongMaterial();
  10. let loader = new THREE.TextureLoader();
  11. loader.setCrossOrigin("Anonymous");
  12. let others = loader.load('images/basic.png', function (map) {
  13. othersMaterial.map = map;
  14. othersMaterial.wireframe = false;
  15. othersMaterial.needsUpdate = true;
  16. });
  17. let top = loader.load('images/top.png', function (map) {
  18. topMaterial.map = map;
  19. topMaterial.wireframe = false;
  20. topMaterial.needsUpdate = true;
  21. });
  22. let front = loader.load('images/front.png', function (map) {
  23. frontMaterial.map = map;
  24. frontMaterial.wireframe = false;
  25. frontMaterial.needsUpdate = true;
  26. });
  27. let elevatorMaterials = [othersMaterial, othersMaterial, topMaterial, othersMaterial, frontMaterial, othersMaterial];
  28. let elevatorMesh = new THREE.Mesh(elevatorGeometry, elevatorMaterials);
  29. // 调整位置,使模型中电梯构件包含外部电梯Mesh
  30. group.add(elevatorMesh);
  31. group.rotation.x = Math.PI / 2;
  32. // _position是根据模型中的电梯计算得出,从外部传入
  33. group.position.set(_position);
  34. group.updateMatrixWorld();
  35. _viewer_.addExternalObject(_name_, group);
  36. _viewer_.render();

经过上述代码的处理,就可以在场景中看见新创建的电梯的大概样子了,效果如下:

目前电梯模型有了,但是为了能够实时显示电梯数据,我把电梯轿厢内的楼层指示牌放在电梯的外表面,以便于观察当前电梯的状态,如目前所在楼层、上下行等信息。

  1. let panelWidth = 200, panelHeight = 200, segments = 100;
  2. // 指示上下行的箭头
  3. let panel = new THREE.PlaneBufferGeometry(panelWidth, panelHeight, segments, segments);
  4. // 指示楼层
  5. let panelFloor = new THREE.PlaneBufferGeometry(panelWidth, panelHeight, segments, segments);
  6. // 定义各个楼层的材质
  7. let belowOneFloorMaterial = new THREE.MeshBasicMaterial();
  8. let OneFloorMaterial = new THREE.MeshBasicMaterial();
  9. let TwoFloorMaterial = new THREE.MeshBasicMaterial();
  10. let ThreeFloorMaterial = new THREE.MeshBasicMaterial();
  11. let FourFloorMaterial = new THREE.MeshBasicMaterial();
  12. let FiveFloorMaterial = new THREE.MeshBasicMaterial();
  13. let SixFloorMaterial = new THREE.MeshBasicMaterial();
  14. // 加载材质
  15. let up = loader.load('images/ele_up.png', function (map) {
  16. upMaterial.map = map;
  17. upMaterial.wireframe = false;
  18. upMaterial.needsUpdate = true;
  19. });
  20. up.wrapS = THREE.RepeatWrapping;
  21. up.wrapT = THREE.RepeatWrapping;
  22. up.repeat.y = 1;
  23. window[_name_] = up;
  24. let down = loader.load('images/ele_down.png', function (map) {
  25. downMaterial.map = map;
  26. downMaterial.wireframe = false;
  27. downMaterial.needsUpdate = true;
  28. });
  29. down.wrapS = THREE.RepeatWrapping;
  30. down.wrapT = THREE.RepeatWrapping;
  31. down.repeat.y = 1;
  32. let pathList = [];
  33. pathList.push({ role: belowOneFloorMaterial, path: 'images/Digit/-1F.png' });
  34. pathList.push({ role: OneFloorMaterial, path: 'images/Digit/1F.png' });
  35. pathList.push({ role: TwoFloorMaterial, path: 'images/Digit/2F.png' });
  36. pathList.push({ role: ThreeFloorMaterial, path: 'images/Digit/3F.png' });
  37. pathList.push({ role: FourFloorMaterial, path: 'images/Digit/4F.png' });
  38. pathList.push({ role: FiveFloorMaterial, path: 'images/Digit/5F.png' });
  39. pathList.push({ role: SixFloorMaterial, path: 'images/Digit/6F.png' });
  40. const buildMaterials = (item) => {
  41. return new Promise((resolve, reject) => {
  42. loader.load(item.path, function (map) {
  43. item.role.map = map;
  44. item.role.wireframe = false;
  45. item.role.needsUpdate = true;
  46. });
  47. });
  48. }
  49. for (let i = 0; i < pathList.length; i++) {
  50. buildMaterials(pathList[i]);
  51. }
  52. // 创建楼层信息面板(上下行指示箭头以及楼层)
  53. let planeUpDownMesh = new THREE.Mesh(panel, upMaterial);
  54. planeUpDownMesh.position.z = 505;
  55. planeUpDownMesh.position.x = 210;
  56. let planeFloorMesh = new THREE.Mesh(panelFloor, OneFloorMaterial);
  57. planeFloorMesh.position.z = 505;
  58. planeFloorMesh.position.x = 210;
  59. planeFloorMesh.position.y = planeUpDownMesh.position.y - 200;
  60. group.add(planeUpDownMesh);
  61. group.add(planeFloorMesh);
  62. _viewer_.addExternalObject(_name_, group);
  63. _viewer_.render();

电梯指示牌由两个尺寸相同的PlaneBufferGeometry作为基底,一个用于指示上下行,采用了两个箭头图片作为材质;另一个指示楼层信息,一共有七个楼层,采用七个数字图片作为材质,以便于切换楼层。

至此,组成电梯模型的各个部分均已经加入到场景中,下一步让电梯、上下行指示箭头和楼层信息动起来!

创建电梯动画

首先先从指示箭头入手,指示箭头指示电梯的上下行状态,默认是向上移动,它是由PlaneBufferGeometry贴上材质得到的,如果想获取动画效果,就要不停地改变材质的offset参数并同时渲染。在上一部分有这样一行代码window[name] = up;作用是将箭头的材质存储到全局变量中,以便于外部修改它的offset参数来实现动画。

  1. // 定义移动速度
  2. const SPEED = 0.04;
  3. let mgr = viewer.getExternalComponentManager();
  4. function animation() {
  5. if (!window[_name_]) {
  6. window[_name_] = up;
  7. }
  8. window[_name_].offset.y += SPEED * _direction_;
  9. mgr.setTransform(_name_, _position_);
  10. requestAnimationFrame(animation.bind(this));
  11. viewer.render();
  12. }
  13. animation();

现在指示箭头可以向上移动了,但是电梯不是单向运行,下行时就要改变箭头的指向以及移动方向,这里就涉及到材质的动态替换了。为了实现更逼真的物理效果,这里引入了Tween.js组件进行动画过渡。

  1. import TWEEN from '../Tween.js'
  2. let tween = new TWEEN.Tween(_position_)
  3. .to({ z: height / 2 }, 10)
  4. .onUpdate(onUpdate)
  5. .onStart(onStart)
  6. .onComplete(onComplete)
  7. .start();
  8. function onStart(object) {
  9. console.log("start");
  10. if (_target_floor_ - _current_floor_ < 0) {
  11. // 下行时替换为向下的箭头并改变材质移动方向
  12. _direction_ = GO_DOWN;
  13. window[_name_] = downMaterial.map;
  14. planeUpDownMesh.material = downMaterial;
  15. } else {
  16. _direction_ = GO_UP;
  17. window[_name_] = upMaterial.map;
  18. planeUpDownMesh.material = upMaterial;
  19. }
  20. };

电梯上下行动画已经解决,下一步让电梯的轿厢动起来,首先获取电梯的起始位置和到达位置,再通过Tween.js实现过渡动画,模拟电梯平稳升降的过程。起始和到达位置可以通过按钮来模拟,以下代码是用于模拟电梯运动的数据,其中data-level表示目标楼层,data-high表示楼层高度:

  1. <div id="levels" style="position: absolute;left:125px;top:25px;width: 60%;height: 30px;">
  2. <button class="fl" data-level="-1" data-high=-5200>B01</button>
  3. <button class="fl" data-level="1" data-high=0>F01</button>
  4. <button class="fl" data-level="2" data-high=4500>F02</button>
  5. <button class="fl" data-level="3" data-high=8300>F03</button>
  6. <button class="fl" data-level="4" data-high=12100>F04</button>
  7. <button class="fl" data-level="5" data-high=15900>F05</button>
  8. <button class="fl" data-level="6" data-high=19700>F06</button>
  9. </div>
  1. let INTERVAL = 2000;
  2. let list = document.getElementsByClassName(_domClass_);
  3. for (let b = 0, len = list.length; b < len; b++) {
  4. list[b].addEventListener("click", (e) => {
  5. let val = list[b].getAttribute('data-high');
  6. _target_floor_ = list[b].getAttribute('data-level');
  7. // 根据电梯跨越的层数计算运行时间
  8. _time_ = Math.abs(_target_floor_ - _current_floor_) * INTERVAL;
  9. let _height = Number(val) + (height / 2);
  10. tween = null;
  11. tween = new TWEEN.Tween(_position_)
  12. .to({ z: _height }, _time_)
  13. .easing(TWEEN.Easing.Cubic.Out)
  14. .onUpdate(onUpdate)
  15. .onStart(onStart)
  16. .onComplete(onComplete)
  17. .start();
  18. });
  19. }

完成上述代码后,我们就可以通过按钮模拟电梯上下行的动画,同时箭头会根据电梯上下行自行调整到正确的指示和移动方向,但是还缺少切换楼层的步骤,当电梯从起始位置出发后,到达目标位置时,应该讲楼层展示为目标楼层,这一步和切换指示箭头方向的逻辑是一致的,通过动态修改材质实现,我们将这一步写在Tween.js完成动画后的complete事件回调函数中,当电梯停止后将材质修改为目标楼层的材质。

  1. function onComplete(object) {
  2. // 完成动画后,切换楼层文本
  3. if (_direction_ < 0) {
  4. _direction_ = -1;
  5. window[_name_] = downMaterial.map;
  6. planeUpDownMesh.material = downMaterial;
  7. } else {
  8. _direction_ = 1;
  9. window[_name_] = upMaterial.map;
  10. planeUpDownMesh.material = upMaterial;
  11. }
  12. _current_floor_ = _target_floor_;
  13. //切换当前坐标
  14. _position_.z = object.z;
  15. //切换楼层
  16. switch (_current_floor_) {
  17. case 1:
  18. planeFloorMesh.material = OneFloorMaterial;
  19. break;
  20. case 2:
  21. planeFloorMesh.material = TwoFloorMaterial;
  22. break;
  23. case 3:
  24. planeFloorMesh.material = ThreeFloorMaterial;
  25. break;
  26. case 4:
  27. planeFloorMesh.material = FourFloorMaterial;
  28. break;
  29. case 5:
  30. planeFloorMesh.material = FiveFloorMaterial;
  31. break;
  32. case 6:
  33. planeFloorMesh.material = SixFloorMaterial;
  34. break;
  35. case -1:
  36. planeFloorMesh.material = belowOneFloorMaterial;
  37. break;
  38. }
  39. };

到这一步,关于电梯模型的创建以及动画的创建就完成了,但是驱动电梯运行的方式还是通过按钮来模拟的,下一步采用接入电梯物联网数据来代替按钮的方式,让IoT实时数据驱动电梯运行。

物联网数据驱动电梯运行

这一部分依赖于websocket连接实现,大概的思路就是后端微服务会提供socket连接池,通过匹配ServerEndpoint进行连接,每当有IoT数据上报时,socket连接就会向前端页面推送电梯运行数据,拿到这些数据后,在websocket的接收消息的回调中处理数据,从而实现整个的数据驱动电梯的过程。下面调整一下代码,将按钮模拟电梯运行的代码重构下,放在websocket的接收消息的回调中。

  1. // 引入websocket代替上面的按钮事件
  2. var socket;
  3. socket = new WebSocket("ws://localhost:8087/websocket/0004/" + _id_);
  4. socket.onopen = () => {
  5. console.log("socket opened!");
  6. }
  7. // msg中包含电梯的IoT运行数据
  8. socket.onmessage = (msg) => {
  9. let _data = JSON.parse(msg.data);
  10. let val = 0;
  11. if (_data.data) {
  12. let _iot_data = JSON.parse(_data.data);
  13. if (_iot_data.hight >= 0 && _iot_data.direction >= 0) {
  14. val = _iot_data.hight;
  15. _target_floor_ = _iot_data.floor;
  16. _time_ = Math.abs(_target_floor_ - _current_floor_) * INTERVAL;
  17. let _height = Number(val) + (height / 2);
  18. tween = null;
  19. tween = new TWEEN.Tween(_position_)
  20. .to({ z: _height }, _time_)
  21. .easing(TWEEN.Easing.Cubic.Out)
  22. .onUpdate(onUpdate)
  23. .onStart(onStart)
  24. .onComplete(onComplete)
  25. .start();
  26. }
  27. }
  28. }
  29. socket.onclose = () => {
  30. console.log("socket closed!");
  31. }
  32. socket.onerror = () => {
  33. console.error("socket error!");
  34. }

效果

在场景中创建两部电梯,一部位于一层,另一部位于二层,通过向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. python操作MySQL数据库报错问题解决

    编写好Python操作数据库的脚本后,运行报错如下: 报错1:“AttributeError: 'NoneType' object has no attribute 'encoding'” 解决办法: ...

  2. nodejs一些比较实用的命令

    在学习node的时候是从express开始的,在express中有一个generate,如果在机器上面全局的安装了express-generate的话,可以直接实用[express project_n ...

  3. sws_接口自动化_demo

    登录接口获取token: import requests import json def get_token(username, password): host = "https://sws ...

  4. s2h-HTTP Status 404 - No result defined for action and result input错误解决

    今天做个小项目,用的是ssh,结果在运行的时候出现HTTP Status 404 - No result defined for action and result input的错误. 首先认真检查所 ...

  5. Java 多线程实现方式二:实现 Runnable 接口

    由于java是单继承,很多时候为了实现多线程 通过继承 Thread 类后,就不能再继承其他类了.为了方便可以通过实现 Runnable 接口来实现,和Tread 类似需要重写run 方法. 下面通过 ...

  6. thinkphp--多表查询

    我们可以将两个表连起来一起查询数据,我现在有两张表,一个是feedback表和member表,如图: 总目录: 上代码: $where = array(); $"; $Model = M(' ...

  7. python学习08排序算法举例

    '''''''''排序算法:前提是所有数按照从小到大的顺序排列.1.冒泡算法将第一数与第二个数比较大小,如果第一个数比第二个数大,则沉底(交换位置,使大数在小数后面,这个过程类似于大泡沉底的过程) ' ...

  8. SpringBoot应用操作Rabbitmq

    记录RabbitMQ的简单应用 1.springboot项目中引入maven包,也是springboot官方的插件 <dependency> <groupId>org.spri ...

  9. poj_2393 Yogurt factory 贪心

    Yogurt factory Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 16669   Accepted: 8176 D ...

  10. http 之 CORS简介

    什么是CORS? CORS:跨域资源共享.是一种机制. 用处? 它使用额外的 HTTP 头来告诉浏览器  让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定 ...