three.jsでカスタム図形を作る(4):アニメーションを付ける

前回の記事までで曲がるチューブ状のオブジェクトを作ったが、実は自前で計算しなくてもthree.jsにはSkinnedMeshという便利なオブジェクトがあり、ボーンによるアニメーションができるみたいだった。

でもアニメーション時に各ボーンの位置等をスクリプトで設定するのもそれはそれで大変かなという予感がしたので、とりあえずこのまま突き進みたいと思う。

パーツをクラス化?する

のちのち使いまわして手足に拡張することを考えて、Limbsのクラス的なものを作成して共通パラメータを吸収する。

ここではまず腕の関節から上をイメージしてupperArmパーツを作成する。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
//limbsクラス
const limbSeg = 5;  //任意の値:Limbsパーツ共通
const limbEdge = 12; //任意の値:Limbsパーツ共通
function Limbs(){
this.seg = limbSeg;
this.edge = limbEdge;
this.ep = new THREE.Vector2( 1,0 );
this.cp = new THREE.Vector2( 1,0 );
this.thick = thick;
this.width = width;
}
//upperarm太さの設定
const upperArmThick = new Array( limbSeg );
for( let i=0; i<( limbSeg+1 ); i++ ){
upperArmThick[i] = 5; //segmentごとの腕の太さ
}
//upperarmのパラメータ設定
const upperArm = new Limbs(
new THREE.Vector2( 10, 0 ),
new THREE.Vector2( 8, 0 ),
upperArmThick, //縦の太さ
upperArmThick //横の太さ
);
//meshを作成
const upperArmPt = makePipePt( upperArm );
const upperArmGeo = makeGeometry( upperArm, upperArmPt );
const upperArmMat = new THREE.MeshNormalMaterial({
side:THREE.DoubleSide,
});
const upperArmMesh = new THREE.Mesh( upperArmGeo, upperArmMat );
scene.add( upperArmMesh );
//limbsクラス const limbSeg = 5;  //任意の値:Limbsパーツ共通 const limbEdge = 12; //任意の値:Limbsパーツ共通 function Limbs(){ this.seg = limbSeg; this.edge = limbEdge; this.ep = new THREE.Vector2( 1,0 ); this.cp = new THREE.Vector2( 1,0 ); this.thick = thick; this.width = width; } //upperarm太さの設定 const upperArmThick = new Array( limbSeg ); for( let i=0; i<( limbSeg+1 ); i++ ){ upperArmThick[i] = 5; //segmentごとの腕の太さ } //upperarmのパラメータ設定 const upperArm = new Limbs( new THREE.Vector2( 10, 0 ), new THREE.Vector2( 8, 0 ), upperArmThick, //縦の太さ upperArmThick //横の太さ ); //meshを作成 const upperArmPt = makePipePt( upperArm ); const upperArmGeo = makeGeometry( upperArm, upperArmPt ); const upperArmMat = new THREE.MeshNormalMaterial({ side:THREE.DoubleSide, }); const upperArmMesh = new THREE.Mesh( upperArmGeo, upperArmMat ); scene.add( upperArmMesh );
//limbsクラス
const limbSeg = 5;  //任意の値:Limbsパーツ共通
const limbEdge = 12; //任意の値:Limbsパーツ共通
function Limbs(){
    this.seg = limbSeg; 
    this.edge = limbEdge;
    this.ep = new THREE.Vector2( 1,0 );
    this.cp = new THREE.Vector2( 1,0 );
    this.thick = thick;
    this.width = width;
 }

//upperarm太さの設定
const upperArmThick = new Array( limbSeg );
for( let i=0; i<( limbSeg+1 ); i++ ){
    upperArmThick[i] = 5; //segmentごとの腕の太さ
}

//upperarmのパラメータ設定
const upperArm = new Limbs(
    new THREE.Vector2( 10, 0 ),
    new THREE.Vector2( 8, 0 ),
    upperArmThick, //縦の太さ
    upperArmThick  //横の太さ
);

//meshを作成
const upperArmPt = makePipePt( upperArm );
const upperArmGeo = makeGeometry( upperArm, upperArmPt );
const upperArmMat = new THREE.MeshNormalMaterial({
    side:THREE.DoubleSide,
});
const upperArmMesh = new THREE.Mesh( upperArmGeo, upperArmMat );
scene.add( upperArmMesh );

Limbクラスのep, cpは0だと面の存在しない図形になってしまいエラーがでるので、適当な値をセットしておいた。

makePipePt、makeGeometryの関数はオブジェクトを渡す形式に変更して以下のように修正した。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function makePipePt( obj ){
//make bone
const curve = new THREE.QuadraticBezierCurve( center2D, obj.cp, obj.ep );
const bone = curve.getPoints( obj.seg );
let zpos = center2D;
//set points
const pt = [];
for( let i=0; i<( obj.seg+1 ); i++ ){
//calc angle
const diff = new THREE.Vector2().subVectors( bone[i], zpos );
const angle = Math.atan2( diff.y, diff.x );
//calc coords
pt[i] = [];
for( let j=0; j<obj.edge; j++ ){
const theta = j * 2 * PI / obj.edge;
const w = obj.thick[i] * cos( theta );
const h = obj.width[i] * sin( theta );
const v = new THREE.Vector2( 0, h );
v.add( bone[i] );
v.rotateAround( bone[i], angle );
pt[i][j] = [v.x, v.y, w];
}
zpos = bone[i];
}
return pt;
}
function makePipePt( obj ){ //make bone const curve = new THREE.QuadraticBezierCurve( center2D, obj.cp, obj.ep ); const bone = curve.getPoints( obj.seg ); let zpos = center2D; //set points const pt = []; for( let i=0; i<( obj.seg+1 ); i++ ){ //calc angle const diff = new THREE.Vector2().subVectors( bone[i], zpos ); const angle = Math.atan2( diff.y, diff.x ); //calc coords pt[i] = []; for( let j=0; j<obj.edge; j++ ){ const theta = j * 2 * PI / obj.edge; const w = obj.thick[i] * cos( theta ); const h = obj.width[i] * sin( theta ); const v = new THREE.Vector2( 0, h ); v.add( bone[i] ); v.rotateAround( bone[i], angle ); pt[i][j] = [v.x, v.y, w]; } zpos = bone[i]; } return pt; }
function makePipePt( obj ){
    //make bone
    const curve = new THREE.QuadraticBezierCurve( center2D, obj.cp, obj.ep );
    const bone = curve.getPoints( obj.seg );
    let zpos = center2D;
    //set points
    const pt = [];
    for( let i=0; i<( obj.seg+1 ); i++ ){
        //calc angle
        const diff = new THREE.Vector2().subVectors( bone[i], zpos );
        const angle = Math.atan2( diff.y, diff.x );
        //calc coords
        pt[i] = [];
        for( let j=0; j<obj.edge; j++ ){
            const theta = j * 2 * PI / obj.edge;
            const w = obj.thick[i] * cos( theta );
            const h = obj.width[i] * sin( theta );
            const v = new THREE.Vector2( 0, h );
            v.add( bone[i] );
            v.rotateAround( bone[i], angle );
            pt[i][j] = [v.x, v.y, w];
        }
        zpos = bone[i];
    }
    return pt;
}

また、makeGeometry内でジオメトリをマージして表面を滑らかに表示させるようにする処理を追加している。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function makeGeometry( obj, pt ){
  const vertices = setVertices( obj.seg, obj.edge, pt );
const indices = setIndices( obj.seg, obj.edge );
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute( vertices, 3 ));
geometry.setIndex(new THREE.BufferAttribute( indices, 1 ));
//マージ処理を追加
const merg = new THREE.Geometry().fromBufferGeometry( geometry );
merg.mergeVertices();
merg.computeVertexNormals();
return merg;
}
function makeGeometry( obj, pt ){   const vertices = setVertices( obj.seg, obj.edge, pt ); const indices = setIndices( obj.seg, obj.edge ); const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute( vertices, 3 )); geometry.setIndex(new THREE.BufferAttribute( indices, 1 )); //マージ処理を追加 const merg = new THREE.Geometry().fromBufferGeometry( geometry ); merg.mergeVertices(); merg.computeVertexNormals(); return merg; }
function makeGeometry( obj, pt ){
  const vertices = setVertices( obj.seg, obj.edge, pt );
    const indices = setIndices( obj.seg, obj.edge );
    const geometry = new THREE.BufferGeometry();
    geometry.setAttribute('position', new THREE.BufferAttribute( vertices, 3 ));
    geometry.setIndex(new THREE.BufferAttribute( indices, 1 ));
    //マージ処理を追加
    const merg = new THREE.Geometry().fromBufferGeometry( geometry );
    merg.mergeVertices();
    merg.computeVertexNormals();
    return merg;
}

アニメーションの設定

THREE.jsのAnimationシステムを利用して、任意の時間で補間された値を取得する。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
//①アニメーション用に空のオブジェクトを作成
const upperArmMove = new THREE.Object3D();
//②キーフレームの作成
const val = [ 0, 10 ]; //数値の変化
const dur = [ 0, 3 ]; //数値が変化する時間
const uppperArmKF = new THREE.NumberKeyframeTrack( '.userData', dur, val );
//③キーフレームをアニメーションクリップに登録
const clip = new THREE.AnimationClip( 'Action', 3, [ uppperArmKF ] );
//④mixerの設定
const mixer = new THREE.AnimationMixer( upperArmMove );
const clipAction = mixer.clipAction( clip );
//⑤アニメーション開始
clipAction.play();
//①アニメーション用に空のオブジェクトを作成 const upperArmMove = new THREE.Object3D(); //②キーフレームの作成 const val = [ 0, 10 ]; //数値の変化 const dur = [ 0, 3 ]; //数値が変化する時間 const uppperArmKF = new THREE.NumberKeyframeTrack( '.userData', dur, val ); //③キーフレームをアニメーションクリップに登録 const clip = new THREE.AnimationClip( 'Action', 3, [ uppperArmKF ] ); //④mixerの設定 const mixer = new THREE.AnimationMixer( upperArmMove ); const clipAction = mixer.clipAction( clip ); //⑤アニメーション開始 clipAction.play();
//①アニメーション用に空のオブジェクトを作成
const upperArmMove = new THREE.Object3D(); 

//②キーフレームの作成
const val = [ 0, 10 ]; //数値の変化
const dur = [ 0, 3 ];  //数値が変化する時間
const uppperArmKF = new THREE.NumberKeyframeTrack( '.userData', dur, val );

//③キーフレームをアニメーションクリップに登録
const clip = new THREE.AnimationClip( 'Action', 3, [ uppperArmKF ] );

//④mixerの設定
const mixer = new THREE.AnimationMixer( upperArmMove );
const clipAction = mixer.clipAction( clip );

//⑤アニメーション開始
clipAction.play(); 

①THREE.jsのAnimationシステム本来オブジェクトのプロパティ(position等)に設定してアニメーションさせる仕組みになっているが、今回は直接オブジェクトを動かすのではなく値だけがほしいので、最初に空のオブジェクト(upperArmMove)を作成している。

②durの時間をかけてvalに変化するキーフレームを作成、①のオブジェクトのuserDataプロパティと紐づける。

③作成したキーフレームをアニメーションクリップに登録。この時別の複数のキーフレームを配列で登録することが可能。(同じオブジェクトに対して複数のプロパティの値を変化させられる)

④ミキサーを作成してオブジェクトとクリップに紐づけ。

⑤アニメーションを開始させる。

レンダリングサイクルでアニメーションをアップデートする

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const clock = new THREE.Clock();
render();
function render(){
//アニメーションのアップデート
mixer.update(clock.getDelta());
const v = upperArmMove.userData;
upperArm.ep = new THREE.Vector2( 10, v );
const upperArmPt2 = makePipePt( upperArm );
updateGeometry( upperArm, upperArmPt2, upperArmGeo );
//ループ
requestAnimationFrame(render);
renderer.render(scene, camera);
}
const clock = new THREE.Clock(); render(); function render(){ //アニメーションのアップデート mixer.update(clock.getDelta()); const v = upperArmMove.userData; upperArm.ep = new THREE.Vector2( 10, v ); const upperArmPt2 = makePipePt( upperArm ); updateGeometry( upperArm, upperArmPt2, upperArmGeo ); //ループ requestAnimationFrame(render); renderer.render(scene, camera); }
const clock = new THREE.Clock();
render();
function render(){
    //アニメーションのアップデート
    mixer.update(clock.getDelta());
    const v = upperArmMove.userData;
    upperArm.ep = new THREE.Vector2( 10, v );
    const upperArmPt2 = makePipePt( upperArm );
    updateGeometry( upperArm, upperArmPt2, upperArmGeo );
    //ループ
    requestAnimationFrame(render);
    renderer.render(scene, camera);
}

レンダーサイクルの中でmixerをアップデート(これをしないと数値が変化しない)。

mixerはupperArmMoveのuserDataプロパティを変化させるよう紐づけられているので、これを参照してupperArmのプロパティであるepのy軸を変化させている。

変更した値でmakePipePt関数で座標を再計算し取得する。

さらに、取得した座標でジオメトリを更新する関数を呼び出し。関数の内容は以下。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function updateGeometry( obj, pt, geometry ){
const indices = setIndices( obj.seg, obj.edge );
const vertices = setVertices( obj.seg, obj.edge, pt );
const newGeo = new THREE.BufferGeometry();
newGeo.setAttribute('position', new THREE.BufferAttribute( vertices, 3 ));
newGeo.setIndex(new THREE.BufferAttribute( indices, 1 ));
const merg = new THREE.Geometry().fromBufferGeometry( newGeo );
merg.mergeVertices();
geometry.vertices = merg.vertices;
geometry.elementsNeedUpdate = true;
geometry.computeVertexNormals();
}
function updateGeometry( obj, pt, geometry ){ const indices = setIndices( obj.seg, obj.edge ); const vertices = setVertices( obj.seg, obj.edge, pt ); const newGeo = new THREE.BufferGeometry(); newGeo.setAttribute('position', new THREE.BufferAttribute( vertices, 3 )); newGeo.setIndex(new THREE.BufferAttribute( indices, 1 )); const merg = new THREE.Geometry().fromBufferGeometry( newGeo ); merg.mergeVertices(); geometry.vertices = merg.vertices; geometry.elementsNeedUpdate = true; geometry.computeVertexNormals(); }
function updateGeometry( obj, pt, geometry ){
    const indices = setIndices( obj.seg, obj.edge );
    const vertices = setVertices( obj.seg, obj.edge, pt );
    const newGeo = new THREE.BufferGeometry();
    newGeo.setAttribute('position', new THREE.BufferAttribute( vertices, 3 ));
    newGeo.setIndex(new THREE.BufferAttribute( indices, 1 ));
    const merg = new THREE.Geometry().fromBufferGeometry( newGeo );
    merg.mergeVertices();
    geometry.vertices = merg.vertices;
    geometry.elementsNeedUpdate = true;
    geometry.computeVertexNormals();
}

引数で渡された新しい座標(pt)でBufferGeometryを作成⇒マージ⇒元のジオメトリに代入⇒ノーマルの再計算。

という流れ。elementsNeedUpdate = trueの設定はレンダリングごとに必要(毎回自動的にfalseになるらしい)。

これで、3秒かけてチューブの端のy座標が0から10に変化するアニメーションが出来た。

アニメーションのようす

「three.jsでカスタム図形を作る(4):アニメーションを付ける」への1件のフィードバック

コメントは停止中です。