12
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

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

わーい! 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の操作で色々できるようになります。
お好みのモデル、モーションで遊んでみてください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
12
Help us understand the problem. What are the problem?