three.jsで3Dキャラを作成:腕を作る(4)

今回で腕は完成です。

指を追加する

const fingerObj = new Limbs();

function armInit(){
    //パラメータの設定
    const fingerLength = upperArmLength / 10;  //よさげな長さ
    const fingerThick = upperArmThick / 5;   //よさげな太さ
    const fingerThicks = new Array( limbSeg );
    for( let i=0; i<( limbSeg+1 ); i++ ){
     //太さ段々すぼめて最後は0(閉じる)
        const t = i / limbSeg;
        fingerThicks[i] = fingerThick - Math.pow( t, 3 ) * fingerThick;
    }
    fingerObj.ep = new THREE.Vector2( fingerLength,0 );
    fingerObj.cp = new THREE.Vector2( fingerLength,0 );
    fingerObj.thick = fingerThicks;
    fingerObj.width = fingerThicks;

  //meshの作成
    lastValClear();    //lastPos, lastAngleの初期化
    const fingerPt = makePipePt( fingerObj );
    const fingerGeo = makeGeometry( fingerObj, fingerPt );
  //指の立て付け
    const fingerAngles = [ -PI/4, -PI/8, 0, PI/4 ];
    const handLength = ( upperArmLength + lowerArmLength ) * 0.11;
    for( let i=0; i<4; i++ ){
        const fingerMesh = new THREE.Mesh( fingerGeo, armMat );
        const z = ( upperArmThick*0.8 ) * Math.sin( fingerAngles[i] );  //指meshのz位置
        const x = ( upperArmThick*0.8 ) * Math.cos( fingerAngles[i] );  //指meshのx位置
        fingerMesh.rotation.y = -fingerAngles[i];              //指meshを回転
        fingerMesh.position.set( x - ( handLength ), 0, z );   //handLength分だけ中に入れる
     //作成したmeshをhandGグループに追加(4つ分)
        handG.add( fingerMesh );
    }
    //lowerArmグループに追加
    lowerArmG.add( handG );
}

//lastPos, lastAngleの初期化関数
function lastValClear(){
    lastAngle = 0;
    lastPos = new THREE.Vector2();
}

指の形状のメッシュを4個分作る。

fingerAnglesの角度に指をくっつける。

なお、指はちっちゃく作るので、特に曲げたりは想定してない。

腕のジオメトリを更新する関数(armUpdate)の最後に、handG(指のグループ)の座標と角度を更新する処理を入れる。

function armUpdate( angle1, angle2 ){
    //upperArm
    lastValClear();
    upperArmUpdate( angle1 );
    //lowerArm
    lowerArmUpdate( angle2 );
    //hand
    handG.rotation.z = lastAngle;    //角度を更新
    handG.position.set( lastPos.x, lastPos.y, 0 );  //位置を更新
}

回転を追加する

armupdate関数に処理を追加して、腕全体(armG)と、ひじから先(lowerArmG)を個別に回転できるようにする。

function armUpdate( angle1, angle2, rotate1, rotate2 ){
    //upperArm
    lastValClear();
    upperArmUpdate( angle1 );
    //lowerArm
    const r = lastAngle;     //upperArmの最終の角度をとっておく
    lowerArmUpdate( angle2 );
    //hand
    handG.rotation.z = lastAngle;
    handG.position.set( lastPos.x, lastPos.y, 0 );
    //回転の処理
    armG.quaternion.set( 0,0,0,1 );
    lowerArmG.quaternion.set( 0,0,0,1 );
    const axis1 = new THREE.Vector3( 1,0,0 );
    const axis2 = new THREE.Vector3( Math.cos(r),Math.sin(r),0 ).normalize();
    const q1 = new THREE.Quaternion().setFromAxisAngle( axis1, rotate1 );
    const q2 = new THREE.Quaternion().setFromAxisAngle( axis2, rotate2 );
    armG.applyQuaternion( q1 );
    lowerArmG.applyQuaternion( q2 );
}

回転にはクオータニオンを使う。クオータニオンの細かいことは分からないが、とりあえず任意の軸にそってメッシュを回転させる、というのが実現できればよい。

まずそれぞれメッシュの回転を初期化。

armG.quaternion.set( 0,0,0,1 );
lowerArmG.quaternion.set( 0,0,0,1 );

次に回転させたい軸の設定をする。腕全体はx軸にそって回転させればいいので、axis1に(1,0,0)のベクトルを設定。

ひじから先はupperArmの曲がっている方向を軸にして回転させたいので、upperArmの最後の角度からベクトルを計算する。

const axis1 = new THREE.Vector3( 1,0,0 );
const axis2 = new THREE.Vector3( Math.cos(r),Math.sin(r),0 ).normalize();

軸に設定するベクトルは正規化されている必要があるので注意。

最後に、setFromAxisAngleメソッドでそれぞれの軸と回転させたい角度を設定し、メッシュのグループに適用する。

const q1 = new THREE.Quaternion().setFromAxisAngle( axis1, rotate1 );
const q2 = new THREE.Quaternion().setFromAxisAngle( axis2, rotate2 );
armG.applyQuaternion( q1 );
lowerArmG.applyQuaternion( q2 );

アニメーション

アニメーションループに腕の回転の動きも追加する。

armGの回転はrotVal1、lowerArmGの回転はrotVal2に値の推移をセットしている。

アニメーションの値を書き込むようのオブジェクトの、positionのプロパティには腕の曲げ角度の方を入れているので、回転はscaleのプロパティを使うことにした。

それぞれのキーフレームを配列としてAnimationClipに渡すことで、同時に曲げと回転の値をアニメーションできる。

const upperArmMove = new THREE.Object3D();
//armUpdateに渡すangleとrotateの値をセット
const dur = [ 0, 2, 4 ];
const posVal1 = [ -0.5, 0, -0.5 ];
const posVal2 = [ 0, 1.5, 0 ];
const rotVal1 = [ 0, 0, 0 ];
const rotVal2 = [ 0, -PI, 0 ];
//配列の形にする
const upperArmPos = [];
const upperArmRot = [];
for( let i=0; i<dur.length; i++ ){
    upperArmPos.push( posVal1[i] );
    upperArmPos.push( posVal2[i] );
    upperArmPos.push( 0 );
    upperArmRot.push( rotVal1[i] );
    upperArmRot.push( rotVal2[i] );
    upperArmRot.push( 0 );
}

//それぞれのキーフレームを作りclipに配列として追加
const uppperArmKF1 = new THREE.NumberKeyframeTrack( '.position', dur, upperArmPos );
const uppperArmKF2 = new THREE.NumberKeyframeTrack( '.scale', dur, upperArmRot );
const clip = new THREE.AnimationClip( 'Action', 4, [ uppperArmKF1, uppperArmKF2 ] );

レンダリングサイクル内でarmUpdate関数を呼び出す。

const clock = new THREE.Clock();
render();
function render(){
    //animation update
    mixer.update(clock.getDelta());
    let angle1 = upperArmMove.position.x;
    let angle2 = upperArmMove.position.y;
    let rot1 = upperArmMove.scale.x;
    let rot2 = upperArmMove.scale.y;
    armUpdate( angle1, angle2, rot1, rot2 );

    //cycle
    requestAnimationFrame(render);
    renderer.render(scene, camera);
}