用 WebGL 渲染的 3D 機房如今也不是什麼新鮮事兒了,這篇文章的主要目的是說明一下,3D 機房中的 eye 和 center 的問題,恰好在項目中用上了,好生思考了一番,最終以爲這個例子最符合個人要求,就拿來做爲記錄。html
http://hightopo.com/demo/3DRo...
這個 3D 機房的 Demo 作的還不錯,比較美觀,基礎的交互也都知足,接下來看看怎麼實現。node
首先從 index.html 中調用的 js 路徑順序一個一個打開對應的 js,server.js 中自定義了一個 Editor.Server 類由 HT 封裝的 ht.Default.def 函數建立的(注意,建立的類名 Editor.Server 前面的 Editor 不能用 E 來替代):json
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); } });
這個類能夠建立一個 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 類的定義方法差很少:app
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.Server 服務器組件類要相對複雜一點,這個類中建立了一個櫃身、櫃門以及機櫃內部的服務器組件:dom
var C = E.Cabinet = function(obj) { var color = obj.color, doorFrontImg = obj.doorFrontImg, doorBackImg = obj.doorBackImg, s3 = obj.s3; var node = this._node = new ht.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: 'rgba(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 文件。