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

ひじの作成

前回作成した上腕に繋がる関節(ひじ)部分を作る。

完成イメージ

ボーンを作成する

makePipeptと同様に、まずボーンの座標を計算する。

//make origin
const radius = upperArmObj.thick[0];
const origin = new THREE.Vector2( 0,-radius );
origin.rotateAround( center2D, lastAngle );

radiusは円の半径(関節の太さ)。上腕の太さから取得。均一の太さにするので配列にしない。

次に、ボーンベクトルを回転させる際の中心点となるoriginを取得する。

originはupperArmを真横から見た時に、先端の一番下側の点になる(下の図参照)

上腕の最後の角度によってorigin座標が変わるので、グローバルにlastAngle変数を用意して、makePipePt関数内で最後の円の角度をlastAngle変数に格納するようにした。

originを中心としてboneベクトルを回転させることで各ボーンの座標を取得する。

const bone = new THREE.Vector2();
const pt = [];
for( let i=0; i<( seg+1 ); i++ ){
    pt[i] = [];
    let angle = i==0 ? 0 : bend / obj.seg;
    bone.rotateAround( origin, angle );
    for( let j=0; j<edge; j++ ){
        const theta = j * 2 * PI / edge;
        const w = radius * cos( theta );
        const h = radius * sin( theta );
        const v = new THREE.Vector2( 0, h );
        v.add( bone );
        v.rotateAround( bone, i * angle + lastAngle );
        pt[i][j] = [ v.x, v.y, w ];
    }
}

angleにボーンのベクトルを回転させる角度をセットし、originを中心にしてセグメントごとに回転させる。

あとはmakePipePtと同じ要領で、各ボーンを中心にした円の座標を計算している。

関数化したものが以下。

function makeJointPt( obj, bend ){
    //make origin
    const radius = obj.thick[0];
    const origin = new THREE.Vector2( 0,-radius );
    origin.rotateAround( center2D, lastAngle );
    //set pt
    const bone = new THREE.Vector2();
    const pt = [];
    for( let i=0; i<( obj.seg+1 ); i++ ){
        pt[i] = [];
        let angle = i==0 ? 0 : bend / obj.seg;
        bone.rotateAround( origin, angle );
        for( let j=0; j<obj.edge; j++ ){
            const theta = j * 2 * PI / obj.edge;
            const w = radius * cos( theta );
            const h = radius * sin( theta );
            const v = new THREE.Vector2( 0, h );
            v.add( bone );
            v.rotateAround( bone, i * angle + lastAngle );
            pt[i][j] = [ v.x, v.y, w ];
        }
    }
    //update values
    lastAngle += bend;  //lastAngleの値を更新する。
    lastBonePos = bone; //boneの終端の値をlastBonePosに格納する。
    return pt;
}

メッシュの作成

function jointInit(){
    const pt = makeJointPt( upperArmObj, -0.05 );
    jointArmGeo = makeGeometry( upperArmObj, pt );
    jointArmMesh = new THREE.Mesh( jointArmGeo, upperArmMat );
    scene.add( jointArmMesh );
}

makeJointPtで座標を取得、この時角度が0だとfacelessなmeshになってしまいエラーが出るので、とりあえず小さい数字を渡している。

ジオメトリとメッシュを作成してシーンに追加。必要パラメータがupperArmと一緒なのでとりあえずupperArmObjを渡しているが、別途jointArmObjを用意してもよいのかも。

アニメーションする

function jointArmUpdate( angle ){
    const bend = mapping( angle, 0.0, 1.5, -0.01, -3*PI/4 );
    const pt = makeJointPt( upperArmObj, bend );
    updateGeometry( upperArmObj, pt, jointArmGeo );
    jointArmMesh.position.set( upperArmObj.ep.x,  upperArmObj.ep.y, 0 );
}

0(曲げない)~1.5(最大限曲げる)の間で角度をマッピングし、makeJointPtで新しい座標を取得、updateGeometryでジオメトリを更新する。

upperArmの先端があるポイントにひじのメッシュの初期位置を移動させる。

function limbupdate( bend1, bend2 ){
    lastAngle = 0;
    lastPos = new THREE.Vector2();
    upperArmUpdate( bend1 );
    jointArmUpdate( bend2 );
};

upperArmUpdate⇒jointUpdateの順番でジオメトリを更新する関数を作成。

ジオメトリをアップデートする関数の最初にlastAngleとlastPosを初期化する処理を入れておく。

(※これがないとアニメーションループごとに値が増えていってしまう)

キーフレームの作成

const upperArmMove = new THREE.Object3D();
const dur = [ 0, 2, 4 ];
const val1 = [ 2, -1, 2 ];
const val2 = [ 0, 1.5, 0 ];

const upperArmPos = [];
for( let i=0; i<dur.length; i++ ){
    upperArmPos.push( val1[i] );
    upperArmPos.push( val2[i] );
    upperArmPos.push( 0 );
}

const uppperArmKF = new THREE.NumberKeyframeTrack( '.position', dur, upperArmPos );

今回はupperArmとJointの両方を同時に動かすため、ダミーで作成したObject3DのuserDataではなく、positionをパラメータとして使う。

positionを使うことでx, y, zの3つのパラメータが同時に設定できる。

position.xにupperArmの曲がり具合を、position.yにJointの曲がり具合を決める値を入れている。

function render(){
    //animation update
    mixer.update(clock.getDelta());
    let angle1 = upperArmMove.position.x;
    let angle2 = upperArmMove.position.y;
    limbupdate( angle1, angle2 );
    //cycle
    requestAnimationFrame(render);
    renderer.render(scene, camera);
}

レンダリングサイクル内で、positionからパラメータをそれぞれ取得し、limbUpdateでメッシュのジオメトリを更新する。

動いているところは以下のようになる。

(境目が気になるので、個別にメッシュを作成⇒くっつける、ではなく1個のメッシュにしようかなとおもう。。)