現在,3D 模型已經用於各種不同的領域。在醫療行業使用它們製作器官的精確模型;電影行業將它們用於活動的人物、物體以及現實電影;視訊遊戲產業將它們作為計算機與視訊遊戲中的資源;在科學領域將它們作為化合物的精確模型;建築業將它們用來展示提議的建築物或者風景表現;工程界將它們用於設計新裝置、交通工具、結構以及其它應用領域;在最近幾十年,地球科學領域開始構建三維地質模型,而且 3D 模型經常做成動畫,例如,在故事片電影以及計算機與視訊遊戲中大量地應用三維模型。它們可以在三維建模工具中使用或者單獨使用。為了容易形成動畫,通常在模型中加入一些額外的資料,例如,一些人類或者動物的三維模型中有完整的骨骼系統,這樣運動時看起來會更加真實,並且可以通過關節與骨骼控制運動。
這些種種都讓我們前端開發者覺得如果我們可以不用學習 unity3d 或者其他遊戲開發工具就能實現 3D 效果,而且能夠精準的靠程式碼來控制移動或者方向就好了。。。於是我利用 HT For Web 中的 3D元件 來實現了一個小例子,用了 HT 中 3D元件 的大部分功能,做這個例子就是想把 3D 元件好好的掌握,儘量放進一個例子中,到時候別人有需要就可以參考了。
先來看看整體實現的效果圖:
用 HT for Web,現有的 3d 模板建立三層底板不是問題,問題是要如何將圖中第一層的“電腦”和“機櫃元件”放上去?我是在網上 down 下來的 obj 格式的檔案,然後我利用 HT 中的 ht.Default.loadObj(objUrl, mtlUrl, params) 函式將模型載入進去,其中的 params 部分可以參考 http://www.hightopo.com/guide/guide/plugin/obj/ht-obj-guide.html,程式碼如下:
ht.Default.loadObj('obj/機櫃元件1.obj', 'obj/機櫃元件1.mtl', { //載入 obj 檔案 cube: true, //是否將模型縮放到單位1的尺寸範圍內,預設為false center: true, //模型是否居中,預設為false,設定為true則會移動模型位置使其內容居中 shape3d: 'box', //如果指定了shape3d名稱,則HT將自動將載入解析後的所有材質模型構建成陣列的方式,以該名稱進行註冊 finishFunc: function(modelMap, array, rawS3){ //用於載入後的回撥處理 if(modelMap){ device2 = createNode('box', floor1); //建立一個節點,在第一層“地板”上 device2.p3([x1-120, y1+13, z1+60]); //設定這個節點座標 device2.s3(rawS3); //設定這個節點大小 createEdge(device1, device2); //建立連線 device3 = createNode('box', floor1); device3.s3(rawS3); device3.p3([x1+120, y1+13, z1+60]); createEdge(device1, device3); } } });
其中 finishiFunc 函式中的三個引數定義如下:
- modelMap:呼叫 ht.Default.parseObj 解析後的返回值,若載入或解析失敗則返回值為空
- array:所有材質模型組成的陣列
- rawS3:包含所有模型的原始尺寸
一般在實際應用中我們都會將圖元的大小設定為模型的原始尺寸。
“電腦”上方有個紅色的立體能旋轉的“警告”,是依靠 ht.Default.setShape3dModel 函式(HT for Web 建模手冊 http://www.hightopo.com/guide/guide/plugin/modeling/ht-modeling-guide.html)註冊的一個 3d 模型,在 ht 中,封裝好的建模函式有很多,比較基礎的就是球體,圓柱,立方體等等,這邊我用的是構造環形的方法 createRingModel 來生成“警告”最外面的環,感嘆號的上部分就是用的 createSmoothSphereModel 構造的球體,感嘆號的下部分就是用 createSmoothCylinderModel 來構造的圓柱。我一開始直接使用了 3d 模型中封裝好的函式,導致後來根本不知道函式中使用的引數是做什麼用的,而且也不明白 3d 模型是怎麼構成的,然後自己又重新看了前面的“模型基礎”,才知道原來 3d 模型採用的一個面,最基礎的是三角面,之後複雜的面也是由多個三角面來形成的,然後繞著一根特定的軸旋轉之後形成的,當然,這個軸是你來決定的,不同的軸可以生成不同的形狀,對於顏色等風格方面的設定可以參考 HT for Web 風格手冊(http://www.hightopo.com/guide/guide/core/theme/ht-theme-guide.html)。至於如何讓這個 3d 模型旋轉起來,ht 中封裝了 addScheduleTask(Task) 方法,我在第三層 Task 中呼叫了 ht 封裝的一個旋轉函式 setRotation 來設定旋轉的順序和方向,並且指定了旋轉的物件。以下是自定義“警告”的 3d 模型的方法(注意:因為本例的模型是自定義組合的,如果要設定整體模型的顏色要用 “all.blend” style 屬性):
function createAlarm(device, formPane) { var ringModel = ht.Default.createRingModel([ 8, 1, 10, 1, 10, -1, 8, -1, 8, 1 ], null, null, false, false, 100);//根據xy平面的曲線,環繞一週形成3D模型。 var sphereModel = ht.Default.createSmoothSphereModel(8, 8, 0, Math.PI*2, 0, Math.PI, 2);//構建光滑球體模型 var cylinderModel = ht.Default.createSmoothCylinderModel(8, true, true, 1, 2, 0, Math.PI*2, 8);//構建光滑圓柱體模型 var alarmArr = [//組合模型 由三個模型ringModel、sphereModel、cylinderModel組合而成 { shape3d: ringModel,//定義模型型別 r3: [Math.PI/2, 0, 0],//設定旋轉角度 color: {//設定模型顏色 func: 'style@all.blend',//資料繫結style樣式中的all.blend屬性,可通過data.s()獲取和設定這個屬性 } },{ shape3d: sphereModel, t3: [0, 4, 0], color: { func: 'style@all.blend', } },{ shape3d: cylinderModel, t3: [0, -3, 0], color: { func: 'style@all.blend', } } ]; ht.Default.setShape3dModel('alarm', {//註冊自定義3D模型 shape3d: alarmArr }); var alarmTip = createNode('alarm', device);//建立shape3d為alarm的節點 alarmTip.s3([2, 2, 2]);//設定節點大小 alarmTip.p3(device.p3()[0], device.p3()[1]+60, device.p3()[2]); alarmTip.s('all.blend', 'red');//改變此屬性可改變模型的顏色,因為模型建立的時候已經資料繫結了 return alarmTip; }
接下來看看怎麼讓這個“告警”節點“閃爍”,我是直接將這個動畫跟節點繫結,這樣可以直接通過節點來控制動畫。所以在上面我們建立 alarm 的模型時就可以直接將動畫綁在節點上:
if(formPane){ alarmNode.scaleFunc = function() {//設定大小變化動畫 var size = alarmNode.s3();//獲取節點的大小 if (size[0] === 2 && size[1] === 2 && size[2] === 2) alarmNode.s3([1, 1, 1]); else alarmNode.s3([2, 2, 2]); alarmNode.scaleTimer = setTimeout(alarmNode.scaleFunc, formPane.v('scaleInterval'));//設定動畫 } alarmNode.blinkFunc = function(){//設定閃爍的動畫 var color = alarmNode.s('all.blend');//獲取節點的style樣式 if (color === 'red') alarmNode.s({'all.blend': 'yellow'});//如果節點顏色為紅色,那麼設定為黃色 else alarmNode.s({'all.blend': 'red'}); alarmNode.blinkTimer = setTimeout(alarmNode.blinkFunc, formPane.v('blinkInterval')); } alarmNode.rotateFunc = function() {//設定旋轉動畫 alarmNode.setRotation(alarmNode.getRotation() + Math.PI/20);//獲取節點當前的旋轉角度,在這個旋轉角度之上新增 Math.PI/20 個角度 alarmNode.rotateTimer = setTimeout(alarmNode.rotateFunc, formPane.v('rotInterval')); } }
上面的動畫我設定了可以通過 form 表單皮膚上的屬性來控制節點閃爍的速度,以及閃爍節點的動畫等等,主要說一下這個功能在 form 表單上的實現:
formPane.addRow([//向form表單皮膚上新增一行元素 { checkBox: {//核取方塊 label: 'Enable Blink',//核取方塊對應的文字內容 selected: true,//設定選中核取方塊 onValueChanged: function(){//核取方塊值變化時回撥的函式 var data = dataModel.getDataByTag('colorAlarm');//通過tag標籤獲取節點 if (this.getValue()) {//獲取核取方塊當前值true/false data.blinkTimer = setTimeout(data.blinkFunc, formPane.v('blinkInterval'));//直接通過設定節點的blinkTimer來設定動畫 } else { clearTimeout(data.blinkTimer);//清除動畫 } } } }, { id: 'blinkInterval',//form可以通過getValue(簡寫為v)來獲取這個item的值 slider: {//設定了該屬性後HT
將根據屬性值自動構建ht.widget.Slider
物件,並儲存在element
屬性上 min: 0,//滑動條最小值 max: 1000,//滑動條最大值 step: 50,//滑動條步進 value: 500,//當前滑動條的值 } } ], [0.1, 0.1]);//設定這行的兩個item元素的寬度小於1的值為比例
最後來說說 3D 管線上的小球流動的部分,這個功能確實非常實用,而且做出來的效果也確實不錯,跟大家分享~
首先,建立一條連線連線起始節點和結束節點並設定這個連線的樣式,用 ht.Edge 可以將連線吸附在起始節點和結束節點上,這樣移動這兩個節點中的任意一個節點連線都會跟著節點移動的位置變化,非常方便:
var polyline = new ht.Edge(source, target);//建立連線 dataModel.add(polyline);//將連線新增進資料容器中 polyline.s({ 'edge.width': 5,//連線寬度 'edge.type': 'points',//連線型別 為points時連線走向將由edge.points屬性決定,用於繪製折線 'edge.points': [//可設定型別為ht.List的{x:100, y:100}格式的點物件陣列,當edge.type為points時起作用 {x: source.getPosition3d()[0]+200, y: source.getPosition3d()[2], e: source.getPosition3d()[1]}, {x: target.getPosition3d()[0]+400, y: target.getPosition3d()[2], e: target.getPosition3d()[1]} ], 'edge.segments': [1, 4],//用於描述點連線樣式,陣列元素為整型值 'shape3d': 'cylinder',//圓柱 'shape3d.color': 'rgba(242, 200, 40, 0.4)', 'shape3d.resolution': 30,//微分段數,可以決定曲線的平滑度 'edge.source.t3': [20, 0, 0],//連線source端偏移,[tx, ty, tz]格式,預設為空 'edge.target.t3': [20, 0, 0]//連線target端偏移,[tx, ty, tz]格式,預設為空 });
因為我們在建立連線的時候設定的 points 僅為曲線上的兩個點,所以如果要獲取曲線目前形成的點,是缺少 source 和 target 兩個點的,我們重新設定一個陣列,將這兩個點新增進去,後面獲取曲線上所有點時會用上:
var list = new ht.List(); list.push({x: source.getPosition3d()[0], y: source.getPosition3d()[2], e: source.getPosition3d()[1]});//向陣列中新增source點 polyline.s('edge.points').each(function(item){//新增style屬性中已設定的兩個點 list.push(item); }); list.push({x: target.getPosition3d()[0], y: target.getPosition3d()[2], e: target.getPosition3d()[1]});//新增target點
然後建立一個在管線上滑動的小球節點,這是僅是設定節點,真正新增進資料容器 dataModel 中需要設定完小球的座標時再新增,如果沒有給節點設定位置就將節點新增進資料容器中,節點的初始位置就是 3D 場景的正中心 [0, 0, 0] 的位置。小球滑動的動畫程式碼如下:
var ball = new ht.Node();//建立小球節點 ball.s({//設定小球節點的樣式 'shape3d': 'sphere',//設定小球的3d模型為球形 'shape3d.color': 'rgba(40, 90, 240, 0.4)'//設定3d模型的顏色 }); var delta = 10, flag = 0; setInterval(function(){ flag++; var length = (polyline.a('total') || 0) % polyline.a('length') + delta;//小球當前走過的曲線長度 var cache = ht.Default.getLineCacheInfo(list, polyline.s('edge.segments'));//獲取曲線上的點的資訊 var lineLength = ht.Default.getLineLength(cache);//獲取曲線的總長度 polyline.a('length', lineLength - 50);//因為我設定了edge的t3(相當於2d中的offset),所以線段長度實際沒有那麼長 var offset = ht.Default.getLineOffset(cache, length);//曲線根據曲線上點的資訊的偏移量 ball.setPosition3d(offset.point.x + 10, offset.point.y, offset.point.z);//設定節點的座標 polyline.a('total', length); if(flag === 1) dataModel.add(ball);//這時候節點已經有了座標了,可以新增進資料容器中了 }, 10);
我們還可以看到第二層上有兩個特殊的多邊形“平行四邊形”和“梯形”,平行四邊形是靠 createParallelogramModel 模型函式,這個函式比較簡單,createExtrusionModel(array, segments, top, bottom, resolution, repeatUVLength, tall, elevation),array 是你要形成的圖形的座標點,這邊只是針對於 xz 軸上畫的平面圖形,segments 指的是如何連線這幾個座標點,可參考 HT for Web 形狀手冊(http://hightopo.com/guide/guide/core/shape/ht-shape-guide.html),top 和 bottom 就是讓你選擇是否有頂部或者底部,resolution 微分段數,我們描繪一段曲線的時候可能只要確認幾個個別的點然後在每兩個點之間的連線上把它分成多個段,這樣這條線段就會變得平滑,ht 為了使用者能夠輕鬆操作這些線段,就封裝了這一個引數,repeatUVLength 預設為空,設定值後頂部和底部的貼圖將根據制定長度值進行重複,tall 模型的高度,預設為 5,elevation 模型中心的 y 軸位置,預設值為 0,設定這個值可以使 xz 上的平面繞著 y 軸旋轉。
底層的一個環形的效果是通過一個演算法來實現的,環形得確認這個環形上有多少個元素,然後算每兩個之間的角度,在通過 sin、cos 來計算每一個元素的位置,得出瞭如下程式碼:
names = ['裝置2', '裝置3', '裝置4', '裝置5', '裝置6', '裝置7', '裝置8', '裝置9']; names.forEach(function(name, index) { x = 400, y = 200, angle = 45, r = 120; x = x3 + Math.sin((2 * Math.PI / 360) * angle * index) * r; y = z3 + Math.cos((2 * Math.PI / 360) * angle * index) * r; device = createRect([x, y3 + 15, y], [w * 0.1, 15, h * 0.1], '', '', floor3); createEdge(device5, device); });
其他如果還有不懂的部分可以去官網(http://hightopo.com)檢視對應的手冊,或者留言私信都可以。
附上本文例子:http://www.hightopo.com/demo/3DTopology/index.html