基於 HTML5 WebGL 的 3D 工控裙房系統

前言

工業物聯網在中國的發展如火如荼,網絡基礎設施建設,以及工業升級的迫切需要都爲工業物聯網發展提供了很大的機遇。中國工業物聯網企業目前呈現兩種發展形式並存狀況:一方面是大型通訊、IT企業的佈局;一方面是傳統工業軟件和工業網絡企業自發地延伸,由產品提供商發展爲方案供應商。什麼叫做裙房?裙房是指附屬於主高樓並與之連成一體的低層建築。本文的 Demo 是針對於裙房做的,但是在工業監控系統中有很多雷同的部分,比如動畫、點擊切換、點擊隱藏、故障展示、開關、數據展示等等,都是比較通用的一些功能。所以針對這個 Demo 將這些內容做一個記錄,在這個 Demo 中我也遇到了一些問題,如何解決的都會拿出來跟大家分享。

http://www.hightopo.com/demo/annexMonitor/

代碼實現

整個 Demo 我們要實現的部分有:

  • 3D 場景的搭建
  • 場景中葉輪的轉動
  • 容器水位的上升下降
  • 葉輪轉動故障展示
  • 開啓/關閉動畫
  • 燈光的開啓/關閉
  • 點擊切換模型
  • 點擊隱藏/顯示屬性窗口
  • 文本內容/顏色變換

上面的功能看起來蠻多的,實際上實現起來還是比較容易的,總共就用了 200+ 行的代碼。

3D 場景搭建

 三維場景地基的搭建就 2 行代碼:

var g3d = new ht.graph3d.Graph3dView();// Hightopo 的 3D 組件(三維場景地基)
g3d.addToDOM();// 將 3D 組將添加到 body 體中

HT 的組件一般都會嵌入 BorderPane(https://hightopo.com/guide/guide/core/borderpane/ht-borderpane-guide.html)、SplitView(https://hightopo.com/guide/guide/core/splitview/ht-splitview-guide.html) 和 TabView(https://hightopo.com/guide/guide/core/tabview/ht-tabview-guide.html) 等容器中使用,而最外層的 HT 組件則需要用戶手工將 getView() 返回的底層 div 元素添加到頁面的 DOM 元素中,這裏需要注意的是,當父容器大小變化時,如果父容器是 BorderPane 和 SplitView 等這些HT預定義的容器組件,則 HT 的容器會自動遞歸調用孩子組件 invalidate 函數通知更新。但如果父容器是原生的 html 元素, 則 HT 組件無法獲知需要更新,因此最外層的 HT 組件一般需要監聽 window 的窗口大小變化事件,調用最外層組件 invalidate 函數進行更新。

爲了最外層組件加載填充滿窗口的方便性,HT 的所有組件都有 addToDOM 函數,其實現邏輯如下,其中 iv 是 invalidate 的簡寫:

addToDOM = function(){
    var self = this,
        view = self.getView(),//獲取組件的底層 div
        style = view.style;
    document.body.appendChild(view);//將組件底層div添加進body中
    style.left = '0';//ht 默認將所有的組件的position都設置爲absolute絕對定位
    style.right = '0';
    style.top = '0';
    style.bottom = '0';
    window.addEventListener('resize', function () { self.iv(); }, false);//窗口大小改變事件,調用刷新函數
}

接下來我們要向場景中添加各種模型,用代碼生成模型是非常無敵痛苦的,我們將整個場景的模型都放到一個 JSON 文件中,並通過 ht.Default.xhrLoad 方法將這個 JSON 轉換爲 3D 場景顯示在界面上:

var dm = g3d.dm();// 獲取 HT 3D 組件的數據容器
ht.Default.xhrLoad('scenes/system.json', function(text) {
    dm.deserialize(text);// 將函數的 text(json)參數傳給 deserialize 反序列化方法,可將 json 內容中的元素添加到 dataModel 數據容器中進行顯示
}

 ht.Default.xhrLoad 方法是一個異步加載 json 文件的方法,第一個參數爲傳入的 json 文件,路徑是相對於 html 文件的,第二個參數是回調函數,在傳入的 json 文件解析完畢之後做的操作。此方法爲異步加載,因此需要對 dm 數據容器中的數據進行獲取或操作的話,需要將獲取/操作的代碼寫在 dm.deserialize(text) 方法之後,因爲此時 dm 數據容器中才有節點。

上面將 JSON 文件發序列化到 dm 數據容器中後界面顯示如下:

上圖中整個場景的背景是我後期用代碼添加的,通過前面的 addToDOM 函數可以知道我們可以通過 getView 方法獲取 HT 3D 組件的底層 div,因此要在此 div 上添加一張背景圖也就不難了。剩下的 3D 模型部分都是由 JSON 反序列化出來的。

場景中葉輪的轉動

當然轉動不可能是整個模型在轉動,而是中間的“滾輪”在轉動,這要求設計師在創建模型的時候就將這個部分分離出來,然後我給此部分設置 tag 唯一標識爲“yelun”,通過 dm.getDataByTag('yelun') 即可獲取到這個節點,然後給這個節點設置旋轉動畫。

 HT 中調度進行的流程是,先通過 DataModel(https://hightopo.com/guide/guide/core/datamodel/ht-datamodel-guide.html) 添加調度任務,DataModel 會在調度任務指定的時間間隔到達時, 遍歷 DataModel 所有圖元回調調度任務的 action 函數,可在該函數中對傳入的 Data 圖元做相應的屬性修改以達到動畫效果。

根據上面對調度任務的說明,我們瞭解到向 dm 數據容器中添加調度任務會遍歷整個數據容器,數據容器中內容不多的時候可能感覺不到,但當數據容器中內容多且模型重的情況下,對 dm 數據容器進行過濾就非常有必要了,而且如果添加多個調度任務都遍歷了整個數據容器,那麼對電腦的性能要求可想而知。我一開始使用的時候就是遺漏了對 dm 數據容器的過濾,因爲場景不大,所以一開始沒有感覺,後來加了燈光後很重,就立馬出現問題了,但是一直找不到原因,後來在高人指點下才發下遺漏了對 data 的過濾判斷。

因此,調度任務傳入的參數對象中 action 方法傳入了一個 data 值,用於設置當前動畫的對象,不是此對象的直接可以 return 掉,不做任何操作:

var task = [];
var yelun = dm.getDataByTag('yelun');// 獲取 tag 爲 yelun 的節點
// 創建一個動畫調度任務
task.yelunTask = {
    interval: 100,// 動畫持續時間
    action: function(data) {// 動畫內容
        if (data !== yelun) return;
        // 設置 yelun 節點的 x 軸旋轉爲當前 x 軸旋轉值再加上 Math.PI/12
        yelun.setRotationX(yelun.getRotationX() + Math.PI/12);
    }
}
dm.addScheduleTask(task.yelunTask);// 將調度任務添加到數據容器中

容器水位的上升下降

這裏將容器水位的上升下降放到一個動畫調度任務裏了,也就是說通過 dm 數據容器操作這個調度任務就能夠同時操作這兩個部分的動畫,將上一小節中的 yelunTask 調度任務的 action 更改一下,因爲上面的代碼只對 yelun 節點進行了操作,我們需要對裝水的容器也進行操作。首先獲取裝水的容器,這裏將這個節點的唯一標識 tag 設置爲“cylinder”:

var cylinder = dm.getDataByTag('cylinder');

然後更改調度中的 action 部分代碼:

action: function(data) {
    if (!(data === yelun || data === cylinder)) return;
    // 葉輪轉動
    yelun.setRotationX(yelun.getRotationX() + Math.PI/12);

    // 容器水位變化
    if (cylinder.getTall() === 100) {
        cylinder.setTall(0);// 容器水位高度到達 100 的值時,重置爲 0 
    }
    else cylinder.setTall(cylinder.getTall() + 1);
}

葉輪轉動故障展示

因爲沒有數據的傳輸,所以這邊故障信息我只能自己造假數據了,我創建了一個 10 以內的整數隨機數,判斷這個值是否爲 1,如果爲 1 就將運作正常的圖標變換成告警圖標,同時我還通過這個值來設置 dm 數據容器添加/移除調度任務來控制當前葉輪轉動/停止、容器水位變化與否:

var alarm = dm.getDataByTag('alarm');// 獲取告警圖標節點
setInterval(function() {
    var random = Math.floor(Math.random()*5);
    if (random === 1) {
        alarm.s('shape3d.image', 'symbols/電信/故障 2.json');// 設置告警圖標節點爲“故障”圖標
        dm.removeScheduleTask(task.yelunTask);// 將葉輪的動畫加上
    }
    else {
        alarm.s('shape3d.image', 'symbols/電信/正常 2.json');// 設置告警圖標節點爲“正常”圖標
        dm.addScheduleTask(task.yelunTask);// 移除葉輪的動畫
    }
}, 1000);

開啓/關閉動畫

上一小節我們已經提到了開啓/關閉動畫的方式,這邊我們運用 form 表單,手動操作動畫的開啓和關閉(注:這裏只說明第一行的“水流開關”)。

首先,我們需要創建一個 formPane 表單組件(https://hightopo.com/guide/guide/plugin/form/ht-form-guide.html),在這個表單組件中添加行數據,這邊操作動畫的開啓和關閉我是用的 checkbox,值變化只有 true 和 false,這個情況用這個是比較優的選擇。然後通過監聽這個 checkbox 的值的變化事件,設置動畫的開啓(添加)或者關閉(移除)。

function createForm(task) {
    var form = new ht.widget.FormPane();// 創建 form 表單組件對象
    form.setWidth(160);// 設置表單組件的寬度
    form.setHeight(90);// 設置表單組件的高度
    // 設置表單組件底層 div 的樣式屬性
    form.getView().style.right = '10px';
    form.getView().style.top = '10px';
    form.getView().style.background = 'rgba(255, 255, 255, 0.2)';
    form.getView().style.borderRadius = '5px';
    document.body.appendChild(form.getView());// 將 form 表單底層 div 添加到 body 體中

    // 水閥開啓和關閉
    form.addRow([// 給 form 表單添加一行數據
        {
            checkBox: {// 複選框類,HT 將此封裝到 form 中 實際上創建了一個 ht.widget.CheckBox 組件
                label: '水流開關',// 設置 checkbox 的文本內容
                labelColor: '#fff',// 設置 checkbox 文本顏色
                selected: true,// 設置此 checkbox 是否選中
                onValueChanged: function() {// 監聽值變化事件
                    if (this.isSelected()) dm.addScheduleTask(task.arrowTask);// 如果這個 checkBox 選中,則添加動畫(開啓水閥)
                    else dm.removeScheduleTask(task.arrowTask);// 如果這個 checkBox 未被選中,則移除動畫(關閉水閥)
                }
            }
        }
    ], [0.1]);// 設置這個行數據中列的寬度
    return form;
}

addRow 方法上面代碼中一言兩語解釋不清楚,參考如下說明:

addRow(items, widths, height, params) 添加一行組件

  • items爲元素數組,元素可爲字符串、json 格式描述的組件參數信息、html 元素或者爲 null 的空
  • widths 爲每個元素寬度信息數組,寬度值大於 1 代表固定絕對值,小於等於 1 代表相對值,也可爲 80+0.3 的組合
  • height 爲行高信息,值大於 1 代表固定絕對值,小於等於 1 代表相對值,也可爲 80+0.3 的組合,爲空時採用默認行高
  • params 爲 json 格式的額外參數,例如插入行索引以及行邊框或背景顏色等,如{index: 2, background: 'yellow', borderColor: 'red'}

上面代碼中提到的 arrowTask 是對場景中的“箭頭”流動添加的動畫調度任務,通過控制 form 表單中 checkbox 複選框是否選中可直接操作 dm 是否添加/移除動畫調度任務。

燈光的開啓/關閉

控制燈光的開啓和關閉,這裏也是通過 form 表單上的 checkbox 複選框來進行操作的。一般建議不要使用燈光,渲染太燒性能了,這裏只是爲了效果而添加做一個說明。

首先我們需要創建一個“燈”節點,然後通過設置樣式屬性 setStyle 來設置燈的類型、顏色、燈照範圍等等屬性:

// 添加燈光
var light = new ht.Light();// 創建一個燈節點(繼承於 ht.Node) (https://hightopo.com/guide/guide/core/lighting/ht-lighting-guide.html)
light.p3([15, 120, 50]);// 設置此節點的位置
light.setTag('light');// 設置此節點的唯一標識
dm.add(light);// 將此節點添加到 dm 數據容器中進行顯示
light.s({// 設置此節點的樣式屬性 setStyle  簡寫爲 s
    'light.type': 'point',// 設置燈類型
    'light.color': 'rgb(252,252,149)',// 設置燈顏色
    'light.range': 1400,// 設置燈照範圍
    '3d.visible': false// 設置此節點在 3d 上不可見
});

 

然後在 form 表單上添加一行用來控制燈的開關、燈的顏色燈功能:

// 9、燈光開啓和關閉 以及顏色切換
form.addRow([// form 中添加一行
    {
        id: 'lightDisabled',// 設置此項的 id 值,可通過 form.getItemById 獲取此項
        checkBox: {// 複選框組件
            label: '開關燈',// 設置複選框文本內容
            labelColor: '#fff',// 設置複選框文本顏色
            selected: true,// 設置複選框是否選中
            onValueChanged: function() {// 監聽值變化事件
                dm.getDataByTag('light').s('light.disabled', !this.getValue());// 獲取燈節點並設置是否關閉燈光效果,light.disabled 屬性默認爲false,可設置爲true關閉燈效果
            }
        }
    },
    {
        colorPicker: {// 顏色選擇器組件
            value: 'rgb(252,252,149)',// 設置當前值
            instant: true,// 設置是否處於即時狀態,將會實時改變模型值
            onValueChanged: function() {// 監聽值變化事件
                dm.getDataByTag('light').s('light.color', this.getValue())// 設置燈的顏色爲當前選中的顏色
            }
        }
    }
], [0.1, 0.1]);

點擊切換模型

 HT 將事件監聽封裝到 mi 事件(https://hightopo.com/guide/guide/core/3d/ht-3d-guide.html#ref_interactionlistener)中,mi 方法中有多種事件,這裏我們需要的是單擊節點的事件監聽 clickData 事件,通過判斷事件類型 e.kind 是否爲 clickData,之後對節點的設置模型即可:

var waterPump6 = dm.getDataByTag('水泵06');// 獲取 tag 爲“水泵06”的節點
waterPump6.s({// 設置該節點的樣式屬性
    'note': '點我切換模型',// 設置標註文字內容
    'note.transparent': true,// 設置標註在 3D 下是否透明
    'note.t3': [0, 0, -50],// 設置標註在 3D 下的偏移
    'note.reverse.flip': true//設置標註背面是否顯示正面的內容
});
g3d.mi(function(e) {// 監聽 3D 組件上的事件
    if(e.kind === 'clickData') {// 點擊節點事件
        // 模型點擊切換
        if (e.data === waterPump6 && e.data.s('shape3d') === 'models/裙房系統/水泵.json') e.data.s('shape3d', 'models/fengji.json');// 設置點擊節點的 shape3d 樣式屬性
        else if (e.data === waterPump6 && e.data.s('shape3d') === 'models/fengji.json') e.data.s('shape3d', 'models/裙房系統/水泵.json');// 設置點擊節點的 shape3d 樣式屬性
    }
});

 HT 設置模型是通過設置節點的樣式屬性 node.setStyle(簡寫爲 node.s)爲 shape3d 來實現的。

點擊隱藏/顯示屬性窗口

上面說到了事件的監聽,既然同爲點擊事件,我們就在一個監聽事件裏面進行具體的操作即可,在上面的 if (e.kind === 'clickData') 判斷中添加顯示/隱藏屬性窗口的邏輯:

var waterPump5 = dm.getDataByTag('水泵05');
waterPump6.s({
    'note': '點我切換模型',
    'note.transparent': true,
    'note.t3': [0, 0, -50],
    'note.reverse.flip': true
});
g3d.mi(function(e) {
    if(e.kind === 'clickData') {
        // 模型點擊切換
        if (e.data === waterPump6 && e.data.s('shape3d') === 'models/裙房系統/水泵.json') e.data.s('shape3d', 'models/fengji.json');
        else if (e.data === waterPump6 && e.data.s('shape3d') === 'models/fengji.json') e.data.s('shape3d', 'models/裙房系統/水泵.json');
        
        // 模型點擊 隱藏/顯示屬性窗口
        if (e.data === waterPump5) {// 判斷點擊的圖元是否爲 waterPump5
            if(giveWater.s('3d.visible')) {// 判斷當前屬性窗口是否爲顯示狀態
                giveWater.s('3d.visible', false);// 設置屬性窗口不可見
                e.data.s('note', '點我顯示屬性窗口');// 更改標註中的顯示內容
            }
            else {
                giveWater.s('3d.visible', true);// 設置屬性窗口可見
                e.data.s('note', '點我隱藏屬性窗口')// 更改標註中的顯示內容
            }
        }
    }
});

文本內容/顏色變換

通過 tag 獲取場景中對應的屬性窗口的節點,此節點爲一個面板,相當於六面體有六面,這個節點類型就只有一面,並通過設置屬性 shape3d.image 設置此節點上的圖片爲 tooltips.json 矢量圖標(https://hightopo.com/guide/guide/core/vector/ht-vector-guide.html)。矢量在 Hightopo(HT)中是矢量圖形的簡稱,常見的 png 和 jpg 這類的柵格位圖, 通過存儲每個像素的顏色信息來描述圖形,這種方式的圖片在拉伸放大或縮小時會出現圖形模糊,線條變粗出現鋸齒等問題。 而矢量圖片通過點、線和多邊形來描述圖形,因此在無限放大和縮小圖片的情況下依然能保持一致的精確度。而且 HT 的矢量圖形還有一個非常重要的特點,就是能夠對矢量圖形上的任何一個部分都進行數據綁定,也就是說上圖中的五張圖,我們可以只繪製一張圖,通過數據綁定來改變這張圖上的文本以及數值內容。

矢量圖標中的數據綁定可以用在工業中的生產看板、大屏中的數據顯示等等,都能夠以一種高效的方式進行產品的整合。

矢量圖形的數據綁定能夠再寫一篇文章進行闡述了,這裏就不多提,大家自行去官網上查看“矢量手冊”以及“數據綁定手冊”,說明的比較詳細。

獲取到對應的節點之後,通過 node.a 方法可以獲取和設置數據綁定(https://hightopo.com/guide/guide/core/databinding/ht-databinding-guide.html#ref_vector)的屬性,這裏我們綁定的是文本內容“label”和數值“value”以及數值顏色“valueColor”:

var billboardArray = [];
// 通過 tag 獲取節點
var temperature1 = dm.getDataByTag('回水溫度1');// 獲取 tag 爲"回水溫度1"的節點
billboardArray.push(temperature1);
var temperature2 = dm.getDataByTag('回水溫度2');
billboardArray.push(temperature2);
var returnPress = dm.getDataByTag('回水壓力');
billboardArray.push(returnPress);
var givePress = dm.getDataByTag('供水壓力');
billboardArray.push(givePress);
var giveTemp = dm.getDataByTag('供水溫度');
billboardArray.push(giveTemp);
var giveWater = dm.getDataByTag('供水流量');
billboardArray.push(giveWater);
// 文字標籤內容變換
billboardArray.forEach(function(billboard) {
    billboard.a('label', billboard.getTag());// 設置數據綁定屬性爲 label 的屬性值爲當前節點的 tag 內容
});
// 文字標籤數字變換+顏色變換 更改圖標中綁定的 value 屬性值
setInterval(function() {
    billboardArray.forEach(function(billboard) {
        var random = Math.random()*100;
        billboard.a('value', random.toFixed(2));

        // 設置圖標中“數值內容顏色”
        if (random > 70 && random <= 80) billboard.a('valueColor', '#00FFFF');
        else if (random > 80 && random <= 90) billboard.a('valueColor', '#FFA000');
        else if (random > 90) billboard.a('valueColor', '#FF0000');
        else billboard.a('valueColor', '');
    });
}, 1000);

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章