式姫で遊ぶ日記

ブラウザ3Dゲームを作る作戦その4: MMDデータを読み込んで動かす

前回はcubeだったので今回は代わりにMMDデータを読み込んで使いたいと思います。

読み込む

MMDLoaderというものがあるので、今回はそれを使っていきます。
threejs.org

MMDデータまわりを使うために、一通り javascriptファイルを読み込んでおくと良さそうです。
追記:RawGitが終了するようなのでアドレスを変更

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/106/three.min.js"></script>

<script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r106/examples/js/libs/mmdparser.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r103/examples/js/libs/ammo.js"></script>
<script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r106/examples/js/loaders/TGALoader.js"></script>
<script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r106/examples/js/loaders/MMDLoader.js"></script>
<script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r106/examples/js/animation/MMDAnimationHelper.js"></script>
<script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r106/examples/js/effects/OutlineEffect.js"></script>
<script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r106/examples/js/animation/CCDIKSolver.js"></script>
<script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r106/examples/js/animation/MMDPhysics.js"></script>

読み込み自体は公式で説明されている通りです。今回は
https://github.com/takahirox/MMDLoader-app#readme
ここで説明されているモデルをお借りしています。

// pmd/pmxファイルの読み込み
var miku;
var mesh_loaded = 0;
	
// Load a MMD model
modelFile = 'https://cdn.rawgit.com/mrdoob/three.js/r87/examples/models/mmd/miku/miku_v2.pmd';

// load のためのオブジェクトを作る
const loader = new THREE.MMDLoader(); 

// ロード部分本体
loader.load( modelFile, function (mesh) {
	// モデルファイルの読み込み
	miku = mesh;
	scene.add( miku );
	mesh_loaded +=1;
	console.log(miku);
    
}, onProgress, onError);

//MMD loading progress
function onProgress( xhr ) {
  if ( xhr.lengthComputable ) {
    let percentComplete = xhr.loaded / xhr.total * 100;
    console.log( Math.round( percentComplete, 2 ) + '% downloaded' );
  }
};

// Error MMD Load
function onError( xhr ) {
};


これでとりあえずモデルだけ差し替えることができました。

移動しているときに歩かせる/走らせる

ざっくり調べた感じだと、特に最近のバージョンでは THREE.MMDAnimationHelper というものを使ってモーションを適用していくようです。

こちらが長いコードになるのですが、大まかな流れは

  1. モデルデータ(.pmd/.pmx)とモーションデータ(.vmd)を読み込む
  2. モデルデータとモーションデータ(全部)をTHREE.MMDAnimationHelperにセットする
  3. セットしたモーションをばらばらに切り出して覚えておく

となっています。

詳細は公式のリファレンス&サンプルを見ると良いと思います。1つのMMDAnimationHelperに対しては1回しかhelper.add できませんが、切り替え方法として

  • モーションを切り替えるたびにモーションと(特にphysics)をセットしなおすとメモリが爆発した。
  • helperを複数用意するとphysicsのつながりが不自然になった。

なので1つのMMDAnimationHelperにモーションを配列として全部セットして、後で切り替える方法が良さそうです。

下準備:モーションファイル情報をまとめておく

 このあたりは個別に変数を作ってもよいと思います。モーションデータは
【MMD】移動モーションv1.3 - BowlRoll
からお借りして改変しています。

// モーションファイル (vmd:Vocaloid Motion Data)
var motionFiles = [
	{filePath : './vmd/walk/walk.vmd',
	loop : THREE.LoopOnce, action : null, fadeIn: 0.1, fadeOut: 0.1  },	
	{filePath : './vmd/walk/run.vmd',
	loop : THREE.LoopOnce, action : null, fadeIn: 0.1, fadeOut: 0.1 },
	{filePath : './vmd/Lat式/Jump_resized/1.段差に飛び乗る/2.ジャンプ_(11f_上移動3~10の間_前移動0~10の間).vmd', 
	loop : THREE.LoopOnce, action : null , fadeIn: 0.1, fadeOut: 1.0},
	{filePath : './vmd/Lat式/Jump_resized/1.段差に飛び乗る/3.着地_(8f_移動なし).vmd',  
	loop : THREE.LoopOnce, action : null , fadein: 0.1, fadeOut: 0.1}
];
モデルデータ・モーションデータをロードしてhelperにセットする
// ロード部分本体
loader.load( modelFile, function (mesh) {
	// モデルファイルの読み込み
	miku = mesh;
    
	// モーションファイル(vmd:Vocaloid Motion Data)
	LoadVmd( 3, miku );
	LoadVmd( 2, miku );
	LoadVmd( 1, miku );
	LoadVmd( 0, miku );
    
    // motionをセットする
	helper.add(mesh,{
	animation: animations,
	physics: true,
	warmup  : 6, 
	unitStep : 1/120
    });
    mesh_loaded =1;
	//console.log(miku);			        
}, onProgress, onError);

// モーションファイル読み込み
function LoadVmd( motionId, mesh ){
	vmdPath = motionFiles[motionId].filePath;
	// アニメーションファイルを読み込む
	loader.loadAnimation(vmdPath, mesh, function(vmd){
		// vmdを保持しておく
		animations[motionId] = vmd;
	}, onProgress, onError);
	return true;
}

//MMD loading progress
function onProgress( xhr ) {
  if ( xhr.lengthComputable ) {
    let percentComplete = xhr.loaded / xhr.total * 100;
    console.log( Math.round( percentComplete, 2 ) + '% downloaded' );
  }
};
// Error MMD Load
function onError( xhr ) {
};

helper.addのwarmupやunitStepは物理演算で骨格以外が伸びて戻ってこなくなった時などに調整すると良さそうです。

セットしたモーションをばらばらに切り出して覚えておく

ロードが完了したら取り出していきます。どこにセットされているかは公式の
https://threejs.org/docs/#examples/en/animations/MMDAnimationHelper.objects
https://threejs.org/docs/#api/en/animation/AnimationMixer.existingAction
のあたりが参考になります。

function initAction(){
	scene.add( miku );
	// 全てのモーションがセットされているmixer
	let mixer = helper.objects.get( miku ).mixer;
	// 全てのモーションの停止
	mixer.stopAllAction();
	for(let i=0;i<4; i++){
		if(animations[i]){
    		// ばらばらに取り出す
			let action = mixer.clipAction( animations[i] );
			// Loopさせるかどうか
			action.setLoop( motionFiles[i].loop );
			// 保持しておく
			motionFiles[i].action= action;
		}else{return false;}
	}
	
	// インバースキネマティクス(IK)の bone連動
	ikHelper = helper.objects.get( miku  ).ikSolver.createHelper();
	ikHelper.visible = true;
	scene.add( ikHelper );
	
	// 物理演算の helper
	physicsHelper = helper.objects.get( miku  ).physics.createHelper();
	physicsHelper.visible = false;
	scene.add( physicsHelper );
	mesh_loaded +=1;	
}


ロードがすべて完了してからこのように取り出していくと良さそうです。

切り出したモーションの適用

アクションの制御に関しては
https://threejs.org/docs/#api/en/animation/AnimationAction.reset
あたりが参考になります。切り替え時の骨格の破綻を防ぐためにaction のweight やfadeOut、fadeInなどを利用すると
良いと思います。これらでほとんど破綻しなくなります。

if(player.isStop==0 ){
	// motionが切り替わった時
	if(player.lastMotion != selectmotion){
		 //切り替え前のモーションを重みを低く適用&フェードアウトさせる
		 motionFiles[player.lastMotion].action.weight =0.3;
		 helper.update( frameTime );
		 motionFiles[player.lastMotion].action.fadeOut(motionFiles[selectmotion].fadeOut);
		 //切り替え後のモーションを重みを低く適用&フェードインさせる
		 motionFiles[selectmotion].action.reset();
		 motionFiles[selectmotion].action.play();
		 motionFiles[selectmotion].action.fadeIn(motionFiles[selectmotion].fadeIn);
		 motionFiles[selectmotion].action.weight =0.3;
	}else{
		 motionFiles[selectmotion].action.weight =1;
	}
	
	helper.update( frameTime );
	player.lastMotion = selectmotion;
}	


フレームごとに呼ばれる animationの中でhelper.update( frameTime ); することで勝手にframeTime分アニメーションを進めてくれます。