基於 HTML5 Canvas 的 3D 機房建立

圖撲軟體發表於2017-12-24

對於 3D 機房來說,監控已經不是什麼難事,不同的人有不同的做法,今天試著用 HT 寫了一個基於 HTML5 的機房,發現果然 HT 簡單好用。本例是將燈光、霧化以及 eye 的最大最小距離等等功能在 3D 機房中進行的一個綜合性的例子。接下來我將對這個例子的實現進行解析,算是自己對這個例子的一個總結吧。整個例子因為沒有設計師的參與,所以樣式上可能比較簡陋,但是在一些細節的地方,比如牆上的貼圖、門框嵌入以及滅火器等等,都是儘可能地還原真實的場景,也是花了些心思做這個例子的!

本例地址:http://www.hightopo.com/guide/guide/core/3d/examples/example_3droom.html

本例動態圖:

圖片描述

從最基礎的開始,場景的佈置,照 HTML 的思路,這個場景就是將整個頁面放在一個 div 中,再向這個 div 中新增一個頂部的 div 以及一箇中間部分的 div,說實在,如果用 HTML 實現這個步驟,我們要寫的程式碼不多,但是如果要寫幾次這段程式碼,想必誰都會覺得煩吧。。。HT 將這種實現方法封裝了起來,整個外部的 div 就是 HT 的基礎元件,這裡我們用到的是 ht.widget.BorderPane 元件,上部的 div 封裝在 ht.widget.Toolbar 工具條類中,下部分是 3D 部分 ht.graph3d.Graph3dView 元件,將頁面中的兩個部分通過下面的方式新增進 BorderPane 元件中,並將 BorderPane 新增進 body 體中:

toolbar = new ht.widget.Toolbar(items);                                                                                                
dataModel = new ht.DataModel();
g3d = new ht.graph3d.Graph3dView(dataModel);    
g3d.getView().style.background = 'black';
g3d.setGridSize(100);
g3d.setGridGap(100); 
g3d.setFar(30000);
g3d.setOrthoWidth(5000);  
g3d.setFogNear(100);//預設為1,代表從該距離起物體開始受霧效果影響
g3d.setFogFar(8000);
g3d.reset = reset;
g3d.reset();
g3d.enableToolTip();                                 
borderPane = new ht.widget.BorderPane();
borderPane.setTopView(toolbar);
borderPane.setCenterView(g3d);       

view = borderPane.getView();  
view.className = 'main';
document.body.appendChild(view);
window.addEventListener('resize', function (e) {
    borderPane.invalidate();
}, false);

上面程式碼將 borderPane.getView() 新增進 body 體中,是因為 Graph3dView 的介面 DOM 結構是由最底層的 div 元素,以及渲染層 canvas 元素組合而成,通過 getView() 可得到最底層的 div 元素。

我們還注意到上面程式碼中沒有提及的一個引數 items,這個 items 是 toolbar 工具條的元素,一個陣列元素,下面展示一下程式碼,這裡簡單地解釋了一下,詳細資訊請參考 HT for Web 工具條手冊

items = [ //工具條中的元素陣列
    {
        label: 'White',//工具條元素的標籤文字
        groupId: 'headLightColor',//對工具條元素進行分組,同一個分組內的元素選中會自動出現互斥效果
        selected: true,//工具條元素是否被選中,值為true或false,對核取方塊、開關按鈕和單選按鈕有效
        action: function(){// 函式型別,工具條元素被點選時呼叫
            g3d.setHeadlightColor('white');
        }
    },        
    {
        label: 'Red',
        groupId: 'headLightColor',
        action: function(){                             
            g3d.setHeadlightColor('red');
        }
    }, 
    {
        label: 'Blue',
        groupId: 'headLightColor',
        action: function(){                             
            g3d.setHeadlightColor('blue');
        }
    },        
    {
        label: 'Yellow',
        groupId: 'headLightColor',        
        action: function(){                             
            g3d.setHeadlightColor('yellow');
        }
    },        
    {
        id: 'step',                        
        unfocusable: true,//工具條元素是否不可獲取焦點,預設滑鼠滑過時會顯示一個矩形邊框,可設定為true關閉此效果
        slider: {
            width: 70,
            step: 500,
            min: 0,
            max: 10000,
            value: 0,                            
            onValueChanged: function(){
                g3d.setHeadlightRange(this.getValue());
            }       
        }                
    },
    'separator', //表示分隔條
    {
        label: 'Fog',
        type: 'check',//工具條元素型別,check表示核取方塊,toggle表示開關按鈕,radio表示單選按鈕                        
        action: function(){
            g3d.setFogDisabled(!this.selected);
        }
    },
    {
        label: 'White',
        groupId: 'fogColor',
        selected: true,
        action: function(){                             
            g3d.setFogColor('white');
        }
    },                     
    {
        label: 'Red',
        groupId: 'fogColor',
        action: function(){                             
            g3d.setFogColor('red');
        }
    },        
    {
        label: 'Yellow',
        groupId: 'fogColor',        
        action: function(){                             
            g3d.setFogColor('yellow');
        }
    },
    {                       
        unfocusable: true,
        label: 'FogNear',
        slider: {
            width: 70,
            min: 10,
            max: 4000,
            value: 100,                            
            onValueChanged: function(){
                g3d.setFogNear(this.getValue());
            }       
        }                
    },                        
    {                       
        unfocusable: true,
        label: 'FogFar',
        slider: {
            width: 70,
            min: 5000,
            max: 15000,
            value: 8000,                            
            onValueChanged: function(){
                g3d.setFogFar(this.getValue());
            }       
        }                
    },                    
    'separator', 
    {
        id: 'movable',
        label: 'Movable',
        type: 'check',
        selected: false                
    }, 
    {
        label: 'Editable',
        type: 'check',
        selected: false,
        action: function(item){
            g3d.setEditable(item.selected);
        }
    },
    {
        label: 'Wireframe',
        type: 'check',
        selected: false,
        action: function(item){
            if(item.selected){
                dataModel.each(function(data){
                    data.s({
                        'wf.visible': 'selected',
                        'wf.color': 'red'                                        
                    });
                });                                
            }else{
                dataModel.each(function(data){
                    data.s({
                        'wf.visible': false                                       
                    });
                });                               
            }                            
        }
    },
    {
        type: 'toggle',
        label: 'Orthographic Projection',                        
        action: function(item){  
            g3d.setOrtho(item.selected);                           
        }                    
    },
    {
        type: 'toggle',
        selected: false,
        label: 'First Person Mode',
        action: function(item){
            g3d.setFirstPersonMode(item.selected);  
            g3d.reset();
        }
    },                                                       
    'separator',                      
    {
        type: 'check',
        label: 'Origin Axis',
        selected: false,
        action: function(item){
            g3d.setOriginAxisVisible(item.selected);                           
        }
    },   
    {
        type: 'check',
        label: 'Center Axis',
        selected: false,
        action: function(item){
            g3d.setCenterAxisVisible(item.selected);                           
        }
    },                             
    {
        type: 'check',
        label: 'Grid',
        selected: false,
        action: function(item){
            g3d.setGridVisible(item.selected);                           
        }
    },                    
    {
        label: 'Export Image',
        action: function(){                             
            var w = window.open();
            w.document.open();                            
            w.document.write("<img src='" + g3d.toDataURL(g3d.getView().style.background) + "'/>");
                            w.document.close();
        }
    }                    
];

接下來就是建立場景中的各個模型,首先是三個燈光,中間部分一個,左右後方各一個(我將光源標記出來了,看圖~),顏色分別為綠、紅、藍,以及地板:

圖片描述

redLight = new ht.Light();//燈光類
redLight.p3(-1600, 200, -2200);
redLight.s({
    'light.color': 'red',
    'light.range': 1000
});
dataModel.add(redLight);

blueLight = new ht.Light();
blueLight.p3(1600, 200, -2200);
blueLight.s({
    'light.color': 'blue',
    'light.range': 1000
});
dataModel.add(blueLight);                                   

greenLight = new ht.Light();
greenLight.p3(-800, 400, -200);
greenLight.s({
    'light.center': [-300, 0, -900],
    'light.type': 'spot',
    'light.color': 'green',
    'light.range': 4000,
    'light.exponent': 10
});
dataModel.add(greenLight);                

floor = createNode([0, -5, -1500], [5200, 10, 4200]);
floor.s({                             
    'shape3d': 'rect',
    'shape3d.top.color': '#F0F0F0'                    
});  

接下來將場景中間部分的伺服器、兩邊的伺服器、滅火器,牆上的空調、牆背面的空調等等等等都新增進場景中:

for(i=0; i<3; i++){
    for(j=0; j<3; j++){
        createServer1(250+i*280, -1200-500*j, j === 2);
        createServer1(-250-i*280, -1200-500*j, j === 1);
    }
}

//建立22的伺服器 放在場景的兩邊
for(i=0; i<2; i++){
    for(j=0; j<2; j++){
        createServer2(1500+i*200, -700-500*j, 
                            (i === 1 ? (j === 1 ? '#00FFFF' : '#C800FF') : null));
        createServer2(-1500-i*200, -700-500*j, 
                            (j === 1 ? (i === 1 ? 'red' : 'yellow') : null));
    }
}                                          

// fire extinguisher
createFireExtinguisher(1300, -1800, [0.45, 0]); 
createFireExtinguisher(-1300, -1800); 
createFireExtinguisher(1100, -2450);   
createFireExtinguisher(-1100, -2450, [0.45, 0]);  

// air condition
createNode([1120, 170, -700], [80, 340, 170]).s({
    'all.color': '#EDEDED',
    'left.image': 'stand'
}).setToolTip('Air Conditioner'); 
createNode([-1120, 170, -700], [80, 340, 170]).s({
    'all.color': '#EDEDED',
    'right.image': 'stand'
}).setToolTip('Air Conditioner'); 
createNode([1680, 400, -1850], [370, 120, 60]).s({
    'all.color': '#767676',
    'front.image': 'air1'
}).setToolTip('Air Conditioner');             
createNode([-1680, 400, -1850], [370, 120, 60]).s({
   'all.color': '#767676',
   'front.image': 'air2'
}).setToolTip('Air Conditioner'); 
for(i=0; i<2; i++){
    createNode([300+i*580, 90, -2630], [260, 180, 60]).s({
        'all.color': '#EDEDED',
        'back.image': 'air3'
    }).setToolTip('Air Conditioner');  
    createNode([-300-i*580, 90, -2630], [260, 180, 60]).s({
        'all.color': '#EDEDED',
        'back.image': 'air3'
    }).setToolTip('Air Conditioner');                      
}

其中 createServer1 方法是用來建立伺服器的方法,由一個 ht.Node 作為機身以及 ht.Shape 作為機門組合而成,並在機櫃的內部新增了隨機數臺裝置:

function createServer1(x, z, disabled){//建立伺服器 1(中間部分)
    var h = 360, w = 150, d = 170, k = 10,
         main = createNode([0, 0, 0], [w, h, d]),
         face = new ht.Shape(),
         s = {'all.visible': false, 'front.visible': true};  //設定node節點只有前面可見       

    dataModel.add(face);
    face.addPoint({x: -w/2, y: d/2-1});//門上的三個點
    face.addPoint({x: w/2, y: d/2-1});
    face.addPoint({x: w+w/2, y: d/2-1});
    face.setSegments([1, 2, 1]);                
    face.setTall(h);
    face.setThickness(1);//設定厚度      
    face.s(s);                
    face.setHost(main);//吸附在main節點上

    main.s({
        'all.color': '#403F46',
        'front.visible': false
    });                

    if(!disabled){                
        face.s({
            'all.color': 'rgba(0, 40, 60, 0.7)',
            'all.reverse.flip': true,//反面是否顯示正面的東西
            'all.transparent': true//設定為透明
       });
       face.face = true;//初始化face屬性             
       face.open = false;
       face.angle = -Math.PI * 0.6;                       
       face.setToolTip('Double click to open the door');

       var up = createNode([0, h/2-k/2, d/2], [w, k, 1]).s(s),
            down = createNode([0, -h/2+k/2, d/2], [w, k, 1]).s(s),
            right = createNode([w/2-k/2, 0, d/2], [k, h, 1]).s(s),
            left = createNode([-w/2+k/2, 0, d/2], [k, h, 1]).s(s);

       up.setHost(face);
       down.setHost(face);
       left.setHost(face);
       right.setHost(face);

       //隨機建立機櫃中的裝置數量 2個或者4個
       var num = Math.ceil(Math.random() * 2),
       start = 20 + 20 * Math.random();
       for(var i=0; i<num; i++){
           var node = createNode([0, start+45*i, 0], [w-6, 16, d-30]);
           node.setHost(main);
           node.s({
               'front.image': 'server',
               'all.color': '#E6DEEC',
               'all.reverse.cull': true
           });
           node.pop = false;//初始化裝置“彈出”的屬性
           node.setToolTip('Double click to pop the server');

           var node = createNode([0, -start-45*i, 0], [w-6, 16, d-30]);
           node.setHost(main);
           node.s({
               'front.image': 'server',
               'all.color': '#E6DEEC',
               'all.reverse.cull': true
           }); 
           node.pop = false;
           node.setToolTip('Double click to pop the server');
       }                    
   }

   main.p3(x, h/2+1, z);
}

這個伺服器我們還製作了門的開啟以及關閉的動作,還有伺服器內部的裝置的彈出以及恢復位置。首先,對 3d 元件新增了過濾函式,對雙擊事件的元素過濾:

圖片描述

g3d.onDataDoubleClicked = function(data, e, dataInfo){//圖元被雙擊時回撥
    if(data.face){//face是定義門時候開啟的屬性 是在節點定義的時候新增的 下面的 pop 也是一樣
        data.getHost().getAttaches().each(function(attach){//遍歷吸附到自身的所有節點的ht.List型別陣列
            if(attach.pop){//pop 是定義裝置是否彈出的屬性
                toggleData(attach);
            }
       });
    }
    toggleData(data, dataInfo.name);
};

以下是對 toggleData 的定義:

function toggleData(data, name){//開關門 以及 裝置彈出縮回的動作
    var angle = data.angle,
    pop = data.pop;

    if(angle != null){
        if(anim){
            anim.stop(true);//ht內建的函式 引數為true時終止動畫
        }
        var oldAngle = data.window ? data.getRotationX() : data.getRotation();
        if(data.open){
            angle = -angle;
        }
        data.open = !data.open;
        anim = ht.Default.startAnim({
            action: function(v){
                if(data.window){
                    data.setRotationX(oldAngle + v * angle);    
               }else{
                   data.setRotation(oldAngle + v * angle);
               }                                
           }
       });
   }
   else if(pop != null){
        if(anim){
           anim.stop(true);
        }
        var p3 = data.p3(),
            s3 = data.s3(),
            dist = pop ? -s3[2] : s3[2];
        data.pop = !data.pop;                        
        if(data.pop){
            data.s({
                'note': 'Detail...',  
                'note.background': '#3498DB',
                'note.font': '26px Arial',
                'note.position': 6,
                'note.t3': [-30, -3, 30],
                'note.expanded': true,
                'note.toggleable': false,
                'note.autorotate': true                        
            });                                     
        }else{
             data.s('note', null);
        }                                                
        anim = ht.Default.startAnim({
             action: function(v){
                 data.p3(p3[0], p3[1], p3[2] + v * dist);                                                               
             }
        }); 
    }            
}

中間部分的伺服器我就不贅述了,這裡聊一下比較有趣的部分,滅火器模型的製作,並不是 Max3d 製作而成的,而是程式碼生成的:

圖片描述

從圖中可以看出來,這個滅火器是由底部一個圓柱,中間一個半圓形以及頂部一個小圓柱並在上面貼圖合成的:

function createFireExtinguisher(x, z, uvOffset){//建立滅火器
    var w = 80, 
          h = 200,
          body = createNode([0, 0, 0], [w, h, w]).s({//身體的圓柱
              'shape3d': 'cylinder',//圓柱
              'shape3d.image': 'fire',
              'shape3d.uv.offset': uvOffset,//決定3d圖形整體貼圖的uv偏移
              'shape3d.reverse.cull': true
          }),                    
          sphere = createNode([0, h/2, 0], [w, w, w]).s({
              'shape3d': 'sphere',//球體
              'shape3d.color': '#F20000',
              'shape3d.reverse.cull': true
          }),
          top = createNode([0, h/2+w/3, 0], [w/2, w/2, w/2]).s({//頭部的圓柱
              'shape3d': 'cylinder',
              'shape3d.color': '#242424',
              'shape3d.reverse.cull': true
          });

    sphere.setHost(body);//設定吸附 只要body移動sphere也會移動
    top.setHost(sphere);
    body.p3([x, h/2, z]);
    body.setToolTip('Fire Extinguisher');
    sphere.setToolTip('Fire Extinguisher');
    top.setToolTip('Fire Extinguisher');
} 

以上!這個 3d 機房的例子非常有代表性,效能也展示得很全面,覺得有必要拿出來講一下,希望能對你們有一定的幫助~

相關文章