背景

在运维场景中,电梯作为运维环节重要的一部分是不可获缺的,如果能够在三维场景中,将逼真的电梯效果,包括外观、运行状态等表现出来,无疑是产品的一大亮点。本文将从无到有介绍如何在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. 【Java】用IDEA搭建源码阅读环境

    用IDEA搭建源码阅读环境 参考自CodeSheep的Mac源码环境搭建, https://www.bilibili.com/video/BV1V7411U78L 但是实际上在Windows搭建的差别 ...

  2. stand up meeting 12/3/2015

    part 组员 今日工作 工作耗时/h 明日计划 工作耗时/h UI 冯晓云 初始化弹窗的弹出位置并捕捉弹窗区域内的鼠标控制事件,初步解决弹窗的拖拽功能:    6 UWP对控件的支持各种看不懂,属性 ...

  3. Cobalt Strike系列教程第七章:提权与横向移动

    Cobalt Strike系列教程分享如约而至,新关注的小伙伴可以先回顾一下前面的内容: Cobalt Strike系列教程第一章:简介与安装 Cobalt Strike系列教程第二章:Beacon详 ...

  4. PHP函数:get_class()

    get_class()  -返回对象的类名 说明: get_class ([ object $object = NULL ] ) : string 参数: object:要测试的对象.如果在类里,此参 ...

  5. 简谈” Top K“

    Top K 快速选择和堆排序都可以求解 Kth Element 和 TopK Elements 问题. 题见215. Kth Largest Element in an Array (Medium) ...

  6. 一个可能是世界上最全的 API 接口集合库开源项目

    对于程序员来说,为自己的程序选择一些合适的API并不是那么简单,有时候还会把你搞得够呛,今天猿妹要和大家分享一个开源项目,这个项目汇集了各种开发的api,涵盖了音乐.新闻.书籍.日历等,无论你是从事W ...

  7. Python之numpy,pandas实践

    Jupyter Notebook(此前被称为 IPython notebook)是一个交互式笔记本,支持运行 40 多种编程语言. Jupyter Notebook 的本质是一个 Web 应用程序,便 ...

  8. [go]包和工程管理

    一.系统环境变量 GOROOT 指定go的安装目录,win是在 C\Go\,Linux在 /usr/local/go下,如果不是默认的目录,则需要指定 GOROOT环境变量,否则不需要 GOPATH ...

  9. PHP(ThinkPHP5.0) + PHPMailer 进行邮箱发送验证码

    GitHub下载最新版第三方类库PHPMailer: 第一步: 打开网址https://github.com/PHPMailer/PHPMailer/ 下载PHPMailer,PHPMailer 需要 ...

  10. ThinkPHP框架初步掌握

    为了帮老师用ThinkSNS二次开发一个微博系统,专门花了几天学习ThinkPHP框架,现在将一些ThinkPHP入门知识作以记录. 首先声明: 本文不是完全教程,只是将开发中碰到的问题作以总结,如果 ...