three.js
MMD

three.jsでMMDのアニメーションを切り替える

More than 1 year has passed since last update.

わーい! three.jsでMMDを使えるの? すごーい! three.jsで遊ぶの、たーのしー!

と遊んでいたら、MMDのアニメーション切り替えに、思いの外、苦戦したので、忘れないように・・・。


使用したもの


アニメーションを切り替えるには

前提として、three.jsの基本的な操作(シーンの作成等)は理解しているものとします。


複数のVMDをloadする

three.jsのexampleにあるMMDLoaderでモデルをloadします。

exampleには、以下のコードが記述されています。


webgl_loader_mmd.html

var loader = new THREE.MMDLoader();

loader.load( modelFile, vmdFiles, function ( object ) {
mesh = object;
}, onProgress, onError );

このvmdFilesには、VMDファイルのパスを配列で渡すことができるのですが、これはモーションをマージする用途で、合成されたアニメーションが1つセットされます。

こんなことになります。

スクリーンショット 2017-02-18 7.19.27.png

複数のアニメーションをセットするには、load()ではなく、loadModel()でモデルデータを読み込んだ後に、loadVmd()createAnimation()を複数回実行してあげる必要があるようです。

load部分の記述は、以下のようにしました。

var loader, helper, mesh;

var init = function() {
var modelFile = 'assets/Lat式ミクVer2.31/Lat式ミクVer2.31_Normal.pmd';

var vmdFiles = [
{name: '朝の背伸び', file: 'assets/日常・非日常モーションセットV104/朝の背伸び.vmd'},
{name: 'えへへへへ', file: 'assets/日常・非日常モーションセットV104/えへへへへ.vmd'},
{name: '腕を組む', file: 'assets/日常・非日常モーションセットV104/腕を組む.vmd'},
{name: '振り向き2', file: 'assets/日常・非日常モーションセットV104/振り向き2.vmd'},
{name: 'ばーん!', file: 'assets/日常・非日常モーションセットV104/ばーん!.vmd'},
{name: 'テレポートアウト2', file: 'assets/日常・非日常モーションセットV104/テレポートアウト2.vmd'}
];

loadmmd(modelFile, vmdFiles);

}

var loadmmd = function (modelFile, vmdFiles) {
var onProgress = function (xhr) {

};
var onError = function (xhr) {
console.log('load mmd error');
};

helper = new THREE.MMDHelper();
loader = new THREE.MMDLoader();
loader.loadModel(modelFile, function (object) {
mesh = object;
scene.add(mesh);
helper.add(mesh);

if (vmdFiles && vmdFiles.length !== 0) {
var vmdIndex = 0;
var loadVmd = function () {
var vmdFile = vmdFiles[vmdIndex].file;
loader.loadVmd(vmdFile, function (vmd) {
loader.createAnimation(mesh, vmd, vmdFiles[vmdIndex].name);
vmdIndex++;
if (vmdIndex < vmdFiles.length) {
loadVmd();
} else {
helper.setAnimation(mesh);

mesh.mixer.stopAllAction();

helper.setPhysics(mesh);
helper.unifyAnimationDuration({afterglow: 1.0});

console.log(mesh.geometry.animations.length);
}
}, onProgress, onError);
};
loadVmd();
}
}, onProgress, onError);
};


すべてのアニメーションを止める

MMDHelper.setAnimations()を行うと、最初に読み込んだアニメーションが自動でループ再生されます。

アニメーションを止めるには、THREE.AnimationMixerを使います。

MMDHelper.setAnimations()mesh.mixerにAnimationMixerがセットされているため、mesh.mixer.stopAllAction()で、meshに紐付くアニメーションがすべて停止します。

helper.setAnimations(mesh);

mesh.mixer.stopAllAction();


特定のアニメーションを再生/停止する

mesh.geometry.animationsにセットされたアニメーションから対象のAnimationClipを指定して、mesh.mixer.clipAction()AnimationActionを作成します。

これで、play()stop()reset()などのメソッドが使えるようになります。

clip = mesh.geometry.animations[i];

action = mesh.mixer.clipAction(clip);

action.reset();
action.play();


特定のアニメーションを探す

悩んだ末に、VMDのload時に指定したnameで探す形にしました。

for (var i = 0; i < mesh.geometry.animations.length; ++i) {

if (mesh.geometry.animations[i].name === name) {
clip = mesh.geometry.animations[i];
}
}

loadの際に読み込んだ順番を持っておいても良いと思います。ただ、後述の理由でindexが一致しないので注意してください。


アニメーションが分裂する

先程のloadmmd()が正しく動作しているかテストするため、

var modelFile = 'assets/Lat式ミクVer2.31/Lat式ミクVer2.31_Normal.pmd';

var vmdFiles = [
{name: '朝の背伸び', file: 'assets/日常・非日常モーションセットV104/朝の背伸び.vmd'},
{name: 'えへへへへ', file: 'assets/日常・非日常モーションセットV104/えへへへへ.vmd'},
{name: '腕を組む', file: 'assets/日常・非日常モーションセットV104/腕を組む.vmd'},
{name: '振り向き2', file: 'assets/日常・非日常モーションセットV104/振り向き2.vmd'},
{name: 'ばーん!', file: 'assets/日常・非日常モーションセットV104/ばーん!.vmd'},
{name: 'テレポートアウト2', file: 'assets/日常・非日常モーションセットV104/テレポートアウト2.vmd'}
];

loadmmd(modelFile, vmdFiles);

を実行した後、アニメーションの数を出力してみます。

console.log(mesh.geometry.animations.length);

スクリーンショット 2017-02-17 22.14.20.png

VMDのファイルパスを6つ渡したので、ちゃんと6に・・・12?

倍になりました!(・∀・)

理由を探ったところ、以下を実行した際にMMDLoader.createAnimation内で、VMDのアニメーションはボーンアニメーション(モーション)とモーフアニメーション(表情など)の2つに分かれるようです。

loader.createAnimation(mesh, vmd, vmdFiles[vmdIndex].name);

最初、このことに気付かず、モーションを切り替えたはずが実はindexがズレていて上手く動かないように見えたり、ボーンアニメーションのみ再生されて無表情でモーションを行うシュールな状態に陥ってたりしていました。

indexで扱う場合、読み込んだ順番(0から始まるインデックス)をiとすると、ボーンアニメーションが2i、モーフアニメーションが2i+1になります。

nameで扱う場合、指定した名前を'Jump'とすると、ボーンアニメーションはそのまま'Jump'、モーフアニメーションは末尾に'Morph'が付与され、'JumpMorph'になっています。


アニメーションのくり返し制御

repetitionプロパティで、アニメーションのくり返し回数を設定できます。

ループさせる場合は'Infinity'、くり返しを行わずに1回だけの場合は0です。

if (loop) {

action.repetitions = 'Infinity';
} else {
action.repetitions = 0;
}


アニメーションの切り替え

試行錯誤した結果、こうなりました。

自分で書いておいて、あまりスマートではない気もしていますが、良きに計らってくださいまし。

var changeAnimation = function (name, loop) {

var clip, action, morph;

for (var i = 0; i < mesh.geometry.animations.length; ++i) {
if (mesh.geometry.animations[i].name === name) {
clip = mesh.geometry.animations[i];
action = mesh.mixer.clipAction(clip);
} else if (mesh.geometry.animations[i].name === name + 'Morph') {
clip = mesh.geometry.animations[i];
morph = mesh.mixer.clipAction(clip);
}
}

if (loop) {
action.repetitions = 'Infinity';
morph.repetitions = 'Infinity';
} else {
action.repetitions = 0;
morph.repetitions = 0;
}

mesh.mixer.stopAllAction();
action.play();
morph.play();
};


アニメーションを合成する

loadmmd()でそれぞれ読み込んだアニメーションですが、複数のアニメーションを同時に再生することができます。

また、アニメーションの再生/停止で使ったAnimationActionには、weightプロパティがあり、アニメーションに強弱を付けて、合成することができます。

アニメーションの合成用に、切り替えと似たような関数を作りました。

違いとしては、weightの指定を行っている点と現在再生しているアニメーションを止めずに新たなアニメーションを重ねて再生している点です。

var blendAnimation = function (name, weight) {

var clip, action, morph;

for (var i = 0; i < mesh.geometry.animations.length; ++i) {
if (mesh.geometry.animations[i].name === name) {
clip = mesh.geometry.animations[i];
action = mesh.mixer.clipAction(clip);
} else if (mesh.geometry.animations[i].name === name + 'Morph') {
clip = mesh.geometry.animations[i];
morph = mesh.mixer.clipAction(clip);
}
}
action.weight = weight;
morph.weight = weight;
action.play();
morph.play();
};

2つのアニメーションを足し合わせて、結果を見てみました。

blendAnimation('振り向き2', 0.3);

blendAnimation('ばーん!', 0.7);

ちゃんと合成されていますね。

weightの本来の使い方としては、"歩く"から"走る"など、アニメーションのスムーズな移行に使うものらしいです。


まとめ

冒頭に書いた通り、軽い気持ちで始めた割には、MMDLoader周りのソースコードをがっつり読みました。

ハマりどころとしては、


  • MMDLoader.loadだとアニメーションが合成される

  • モーフアニメーションが別に保存される

といったあたりです。

MMDLoaderの中身を読んで動作を理解してしまえば、AnimationMixerの操作で色々できるようになります。

お好みのモデル、モーションで遊んでみてください。