基於 HTML5 WebGL 的樓宇智慧化整合系統(一)
// 建立二維拓撲檢視 this.g2d = new ht.graph.GraphView(); this.g2dDm = this.g2d.dm(); // 建立三維拓撲檢視 this.g3d = new ht.graph3d.Graph3dView(); this.g3dDm = this.g3d.dm(); // 將二維圖紙嵌入到三維場景中 this.g2d.addToDOM(this.g3d.getView()); // 修改左右鍵互動方式 let mapInteractor = new ht.graph3d.MapInteractor(this.g3d); this.g3d.setInteractors([mapInteractor]); // 修改最大仰角為 PI / 2 mapInteractor.maxPhi = Math.PI / 2; const G = {}; window.G = G; // 事件派發 G.event = new ht.Notifier();
3D 場景載入主檢視為:
首先我搭建了一個 3D 的場景用來放置我們的 json 場景資料,利用 ht.Default.xhrLoad 函式解析 json 場景資料,並通過 deserialize 將反序列化的物件加入DataModel來顯示載入 3D 場景,有興趣的可以通過<HT的序列化手冊>來了解這一機制的實現。
ht.Default.xhrLoad('scenes/demo.json', (json) => { if (!json) return; g3dDm.deserialize(json); // 設定三維檢視的中心點和相機位置 g3d.setCenter([-342, -64, 389]); g3d.setEye([-355, 10833, 2642]); // 設定最遠距離 g3d.setFar(1000000); // 獲取球圖示,設定為天空球 let skybox = g3dDm.getDataByTag('skyBox'); g3d.setSkyBox(skybox); // 模型載入完後執行動畫 const modelList = []; g3dDm.each(d => { const shape3d = d.s('shape3d'); if (!shape3d || !shape3d.endsWith('.json')) return; if (ht.Default.getShape3dModel(shape3d)) return; modelList.push(shape3d); }); ht.Default.handleModelLoaded = (name, model) => { const index = modelList.indexOf(name); if (index < 0) return; modelList.splice(index, 1); if (modelList.length > 91) return; ht.Default.handleModelLoaded = () => { }; // 模型載入完侯,預設執行場景切換動畫 g3d.moveCamera([257, 713, 1485], [7, 40, 144], { duration: 2000, finishFunc: () => { this.load2D(); } }); }; });
2D 面板載入檢視為:
同樣,我搭建了一個 2D 的場景用來放置我們的 json 向量圖,利用 ht.Default.xhrLoad 函式將 json 向量背景圖反序列化顯示在 2D 面板資料。
ht.Default.xhrLoad('displays/demo.json', (json) => { if (!json) return; g2dDm.deserialize(json); // 面板動畫入口 this.tittleAnim(); this.panelTime(); // 2D圖紙載入完後執行事件處理 this.loaded2DHandler(); });
二、3D 動畫效果以及切換漫遊
對於 3D 建模下的樓宇建築,加上場景的全方位漫遊,可使使用者達到一種沉浸式的體驗,更加直觀地去感受這個樓宇下各個場景的聯絡,依次地介紹了冷站、智慧末端以及熱站的位置以及功能運作的動畫 。主要運用的方法是通過藉助 HT 提供的 ht.Shape 圖元型別,可以在 GraphView 和 Graph3dView 元件上展示出各種二維和三維的形狀效果,而漫遊的管道路線就是由其擴充套件子類 ht.Polyline 去繪製實現一條三維的管道,然後用這條繪製的管道加上漫遊的時間去呼叫這個漫遊的方法,其本質上是圍繞著中心點,然後根據管道去不斷地改變視角下的 eye 和 center 的數值,達到環視這個建築的整體視角。
這裡可以瞭解一下關於空間軌道的繪製,詳見<HT的形狀手冊>的空間管線章節。
以下是環視漫遊動畫的虛擬碼:
polyLineRoam(polyLine, time) { const g3d = this.g3d; const g3dDm = this.g3dDm; this.roamButton.a('active', true); this.roamAnim = ht.Default.startAnim({ duration: time, easing: t => t, action: (v, t) => { let length = this.main.g3d.getLineLength(polyLine), offset = this.main.g3d.getLineOffset(polyLine, length * v), point = offset.point, px = point.x, py = point.y, pz = point.z; g3d.setEye(px, py, pz); g3d.setCenter(7, 40, 144); }, finishFunc: () => { this.roam1(); } }); }
在整體建築的環視漫遊完後,我們可以通過拉近各個場景的視角,來依次巡視各個場景所執行的動畫。在根據管道改變 eye 和 center 環視漫遊方法結束後,用動畫的結束回撥 finishFunc 去呼叫下一個動畫的執行,而巡視漫遊就在這裡去呼叫,以下我們以巡視冷站的漫遊動畫為例去介紹實現的方法。
巡視漫遊的主要實現方法是通過 HT 核心包的相機移動 moveCamera 來實現的, 通過引數 (eye, center, animation) 來呼叫這個方法:
- eye:新的相機位置,形如[-291, -8, 283],如果為 null 則使用當前相機的位置;
- center:新的目標中心點位置(相機看向的位置),形如[148, -400, 171],如果為 null 則使用當前中心點位置;
- animation:預設 false,是否啟用動畫,可以設定為 true 或者 flase 或者 animation 動畫物件;
每次執行完一個場景的視角移動後,再通過相機移動動畫的結束回撥 finishFunc 呼叫下一個相機移動的動畫,達到巡視漫遊的效果。
// 切換到冷站視角 roam1() { const g3d = this.g3d; const g3dDm = this.g3dDm; this.roamAnim = g3d.moveCamera([-291, -8, 283], [148, -400, 171], { duration: 500, easing: t => t * t, finishFunc: () => { this.roam2(); } }); }
在環視漫遊和巡視漫遊的執行下,我們也可以觸發 2D 圖紙右面板下的按鈕面板去觀看我們想要瀏覽的指定場景,這時候就會關閉當前在執行的環視漫遊或者巡視漫遊,再次點選改按鈕則返回場景的主視角,或者點選左上角漫遊按鈕又可以進入環視漫遊,這樣的互動體驗,可以方便使用者即使地檢視想要瀏覽的場景,而不用依靠等待逐一漫遊下去檢視,也不會干擾到漫遊的整體體驗。相應地通過介紹冷站按鈕的點選觸發介紹一下實現的方法。
一般的互動方式存在三種事件互動的方法,包括事件通知管理器 ht.Notifier 類,內建的 Interator 在互動過程會派發出事件和資料繫結的監聽來實現,而這裡使用的是第三種互動方式。
通過資料繫結監聽到 onDown 執行按下的事件後,通過改變按下和再次按下的按鈕狀態 active 來分別執行相機移動去切換視角,主要實現的虛擬碼如下:
// 設定圖元可互動 this.coolingCentralStationButton.s('interactive', true); // 通過資料繫結監聽到onDown執行按下的事件 this.coolingCentralStationButton.s('onDown', () => { // 切換到冷站時,2d面板所執行的切換動畫 this.switchToColdStation(); // 按鈕初始化 this.buttonTearDown(); // 按鈕按下效果的狀態 let active = this.coolingCentralStationButton.a('active'); // button為按鈕集合陣列,當按下電梯按鈕,其他按鈕預設false button.forEach(btn => { btn.a('active', false); }); // 冷站按鈕的狀態切換 this.coolingCentralStationButton.a('active', !active); // 根據冷站按鈕的狀態執行切換到冷站或者切換回主視角 if (active) { // 相機移動切換到主視角 moveCamera(g3d, [257, 713, 1485], [7, 40, 144], { duration: 2000, easing: t => t * t }); } else { // 漫遊動畫物件如果不為空,則暫停漫遊動畫物件並且設定為空 if (this.roamAnim !== null) { this.roamAnim.pause(); this.roamAnim = null; } // 相機移動切換到冷站視角 coolingCentralStationAnimation = moveCamera(g3d, [-291, -8, 283], [148, -400, 171], { duration: 2000, easing: t => t * t }); } });
當然,在 3D 場景下還有一些很有趣的動畫效果,比如車流效果、飛光效果和圓環擴散效果。車流效果主要通過採用了貼圖的 uv 的偏移來實現達到車流穿梭的科技感效果;而飛光效果則是採用排程動畫的方法來間隔設定飛光的高度,達到最高點則消失然後重新輪迴動畫展示;圓環擴散效果則是同樣採用排程動畫的方法來間隔設定圓環的縮放值和透明度,來達到擴散消失的效果。
對於間隔的排程動畫,為了實現動畫的流暢性,這裡排程使用的 loop 是運用到自己封裝 HT 的動畫 ht.Default.startAnim 的一個方法:
- frames 動畫幀數,這裡不鎖定幀數,可以適應本身動畫的幀數;
- interval 動畫間隔,單位ms,預設設定20ms。
loop(action, interval = 20) { return ht.Default.startAnim({ frames: Infinity, interval: interval, action: action }); }
然後通過呼叫這個 loop 的間隔動畫方法,我們來實現車流效果、飛光效果和圓環擴散效果,實現的參考虛擬碼如下:
// 車流圖元的初始化 let traffic = g3dDm.getDataByTag('traffic'); // 圓環擴散圖元的初始化 let lightRing = this.lightRing = g3dDm.getDataByTag('lightRing'); // 飛光圖元設定三種透明狀態陣列集合flyMap的初始化 [1, 2, 3].forEach(i => { const data = flyMap['fly' + i] = g3dDm.getDataByTag('fly' + i); data.eachChild(d => { d.s({ // 開啟透明度 'shape3d.transparent': true, // 根據不同的陣列集合設定不同的透明度 'shape3d.opacity': i === 3 ? 0.5 : 0.7, // 設定沿著y軸自動旋轉 'shape3d.autorotate': 'y' }); }); }); if (this.flyAnim) return; this.flyAnim = loop(() => { // 飛光根據間隔設定高度來達到上升的效果 for (let k in flyMap) { const data = flyMap[k]; let e = data.getElevation() + flyDltMap[k]; if (e >= 500) e = -400; data.setElevation(e); } // 車流根據設定間隔增長uv偏移量來實現穿梭的效果 traffic.eachChild(c => { c.s('all.uv.offset', [location, 0]); }); location -= 0.03; // 旋轉震盪波透明度漸降 let percent = lightRing.a('percent') || 0, scale = 15 * percent + 0.5; lightRing.setScale3d([scale + 1, scale, scale + 1]); lightRing.s('shape3d.opacity', (1 - percent) * 0.5); percent += 0.01; if (percent >= 1) { percent = 0; } lightRing.a('percent', percent); }, 50);
三、冷站場景和熱站場景的動畫實現
場景動畫中機組的風扇、集水器的蓄滿以及水的流動效果:
動畫的實現主要還是通過 HT 自帶的 ht.Default.startAnim 動畫
函式,支援 Frame-Based 和 Time-Based 兩種方式的動畫。同樣的,我們這裡使用的是 Frame-Based 來封裝一個 loop 函式來執行每一幀間隔的動畫。
一般來說,動畫可通過自行配置來達到自己想要實現的方法,這裡可以瞭解< HT 的入門手冊>關於動畫
函式的介紹。
if (this.stationAnim) return; this.stationAnim = loop(() => { // 冷站水管流動 coldFlow_blue.eachChild(c => { c.s('shape3d.uv.offset', [-location, 0]); }); coldFlow_yellow.eachChild(c => { c.s('shape3d.uv.offset', [location, 0]); }); // 熱站水管流動 heatFlow_blue.eachChild(c => { c.s('shape3d.uv.offset', [-location, 0]); }); heatFlow_yellow.eachChild(c => { c.s('shape3d.uv.offset', [location, 0]); }); location -= 0.03; // 冷站風扇旋轉 cold_fan.eachChild(c => { c.setRotation3d(c.r3()[0], c.r3()[1] + (Math.PI / 10), c.r3()[2]); }); // 熱站風扇旋轉 heat_fan.eachChild(c => { c.setRotation3d(c.r3()[0], c.r3()[1] + (Math.PI / 10), c.r3()[2]); }); // 集水器水位變化 HotWaterTankTall += 0.25; if (HotWaterTankTall > 15) { HotWaterTankTall = 0; } coldWaterTankTall1 += 0.25; if (coldWaterTankTall1 > 20) { coldWaterTankTall1 = 0; } coldWaterTankTall2 += 0.25; if (coldWaterTankTall2 > 20) { coldWaterTankTall2 = 0; } hotWaterTank.setTall(HotWaterTankTall); coldWaterTank1.setTall(coldWaterTankTall1); coldWaterTank2.setTall(coldWaterTankTall2); }, 50);
四、中央空調末端智慧群控系統場景效果
這裡採用了模擬資料的方式來體現末端智慧節能控制的效果。應用於真實專案的時候,可以採用資料介面的方式來實時對接真實資料,可以達到實時監控的效果。
我使用了自己 mock 的末端群控的資料引數,格式如下:
var boxData = [ [{ // 裝置編號 id: 'box1', // 裝置的溫度 temperature: 23.8, // 裝置的頻率 frequency: 45.8 }, ...] ... ];
這裡的實現也是通過 loop 迴圈執行資料的讀取,當陣列指標 index 讀取到最後一個數據時,立即關閉迴圈並清空 loop排程。
boxAnimation = loop(() => { for (let i = 0, l = 16; i <= l-1; i++) { let roomTag, roomBox, tag; tag = i+1; roomTag = 'boxPanel' + tag; roomBox = 'box' + tag; let panel = g3dDm.getDataByTag(roomTag); let box = g3dDm.getDataByTag(roomBox); if (panel) { panel.a('valueT', boxData[index][i].temperature + '℃'); panel.a('valueK', boxData[index][i].frequency + 'Hz'); // 手動更新快取的面板資訊 g3d.invalidateShape3dCachedImage(panel); // 根據溫度判斷裝置的顏色 if (box && parseFloat(panel.a('valueT')) < 26) { box.s('shape3d.blend', 'rgb(4,67,176)'); box.s('wf.color', 'rgb(4,67,176)'); } else if (box && parseFloat(panel.a('valueT')) >= 26 && parseFloat(panel.a('valueT')) <= 28) { box.s('shape3d.blend', 'rgb(28,189,87)'); box.s('wf.color', 'rgb(28,189,87)'); } else if (box && parseFloat(panel.a('valueT')) > 28) { box.s('shape3d.blend', 'rgb(181,43,43)'); box.s('wf.color', 'rgb(181,43,43)'); } } } index++; if (index >= 10) { boxAnimation.pause(); boxAnimation = null; } }, 500);
總結
IBMS 智慧化整合系統管理對於建築園區管理的重要性日趨上升,在資訊時代裡不僅可以很好地體現出資訊資料管理的明確性,也體現了智慧管理的便利有效性。通過 3D 場景樓宇園區的動畫加上環視漫遊和巡視漫遊的配合,充分體現了 3D 場景的擬真優點,但是如何實現場景動畫的觸發實現呢?這裡當然必不可少了 2D 面板上的互動和動畫,在下期我們會為大家介紹一些 2D 面板的互動和動畫實現,帶您解讀不一樣的 2D/3D 融合。 2019 我們也更新了數百個工業網際網路 2D/3D 視覺化案例集,在這裡你能發現許多新奇的例項,也能發掘出不一樣的工業網際網路:https://mp.weixin.qq.com/s/ZbhB6LO2kBRPrRIfHlKGQA 同時,你也可以檢視更多案例及效果:https://www.hightopo.com/demos/index.html