Three.js骨骼動畫(SkinnedMesh)
本文是Three.js電子書的12.1節
所謂骨骼動畫,以人體爲例簡單地說,人體的骨骼運動,骨骼運動會帶動肌肉和人體皮膚的空間移動和表面變化,下面將會提到的蒙皮概念你可以類比人體的皮膚。
Threejs骨骼動畫需要通過骨骼網格模型類SkinnedMesh來實現,一般來說骨骼動畫模型都是3D美術創建,然後程序員通過threejs引擎加載解析,爲了讓大家更深入理解骨骼動畫,下面就通過threejs程序編寫一個簡易的骨骼動畫。
相關類
直接使用Threejs編寫一個骨骼動畫還是比較複雜的,你首先應該瞭解骨頭關節Bone、骨骼網格模型SkinnedMesh、骨架對象Skeleton這三個骨骼相關的類,除此之外還需要了解幾何體Geometry和骨骼動畫相關的頂點數據。
Bone
通過Bone類可以實例化一個骨關節對象,然後通過多個骨關節對象可以構成一個骨骼層級系統,Bone
基類是Object3D,可以通過add方法給一個骨關節對象Bone
添加一個子骨關節Bone
。
var Bone1 = new THREE.Bone(); //關節1,用來作爲根關節
var Bone2 = new THREE.Bone(); //關節2
var Bone3 = new THREE.Bone(); //關節3
// 設置關節父子關係 多個骨頭關節構成一個樹結構
Bone1.add(Bone2);
Bone2.add(Bone3);
// 設置關節之間的相對位置
//根關節Bone1默認位置是(0,0,0)
Bone2.position.y = 60; //Bone2相對父對象Bone1位置
Bone3.position.y = 40; //Bone3相對父對象Bone2位置
骨架Skeleton
Threejs通過Skeleton類可以把所有骨關節對象Bone
包含進來。
// 所有Bone對象插入到Skeleton中,全部設置爲.bones屬性的元素
var skeleton = new THREE.Skeleton([Bone1, Bone2, Bone3]); //創建骨骼系統
// 查看.bones屬性中所有骨關節Bone
console.log(skeleton.bones);
// 返回所有關節的世界座標
skeleton.bones.forEach(elem => {
console.log(elem.getWorldPosition(new THREE.Vector3()));
});
Geometry
(.skinWeights
和.skinIndices
屬性)
前面課程講解過幾何體Geometry的多種頂點數據。幾何體Geometry
的屬性.skinWeights
和.skinIndices
主要作用是用來設置幾何體的頂點位置是如何受骨關節運動影響的。比如幾何體Geometry
的頂點位置數據是你皮膚上的一個個點位,如果你的骨關節運動了,你的皮膚外形會跟着變化,就相當於Geometry
的頂點座標需要跟着骨關節變化,這時候需要注意,關節外面包裹的一層皮膚,不同區域變形程度不同,那也就是說如果骨關節Bone
變化了,幾何體Geometry
頂點要像皮膚一樣不同區域的頂點變化程度不同。這也正是.skinWeights
和.skinIndices
屬性出現的原因,.skinWeights
的字面意思就是設置骨骼蒙皮的權重。
骨骼網格模型SkinnedMesh
SkinnedMesh類的字面意思就是骨骼網格模型,骨骼網格模型SkinnedMesh
的基類是普通網格模型Mesh,SkinnedMesh
和Mesh
一樣都是網格模型,只是一個有骨骼動畫功能,一個沒有骨骼動畫功能。
骨骼網格模型SkinnedMesh
綁定骨骼系統。
//骨骼關聯網格模型
SkinnedMesh.add(Bone1); //根骨頭關節添加到網格模型
SkinnedMesh.bind(skeleton); //網格模型綁定到骨骼系統
程序創建一個骨骼動畫
下面的的代碼通過SkinnedMesh
構造函數創建一個骨骼動畫,如果你想深入理解骨骼動畫可以研究一下下面的代碼,下面代碼還是比較複雜的,涉及到的知識點比較多,如果不想深入研究,可以大致看下有個印象,直接學習下一節,實際開發的時候,只需要會加載解析骨骼動畫就可以。
/**
* 創建骨骼網格模型SkinnedMesh
*/
// 創建一個圓柱幾何體,高度120,頂點座標y分量範圍[-60,60]
var geometry = new THREE.CylinderGeometry(5, 10, 120, 50, 300);
geometry.translate(0, 60, 0); //平移後,y分量範圍[0,120]
console.log("name", geometry.vertices); //控制檯查看頂點座標
//
/**
* 設置幾何體對象Geometry的蒙皮索引skinIndices、權重skinWeights屬性
* 實現一個模擬腿部骨骼運動的效果
*/
//遍歷幾何體頂點,爲每一個頂點設置蒙皮索引、權重屬性
//根據y來分段,0~60一段、60~100一段、100~120一段
for (var i = 0; i < geometry.vertices.length; i++) {
var vertex = geometry.vertices[i]; //第i個頂點
if (vertex.y <= 60) {
// 設置每個頂點蒙皮索引屬性 受根關節Bone1影響
geometry.skinIndices.push(new THREE.Vector4(0, 0, 0, 0));
// 設置每個頂點蒙皮權重屬性
// 影響該頂點關節Bone1對應權重是1-vertex.y/60
geometry.skinWeights.push(new THREE.Vector4(1 - vertex.y / 60, 0, 0, 0));
} else if (60 < vertex.y && vertex.y <= 60 + 40) {
// Vector4(1, 0, 0, 0)表示對應頂點受關節Bone2影響
geometry.skinIndices.push(new THREE.Vector4(1, 0, 0, 0));
// 影響該頂點關節Bone2對應權重是1-(vertex.y-60)/40
geometry.skinWeights.push(new THREE.Vector4(1 - (vertex.y - 60) / 40, 0, 0, 0));
} else if (60 + 40 < vertex.y && vertex.y <= 60 + 40 + 20) {
// Vector4(2, 0, 0, 0)表示對應頂點受關節Bone3影響
geometry.skinIndices.push(new THREE.Vector4(2, 0, 0, 0));
// 影響該頂點關節Bone3對應權重是1-(vertex.y-100)/20
geometry.skinWeights.push(new THREE.Vector4(1 - (vertex.y - 100) / 20, 0, 0, 0));
}
}
// 材質對象
var material = new THREE.MeshPhongMaterial({
skinning: true, //允許蒙皮動畫
// wireframe:true,
});
// 創建骨骼網格模型
var SkinnedMesh = new THREE.SkinnedMesh(geometry, material);
SkinnedMesh.position.set(50, 120, 50); //設置網格模型位置
SkinnedMesh.rotateX(Math.PI); //旋轉網格模型
scene.add(SkinnedMesh); //網格模型添加到場景中
/**
* 骨骼系統
*/
var Bone1 = new THREE.Bone(); //關節1,用來作爲根關節
var Bone2 = new THREE.Bone(); //關節2
var Bone3 = new THREE.Bone(); //關節3
// 設置關節父子關係 多個骨頭關節構成一個樹結構
Bone1.add(Bone2);
Bone2.add(Bone3);
// 設置關節之間的相對位置
//根關節Bone1默認位置是(0,0,0)
Bone2.position.y = 60; //Bone2相對父對象Bone1位置
Bone3.position.y = 40; //Bone3相對父對象Bone2位置
// 所有Bone對象插入到Skeleton中,全部設置爲.bones屬性的元素
var skeleton = new THREE.Skeleton([Bone1, Bone2, Bone3]); //創建骨骼系統
// console.log(skeleton.bones);
// 返回所有關節的世界座標
// skeleton.bones.forEach(elem => {
// console.log(elem.getWorldPosition(new THREE.Vector3()));
// });
//骨骼關聯網格模型
SkinnedMesh.add(Bone1); //根骨頭關節添加到網格模型
SkinnedMesh.bind(skeleton); //網格模型綁定到骨骼系統
console.log(SkinnedMesh);
/**
* 骨骼輔助顯示
*/
var skeletonHelper = new THREE.SkeletonHelper(SkinnedMesh);
scene.add(skeletonHelper);
// 轉動關節帶動骨骼網格模型出現彎曲效果 好像腿彎曲一樣
skeleton.bones[1].rotation.x = 0.5;
skeleton.bones[2].rotation.x = 0.5;
程序實現骨骼動畫
通過骨骼骨骼系統代碼實現骨骼動畫效果。
var n = 0;
var T = 50;
var step = 0.01;
// 渲染函數
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
n += 1;
if (n < T) {
// 改變骨關節角度
skeleton.bones[0].rotation.x = skeleton.bones[0].rotation.x - step;
skeleton.bones[1].rotation.x = skeleton.bones[1].rotation.x + step;
skeleton.bones[2].rotation.x = skeleton.bones[2].rotation.x + 2 * step;
}
if (n < 2 * T && n > T) {
skeleton.bones[0].rotation.x = skeleton.bones[0].rotation.x + step;
skeleton.bones[1].rotation.x = skeleton.bones[1].rotation.x - step;
skeleton.bones[2].rotation.x = skeleton.bones[2].rotation.x - 2 * step;
}
if (n === 2 * T) {
n = 0;
}
}
render();
解析外部骨骼動畫模型
開發的時候,3D美術如果導出一個包含骨骼動畫數據的三維模型,你可以通過下面的代碼進行加載解析,查看骨骼動畫的運動效果。
骨骼動畫除了需要創建一個骨骼動畫模型SkinnedMesh
外,還需要通過幀動畫存儲相關的關節動畫數據。
var mixer = null; //聲明一個混合器變量
loader.load("./marine_anims_core.json", function(obj) {
console.log(obj)
scene.add(obj); //添加到場景中
//從返回對象獲得骨骼網格模型
var SkinnedMesh = obj.children[0];
//骨骼網格模型作爲參數創建一個混合器
mixer = new THREE.AnimationMixer(SkinnedMesh);
// 查看骨骼網格模型的幀動畫數據
// console.log(SkinnedMesh.geometry.animations)
// 解析跑步狀態對應剪輯對象clip中的關鍵幀數據
var AnimationAction = mixer.clipAction(SkinnedMesh.geometry.animations[1]);
// 解析步行狀態對應剪輯對象clip中的關鍵幀數據
// var AnimationAction = mixer.clipAction(SkinnedMesh.geometry.animations[3]);
AnimationAction.play();
// 骨骼輔助顯示
// var skeletonHelper = new THREE.SkeletonHelper(SkinnedMesh);
// scene.add(skeletonHelper);
})
// 創建一個時鐘對象Clock
var clock = new THREE.Clock();
// 渲染函數
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
if (mixer !== null) {
//clock.getDelta()方法獲得兩幀的時間間隔
// 更新混合器相關的時間
mixer.update(clock.getDelta());
}
}
render();
皮膚頂點權重屬性.skinWeights
.skinWeights
表示的是幾何體頂點權重數據,當使用骨骼動畫網格模型SkinnedMesh
的時候, 每個頂點最多可以有4個骨關節Bone影響它. skinWeights
屬性是一個權重值數組,對應於幾何體中頂點的順序。 例如,第一個skinWeight將對應於幾何體中的第一個頂點. 由於每個頂點可以被4個骨關節Bone修改,因此使用四維向量對象Vector4表示一個頂點的權重.
四維向量Vector4
每個分量的值通常應在0和1之間。當設置爲0時,骨關節Bone變換將不起作用;設置爲0.5時,將產生50%的影響;設置爲100%時,會產生100%的影響。 如果只有一個骨關節Bone與頂點關聯,那麼你只需要考慮設置四維向量Vector4
的第一個分量,其餘分量的可以忽略並設置爲0.
頂點索引屬性.skinIndices
頂點索引屬性.skinIndices
就像skinWeights
屬性一樣,skinIndices
的值對應幾何體的頂點. 每個頂點最多可以有4個與之關聯的骨關節Bone。
骨骼動畫頂點數據
骨骼動畫的相關的一些頂點數據,主要是描述幾何體表面的頂點數據是如何受骨骼系統關節運動影響的。加載外部模型你可以訪問骨骼動畫網格模型的幾何體對象查看骨骼動畫相關頂點數據。網格模型的幾何體類型可能是Geometry
,也可能是BufferGeometry
,一般是緩衝類型的幾何體BufferGeometry
比較常見。
Geometry
的骨骼動畫頂點數據,直接查看.skinWeights
和.skinIndices
屬性
console.log('骨骼動畫皮膚頂點權重數據',SkinnedMesh.geometry.skinWeights);
console.log('骨骼動畫皮膚頂點索引數據',SkinnedMesh.geometry.skinIndices);
BufferGeometry
的骨骼動畫頂點數據,如果你熟悉BufferGeometry
的結構,應該都知道該幾何體通過.attributes
屬性存儲各種頂點數據。
console.log('骨骼動畫皮膚頂點權重數據',SkinnedMesh.geometry.attributes.skinWeights);
console.log('骨骼動畫皮膚頂點索引數據',SkinnedMesh.geometry.attributes.skinIndices);