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

今回で腕は完成です。

指を追加する

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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();
}
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(); }
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(指のグループ)の座標と角度を更新する処理を入れる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function armUpdate( angle1, angle2 ){
//upperArm
lastValClear();
upperArmUpdate( angle1 );
//lowerArm
lowerArmUpdate( angle2 );
//hand
handG.rotation.z = lastAngle; //角度を更新
handG.position.set( lastPos.x, lastPos.y, 0 ); //位置を更新
}
function armUpdate( angle1, angle2 ){ //upperArm lastValClear(); upperArmUpdate( angle1 ); //lowerArm lowerArmUpdate( angle2 ); //hand handG.rotation.z = lastAngle; //角度を更新 handG.position.set( lastPos.x, lastPos.y, 0 ); //位置を更新 }
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)を個別に回転できるようにする。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 );
}
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 ); }
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 );
}

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
armG.quaternion.set( 0,0,0,1 );
lowerArmG.quaternion.set( 0,0,0,1 );
armG.quaternion.set( 0,0,0,1 ); lowerArmG.quaternion.set( 0,0,0,1 );
armG.quaternion.set( 0,0,0,1 );
lowerArmG.quaternion.set( 0,0,0,1 );

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const axis1 = new THREE.Vector3( 1,0,0 );
const axis2 = new THREE.Vector3( Math.cos(r),Math.sin(r),0 ).normalize();
const axis1 = new THREE.Vector3( 1,0,0 ); const axis2 = new THREE.Vector3( Math.cos(r),Math.sin(r),0 ).normalize();
const axis1 = new THREE.Vector3( 1,0,0 );
const axis2 = new THREE.Vector3( Math.cos(r),Math.sin(r),0 ).normalize();

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const q1 = new THREE.Quaternion().setFromAxisAngle( axis1, rotate1 );
const q2 = new THREE.Quaternion().setFromAxisAngle( axis2, rotate2 );
armG.applyQuaternion( q1 );
lowerArmG.applyQuaternion( q2 );
const q1 = new THREE.Quaternion().setFromAxisAngle( axis1, rotate1 ); const q2 = new THREE.Quaternion().setFromAxisAngle( axis2, rotate2 ); armG.applyQuaternion( q1 ); lowerArmG.applyQuaternion( q2 );
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に渡すことで、同時に曲げと回転の値をアニメーションできる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 ] );
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 ] );
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関数を呼び出す。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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);
}
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); }
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);
}