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

背景

在運維場景中,電梯作為運維環節重要的一部分是不可獲缺的,如果能夠在三維場景中,將逼真的電梯效果,包括外觀、運行狀態等表現出來,無疑是產品的一大亮點。本文將從無到有介紹如何在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();

經過上述代碼的處理,就可以在場景中看見新創建的電梯的大概樣子了,效果如下:

elevator

目前電梯模型有了,但是為了能夠實時顯示電梯數據,我把電梯轎廂內的樓層指示牌放在電梯的外表面,以便於觀察當前電梯的狀態,如目前所在樓層、上下行等信息。

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方式接入進來以便於驅動電梯運行。

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

Tags: