基於H5與webGL的 3d 電子圍欄展示
前言
現代工業化的推進在極大加速現代化程序的同時也帶來的相應的安全隱患,在傳統的視覺化監控領域,一般都是基於 Web SCADA 的前端技術來實現 2D 視覺化監控,本系統採用 Hightopo 的 HT for Web 產品來構造輕量化的 3D 視覺化場景,該 3D 場景從正面展示了一個現代化工廠的現實場景,包括工廠工人的實時位置、電子圍欄的範圍、現場的安全情況等等,幫助我們直觀的瞭解當前工廠人員的安全狀況。
本篇文章通過對工廠視覺化場景的搭建和模型的載入,人物實時定位程式碼的實現、電子圍欄和軌跡圖的實現進行闡述,幫助我們瞭解如何通過使用HT實現一個簡單的3D電子圍欄視覺化。
以下是專案地址:基於HTML5 WebGL的工業化3D電子圍欄、軌跡圖
效果預覽
工廠人員實時定位效果及電子圍欄效果
軌跡圖效果圖
程式碼實現
人物模型及場景
專案中使用的人物模型是通過 3dMax 建模生成的,該建模工具可以匯出 obj 與 mtl 檔案,在 HT 中可以通過解析 obj 與 mtl 檔案來生成 3d 場景中的攝像頭模型。
專案中場景通過 HT 的 3d 編輯器進行搭建,場景中的模型有些是通過 HT 建模,有些通過 3dMax 建模,之後匯入 HT 中。
繪製電子圍欄
場景中的電子圍欄並不是使用3dMax搭建的模型,HT提供了多種基礎形體型別供使用者建模使用,不同於傳統的3D建模方式,HT的建模核心都是基於API的介面方式, 通過預定義的圖元型別和引數介面,進行設定達到三維模型的構建。根據形狀,我將電子圍欄分成圓柱、長方體和底部為多邊形的稜柱。
以下是我繪製電子圍欄的相關虛擬碼:
1 G.makeShapes = function (data, typeName, color, lastColor, g3dDm) { 2 //data是包含電子圍欄圖形資訊的json物件陣列 3 let shapes = data; 4 for (let i = 0; i < shapes.length; i++) { 5 let shape = shapes[i]; 6 let type = Number(shape['type']); 7 let x = Number(shape['x']); 8 let y = Number(shape['y']); 9 let z = Number(shape['z']); 10 let width = Number(shape['width']); 11 let height = Number(shape['height']); 12 let tall = Number(shape['tall']); 13 let radius = Number(shape['radius']); 14 let vertexX = shape['vertexX']; 15 let vertexY = shape['vertexY']; 16 let nodePoints = []; 17 let p3 = []; 18 let s3 = []; 19 let centerX = 0; 20 let centerY = 0; 21 let centerZ = 0; 22 let node = new ht.Node(); 23 node.setTag(typeName + i); 24 switch (type) { 25 //第一種形狀:圓柱 26 case 1: 27 p3 = [-x, tall / 2, -y]; 28 s3 = [radius, tall, radius]; 29 //定義電子圍欄樣式 30 node.s({ 31 "shape3d": "cylinder", 32 "shape3d.color": color, 33 "shape3d.transparent": true, 34 "shape3d.reverse.color": color, 35 "shape3d.top.color": color, 36 "shape3d.top.visible": false, 37 "shape3d.bottom.color": color, 38 "shape3d.from.color": color, 39 "shape3d.to.color": color 40 }); 41 node.p3(p3); //設定三維座標 42 node.s3(s3); //設定形狀資訊 43 break; 44 //第二種形狀:長方體 45 case 2: 46 centerX = x - width / 2; 47 centerY = y - height / 2; 48 centerZ = z + tall / 2; 49 p3 = [-Number(centerX) - width, Number(centerZ), -Number(centerY) - height]; 50 s3 = [width, tall, height]; 51 node.s({ 52 "all.color": color, 53 "all.reverse.color": color, 54 "top.visible": false, 55 "all.transparent": true 56 }); 57 node.p3(p3); 58 node.s3(s3); 59 break; 60 //第三種形狀:底部為不規則形狀的等高體 61 case 3: 62 let segments = []; 63 for (let i = 0; i < vertexX.length; i++) { 64 let x = -vertexX[i]; 65 let y = -vertexY[i]; 66 let newPoint = { x: x, y: y }; 67 nodePoints.push(newPoint); 68 //1: moveTo,佔用1個點資訊,代表一個新路徑的起點 69 if (i === 0) { 70 segments.push(1); 71 } 72 else { 73 //2: lineTo,佔用1個點資訊,代表從上次最後點連線到該點 74 segments.push(2); 75 if (i === vertexX.length - 1) { 76 //5: closePath,不佔用點資訊,代表本次路徑繪製結束,並閉合到路徑的起始點 77 segments.push(5); 78 } 79 } 80 } 81 node = new ht.Shape(); 82 node.setTag(typeName + i); 83 node.s({ 84 'shape.background': lastColor, 85 'shape.border.width': 10, 86 'shape.border.color': lastColor, 87 'all.color': lastColor, 88 "all.transparent": true, 89 'all.opacity': 0.3, 90 }); 91 p3 = [nodePoints[0]['x'], tall / 2, nodePoints[0]['y']]; 92 node.p3(p3); 93 node.setTall(tall); 94 node.setThickness(5); 95 node.setPoints(nodePoints); //node設定點集位置資訊 96 node.setSegments(segments); //node設定點集連線規則 97 break; 98 } 99 g3dDm.add(node); 100 } 101 }
考慮到電子圍欄在某些情況下可能會影響到對人物位置的觀察,設定了隱藏電子圍欄的功能。在HT中使用者可以自定義設定標籤Tag作為模型唯一的標識,我將所有的電子圍欄模型的標籤字首都統一併且儲存在fenceName中,需要隱藏的時候則遍歷所有標籤名稱字首為fenceName的模型,並且根據模型種類的不同設定不同的隱藏方式。
以下是相關虛擬碼:
1 g3dDm.each((data) => { 2 if (data.getTag() && data.getTag().substring(0, 4) === fenceName) { 3 if (data.s('all.opacity') === '0') { 4 data.s('all.opacity', '0.3'); 5 } 6 else { 7 data.s('shape3d.visible', true); 8 data.s('all.visible', true); 9 data.s("2d.visible", true); 10 data.s("3d.visible", true); 11 } 12 } 13 });
人物模型實時定位
因為專案使用的是http協議獲取資料,因此使用定時器定時重新整理人物資料資訊,HT有設定節點位置的setPosition3d方法,因此不做過多介紹,但是人物節點的位置的重新整理還包括人物的朝向,因此每次人物移動都需要和上次位置進行比對,計算出偏移的角度。
相關虛擬碼如下:
1 // 重新整理資料的人物結點與原來的人物節點標籤相同,則存在做位置更新 2 if (realInfoData.tagId === tag.getTag()) { 3 //計算位置朝向偏移引數 4 let angleNumber = Math.atan2(((-p3[2]) - (-tag.p3()[2])), ((-p3[0]) - (-tag.p3()[0]))); 5 //如果在原地就不轉向,判斷人物在平面位置是否發生變化 6 if (p3[0] !== tag.p3()[0] || p3[2] !== tag.p3()[2]) { 7 if (angleNumber > 0) { 8 angleNumber = Math.PI - angleNumber; 9 } else { 10 angleNumber = -Math.PI - angleNumber; 11 } 12 //設定人物朝向 13 tag.setRotation3d(0, angleNumber + Math.PI / 2, 0); 14 } 15 //設定人物位置 16 tag.p3(p3); 17 }
人物觸發警報
當人物觸發警報時,有2種方式同時提醒系統使用者。一是人物頭上的面板顏色發生改變,並且顯示報警資訊。
相關程式碼如下:
1 switch(obj.alarmType){ 2 case null: 3 if(panel){//無警報 4 panel.a('alarmContent',''); 5 panel.a('bg','rgba(6,13,36,0.80)'); 6 } 7 break; 8 case '0': 9 panel.a('alarmContent','進入圍欄'); 10 panel.a('bg','rgb(212,0,0)'); 11 break; 12 case '1': 13 panel.a('alarmContent','SOS'); 14 panel.a('bg','rgb(212,0,0)'); 15 break; 16 case '2': 17 panel.a('alarmContent',''); //離開圍欄 18 panel.a('bg','rgba(6,13,36,0.80)'); 19 break; 20 case '3': 21 panel.a('alarmContent','長時間未動'); 22 panel.a('bg','rgb(212,0,0)'); 23 break; 24 }
二是頁面的右側面板會增加警報資訊。
相關程式碼如下:
1 data.a('text', info); 2 list.dm().add(data);
軌跡圖軌跡實現原理
在發生警報後,需要根據人物的軌跡圖回溯發生警報的來龍去脈。如果使用根據點集每走一步就繪製一個canvas腳步節點的方式去重現軌跡,很容易造成節點繪製過多,頁面卡頓的情況,因此我使用一整條管道的方式代替一個人物的所有腳步節點,使用管道的好處是,每個人物的軌跡圖從開始到結束只有一個管道的圖元資訊,因此對頁面的渲染更加友好和流暢。
生成管道軌跡的程式碼如下:
1 //生成軌跡 2 this.ployLines[i] = new ht.Polyline(); 3 this.ployLines[i].setParent(node); 4 this.points[i] = []; 5 this.points[i].push({ x: p3[0], y: p3[2], e: p3[1] -50 }); 6 this.ployLines[i].setPoints(this.points[i]); 7 this.ployLines[i].s({ 8 'shape.border.color': 'red' 9 }); 10 g3dDm.add(this.ployLines[i]);
人物前進一步,則往管道的點集中推進一個點的座標,同時繪製新的管道部分。同理,人物後退一步,則管道的點集中推出當前最後一個點的座標,同時管道失去最後兩點連線的部分。另外我通過使用定時器,對軌跡圖的前進和後退分別做了快進和快退的處理。以下為軌跡圖的執行效果: