1. 程式人生 > >基於 HTML5 WebGL 的高爐鍊鐵廠視覺化系統

基於 HTML5 WebGL 的高爐鍊鐵廠視覺化系統

前言       在當今 工業4.0 新時代的推動下,不僅迎來了 工業網際網路 的發展,還開啟了 5G 時代的新次元。而伴隨著頻寬的提升,網路資訊飛速發展,能源管控上與實時預警在工業網際網路中也佔著舉足輕重的地位,而對於高爐鍊鐵的發展上來看,目前已完成國內260座高爐的數字化和智慧化落地,並推動鍊鐵大資料平臺在俄羅斯、越南、伊朗、印尼等“一帶一路”國家鋼鐵企業中應用,充分體現了高爐智慧化大屏產業應運而生。我們將使用 Hightopo(以下簡稱 HT )的 HT for Web 產品上的 web 組態跟大家介紹一下通過 2/3D 融合搭建的高爐鍊鐵廠視覺化系統。       HT 可以快速實現豐富的 2D 組態和 3D 組態效果,可以根據需求發揮自己的想象,玩轉很多新奇的功能,並且通過優勢互補的作用下,完善出一套完整的視覺化系統解決方案。所以在視覺化系統的實現上,3D 場景採用以 HT 輕量化 HTML5/WebGL 建模的方案,實現快速建模、執行時輕量化到甚至手機終端瀏覽器即可 3D 視覺化運維的良好效果;而在對應的 2D 圖紙上,使用特有的向量,在各種比例下不失真,加上佈局機制,解決了不同螢幕比例下的展示問題。   本文將從以下三個方面與大家分享高爐鍊鐵廠在大屏展示上的實現:       1、頁面搭建:介紹基礎的 2D 圖紙與 3D 場景融合的專案搭建;       2、資料對接:進行面板資料的對接展示;       3、動畫實現:鐵水罐車運輸、傳送帶運送以及場景漫遊的實現;   介面簡介及效果預覽       在整個高爐鍊鐵廠視覺化系統的 2D 面板上,呈現了昨日曆史與今日實時的一些重要預警資料,在管控上能起到實時監控的作用,也能與歷史資料進行對比,從而使生產與安全達到預期的預警效果;其次 3D 場景通過輕量化的模型呈現出一座高爐鍊鐵廠的基本運作流程以及鐵水罐車運送鋼鐵的動畫,加上環繞的漫遊效果,起到全方位的實時監控狀態的變化。   程式碼實現 一、頁面搭建         在內容實現上,採用了 HT 輕量化模型以及 web 組態,以 2/3D 結合的方式,通過的 json 反序列化得到 2D 圖紙和 3D 場景的完整呈現。首先會通過建立 ht.graph.GraphView 和 ht.graph3d.Graph3dView 來呈現 2D 和 3D 的內容。 2D 檢視元件和 3D 檢視元件進行 deserialize() 反序列化對應的 url 寄存的 json 呈現出場景與圖紙的內容,兩者通過對資料模型 DataModel 裡的子元素設定標籤來進行資料繫結,實現功能上的展示。
// 三維拓撲檢視
let g2d = new ht.graph.GraphView();
let g3dDm = g2d.dm();
// 三維拓撲檢視
let g3d = new ht.graph3d.Graph3dView();
let g3dDm = g3d.dm(); 
// 2D 檢視元件和 3D 檢視元件進行反序列化
g2d.deserialize('displays/index.json');
g3d.deserialize('scenes/index.json');

 

        在內容呈現上還需要將元件加入到 body 下,一般 2/3D 結合的專案上,都會使用 2D 元件加入到 3D 元件的根 div 下,然後 3D 元件再加入到 body下的方式實現面板與場景的載入。

// 將 3D 元件加入到 body 下
g3d.addToDOM();
// 將 2D 元件加入到 3D 元件的根 div 下,父子 DOM 事件會冒泡,這樣不會影響 3D 場景的互動
g2d.addToDOM(g3d.getView());

 

      同時,在互動與呈現上改變了一些實現方式。例如,修改了左右鍵的互動方式,設定左鍵點選旋轉 3D 場景,右鍵點選為 pan 抓圖的場景移動方式。其次,在點選 2D 有點到圖元畫素時,我們希望不觸發 3D 的互動,例如在對 2D 面板表格中用滾輪滑動的時候,會觸發 3D 場景的縮放,這裡通過監聽 moudedown、touchstart 和 wheel 三種互動來進行控制,對於 wheel 的監聽方式,為了保證相容性就通過封裝一個 getWheelEventName() 的方法來得到事件名。

// 修改左右鍵互動方式
let mapInteractor = new ht.graph3d.MapInteractor(this.g3d);
g3d.setInteractors([mapInteractor]);
// 設定修改最大仰角為 PI / 2
mapInteractor.maxPhi = Math.PI / 2;

// 避免 2D 與 3D 互動重疊
let div2d = g2d.getView();
const handler = e => {
    if (g2d.getDataAt(e)) {
        e.stopPropagation();
    }
};
div2d.addEventListener('mousedown', handler);
div2d.addEventListener('touchstart', handler);
div2d.addEventListener(getWheelEventName(div2d), handler);

// 在一個 HTMLElement 上,可能支援下面三個事件的一種或者兩種,但實際回撥只會回撥一種事件,優先回調標準事件,觸發標準事件後,不會觸發相容性事件
function getWheelEventName(element) {
    if ('onwheel' in element) {
// 標準事件
        return 'wheel';
    } else if (document.onmousewheel !== undefined) {
// 通用舊版事件
        return 'mousewheel';
    } else {
// 舊版 Firefox 事件
        return 'DOMMouseScroll';
    }
}

 

二、資料對接       在 2D 面板的呈現上,會有許多的圖表資料資訊,我們可以通過訪問後臺資料介面得到資料,然後在 2D 或者 3D 對應的元件上取得相應的資料模型 dataModel,通過對資料模型裡設定唯一的標識 tag 的子節點進行對接資料就可以了。例如現在我們要對 2D 面板的資料進行繫結,我們只需要通過 2D 元件的 g2d 得到資料模型,通過 g2d.dm().getDataByTag(tag) 就可以得到設定有唯一標識的 tag 節點,來對接資料或者設定狀態展示了。       對於資料介面的獲取,可以運用主流的 jQuery 框架下的 ajax、基於 promise 的 HTTP 庫的 axios  通過輪詢呼叫介面實時獲取資料或者使用 HTML5 提供的一種在單個 TCP 連線上進行全雙工通訊的協議 WebSocket,可以雙向進行資料傳輸,在選擇運用上可以匹配自己的實現需求,而本系統是採用通過 axios 呼叫介面獲取實時資料。
// 昨日利用係數資料對接
axios.get('/yesterdayUse').then(res => {
    setBindingDatasWithAnim(dm, res, undefined, v => v.toFixed(2));
});
// 昨日燃料比資料對接
axios.get('/yesterdayFuel').then(res => {
    setBindingDatasWithAnim(dm, res, undefined, v => v.toFixed(2));
});
// 昨日入爐品位資料對接
axios.get('/yesterdayIn').then(res => {
    setBindingDatasWithAnim(dm, res, undefined, v => v.toFixed(2));
});
// 昨日燃氣利用率資料對接
axios.get('/yesterdayCoal').then(res => {
    setBindingDatasWithAnim(dm, res, undefined, v => v.toFixed(2));
});
// 實時警報資訊面板表格輪詢載入資料進行滾動播放
this.addTableRow();
setInterval(() => {
    this.addTableRow();
}, 5000);
        通過 axios 輪詢呼叫介面,實時獲取安全指數和實時資料資訊(風量、風溫和富氧量):
requestData() {
    let dm = this.view.dm();
    // 安全指數資料對接並載入圓環動畫
    axios.get('/levelData').then(res => {
        setBindingDatasWithAnim(dm, res, 800, v => Math.round(v));
    });
    // 實時資料(風量、風溫和富氧量)資料對接並載入進度條動畫
    axios.post('/nature', [
        'windNumber', 'windTemp', 'oxygenNumber'
    ]).then(res => {
        setBindingDatasWithAnim(dm, res, 800, v => parseFloat(v.toFixed(1)));
    });
}

 

  

       對接資料後,實現一些圓環或者進度條值的增減動畫,其本質上是運用 HT 自帶的動畫函式 ht.Default.startAnim(),通過判斷資料繫結的屬性後,設定新值與舊值差額的範圍動畫,然後使用者定義函式 easing 引數通過數學公式來控制動畫的運動的快慢,例如勻速變化、先慢後快等效果。

       這裡通過動畫函式封裝了一個差值的動畫效果,引數如下:

  • node:動畫處理的節點;
  • name:資料繫結的名稱;
  • value:資料繫結的值;
  • format:繫結資料值的格式規範;
  • accesstype:資料繫結的屬性從屬 ;
  • duration:動畫時間; 
setValueWithAnimation (node, name, value, format, accesstype = 's', duration = 300) {
    let oldValue;
    // 判斷資料繫結為自定義屬性 attr 後根據繫結名字取出舊值
    if (accesstype === 'a') {
        oldValue = node.a(name);
    }
    // 判斷資料繫結為樣式屬性 style 後根據繫結名字取出舊值
    else if (accesstype === 's') {
        oldValue = node.s(name);
    }
    // 預設通過取值器 getter 得到資料繫結的值
    else {
        oldValue = node[ht.Default.getter(name)]();
    }
    // 設定新舊值的差額
    let range = value - oldValue;
    // 執行動畫函式
    ht.Default.startAnim({
        duration: duration,
        easing: function (t) { return 1 - (--t) * t * t * t; },
        action: (v, t) => {
            // 新值增長的動畫範圍
            let newValue = oldValue + range * v;
            // 判斷有格式則制定資料格式
            if (format) {
                newValue = format(newValue);
            }
            // 判斷資料繫結為自定義屬性 attr 後設定新值
            if (accesstype === 'a') {
                node.a(name, newValue);
            }
            // 判斷資料繫結為樣式屬性 style 後設定新值
            else if (accesstype === 's') {
                node.s(name, newValue);
            }
            // 預設通過存值器 setter 設定資料繫結的新值
            else {
                node[ht.Default.setter(name)]()(node, newValue);
            }
        }
    });
}

 

      我們時常會在公開的預警場合或者宣傳場合看見輪播滾動的資料資訊,採用這種方法在公示的同時也不會遺漏掉任何一條資料資訊,如果搭配上一些例如淡入淡出的過場效果,更會吸引關注的眼球。而對於實時警報資訊的面板表格的實現,也是在新增新資料時,實現了一種過渡的 UI 互動上的沉浸感,主要還是運用了 HT 自帶的動畫函式 ht.Default.startAnim(),橫向通過滾動 100 寬度並資料透明度慢慢浮現,縱向採用向下偏移一行表格行高 54 來新增新的警報資訊。
addTableRow() {
    // 獲取表格節點
    let table = this.right3;
    // 通過 axios 的 promise 請求介面資料
    axios.get('getEvent').then(res => {
        // 獲取表格節點滾動資訊的資料繫結
        let tableData = table.a('dataSource');
        // 通過向 unshift() 方法可向滾動資訊陣列的開頭新增一個或更多元素
        tableData.unshift(res);
        // 初始化表格的縱向偏移
        table.a('ty', -54);
        // 開啟表格滾動動畫
        ht.Default.startAnim({
            duration: 600,
            // 動畫執行函式 action
            action: (v, t) => {
                table.a({
                    // 通過新增資料後,橫向滾動 100
                    'firstRowTx': 100 * (1 - v),
                    // 第一行行高出現的透明度漸變效果
                    'firstRowOpacity': v,
                    // 縱向偏移 54 的高度
                    'ty': (v - 1) * 54
                });
            }
        });
    });
}

  

三、動畫實現       在靜態的場景以及面板下,很難直觀地去體現一個 2/3D 嵌合的系統的優越性。動畫卻是賦予生命靈魂的所在,一個恰到好處的 UI 動畫設計可以使面板的互動體驗鮮活起來,而在 3D 場景中,通過一組簡單形象的鐵水罐車運輸和傳送帶運送可以讓人清晰地明白生產運輸的流程,對於模型建築的管控,利用好視角切入點,我們可以設定全方位的沉浸式漫遊巡視。綜上,通過輕量模型場景與向量元件面板的優勢疊加,可以呈現出一套靈活的高爐鍊鐵廠生產預警系統。       在漫遊巡視下,為了更全方位地體現場景,我們通過裁剪的方式來顯示和隱藏兩側的面板資料,以下以隱藏面板的裁剪動畫為例:
hidePanel() {
    // 將左側資料繫結裁剪的子元素存放進一個數組裡
    let leftStartClipIndexs = (() => {
        let arr = [];
        for (let i = 1; i <= 4; i++) arr.push(this['left' + i].s('clip.percentage'));
        return arr;
    })();
    // 將右側資料繫結裁剪的子元素存放進一個數組裡
    let rightStartClipIndexs = (() => {
        let arr = [];
        for (let i = 1; i <= 3; i++) arr.push(this['right' + i].s('clip.percentage'));
        return arr;
    })();
    // 設定面板裁剪的延遲時間,使得視覺上更有層次感
    let delayArrays = [400, 800, 1200, 1600];
    // 動畫執行函式
    let action = (index) => {
        ht.Default.startAnim({
            duration: 700,
            easing: Easing.swing,
            action: (v, t) => {
                this['left' + index].s('clip.percentage', leftStartClipIndexs[index - 1] + (0 - leftStartClipIndexs[index - 1]) * v);
                this['right' + index].s('clip.percentage', rightStartClipIndexs[index - 1] + (0 - rightStartClipIndexs[index - 1]) * v);
            }
        });
    };
    // 通過判定延遲時間陣列的長度,回撥 action 動畫的執行
    for (let i = 0, l = delayArrays.length; i < l; i++) {
        ht.Default.callLater(action, this, [i + 1], delayArrays.shift());
    }
}

      data.s('clip.percentage') 是 HT 節點自帶的樣式屬性,其本質意義就是可以通過指定的方向進行對於整個向量圖示的裁剪:

 

      一部電影可以通過各種鏡頭的切換下呈現不盡相同的敘事效果,日劇夕陽下熱血跑的急速切換或者幽暗角落下驚恐的淡入淡出,都是一種敘事的處理手段。在 HT 設定的 3D 場景中同樣地也存在著許許多多敘述的手法,最為基礎的設定就是通過場景中的主觀眼睛 eye 和場景中心 center 來搭配各種動畫的實現,可以自己設定值的方法函式來修改,也可以通過 HT 自身封裝的方法函式來處理,例如 flyTo() 和 moveCamera() 就是最為基礎的相機動畫,有興趣的話可以瞭解一下,自己動手嘗試搭配,肯定能最大地發揮 3D 場景的優勢所在。

      漫遊動畫是為了更好地從不同的視角去巡視場景,只要通過設定幾組眼睛視角,運用 HT 的 moveCamera() 相機視角移動的動畫,依次去對應眼睛的視角就可以自動地切換不同視角下場景的效果。
// 預設設定的眼睛視角陣列
const ROAM_EYES = [
    [1683.6555274005063, 939.9999999999993, 742.6554147474625],
    [1717.1004359371925, 512.9256996098727, -1223.5575465999652],
    [-181.41773461002046, 245.58303266170844, -2043.6755074222654],
    [-1695.7113902533574, 790.0214102589537, -877.645744191523],
    [-1848.1700283399357, 1105.522705042774, 1054.1519814237804],
    [-108, 940, 1837]
];
// 開啟相機移動漫遊動畫
playRoam() {
    // 設定場景眼睛視角
    let eye = ROAM_EYES[this.roamIndex];
    // 開啟相機視角移動動畫 moveCamera
    this._roamAnim = this.view.moveCamera(eye, [0, 0, 0], {
        duration: this.roamIndex ? 3000 : 4000,
        easing: Easing.easeOut,
        finishFunc: () => {
            this.roamIndex ++;
            let nextEye = ROAM_EYES[this.roamIndex];
            // 判斷是否有下一組眼睛視角,有的話繼續執行相機視角移動動畫,反之則重置漫遊動畫
            if (nextEye) {
                this.playRoam();
            }
            else {
                // 事件派發執行顯示面板動畫
                event.fire(EVENT_SHOW_PANEL);
                this.resetRoam();
            }
        }
    });
}

 

      如果說場景視角漫遊是一種大局整體觀的體現,那麼鐵水罐車裝載與運輸以及傳送帶的運送則是一個高爐鍊鐵流程的拼圖。通過一系列動畫流程的表達,你會很清晰地發現,特定的 3D 場景下的講解說明具有完整的故事串聯性。

      以下是鐵水罐車裝載與運輸的動畫流程:

      在 3D 場景中是用 x, y, z 來分別表示三個軸,通過不斷修改節點的 3D 座標就可以實現位移效果 car.setPosition3d(x, y, z),而對於鐵水罐車上的裝載標籤則使用吸附的功能,使其吸附在鐵水罐車上就能跟著一起行駛移動,然後在指定的空間座標位置上通過 car.s('3d.visible', true | false) 來控制鐵水罐車的出現與隱藏的效果。

 

      而關於傳送帶上煤塊、鐵礦的傳輸和管道氣體流通的指示,通過使用 UV 紋理貼圖的偏移來實現會方便很多,先來看看效果上的呈現:

  

      對於三維模型,有兩個重要的座標系統,就是頂點的位置座標(X、Y、Z)以及 UV 座標。形象地說,UV 就是貼圖影射到模型表面的依據,U 和 V 分別是圖片在顯示器水平、垂直方向上的座標,取值一般都是0~1。而傳送帶以及管道的指示就是用這種方法實現的,HT 的模型節點自帶 uv 值的樣式屬性,我們只需要不斷地控制其偏移變化,就能實現傳輸的效果:

// 設定初始偏移值
let offset1 = 0, trackOffset = 0;
// 一直呼叫設定偏移值
setInterval(() => {
    flows.each(node => {
        node.s({
            'top.uv.offset': [-offset1, 0],
            'front.uv.offset': [-offset1, 0],
        });
    });
    track.s('shape3d.uv.offset', [0, -trackOffset]);
    // 偏移值增加
    offset1 += 0.1;
    trackOffset += 0.03;
}, 100);

 

總結       數字化 和 智慧化 大屏管控是 工業網際網路 的發展趨勢,在很大程度上解放了人力和勞力,在資訊飛速傳訊的時代,大資料視覺化和智慧管控的結合,會演繹出許多驚奇的效果碰撞。對實時資料監管下,預警資訊也相當重要,保障生產有序進行的同時,我們也要關注安全問題,所以在大屏上呈現的許多內容,都極其具有行業跟上工業網際網路的步伐代表性。       2019 我們也更新了數百個工業網際網路 2D/3D 視覺化案例集,在這裡你能發現許多新奇的例項,也能發掘出不一樣的工業網際網路:https://mp.weixin.qq.com/s/ZbhB6LO2kBRPrRIfHlKGQA       同時,你也可以檢視更多案例及效果:https://www.hightopo.com/demos/index.ht