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

前言

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

效果圖: html

圖片描述

代碼生成

建立場景

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 函數有必要解釋一下(我每次都提,這個真的很重要!)。java

HT 的組件通常都會嵌入 BorderPane、SplitView 和 TabView 等容器中使用,而最外層的 HT 組件則須要用戶手工將 getView() 返回的底層 div 元素添加到頁面的 DOM 元素中,這裏須要注意的是,當父容器大小變化時,若是父容器是 BorderPane 和 SplitView 等這些HT預約義的容器組件,則 HT 的容器會自動遞歸調用孩子組件 invalidate 函數通知更新。但若是父容器是原生的 html 元素, 則 HT 組件沒法獲知須要更新,所以最外層的 HT 組件通常須要監聽 window 的窗口大小變化事件,調用最外層組件 invalidate 函數進行更新。node

爲了最外層組件加載填充滿窗口的方便性,HT 的全部組件都有 addToDOM 函數,其實現邏輯以下,其中 iv 是 invalidate 的簡寫:json

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 節點,做爲與地平面的參照,在這樣的對比下立體感會更強一點。下面是這個節點的定義:windows

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 建立節點的函數比較簡單:app

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 方法來添加,參數一爲用戶自定義名稱,參數二爲用戶自定義值,不只能傳常量,也能傳變量、對象,還能傳函數!又是一個很是強大的功能。ide

拖拽功能

拖拽基本上就是響應 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 函數簡單好用,這個真的是功不可沒。

相關文章
相關標籤/搜索