用WebGL 渲染的3D 機房現在也不是什麼新鮮事兒了,這篇文章的主要目的是說明一下,3D 機房中的eye 和center 的問題,剛好在項目中用上了,好生思考了一番,最終覺得這個例子最符合我的要求,就拿來當記錄。
效果圖這3D 機房的Demo 做的還不錯,比較美觀,基礎的互動也都滿足,接下來看看怎麼實現。
程式碼生成定義類別首先從index.html 中呼叫的js 路徑順序一個一個開啟對應的js,server.js 中自訂了一個Editor.Server 類別由HT 封裝的ht.Default.def 函式建立的(注意,建立的類別名稱Editor .Server 前面的Editor 不能用E 來取代):
ht.Default.def('Editor.Server', Object, {//第一個參數為類別名,如果為字串,自動註冊到HT的classMap中;第二個參數為此要繼承的父類;第三個參數為方法和變數的宣告addToDataModel: function(dm) { //將節點加入資料容器dm.add(this._node);// ht 中的預設函數,將節點經由add方法加入資料容器}, setHost: function() { //設定吸附this._node.setHost.apply(this._node, arguments); }, s3: function() {//設定節點的大小this._node .s3.apply(this._node, arguments); }, setElevation: function() {//控制Node圖元中心位置所在3D座標系的y軸位置this._node.setElevation.apply(this._node, arguments); }});
建立Editor.Server 類
這個類別可以建立一個ht.Node 節點,並設定節點的顏色和前面貼圖:
var S = E.Server = function(obj) {//伺服器元件var color = obj.color, frontImg = obj.frontImg; var node = this._node = new ht.Node();//建立節點node.s ({//設定節點的樣式s 為setStyle 的縮寫'all.color': color,//設定節點六面的顏色'front.image': frontImg //設定節點正面的圖片});};
這樣我在需要建立伺服器元件的位置直接new 一個新的伺服器元件物件即可,並且能夠直接呼叫我們上面聲明的setHost 等函數,很快我們就會用上。
接下來建立Editor.Cabinet 機櫃類,方法跟上面Editor.Server 類別的定義方法差不多:
ht.Default.def('Editor.Cabinet', Object, { addToDataModel: function(dm) { dm.add(this._door); dm.add(this._node); this._serverList.forEach(function(s) { s.addToDataModel(dm); }); }, p3: function() { this._node.p3.apply(this._node, arguments);//設定節點的3d 座標}});
建立Editor.Cabinet 類
這個類別相對於前面的Editor.Server 伺服器元件類別要相對複雜一點,這個類別中創建了一個櫃身、櫃門以及機櫃內部的伺服器元件:
var C = E.Cabinet = function(obj) { var color = obj.color, doorFrontImg = obj.doorFrontImg, doorBackImg = obj.doorBackImg, s3 = obj.s3; var node = this._node = new Node.(Node) ; //櫃身node.s3(s3);//設定節點的大小為setSize3d node.a('cabinet', this);//自訂cabinet 屬性node.s({//設定節點的樣式為setStyle 'all. color': color,//設定節點六面的顏色'front.visible': false//設定節點前面是否可見}); if (Math.random() > 0.5) { node.addStyleIcon('alarm', {//向節點上新增icon 圖示names: ['icon 溫度計'],//包含多個字串的數組,每個字串對應一張圖片或向量(透過ht.Default.setImage註冊) face: 'top',//預設值為front,圖示在3D下的朝向,可取值left|right|top|bottom|front|back|center position: 17,//指定icons的位置autorotate: 'y',//預設值為false,圖示在3D下是否自動朝向眼睛的方向t3: [0, 16, 0],//預設值為undefined,圖示在3D下的偏移,格式為[x,y,z] width: 37,//指定每個icon的寬度,預設根據註冊圖片時的寬度height: 32,//指定每個icon的高度,預設根據註冊圖片時的高度textureScale: 4,//預設值為2,該值代表記憶體實際產生貼圖的倍數,不宜設定過大否則影響效能visible: { func : function() { return !!E.alarmVisible; }}//表示群組圖片是否顯示}); } var door = this._door = new ht.DoorWindow();//櫃門door.setWidth(s3[0]);//置圖元在3D拓撲中的x軸方向的長度door.setHeight(1);//設定圖元在3D拓撲中的z軸長度door.setTall(s3[1]);//控制Node圖元在y軸的長度door.setElevation(0);//設定圖元中心在3D座標系的y座標door.setY(s3[2 ] * 0.5);//設定節點在y 軸的位置door.setHost(node);//設定吸附door.s({//設定節點樣式setStyle 'all.color': color,//設定節點六面顏色'front.image': doorFrontImg,//設定節點正面圖片'front.transparent': true,//設定節點正面是否透明'back.image': doorBackImg,//設定節點背面的圖片'back.uv': [1,0, 1,1, 0,1, 0,0],//自訂節點後面uv貼圖,為空採用預設值[0, 0, 0,1, 1,1, 1,0] 'dw.axis': 'right'//設定DoorWindow圖元展開和關閉操作的旋轉軸,可取值left|right|top|bottom|v| h }); var serverList = this._serverList = []; var max = 6, list = E.randomList(max, Math.floor(Math.random() * (max - 2)) + 2); //global. js 中宣告的取得隨機數的函式var server, h = s3[0] / 4; list.forEach(function(r) { var server = new E.Server({ //伺服器元件color: 'rgb(51,49,49)', frontImg: '伺服器元件精細' }); server.s3(s3[0] - 2, h, s3[2] - 4);//設定節點大小server.setElevation((r - max * 0.5) * (h + 2));//設定節點中心點在y軸的座標server.setHost(node);//設定節點的吸附serverList.push(server);//在serverList 中加入server 節點});};
上面程式碼中唯一沒提到的是Editor.randomList 函數,這個函數是在global.js 檔案中宣告的,宣告如下:
var E = window.Editor = { leftWidth: 0, topHeight: 40, randomList: function(max, size) { var list = [], ran; while (list.length < size) { ran = Math.floor(Math. random() * max); if (list.indexOf(ran) >= 0) continue; list.push(ran); } return list; }};
好了,場景中的各個部分的類別都創建完成,那我們就該將場景創建起來,然後將這些圖元都堆進去!
場景創建如果熟悉的同學應該知道,用HT 建立一個3D 場景只需要new 一個3D 元件,再將透過addToDOM 函數將這個場景加入到body 中即可:
var g3d = E.main = new ht.graph3d.Graph3dView(); //3d 場景
main.js 檔案中主要做的是在3D 場景中一些必要的元素,例如牆面,地板,門,空調以及所有的機櫃的生成和排放位置,還有非常重要的互動部分。
牆體,地板,門,空調和機櫃的創建我就不貼代碼出來了,有興趣的請自行查看代碼,這裡主要說一下雙擊機櫃以及與機櫃有關的任何物體(櫃門,服務器設備)則3D中camera 的視線就會移動到雙擊的機櫃的前方某個位置,而且這個移動是非常順滑的,之前技藝不精,導致這個部分想了很久,最後參考了這個Demo 的實現方法。
為了能夠重複設定eye 和center,將設定這兩個參數對應的內容封裝為setEye 和setCenter 方法,setCenter 方法與setEye 方法類似,這裡不重複贅述:
// 設定眼睛位置var setEye = function(eye, finish) { if (!eye) return; var e = g3d.getEye().slice(0),//取得目前eye 的值dx = eye[0] - e[0], dy = eye[1] - e[1], dz = eye[2] - e[2]; // 啟動500毫秒的動畫過度ht.Default.startAnim({ duration: 500, easing: easing,//動畫緩動函數finishFunc: finish || function() {}, //動畫結束後呼叫的函數 action: function(v, t) {//設定動畫v代表透過easing(t)函數運算後的值,t代表目前動畫進行的進度[0~1],一般屬性變化根據v參數進行g3d.setEye([ //設定3D 場景中的eye 眼睛的值,為一個數組,分別對應x,y,z 軸的值e[0] + dx * v, e[1] + dy * v, e[2] + dz * v ]); } });};
我沒有重複聲明setCenter 函數不代表這個函數不重要,恰恰相反,這個函數在視線移動的過程中起到了決定性的作用,上面的setEye 函數相當於我想走到我的目標位置的前面(至少我定義的時候是這種用途),而sCenter的定義則是將我的視線移到了目標的位置(例如我可以站在我現在的位置看我右後方的物體,也可以走到我右後方去,站在那個物體前面看它),這點非常重要,請大家好好品味一下。
雙擊事件倒是簡單,只要監聽HT 封裝好的事件,判斷事件類型,並作出對應的動作即可:
g3d.mi(function(e) {//addInteractorListener 事件監聽函數if (e.kind !== 'doubleClickData') //判斷事件類型為雙擊節點return; var data = e.data, p3; if (data. a('cabinet')) //機身p3 = data.p3(); else { host = data.getHost(); //取得點擊節點的吸附物件if (host && host.a('cabinet')) {//如果吸附物件為cabinet p3 = host.p3(); } } if (!p3) return; setCenter(p3) ; //設定center 目標的要移向位置為cabinet 的位置setEye([p3[0], 211, p3[2] + 247]); //設定eye眼睛要移向的位置});頂部導覽列
一開始看到這個例子的時候我在想,這人好厲害,我用HT 這麼久,用HT 的ht.widget.Toolbar 還沒能做出這麼漂亮的效果,看著看著發現這原來是用form 表單做的,厲害厲害,我真是太愚鈍了。
var form = E.top = new ht.widget.FormPane(); //頂部表單元件form.setRowHeight(E.topHeight);//設定行高form.setVGap(-E.topHeight);//設定表單元件水平間距設定為行高的負值則可以讓多行處於同一行form.setVPadding(0);//設定表單頂部和頂部與元件內容的間距form.addRow([null, {//在表單中新增一行元件,第一個參數為元素數組,元素可為字串、json格式描述的元件參數資訊、html元素或為null image: { icon: './symbols/inputBG.json ', stretch: 'centerUniform' }}], [40, 260]);//第二個參數為每個元素寬度資訊數組,寬度值大於1代表固定絕對值,小於等於1代表相對值,也可為80+0.3的組合form.addRow([null, null , { id: 'searchInput', textField: {}}, { element: '機房視覺化管理系統', color: 'white', font: '18px arial, sans-serif'}, null, { button: { // label: '視圖切換', icon: './symbols/viewChange.json', background: null, selectBackground: 'rgb(128,128,128)', borderColor: 'rgb(128,128,128)', borderColor: 'gba (0, 0, 0, 0)', onClicked: function() { E.focusTo(); } }}, null, { button: { // label: '告警', icon: './symbols/alarm.json', togglable: true, selected: false, background: null, selectBackground: 'rgb(128,128,128)', borderColor: 'rgba(0, 0, 0, 0)', onClicked: function(e) { E.setAlarmVisible(this.isSelected()); } }}, null], [40, 42, 218, 300, 0.1, 50, 10, 50, 10]);
以上都只是能實現,但是並沒有真正地添加進html 標籤中,也就意味著,現在介面上什麼都沒有!別忘了在頁面載入的時候將3D 場景加入到body 中,同時也別忘了將form 表單加入到body 中,並且設定視窗大小變化事件時,form 表單也需要即時更新:
window.addEventListener('load', function() { g3d.addToDOM(); //將3D 場景加入body 中document.body.appendChild(E.top.getView()); //將form 表單元件底層div在加入body 中window.addEventListener('resize', function() {//視窗大小變化事件監聽E.top.iv();//更新form表單的底層div });});
這裡說明一下addToDOM 函數,對於了解HT 的機制非常重要。 HT 的元件通常會嵌入BorderPane、SplitView 和TabView 等容器中使用,而最外層的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(), style = view.style; document.body.appendChild(view); //將場景的底層div 加入body 中style.left = ' 0';//HT 預設將所有的元件底層div的position設定為absolute style.right = '0'; style.top = '0'; style.bottom = '0'; window.addEventListener('resize', function () { self.iv(); }, false); //視窗大小變更監聽事件,通知元件變更更新}
這樣,所有的程式碼就結束了,可以自己右鍵檢查,network 中可以取得相對應的json 檔案。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持VeVb武林網。