基於 HTML5 WebGL 的 3D 模型斜面生成

前言

3D 場景中的面不只有水平面這一個,空間是由無數個面組成的,所以我們有可能會在任意一個面上放置物體,而空間中的面如何確定呢?我們知道,空間中的面可以由一個點和一條法線組成。這個 Demo 左側爲面板,從面板中拖動物體到右側的 3D 場景中,當然,我鼠標拖動到的位置就是物體放置的點,但是這次我們的重點是如何在斜面上放置模型。

效果圖:
圖片描述

代碼生成

創建場景

dm = new ht.DataModel(); // 數據模型(http://hightopo.com/guide/guide/core/datamodel/ht-datamodel-guide.html)
g3d = new ht.graph3d.Graph3dView(dm); // 3D 場景組件(http://hightopo.com/guide/guide/core/3d/ht-3d-guide.html)
palette = new ht.widget.Palette(); // 面板組件(http://hightopo.com/guide/guide/plugin/palette/ht-palette-guide.html)
splitView = new ht.widget.SplitView(palette, g3d, 'h', 0.2); // 分割組件,第三個參數爲分割的方式 h 爲左右分,v 爲上下分;第四個參數爲分割比例,大於 1 的值爲絕對寬度,小於 1 則爲比例
splitView.addToDOM();//將分割組件添加進 body 體中

關於這些組件的定義可以到對應的鏈接裏面查看,至於將分割組件添加進 body 體中的 addToDOM 函數有必要解釋一下(我每次都提,這個真的很重要!)。

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(), // 獲取組件的底層 div
        style = view.style;
    document.body.appendChild(view); // 將組件底層 div 添加進 body 中
    style.left = '0'; // ht 默認將所有的組件的 position 都設置爲 absolute 絕對定位
    style.right = '0';
    style.top = '0';
    style.bottom = '0';
    window.addEventListener('resize', function () { self.iv(); }, false); // 窗口大小改變事件,調用刷新函數
}

大家可能注意到了,場景中我添加的斜面實際上就是一個 ht.Node 節點,作爲與地平面的參照,在這樣的對比下立體感會更強一點。下面是這個節點的定義:

node = new ht.Node();
node.s3(1000, 1, 1000); // 設置節點的大小
node.r3(0, 0, Math.PI/4); // 設置節點旋轉 這個旋轉的角度是有學問的,跟下面我們要設置的拖拽放置的位置有關係
node.s('3d.movable', false); // 設置節點在 3d 上不可移動 因爲這個節點只是一個參照物,建議是不允許移動
dm.add(node); // 將節點添加進數據容器中

左側內容構建

圖片描述
Palette 和 GraphView 類似,由 ht.DataModel 驅動,用 ht.Group 展示分組,ht.Node 展示按鈕元素。我將加載 Palette 面板中的圖元函數封裝爲 initPalette,定義如下:

function initPalette() { // 加載 palette 面板組件中的圖元
    var arrNode = ['displayDevice', 'cabinetRelative', 'deskChair', 'temperature', 'indoors', 'monitor','others'];
    var nameArr = ['展示設施', '機櫃相關', '桌椅儲物', '溫度控制', '室內', '視頻監控', '其他']; // arrNode 中的 index 與 nameArr 中的一一對應
    
    for (var i = 0; i < arrNode.length; i++) {
        var name = nameArr[i];
        var vName = arrNode[i];

        arrNode[i] = new ht.Group(); // palette 面板是將圖元都分在“組”裏面,然後向“組”中添加圖元即可
        palette.dm().add(arrNode[i]); // 向 palette 面板組件中添加 group 圖元
        arrNode[i].setExpanded(true); // 設置分組爲打開的狀態
        arrNode[i].setName(name); // 設置組的名字 顯示在分組上
        
        var imageArr = [];
        switch(i){ // 根據不同的分組設置每個分組中不同的圖元
            case 0:
                imageArr = ['models/機房/展示設施/大屏.png'];
                break;
            case 1: 
                imageArr = ['models/機房/機櫃相關/配電箱.png', 'models/機房/機櫃相關/室外天線.png', 'models/機房/機櫃相關/機櫃1.png', 
                            'models/機房/機櫃相關/機櫃2.png', 'models/機房/機櫃相關/機櫃3.png', 'models/機房/機櫃相關/機櫃4.png', 
                            'models/機房/機櫃相關/電池櫃.png'];
                break;
            case 2: 
                imageArr = ['models/機房/桌椅儲物/儲物櫃.png', 'models/機房/桌椅儲物/桌子.png', 'models/機房/桌椅儲物/椅子.png'];
                break;
            case 3: 
                imageArr = ['models/機房/溫度控制/空調精簡.png', 'models/機房/消防設施/消防設備.png'];
                break;
            case 4:
                imageArr = ['models/室內/辦公桌簡易.png', 'models/室內/書.png', 'models/室內/辦公桌鏡像.png', 'models/室內/辦公椅.png'];
                break;
            case 5:
                imageArr = ['models/機房/視頻監控/攝像頭方.png', 'models/機房/視頻監控/對講維護攝像頭.png', 'models/機房/視頻監控/微型攝像頭.png'];
                break;
            default: 
                imageArr = ['models/其他/信號塔.png'];
                break;
        }
        setPalNode(imageArr, arrNode[i]); // 創建 palette 上節點及設置名稱、顯示圖片、父子關係
    }
}

我在 setPalNode 函數中做了一些名稱的設置,主要是想要根據上面 initPalette 函數中我傳入的路徑名稱來設置模型的名稱以及在不同文件在不同的文件夾下的路徑:

function setPalNode(imageArr, arr) {
    for (var j = 0; j < imageArr.length; j++) {
        var imageName = imageArr[j];
        var jsonUrl = imageName.slice(0, imageName.lastIndexOf('.')) + '.json'; // shape3d 中的 json 路徑
        var name = imageName.slice(imageName.lastIndexOf('/')+1, imageName.lastIndexOf('.')); // 取最後一個/和.之間的字符串用來設置節點名稱
        var url = imageName.slice(imageName.indexOf('/')+1, imageName.lastIndexOf('.')); // 取第一個/和最後一個.之間的字符串用來設置拖拽生成模型 obj 文件的路徑
        
        createNode(name, imageName, arr, url, jsonUrl); // 創建節點,這個節點是顯示在 palette 面板上
    }
}

createNode 創建節點的函數比較簡單:

function createNode(name, image, parent, urlName, jsonUrl) { // 創建 palette 面板組件上的節點
    var node = new ht.Node();
    palette.dm().add(node);
    node.setName(name); // 設置節點名稱 palette 面板上顯示的文字也是通過這個屬性設置名稱
    node.setImage(image); // 設置節點的圖片
    node.setParent(parent); // 設置父親節點
    node.s({
        'draggable': true, // 設置節點可拖拽
        'image.stretch': 'centerUniform', // 設置節點圖片的繪製方式
        'label': '' // 設置節點的 label 爲空,這樣即使設置了 name 也不會顯示在 3d 中的模型下方
    });
    node.a('urlName', urlName); // a 設置用戶自定義屬性
    node.a('jsonUrl', jsonUrl);
    return node;
}

雖然簡單,但是還是要提一下,draggable: true 爲設置節點可拖拽,否則節點不可拖拽;還有 node.s 是 HT 默認封裝好的樣式設置方法,如果用戶需要自己添加方法,則可通過 node.a 方法來添加,參數一爲用戶自定義名稱,參數二爲用戶自定義值,不僅能傳常量,也能傳變量、對象,還能傳函數!又是一個非常強大的功能。

拖拽功能

拖拽基本上就是響應 windows 自帶的 dragover 以及 drop 事件,要在放開鼠標的時候創建模型,就要在事件觸發時生成模型:

function dragAndDrop() { // 拖拽功能
    g3d.getView().addEventListener("dragover", function(e) { // 拖拽事件
        e.dataTransfer.dropEffect = "copy";
        handleOver(e);
    });
    g3d.getView().addEventListener("drop", function(e) { // 放開鼠標事件
        handleDrop(e);
    });
}

function handleOver(e) {
    e.preventDefault(); // 取消事件的默認動作。
}

function handleDrop(e) { // 鼠標放開時
    e.preventDefault(); // 取消事件的默認動作。
    
    var paletteNode = palette.dm().sm().ld(); // 獲取 palette 面板中最後選中的節點
    if (paletteNode) {
        loadObjFunc('assets/objs/' + paletteNode.a('urlName') + '.obj', 'assets/objs/' + paletteNode.a('urlName') + '.mtl', 
                             paletteNode.a('jsonUrl'), g3d.getHitPosition(e, [0, 0, 0], [-1, 1, 0])); // 加載obj模型
    }
}

這裏完全有必要說明一下,這個 Demo 的重點來了! loadObjFunc 函數中的最後一個參數爲生成模型的 position3d 座標,g3d.getHitPosition 這個方法總共有三個參數,第一個參數爲事件類型,第二和第三個參數如果不設置,則默認爲水平面的中心點也就是 [0, 0, 0] 以及法線爲 y 軸,也就是 [0, 1, 0],一條法線和一個點就可以確定一個面,所以我們通過這個方法來設置這個節點所要放置的平面是在哪一個面上,我前面將 node 節點設置爲繞 z 軸旋轉 45° 角,所以這邊的法線也就要好好想想如何設置了,這是數學上的問題,要自己思考了。

加載模型

HT 通過 ht.Default.loadObj 函數來加載模型,但是前提是要有一個節點,然後再在這個節點上加載模型:

function loadObjFunc(objUrl, mtlUrl, jsonUrl, p3) { // 加載 obj 模型
    var node = new ht.Node();
    var shape3d = jsonUrl.slice(jsonUrl.lastIndexOf('/')+1, jsonUrl.lastIndexOf('.'));
    
    ht.Default.loadObj(objUrl, mtlUrl, { // HT 通過 loadObj 函數來加載 obj 模型
        cube: true, // 是否將模型縮放到單位 1 的尺寸範圍內,默認爲 false
        center: true, // 模型是否居中,默認爲 false,設置爲 true 則會移動模型位置使其內容居中
        shape3d: shape3d, // 如果指定了 shape3d 名稱,則 HT 將自動將加載解析後的所有材質模型構建成數組的方式,以該名稱進行註冊
        finishFunc: function(modelMap, array, rawS3) { // 用於加載後的回調處理
            if (modelMap) {
                node.s({ // 設置節點樣式
                    'shape3d': jsonUrl, // jsonUrl 爲 obj 模型的 json 文件路徑
                    'label': '' // 設置 label 爲空,label 的優先級高於 name,所以即使設置了 name,節點的下方也不會顯示 name名稱
                });
                g3d.dm().add(node); // 將節點添加進數據容器中

                node.s3(rawS3); // 設置節點大小 rawS3 模型的原始尺寸
                node.p3(p3); // 設置節點的三維座標
                node.setName(shape3d); // 設置節點名稱
                node.setElevation(node.s3()[1]/2); // 控制 Node 圖元中心位置所在 3D 座標系的y軸位置
                g3d.sm().ss(node); // 設置選中當前節點
                g3d.setFocus(node); // 將焦點設置在當前節點上
                return node;
            }
        }
    });
}

代碼結束!

總結

說實在的這個 Demo 真的是非常容易,難度可能在於空間思維能力了,先確認法線和點,然後根據法線和點找到那個面,這個面按照我的這種方式有個對照還比較能夠理解,真幻想的話,可能容易串。這個 Demo 容易主要還是因爲封裝的 hitPosition 函數簡單好用,這個真的是功不可沒。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章