HT for Web (Hightopo) 使用心得(4)- 3D 場景 Graph3dView 與 Obj 模型

一隻物聯網鯨魚發表於2023-11-20

在前一篇文章《Hightopo 使用心得(3)- 吸附與錨點》中,我們在結尾處提到過 HT 的 3D 場景。這裡我們透過程式碼建立一個 3D 場景並新增一個 Obj 模型來介紹一下 HT for Web 在 3D 場景和模型載入方面的使用。

這是我們最終實現的效果:

HT for Web (Hightopo) 使用心得(4)- 3D 場景 Graph3dView 與 Obj 模型

3D座標系

在搭建 3D 場景之前,先介紹一下基本的 3D 概念。

HT for Web 在 3D 場景中採用的是右手座標系,遵循右手螺旋法則。也就是:x軸正方向朝右,y軸正方向朝上,z軸正方向朝向螢幕外。

HT for Web (Hightopo) 使用心得(4)- 3D 場景 Graph3dView 與 Obj 模型

與 2D 座標系(x, y)相比,這裡多了一條座標軸,也就是高度軸。2/3D 座標系具體對應關係如下:

HT for Web (Hightopo) 使用心得(4)- 3D 場景 Graph3dView 與 Obj 模型

從圖片和表格中可以看到,在右手座標系下,2D座標系中的 x,y 平面,在 3D 中對應的是 x,z 平面,也就是地平面。而在 3D 中多出來的一條座標軸是高度座標軸,也就是 y 軸。

有了三條座標軸後,顯而易見,我們在配置節點(ht.Node)屬性時就不能使用原來的方法。為此,HT 為ht.Node 擴充套件了一些新的方法。其中比較常用的有:

3D位置函式

  • 設定位置(座標):setPosition3d(x, y, z)|setPosition3d([x, y, z]),可傳入x, y, z三個引數,或傳入[x, y, z]的陣列
  • 獲取位置(座標):getPosition3d()的新函式,返回[x, y, z]陣列值,即[getPosition().x, getElevation(), getPosition().y]
  • 設定大小(尺寸):setSize3d(x, y, z)|setSize3d([x, y, z]),可傳入x, y, z三個引數,或傳入[x, y, z]的陣列
  • 獲取大小(尺寸):getSize3d()的新函式,返回[x, y, z]陣列值,即[getWidth(), getTall(), getHeight()]

3D錨點

3D中節點同樣有錨點的概念,同樣HT為ht.Node圖元增加了以下新函式:

-設定 3D 錨點: setAnchor3d(x, y, z)|setAnchor3d([x, y, z]),可傳入x, y, z三個引數,或傳入[x, y, z]的陣列

-設定獲取y軸方向錨點: getAnchorElevation()|setAnchorElevation(elevation)

-獲取 3D 錨點 getAnchor3d()的新函式,返回[x, y, z]陣列值,即[getAnchor().x, getAnchorElevation(), getAnchor().y]

3D旋轉函式

ht.Node在2D座標系下由getRotation()和setRotation(rotation)函式控制旋轉,該引數對應於3D座標系下沿y軸的負旋轉值。 同時3D座標系下增加了rotationX和rotationZ兩個分別沿著x軸和z軸的新旋轉變數,同時增加以下新函式:

  • setRotationY(y)設定沿y軸旋轉弧度,相當於setRotation(-y)
  • getRotationY()獲取沿y軸旋轉弧度,相當於getRotation()
  • 設定圍繞三個座標軸的旋轉角度:setRotation3d(x, y, z)|setRotation3d([x, y, z]),可傳入x, y, z三個引數,或傳入[x, y, z]的陣列
  • 獲取當前在三個座標軸的旋轉角度:getRotation3d()的新函式,返回[x, y, z]陣列值,即[getRotationX(), -getRotation(), getRotationZ()]

3D場景搭建 - ht.graph3d.Graph3dView

在前面文章的例子中,建立一張 2D 圖紙,使用的是 new ht.graph.GraphView(); 而在這裡,建立一個 3D 場景,我們需要使用 new ht.graph3d.Graph3dView();

與HT其他檢視元件一樣, ht.graph3d.Graph3dView也是基於於統一的ht.DataModel資料模型來驅動圖形顯示。熟悉了2D圖紙的同學可能會發現,其在場景配置,節點配置上與 2D 相似。

使用下面的程式碼,我們建立和配置了一個 3D 場景,並獲取了其對應的資料模型(dataModel):

/*************** 建立一個3D場景,新增到body下,並配置各種屬性 ******************/

const g3d = new ht.graph3d.Graph3dView();

g3d.addToDOM(); // 新增到DOM

g3d.setGridVisible(true); // 顯示網格

g3d.setEye(2000, 1000, 0); // 設定相機位置

g3d.setCenter(0, 0, 0); // 設定中心點

g3d.setUp(0, 1, 0); // 設定相機角度;這裡預設值就是 [0, 1, 0]

g3d.setRotatable(true); // 允許旋轉,預設值:true

g3d.setZoomable(true); // 允許滾輪縮放,預設值:true

g3d.setPannable(true); // 允許平移,預設值:true

g3d.setEditable(true); // 允許在場景中對節點進行編輯

const dm = g3d.getDataModel(); // 獲取場景的 DataModel,簡寫形式:g3d.dm()

dm.setBackground('white'); // 同 dm.setBackground('rgba(255, 255 255, 1)'); 預設為黑色

HT for Web (Hightopo) 使用心得(4)- 3D 場景 Graph3dView 與 Obj 模型

建立場景後,我們又讓它顯示了輔助網格。我們可以將這些網格理解成地平面。模型在網格上方就相當於在地面之上。反之就是在地面下方。

相機的up座標

在上例中,比較特殊的一個操作是 g3d.setUp(0, 1, 0)。這裡是指設定相機的 up 座標。在計算機圖形學中,相機的 up 座標通常是指相機座標系中的一個向量,用於定義相機的上方向。

例如在一個場景中,在水平面上有一棟房子,如果相機的 up 座標是 (0,1,0),則相機將看到一個朝上的房子。如果相機的 up 座標是(1,0,0),則相機將看到一個朝右的房子和豎直的地面(整個視角都會旋轉)。在 HT for Web 中,預設的相機 up 座標是 [0, 1, 0],也就是我們會看到一個正常的朝上的房子。

OBJ模型

HT 的 3D 場景支援 FBX,OBJ,GLTF 等多種模型格式。這裡我們選擇比較通用的 OBJ 模型來進行舉例。

要使用 OBJ 模型,首先需要在 index.html 中引入 ht-obj.js 外掛:

<script src="../../lib/plugin/ht-obj.js"></script>

載入OBJ模型

透過使用 ht.Default.loadObj() 方法可以將 OBJ 模型載入到記憶體中。在執行loadObj() 時,需要配置 OBJ 路徑,材質路徑以及相關引數。其中引數 params 的詳細說明可以從以下連線獲取:

它裡面比較常用的幾個引數有:

HT for Web (Hightopo) 使用心得(4)- 3D 場景 Graph3dView 與 Obj 模型

/**

* 載入 obj 模型

*

* @param {*} modelName

* @return {*}

*/

function loadObj(objPath, mtlPath, modelName) {

return new Promise((resolve, reject) => {

/**

* 模型引數,具體引數參考:

*/

const params = {

center: true,

prefix: 'obj/',

shape3d: modelName,

finishFunc: (modelMap, array, rawS3) => {

resolve({modelMap, array, rawS3});

},

};

// 載入模型

ht.Default.loadObj(objPath, mtlPath, params);

});

}

其中的 shape3d 引數是一個自定義字串,可以將該字串理解為我們為模型配置了一個名字。HT 在載入完 OBJ 模型後,它會把該模型儲存到記憶體中。儲存的方式就是透過 ht.Default.setShape3dModel(name, model) 方法。

在上面的程式碼中,我們為要載入的模型起了一個名字:modelName。在想使用該模型的時候,再透過ht.Default.getShape3dModel(name) 方法便可把模型從記憶體中取出來。

將Obj模型新增到3D場景

在上面的 loadObj() 只是將 OBJ 模型新增到了記憶體中。我們還需要在之後將 OBJ 模型新增到場景中。

由於 loadObj() 方法為非同步執行,因此其引數裡面需要攜帶一個 finishFunc 作為回撥引數。為了減少程式碼層級,我們將上面的方法封裝成了Promise 性質。後面我們可以等待這個 Promise 完成後再執行新增動作。

HT for Web (Hightopo) 使用心得(4)- 3D 場景 Graph3dView 與 Obj 模型

const MODELS = {

// 直升機

HELICOPTER: {

name: 'helicopter',

obj: 'obj/helicopterhspt_1002_01.obj',

mtl: 'obj/helicopterhspt_1002_01.mtl',

},

// 螺旋槳

PROPELLER: {

name: 'propeller',

obj: 'obj/helicopterhspt_1002_02.obj',

mtl: 'obj/helicopterhspt_1002_02.mtl',

},

};

/**

* 載入模型;模型初始化;建立模型Node; 新增模型到3D場景中

*

* @return {*}

*/

async function createObj(name, obj, mtl) {

const objInfo = await loadObj(obj, mtl, name); // 載入計量表模型,此處為非同步

// * @param {*} modelMap 呼叫ht.Default.parseObj解析後的返回值,若載入或解析失敗則返回值為空

// * @param {*} array 所有材質模型組成的陣列

// * @param {*} rawS3 包含所有模型的原始尺寸

const {modelMap, array, rawS3} = objInfo;

console.log('createObj: ', objInfo);

if (!modelMap) {

return;

}

// 建立 Node 用來存放該模型,後續對模型的操作透過該 Node 進行

const node = new ht.Node();

node.s({

'shape3d': name, // 對應ht.Default.getShape3dModel(name)註冊的模型

'shape3d.scaleable': false

});

node.setSize3d(rawS3); // 存放模型在三個座標軸方向上的大小。簡寫:node.s3()

node.setPosition3d(0, 0, 0); // 此處可以將其放到水平面上。簡寫:node.p3()

dm.add(node);

return node;

}

const helicopterNode = await createObj(MODELS.HELICOPTER.name, MODELS.HELICOPTER.obj, MODELS.HELICOPTER.mtl);

const propellerNode = await createObj(MODELS.PROPELLER.name, MODELS.PROPELLER.obj, MODELS.PROPELLER.mtl);

直升機模型分為兩部分,分別是機體和螺旋槳。由於他們是兩個模型,因此需要分別新增。

在 loadObj 結束後,HT 會將模型透過 ht.Default.setShape3dModel(name, model) 註冊到記憶體中,之後會給 finishFunc 傳遞三個引數:modelMap, array, rawS3。其解釋參考上面程式碼註釋。目前我們用到的只有 rawS3 引數,也就是模型尺寸(大小)。

有了模型和尺寸(大小),我們便可以建立 ht.Node 用來對模型進行管理。將模型新增到 3D 場景中進行管理的主要邏輯如下:

模型 -(繫結到)→ ht.Node -(新增到)→ dataModel -(繫結到)→ Graph3dView

這裡面的一個關鍵步驟是設定 ht.Node 的 shape3d 屬性。由於在 loadObj 的時候系統已經對模型進行註冊,因此這裡我們只需要透過將註冊的模型名稱賦值給 ht.Node 的 shape3d 屬性,HT 便可自動匹配到記憶體中對應的 OBJ 模型。

需要注意的是:在載入了模型並將模型繫結到 ht.Node 後並不能使其在 3D 場景中顯示。只有透過 dataModel.add(node) 將節點新增到 3D 場景對應的資料模型中時,HT 才會在場景中將模型渲染出來。

模型位置

在上圖中我們可以發現,直升機和螺旋槳重合了,並且二者也不在地面上。這裡我們詳細解釋一下。

仔細檢視程式碼,在建立 ht.Node 時,我們執行了下面的操作:

node.setSize3d(rawS3); // 存放模型在三個座標軸方向上的大小。簡寫:node.s3()

node.setPosition3d(0, 0, 0); // 此處可以將其放到水平面上。簡寫:node.p3()

這兩行命令分別是設定節點的大小和位置。這裡的節點尺寸採用的是模型尺寸。而位置預設放到的座標系中心點。

由於在 3D 場景中,ht.Node 的預設錨點是 [0.5, 0.5, 0.5],也就是在模型的三維中心點。因此其位置座標也要對應到其中心點。這樣,模型就會有一半在網格上方,另一半在網格下方。

該如何將直升機放到地平面上呢?我們可以透過模型的高度來計算出對應的位置從而將模型放到地平面上。具體程式碼如下:

// 由於預設建立 Node 的時候,其錨點是在 [0.5, 0.5, 0.5],位置是在 [0, 0, 0]。導致模型並不在水平面以上。

let size3d = helicopterNode.getSize3d(); // 獲取直升機模型的 [長,寬,高]

let height = size3d[1]; // 獲取模型高度

helicopterNode.setPosition3d([0, height/2, 0]); // 將直升機放到地面上

HT for Web (Hightopo) 使用心得(4)- 3D 場景 Graph3dView 與 Obj 模型

而對於螺旋槳,情況又有些複雜。這裡需要一些技巧才能將其配置到合適的位置。

我們過手動調整螺旋槳來獲取其應該擺放的位置和角度。這裡就用到了g3d.setEditable(true)功能。開啟編輯功能後,選中模型,場景中會顯示座標軸,透過拖動不同的座標軸我們可以對模型進行移動,旋轉和縮放。

HT for Web (Hightopo) 使用心得(4)- 3D 場景 Graph3dView 與 Obj 模型

將螺旋槳移動到機體合適的位置後,在console中透過 node.getPosition3d() 和 node.getRotation3d() 來獲取螺旋槳當前的位置和角度:

HT for Web (Hightopo) 使用心得(4)- 3D 場景 Graph3dView 與 Obj 模型

然後配置到程式碼中。與此同時,我們透過 setHost() 將螺旋槳吸附到了直升機上。這樣,後面直升機移動時會帶著螺旋槳移動。使二者不會脫離。

propellerNode.setRotation3d([0.10506443461595279, 4.550746858974086, -0.007825951889059535]); // 讓螺旋槳水平

propellerNode.setPosition3d([0, 215, -99.00152946490829]); // 將螺旋槳放到直升機上

propellerNode.setHost(helicopterNode); // 螺旋槳吸附到直升機上

直升機動畫

在直升機和螺旋槳都載入完成後,我們現在就可以為其增加相應的動畫。

HT for Web (Hightopo) 使用心得(4)- 3D 場景 Graph3dView 與 Obj 模型

這裡的動畫分為兩部分:

1. 螺旋槳旋轉

2. 直升機移動

/**

* 迴圈前進與後退

*

* @param {*} node

*/

function startAnim(node) {

const p1 = node.p3(); // 原始位置

const p2 = [p1[0], p1[1], p1[2] - 400]; // 目標位置,

const forwardParams = {

duration: 3 * 1000, // 動畫幀數

easing: (t) => { return t; }, // 動畫緩動函式,預設採用`ht.Default.animEasing`

finishFunc: () => {

ht.Default.startAnim(backwardParams);// 迴圈播放該動畫

}, // 動畫結束後呼叫的函式。

action: (v, t) => { // action函式必須提供,實現動畫過程中的屬性變化。

node.setPosition3d( // 此例子展示將節點`node`從位置`p1`動畫到位置`p2`。

p1[0] + (p2[0] - p1[0]) * v,

p1[1] + (p2[1] - p1[1]) * v,

p1[2] + (p2[2] - p1[2]) * v,

);

}

};

const backwardParams = {

duration: 3 * 1000, // 動畫幀數

easing: (t) => { return t; }, // 動畫緩動函式,預設採用`ht.Default.animEasing`

finishFunc: () => {

ht.Default.startAnim(forwardParams);// 迴圈播放該動畫

}, // 動畫結束後呼叫的函式。

action: (v, t) => { // action函式必須提供,實現動畫過程中的屬性變化。

node.setPosition3d( // 此例子展示將節點`node`從位置`p1`動畫到位置`p2`。

p2[0] + (p1[0] - p2[0]) * v,

p2[1] + (p1[1] - p2[1]) * v,

p2[2] + (p1[2] - p2[2]) * v,

);

}

};

ht.Default.startAnim(forwardParams);

}

/**

* 螺旋槳旋轉動畫

*

*/

function startPropellerAnim(node) {

setInterval(() => {

const r3 = node.getRotation3d();

node.setRotation3d([r3[0], r3[1] + 0.4, r3[2]]); // 繞 Y 軸旋轉。單位:弧度

}, 20);

}

螺旋槳旋轉動畫比較簡單。我們只需要讓其繞著 y 軸轉動就可以了。這裡我們利用 setInterval() 起一個定時器,每隔 20 毫秒讓其沿著 y 軸旋轉 0.4°。

關於直升機動畫,我們為其找了兩個點,讓它在這兩點之間來回移動。在動畫的實現上,我們依然採用前幾篇文章提到的 ht.Default.startAnim() 方法。具體實現見上面程式碼部分。

總結

這篇文章介紹瞭如何使用 HT for Web 的 Graph3dView 和 OBJ 模型來建立 3D 場景。裡面介紹了 3D 的一些基本概念以及 3D 場景的基本搭建與配置。另外,除了 3D 場景,我這裡還重點描述瞭如何載入 OBJ 檔案,如何新增模型節點到 3D 場景中,以及如何為節點新增動畫。希望這些基本知識能對大家有所幫助。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69997639/viewspace-2996130/,如需轉載,請註明出處,否則將追究法律責任。

相關文章