其實,在 HT for Web 中,有多種手段可以用來實現(xiàn)動畫。我們這里仍然用直升機為例,只是更換了場景。增加了巡游過程。
使用 HT 開發(fā)的一個簡單網(wǎng)頁直升機巡邏動畫(Hightopo 使用心得(5))
這里主要用到的動畫實現(xiàn)方式有三種:
ht.Default.startAnim()
DataModel.addScheduleTask(task)
場景搭建
具體3D場景的相關概念請參考《Hightopo 使用心得(4)- 3D 場景 Graph3dView 與 Obj 模型》。
這里的主要工作分為:3D 場景配置以及模型加載。其中 3D 場景部分的設置代碼如下:
this.g3d = new ht.graph3d.Graph3dView();
this.g3d.setGridVisible(true);
this.g3d.setGridSize(5000);
this.g3d.setGridGap(2000);
this.g3d.setNear(10)
this.g3d.setFar(10000000)
this.g3d.addToDOM();
this.dataModel = this.dm = this.g3d.dm();
為了給直升機搭建一個逼真的環(huán)境。這里我們增加了一個山體模型。另外,由于直升機機體與螺旋槳模型是分開的,因此需要分別加載并調(diào)整其位置讓二者合并成一個模型。
// 加載山體模型
this.mountains = await this.createObj(MODELS.MOUNTAINS.name, MODELS.MOUNTAINS.obj, MODELS.MOUNTAINS.mtl);
this.mountains.s('3d.selectable',false);
this.mountains.s('shape3d.scaleable',true);
this.mountains.setScale3d([0.01, 0.1, 0.01]);
this.mountains.setElevation(1800); // 讓山體在地面以上
// 分別加載直升機及螺旋槳模型
this.helicopterNode = await this.createObj(MODELS.HELICOPTER.name, MODELS.HELICOPTER.obj, MODELS.HELICOPTER.mtl);
this.propellerNode = await this.createObj(MODELS.PROPELLER.name, MODELS.PROPELLER.obj, MODELS.PROPELLER.mtl);
// 由于默認創(chuàng)建 Node 的時候,其錨點是在 [0.5, 0.5, 0.5],位置是在 [0, 0, 0]。導致模型并不在水平面以上。
let size3d = this.helicopterNode.getSize3d(); // 獲取直升機模型的 [長,寬,高]
let height = size3d[1]; // 獲取模型高度
this.helicopterNode.setPosition3d([0, height/2, 0]); // 將直升機放到地面上
this.propellerNode.setRotation3d([0.10506443461595279, 4.550746858974086, -0.007825951889059535]); // 讓螺旋槳水平
this.propellerNode.setPosition3d([0, 215, -99.00152946490829]); // 將螺旋槳放到直升機上
this.propellerNode.setHost(this.helicopterNode); // 螺旋槳吸附到直升機上
this.helicopterNode.p3(0,2000,0); // 直升機
螺旋槳動畫 - setInterval
螺旋槳動畫比較簡單,我們在《Hightopo 使用心得(4)- 3D 場景 Graph3dView 與 Obj 模型_CodingInProgress的博客-CSDN博客》中已經(jīng)提過。其本質(zhì)是通過不斷地修改螺旋槳節(jié)點在豎直方向(Y 軸)的角度。
/**
* 螺旋槳旋轉(zhuǎn)動畫
*
*/
startPropellerAnim(node) {
setInterval(() => {
const r3 = node.getRotation3d();
node.setRotation3d([r3[0], r3[1] + 0.4, r3[2]]); // 繞 Y 軸旋轉(zhuǎn)
}, 20);
}
創(chuàng)建直升機巡游路徑
有了直升機及環(huán)境,我們需要讓直升機動起來。例如在這里,我們計劃讓直升機圍繞山體巡邏。這里該如何實現(xiàn)呢?
在 HT for Web 官方手冊中,其提供了一種實現(xiàn)方式,我們這里稍微加以改造便可讓直升機圍繞山體巡邏。
在代碼層面,我們創(chuàng)建了一條三維線段(Polyline)。該線段實現(xiàn)的是一個圓環(huán),懸浮在山體上面。有了這條路徑,直升機便可沿著該路徑前進實現(xiàn)巡游動畫。
polyline的形狀主要由points和segments這兩個屬性描述。二者都是數(shù)組。其中 points 可以理解成組成 polyline 所要用到的點集合,而 segments 數(shù)組主要用來定義如何使用前面的點來組成 polyline。
points 中的每一項為 {x,y,e} 格式,需要注意的是,這里代表高度的是 e(elevation),而不是 y。
segments 數(shù)組里面有5種值。分別為:
1: moveTo,占用1個點信息,代表一個新路徑的起點
2: lineTo,占用1個點信息,代表從上次最后點連接到該點
3: quadraticCurveTo,占用2個點信息,第一個點作為曲線控制點,第二個點作為曲線結束點
4: bezierCurveTo,占用3個點信息,第一和第二個點作為曲線控制點,第三個點作為曲線結束點
5: closePath,不占用點信息,代表本次路徑繪制結束,并閉合到路徑的起始點
/**
* 創(chuàng)建直升機巡游路徑
*
* @memberof Index3d
*/
createPath() {
this.g3d.setDashDisabled(false); // 顯示虛線
let height = 2000; // 線段離地高度
let dataModel = this.dataModel;
let polyline = this.polyline = new ht.Polyline();
polyline.setThickness(5); // 線段粗細
polyline.s({
'shape3d.image': 'assets/flow.png', // 貼圖
"shape3d": "cylinder", // polyline類型,這里是圓柱。也可以是
'repeat.uv.length': 400, // 貼圖寬度
'shape3d.resolution': 1600, // 管線分辨率,分辨率越高越平滑
});
dataModel.add(polyline);
// 起始點
const points = [{
x: -15000,
y: 0,
e: height,
}];
const segments = [1];
// 二次曲線,占用兩個點。生成一條弧線。下同。
points.push({
x: -15000,
y: -15000,
e: height
});
points.push({
x: 0,
y: -15000,
e: height
});
segments.push(3);
points.push({
x: 15000,
y: -15000,
e: height
});
points.push({
x: 15000,
y: 0,
e: height
});
segments.push(3);
points.push({
x: 15000,
y: 15000,
e: height
});
points.push({
x: 0,
y: 15000,
e: height
});
segments.push(3);
points.push({
x: -15000,
y: 15000,
e: height
});
points.push({
x: -15000,
y: 0,
e: height,
});
segments.push(3);
polyline.setPoints(points);
polyline.setSegments(segments);
polyline.setAnchorElevation(0)
}
直升機巡游動畫 - ht.Default.startAnim
接下來,我們需要讓直升機沿著巡游路徑前進。在實現(xiàn)的時候,我們使用了 ht.Default.startAnim() 方法。該方法我們在前幾篇文章中都用過,這里就不再詳細介紹。
ht.Default.startAnim() 會執(zhí)行 duration 毫秒,在執(zhí)行過程中,其會自動計算所需要的幀數(shù)并在每一幀都調(diào)用一次action 方法。也就是說,如果我們想讓直升機 40 秒圍繞路徑飛行一圈,我們只需要將 duration 設置成40*1000 毫秒,并且在每一幀拿到當前時刻 polyline 上的點的坐標及方向。同時,使用該坐標與方向設置直升機位置及朝向就可以實現(xiàn)巡游動畫。
這里面比較關鍵的一個方法是 g3d.getLineOffset(polyline, length * v) 。該方法會返回一個對象:{point: p.M…h(huán).Vector3, tangent: p.M…h(huán).Vector3}。其分別代表當前時刻 polyline 上的點的坐標及放向。根據(jù)這兩個值,我們可以進一步配置直升機的位置和朝向。
/**
* 直升機沿著巡游路徑飛行
*
* @param {number} [duration=40 * 1000]
* @memberof Index3d
*/
startFly(duration = 40 * 1000) {
const {
g3d,
polyline
} = this;
/** 獲取巡游路徑總長度 */
let length = g3d.getLineLength(polyline);
const params = {
delay: 0,
duration,
easing: (t) => {
return t;
},
action: (v, t) => {
let offset = g3d.getLineOffset(polyline, length * v),
point = offset.point,
px = point.x,
py = point.y + 200, // 讓直升機高于polyline
pz = point.z,
tangent = offset.tangent,
tx = tangent.x,
ty = tangent.y,
tz = tangent.z;
this.helicopterNode.p3(px, py, pz);
this.helicopterNode.lookAt([px + tx, py + ty, pz + tz], 'back'); // 一個模型有6個面,這里需要確定機頭處于哪個面
// 視角盯住直升機
if (this._cameraType == 1) {
g3d.setCenter(px, py, pz);
} else if (this._cameraType == 2) { // Camera跟隨直升機運動
g3d.setEye(px - tx * 1800 + 1000, py - ty * 1800 + 1000, pz - tz * 1800); // 讓鏡頭高于直升機并在尾部進行觀察
g3d.setCenter(px, py, pz);
}
this.helicopterNode.a('angle', v * Math.PI * 120);
},
finishFunc: () => {
ht.Default.startAnim(params);
}
};
ht.Default.startAnim(params);
}
管道流動動畫 - DataModel.addScheduleTask()
實現(xiàn)管道流動的動畫有多種方式,其本質(zhì)是定期改變管道的貼圖偏移。
這里我們采用DataModel#addScheduleTask(task)實現(xiàn)流動動畫。DataModel#addScheduleTask(task)實際上是添加了一個調(diào)度任務。由于該方法是在 DataModel 上執(zhí)行,因此在每次執(zhí)行的時候,DataModel 里面的每個 Data 都會被調(diào)用。我們可以在 action 參數(shù)里面對 Data 進行過濾。DataModel#addScheduleTask(task)方法的參數(shù)task為json對象,可指定如下屬性:
interval:間隔毫秒數(shù),默認值為10
enabled:是否啟用開關,默認為true
beforeAction:調(diào)度開始之前的動作函數(shù)
action:間隔動作函數(shù),對DataModel上的每個data節(jié)點都會執(zhí)行一次action操作
afterAction:調(diào)度結束之后的調(diào)度函數(shù)
另外,可以用DataModel#removeScheduleTask(task)刪除調(diào)度任務,其中task為以前添加過的調(diào)度任務對象。
/**
* 通過DataModel的addScheduleTask實現(xiàn)流動效果
*
* @memberof Index3d
*/
addScheduleTasks() {
const task = {
interval: 50, // 間隔毫秒數(shù),默認值為10
enabled: true, // 是否啟用開關,默認為true
beforeAction: () => {}, // 調(diào)度開始之前的動作函數(shù)
afterAction: () => {}, // 調(diào)度結束之后的調(diào)度函數(shù)
action: (data) => { // 間隔動作函數(shù),對DataModel上的每個data節(jié)點都會執(zhí)行一次action操作
if (data.getClassName() == 'ht.Polyline') {
const offset = (data.s('shape3d.uv.offset') || [0,0]);
data.s('shape3d.uv.offset', [offset[0] + 0.1, offset[1]]);
}
}
};
this.dataModel.addScheduleTask(task);
// this.dataModel.removeScheduleTask(task); // 刪除調(diào)度任務
}
這里我們只是舉例介紹一下DataModel#addScheduleTask(task)的用法。對于一個 DataModel 中大部分 Data 都需要動畫的時候,可以考慮使用該方法。
在代碼執(zhí)行的時候,我們可以選擇把巡游路徑隱藏。這樣看起來直升機就是沿著一個圓形持續(xù)巡游。
hidePath() {
this.polyline.s('3d.visible', false);
}
總結
本文介紹了如何通過代碼實現(xiàn)一個直升機繞山巡游的動畫,包括創(chuàng)建路徑和實現(xiàn)直升機的飛行動畫。另外,還介紹了如何通過DataModel#addScheduleTask(task)實現(xiàn)流動效果的動畫。讀完本文,你將了解到如何使用 HT for Web 實現(xiàn)各種動畫效果。
審核編輯 黃宇
-
數(shù)字孿生
+關注
關注
4文章
1264瀏覽量
12174
發(fā)布評論請先 登錄
相關推薦
評論