three.js之初探骨骼動畫

郭先生的部落格發表於2020-07-30

今後的幾篇郭先生主要說說three.js骨骼動畫。three.js骨骼動畫十分有意思,但是對於初學者來說,學起來要稍微困難一些,官方文件比較少,網上除了用圓柱體的例子就是引用外部模型的,想要熟練使用骨骼動畫就需要不斷地探索和練習。這篇是初探three.js骨骼動畫,也不深入講解,先說說它的實現和原理,然後一點一點解讀官網案例,骨骼動畫官網案例

1. 骨骼動畫的實現和原理

1. 骨骼動畫的實現

骨骼動畫主要有以下三個部分構成:
(1) 幾何體–在新版本中這個幾何體要求必須是一個BufferGeometry而非Geometry,而骨骼動畫需要的幾何體還有兩個十分重要的屬性,

  1. skinWeights : Array
    當在處理一個 SkinnedMesh 時,每個頂點最多可以有 4 個相關的 bones 來影響它。 skinWeights 屬性是一個權重佇列,順序同幾何體中的頂點保持一致。因而,佇列中的第一個 skinWeight 就對應幾何體中的第一個頂點。由於每個頂點可以被 4 個 bones 營銷,因而每個頂點的 skinWeights 就採用一個 Vector4 表示。skinWeight 向量中每個元素的取值範圍應該在 0 到 1 之間。例如,當設定為 0,骨骼對該頂點的位置沒有影響。當設定為 0.5, 則對頂點的影響為 50%。 當設定為 100% 則對頂點的影響是 100%。如果向量中只有一個骨骼與頂點相關聯,則你只需要關注向量中的第一個元素, 剩餘的元素可以忽略,他們的值可以都設定為 0。
  2. skinIndices : Array
    就如同 skinWeights 屬性一樣。skinWeights 的值也是與幾何體的頂點相對應。每個頂點可以最多有 4 個骨骼與之相關聯。 因而第一個 skinIndex 就與幾何體的第一個頂點相關聯,skinIndex 的值就指明瞭影響該頂點的骨骼是哪個。例如,第一個頂點的值是 ( 10.05, 30.10, 12.12 ),第一個 skinIndex 的值是( 10, 2, 0, 0 ),第一個 skinWeight 的值是 ( 0.8, 0.2, 0, 0 )。上述值表明第一個頂點受到mesh.bones[10]骨骼的影響有 80%, 受到 skeleton.bones[2] 的影響是 20%,由於另外兩個 skinWeight 的值是 0,因而他們對頂點沒有任何影響。

(2) 其材質必須支援蒙皮,並且已經啟用了蒙皮,既skinning = true;
(3) 建立骨骼和骨架

2. 骨骼動畫的原理

骨骼(Bone)其實就是一個Object3D物件,可以把骨架看成是人體骨架,假如脊柱的根節點,那麼大腿就是下一級節點,小腿就是更下一級的節點,如果大腿轉動,那麼小腿在世界座標系必然會動,而小腿動,不一定影響大腿。
現在我們假如有一個幾何體(這個幾何體加上帶蒙皮的材質就是我們的腿的網格),想讓這個幾何體跟著這個骨骼運動,那麼這個動畫就是骨骼動畫,現在我們假設bones[0]為大腿上端點,bones[1]為大小腿關節點,bones[2]為小腿下端點,這裡如果我們把腿看成是圓柱體(官方案例就是這樣做的),將極大的降低了難度,讓heightSegments為2(就是分兩段)也就生成了沿高度分佈的3層點。

我們將最上層點對應的skinIndices設定成0,skinWeights設定成1。中間層點對應的skinIndices設定成1,skinWeights設定成1。最下層點對應的skinIndices設定成2,skinWeights設定成1。這樣幾何體的頂點就和骨骼的端點建立了聯絡。

2. 官網上的骨骼動畫

1. 初始化蒙皮網格

//這是生成蒙皮網格的主方法
initBones() {
        //下面是一些會用到的引數
    var segmentHeight = 8; //每段的高度
    var segmentCount = 4;  //段數
    var height = segmentHeight * segmentCount; //總高度
    var halfHeight = height * 0.5; //一般高度

    var sizing = {
        segmentHeight: segmentHeight,
        segmentCount: segmentCount,
        height: height,
        halfHeight: halfHeight
    };

    var geometry = this.createGeometry( sizing ); //這是生成幾何體的方法,主要是根據頂點生成對應的skinIndex和skinWeight屬性
    var bones = this.createBones( sizing ); //這是生成骨骼的方法
    mesh = this.createMesh( geometry, bones ); //這是生成蒙皮網格的方法

    mesh.scale.multiplyScalar( 1 );
    scene.add( mesh );

    this.render();
    document.getElementById("loading").style.display = "none";  
},

2. 生成帶有skinIndex和skinWeight屬性的幾何體

createGeometry(sizing) {
        //建立一個圓柱體
    var geometry = new THREE.CylinderBufferGeometry(
        5, // 上面半徑
        5, // 下面半徑
        sizing.height, // 總高度
        8, // 圓形面分段數
        sizing.segmentCount * 3, // 沿高度的分段數4*3
        true // 無上下面
    );

    var position = geometry.attributes.position; //圓柱體頂點位置集合

    var vertex = new THREE.Vector3(); //建立一個三維向量用於儲存頂點座標

    var skinIndices = []; //頂點索引聚合
    var skinWeights = []; //頂點權重聚合

    for ( var i = 0; i < position.count; i ++ ) { //遍歷頂點

        vertex.fromBufferAttribute( position, i ); //依次取出每個點

        var y = ( vertex.y + sizing.halfHeight ); //y儲存相對於圓柱體底面的高度值。

                //這兩行比較重要
        var skinIndex = Math.floor( y / sizing.segmentHeight ); //高度除以總高度在向下取整,得到當前的skinIndex
        var skinWeight = ( y % sizing.segmentHeight ) / sizing.segmentHeight; //當前的y值佔該段的百分比

        skinIndices.push( skinIndex, skinIndex + 1, 0, 0 ); //該點關聯bone[skinIndex]和bone[skinIndex+1]
        skinWeights.push( 1 - skinWeight, skinWeight, 0, 0 ); //關聯bone[skinIndex]的比重為1 - skinWeight,關聯bone[skinIndex+1]的比重為skinWeight。
                //舉個例子,第一個y值剛好為0。那麼skinIndex為0,skinWeight也為0。所以呢該點相關的骨骼索引為0和1,權重分別是1和0,也就是該點只與bone[0]有關。
                //再比如y值為4,那麼skinIndex為0,skinWeight也為0.5,所以呢該點相關的骨骼索引為0和1,權重分別是0.5和0.5,也就是該點與bone[0]和bone[1]都相關。其實也很容易理解,因為4恰好在該分段的中間,所以決定於兩個骨骼點的狀態。

    }

    geometry.setAttribute( 'skinIndex', new THREE.Uint16BufferAttribute( skinIndices, 4 ) ); //幾何體中新增skinIndex屬性
    geometry.setAttribute( 'skinWeight', new THREE.Float32BufferAttribute( skinWeights, 4 ) ); //幾何體中新增skinWeight屬性

    return geometry;
},

3. 生成骨骼

createBones(sizing) {
    bones = []; //骨骼陣列

    var prevBone = new THREE.Bone(); //根骨骼節點
    bones.push( prevBone ); //陣列中新增根骨骼節點
    prevBone.position.y = - sizing.halfHeight; //為根骨骼新增位置

    for ( var i = 0; i < sizing.segmentCount; i ++ ) { //遍歷分段

        var bone = new THREE.Bone(); //建立骨骼節點
        bone.position.y = sizing.segmentHeight; //為骨骼節點新增本地位置 雖然本地設定的位置都是一樣的,但是由於這些骨骼都是父子關係,所以在世界座標系上位置不同
        bones.push( bone ); //陣列中繼續新增骨骼
        prevBone.add( bone ); //根骨骼新增當前骨骼
        prevBone = bone; //再將當前骨骼賦值給根骨骼

    }
    return bones;
},

4. 建立蒙皮網格並新增骨骼顯示助手

createMesh(geometry, bones) {
        //建立一個帶蒙皮的材質
    var material = new THREE.MeshPhongMaterial( {
        skinning: true, //重點
        color: 0x156289,
        emissive: 0x072534,
        side: THREE.DoubleSide,
        flatShading: true
    } );

    var mesh = new THREE.SkinnedMesh( geometry,    material ); //建立蒙皮網格
    var skeleton = new THREE.Skeleton( bones ); //建立骨架

    mesh.add( bones[ 0 ] ); //網格新增根骨骼節點(此例bones[0]為根節點)

    mesh.bind( skeleton ); //網格繫結骨架

    skeletonHelper = new THREE.SkeletonHelper( mesh ); //建立骨骼顯示助手
    skeletonHelper.material.linewidth = 2;
    scene.add( skeletonHelper );

    return mesh;
},

最後就是使用gui進行介面控制,這裡只說一下蒙皮網格有一個pose()方法,使用後骨架還原為初始狀態。官方的骨骼動畫解析就到此為止,後面還會繼續說說骨骼動畫。

轉載請註明地址:郭先生的部落格

相關文章