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

わーい！ three.jsでMMDを使えるの？ すごーい！ three.jsで遊ぶの、たーのしー！
と遊んでいたら、MMDのアニメーション切り替えに、思いの外、苦戦したので、忘れないように・・・。


## 使用したもの
* [three.js](https://threejs.org/) r84
* [Lat式ミク](https://bowlroll.net/file/30199) Ver2.31
* [日常・非日常モーションセット](http://www.nicovideo.jp/watch/sm23462394) V104（えれこみゅ＠すのこバーンP様）


## アニメーションを切り替えるには
前提として、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](https://qiita-image-store.s3.amazonaws.com/0/153460/08feba5b-19dc-4f64-1d58-fb8c1b3636f2.png)

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

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

```js
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](https://threejs.org/docs/index.html#Reference/Animation/AnimationMixer)を使います。
`MMDHelper.setAnimations()`で`mesh.mixer`にAnimationMixerがセットされているため、`mesh.mixer.stopAllAction()`で、meshに紐付くアニメーションがすべて停止します。

```js
helper.setAnimations(mesh);
mesh.mixer.stopAllAction();
```

### 特定のアニメーションを再生/停止する
`mesh.geometry.animations`にセットされたアニメーションから対象の[AnimationClip](https://threejs.org/docs/index.html#Reference/Animation/AnimationClip)を指定して、`mesh.mixer.clipAction()`で[AnimationAction](https://threejs.org/docs/index.html#Reference/Animation/AnimationAction)を作成します。
これで、`play()`、`stop()`、`reset()`などのメソッドが使えるようになります。

```javascript
clip = mesh.geometry.animations[i];
action = mesh.mixer.clipAction(clip);

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

### 特定のアニメーションを探す
悩んだ末に、VMDのload時に指定したnameで探す形にしました。

```js
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()`が正しく動作しているかテストするため、

```js
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);
```
を実行した後、アニメーションの数を出力してみます。

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

![スクリーンショット 2017-02-17 22.14.20.png](https://qiita-image-store.s3.amazonaws.com/0/153460/578fe46b-f3c3-6ff6-0c73-a5da27f17c76.png)

VMDのファイルパスを6つ渡したので、ちゃんと6に・・・12？
倍になりました！(・∀・)

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

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

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

indexで扱う場合、読み込んだ順番（0から始まるインデックス）を**i**とすると、ボーンアニメーションが**2i**、モーフアニメーションが**2i+1**になります。
nameで扱う場合、指定した名前を**'Jump'**とすると、ボーンアニメーションはそのまま**'Jump'**、モーフアニメーションは末尾に**'Morph'**が付与され、**'JumpMorph'**になっています。


### アニメーションのくり返し制御
`repetition`プロパティで、アニメーションのくり返し回数を設定できます。
ループさせる場合は'Infinity'、くり返しを行わずに1回だけの場合は0です。

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

### アニメーションの切り替え
試行錯誤した結果、こうなりました。
自分で書いておいて、あまりスマートではない気もしていますが、良きに計らってくださいまし。

```javascript
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();
};
```

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">three.jsでアニメーション切り替え。Lat式ミクとえれこみゅ＠すのこバーンP様のモーションデータを使用しています。 <a href="https://t.co/H4tp9NQGgh">pic.twitter.com/H4tp9NQGgh</a></p>&mdash; jyuko (@jyuko49) <a href="https://twitter.com/jyuko49/status/832599649458360320">2017年2月17日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>


### アニメーションを合成する
`loadmmd()`でそれぞれ読み込んだアニメーションですが、複数のアニメーションを同時に再生することができます。
また、アニメーションの再生/停止で使ったAnimationActionには、`weight`プロパティがあり、アニメーションに強弱を付けて、合成することができます。

アニメーションの合成用に、切り替えと似たような関数を作りました。
違いとしては、weightの指定を行っている点と現在再生しているアニメーションを止めずに新たなアニメーションを重ねて再生している点です。

```js
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つのアニメーションを足し合わせて、結果を見てみました。

```js
blendAnimation('振り向き2', 0.3);
blendAnimation('ばーん！', 0.7);
```

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">複数アニメーションの合成。 <a href="https://t.co/1FTkR3zuBf">pic.twitter.com/1FTkR3zuBf</a></p>&mdash; jyuko (@jyuko49) <a href="https://twitter.com/jyuko49/status/832601078474477570">2017年2月17日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

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

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


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

ハマりどころとしては、

- MMDLoader.loadだとアニメーションが合成される
- モーフアニメーションが別に保存される

といったあたりです。

MMDLoaderの中身を読んで動作を理解してしまえば、AnimationMixerの操作で色々できるようになります。
お好みのモデル、モーションで遊んでみてください。