わーい! three.jsでMMDを使えるの? すごーい! three.jsで遊ぶの、たーのしー!
と遊んでいたら、MMDのアニメーション切り替えに、思いの外、苦戦したので、忘れないように・・・。
使用したもの
- three.js r84
- Lat式ミク Ver2.31
- 日常・非日常モーションセット V104(えれこみゅ@すのこバーンP様)
アニメーションを切り替えるには
前提として、three.jsの基本的な操作(シーンの作成等)は理解しているものとします。
複数のVMDをloadする
three.jsのexampleにあるMMDLoaderでモデルをloadします。
exampleには、以下のコードが記述されています。
var loader = new THREE.MMDLoader();
loader.load( modelFile, vmdFiles, function ( object ) {
mesh = object;
}, onProgress, onError );
このvmdFiles
には、VMDファイルのパスを配列で渡すことができるのですが、これはモーションをマージする用途で、合成されたアニメーションが1つセットされます。
複数のアニメーションをセットするには、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);
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();
};
three.jsでアニメーション切り替え。Lat式ミクとえれこみゅ@すのこバーンP様のモーションデータを使用しています。 pic.twitter.com/H4tp9NQGgh
— jyuko (@jyuko49) 2017年2月17日
アニメーションを合成する
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);
複数アニメーションの合成。 pic.twitter.com/1FTkR3zuBf
— jyuko (@jyuko49) 2017年2月17日
ちゃんと合成されていますね。
weightの本来の使い方としては、"歩く"から"走る"など、アニメーションのスムーズな移行に使うものらしいです。
まとめ
冒頭に書いた通り、軽い気持ちで始めた割には、MMDLoader周りのソースコードをがっつり読みました。
ハマりどころとしては、
- MMDLoader.loadだとアニメーションが合成される
- モーフアニメーションが別に保存される
といったあたりです。
MMDLoaderの中身を読んで動作を理解してしまえば、AnimationMixerの操作で色々できるようになります。
お好みのモデル、モーションで遊んでみてください。