前言
3D 場景中的面不只有水平面這一個,空間是由無數個面組成的,所以我們有可能會在任意一個面上放置物體,而空間中的面如何確定呢?我們知道,空間中的面可以由一個點和一條法線組成。這個 Demo 左側為面板,從面板中拖動物體到右側的 3D 場景中,當然,我鼠標拖動到的位置就是物體放置的點,但是這次我們的重點是如何在斜面上放置模型。
效果圖
http://www.hightopo.com/demo/Simple3DEditor/index.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 函數有必要解釋一下(我每次都提,這個真的很重要!)。
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 函數簡單好用,這個真的是功不可沒。