1. 程式人生 > >【BIM】BIMFACE中實現電梯實時動效

【BIM】BIMFACE中實現電梯實時動效

## 背景 在運維場景中,電梯作為運維環節重要的一部分是不可獲缺的,如果能夠在三維場景中,將逼真的電梯效果,包括外觀、執行狀態等表現出來,無疑是產品的一大亮點。本文將從無到有介紹如何在[bimface](https://bimface.com/)中實現逼真的電梯執行效果,主要包括電梯模型的建立、電梯上下行和停靠樓層動畫的實現以及如何對接實時物聯網資料來驅動電梯模型執行。 ## 實踐 ### 建立電梯模型 首先建立一個立方體模型作為電梯,因為該電梯是外部構件,姑且稱之為外部電梯,運維場景中已經包含了電梯模型,這個電梯是建模期間就已經完成的,暫時稱之為內部電梯,用來為外部電梯提供起始位置資訊。基於以上前提來說說大概的思路方法,簡化的電梯實際上就是一個有寬高深的立方體,然後為立方體的每一個面貼上相應的材質,以便於區分電梯的頂部、正面和其他側面,然後把建立好的電梯作為外部構件加入到場景中正確的位置,那如何獲取正確的位置呢?可以獲取模型中的內部電梯的包圍盒資料,通過包圍盒資料計算出外部電梯的位置即可。 ```javascript 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(); ``` 經過上述程式碼的處理,就可以在場景中看見新建立的電梯的大概樣子了,效果如下: ![elevator](https://files-cdn.cnblogs.com/files/xhb-bky-blog/elevator01.bmp) 目前電梯模型有了,但是為了能夠實時顯示電梯資料,我把電梯轎廂內的樓層指示牌放在電梯的外表面,以便於觀察當前電梯的狀態,如目前所在樓層、上下行等資訊。 ```javascript 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作為基底,一個用於指示上下行,採用了兩個箭頭圖片作為材質;另一個指示樓層資訊,一共有七個樓層,採用七個數字圖片作為材質,以便於切換樓層。 ![電梯](https://files-cdn.cnblogs.com/files/xhb-bky-blog/elevator02.bmp) 至此,組成電梯模型的各個部分均已經加入到場景中,下一步讓電梯、上下行指示箭頭和樓層資訊動起來! ### 建立電梯動畫 首先先從指示箭頭入手,指示箭頭指示電梯的上下行狀態,預設是向上移動,它是由PlaneBufferGeometry
貼上材質得到的,如果想獲取動畫效果,就要不停地改變材質的offset引數並同時渲染。在上一部分有這樣一行程式碼window[_name_] = up;作用是將箭頭的材質儲存到全域性變數中,以便於外部修改它的offset引數來實現動畫。 ```javascript // 定義移動速度 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(); ``` ![](https://blog-static.cnblogs.com/files/xhb-bky-blog/elevator03.gif) 現在指示箭頭可以向上移動了,但是電梯不是單向執行,下行時就要改變箭頭的指向以及移動方向,這裡就涉及到材質的動態替換了。為了實現更逼真的物理效果,這裡引入了Tween.js
元件進行動畫過渡。 ```javascript 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表示樓層高度: ```html ``` ```javascript 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事件回撥函式中,當電梯停止後將材質修改為目標樓層的材質。 ```javascript 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的接收訊息的回撥中。 ```javascript // 引入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資料實現驅動電梯效果。 ![](https://blog-static.cnblogs.com/files/xhb-bky-blog/elevator04.gif) ## 總結 整個模擬真實電梯場景的過程主要由三個部分構成,首先通過形狀BoxBufferGeometryPlaneBufferGeometry和材質MeshPhongMaterialMeshBasicMaterial創建出電梯並初始化在正確的位置;其次將動畫應用於電梯的各個組成部分,主要是應用了Tween.js以及requestAnimationFrame;最後將電梯的物聯網資料通過websocket方式接入進來以便於驅動電梯執行。

作者:悠揚的牧笛
地址:https://www.cnblogs.com/xhb-bky-blog/p/12819796.html
宣告:本部落格原創文字只代表本人工作中在某一時間內總結的觀點或結論,與本人所在單位沒有直接利益關係。非商業,未授權貼子請以現狀保留,轉載時必須保留此段宣告,且在文章頁面明顯位置給出原文連