three.jsサンプルを解析してみた。アニメーションミキサー

今回はアニメーションをブレンドするサンプルを作ってみました。このサンプルでは「静止状態」→「ゆっくり歩く」→「早く歩く」とアニメーション情報をブレンドして、より自然な動きになるように設定しています。また、「歩きながらジャンプ」「静止状態からジャンプ」といったアニメーションのブレンドも行われています。

20160419_img

スマートフォンでご覧の方はスマートフォン用の表示に切り替わります。スマートフォンでのパフォーマンスが気になるところだと思いますが、 僕のiPhone6では問題なく動いているようです。

ソースコードは以下より参照ください。

├── build : srcディレクトリからサーバアップ用に書き出すディレクトリ
│   ├── js
│   │   └── app.js : src/babel/*.es6ファイルをBabelで同一ディレクトリにjsにトランスパイラして、WebPackで結合したもの
│   ├── models
│   │   └── legoman.json : 本場用モデリングデータ(src/assets/ディレクトリからのコピー)
│   └── texture
│       └── field.jpg : フィールト用テクスチャ(src/assets/ディレクトリからのコピー)
└── src
    ├── assets : このディレクトリ以下のものは、buildディレクトリにコピーされます
    │   ├── models
    │   │   ├── legoman.blend : レゴのモデリングとアニメーションを生成するBlenderデータ
    │   │   └── legoman.json : モデリングデータ(モデル、マテリアル、アニメーション)
    │   └── texture
    │       └── field.jpg
    └── babel
        ├── Main.es6 : 実行用のメインClass
        ├── animation
        │   └── mixer
        │       ├── LegoMan.es6 : レゴの機能をまとめたClass
        └── controller
            └── Controller.es6 : スマートフォン時に表示されるコントローラーの機能をまとめたClass

トピックに分けて、構築の流れを説明します。

モデリングデータの作成とアニメーションの設定

まずはBlenderを使って、モデリングを行いアニメーションを設定する必要があります。今回使用したBlenderのバージョンは「2.77」になります。以前の記事(Blenderとthree.jsを使って行うモーフィングアニメーション)で、Blenderについてとアニメーションについては触れているので改めて説明しませんが、以下のサイトでBlenderの基礎が丁寧に説明されていますので、Blenderを少し使えるようになりたいという方は、オススメします。

モデリングとアーマチュア(アニメーション用の骨格の様なもの)を作成し、ウェイトペイント(ボーンと頂点の関連性)を設定したら、アニメーションの設定になります。アニメーションの設定はEditorTypeを「Dope Sheet」に変更して、「ActionEditor」で編集します。「+」ボタンで新規にアニメーションを作成したら、「F」ボタンを押す事でアニメーションが保存されます。逆にアニメーションを削除する場合は「F」ボタンを解除して「×」ボタンを押します。アニメーションのリストで、アニメーション名の左に「0」が表示されている状態だと、ファイルを再び開いた際にアニメーションが削除されているのが確認できるかと思います。

20160419_img5

アニメーションの設定方法は、ボーンに位置、回転、拡大縮小といった変更を加えて、タイムラインにキーフレームを打つという方法をとります。Flashと似ていますね。

20160419_img2

three.js用のJSONデータに書き出すだけでしたら、以下の様に「ActionEditor」のアニメーションリストに登録されていれば、書き出す事ができます。「NLA Editor」などを使う必要はありません。

20160419_img3

今回作成したBlenderのデータは、以下にコミットしています。

※使用したBlenderのバージョンは「2.77」になります。

データの書き出し

次にBlenderからthree.jsようにJSONデータに書き出します。JSONデータに書き出すには、Blenderのthree.js用のプラグインが必要になりますのでご注意ください。以前の記事(Blenderとthree.jsを使って行うモーフィングアニメーション)で詳細は確認できるかと思います。また、書き出す際には、必ず以下の様に書き出す対象となるメッシュが選択されているのを確認してください。アーマチュアが選択されている状態だとエラーとなります。

20160419_img4

書き出しの設定は以下の様にしています。

20160419_img6

書き出しに関しては情報が少なく、試行錯誤しましたが、以下のサイトを参考にさせていただきました。

THREE.AnimationMixer

書き出したモデルデータを、three.jsに読み込んでアニメーションを操作します。THREE.AnimationMixerクラスのコンストラクタに、読み込んだJSONデータから生成したメッシュを渡して、アニメーションミキサーオブジェクトを生成します。このアニメーションミキサーのclipActionメソッドにJSONデータから読み込んだアニメーション(geometry.animations[0])を渡して、ミキシング用のActionを生成します。

/src/babel/animation/mixer/LegoMan.es6 Line:40

load() {
    return new Promise((resolve, reject)=> {
        var loader = new THREE.JSONLoader();
        loader.load("./models/legoman.json", (geometry, materials)=> {
            materials.forEach((material)=> {
                material.skinning = true;
            });
            this.meth_ = new THREE.SkinnedMesh(geometry, new THREE.MeshFaceMaterial(materials));
            this.meth_.castShadow = true;
            this.meth_.rotation.y = -90 * (Math.PI / 180);
            this.mixer_ = new THREE.AnimationMixer(this.meth_);
            this.defaultAction_ = new Action(this.mixer_.clipAction(geometry.animations[0]), 1, false);
            this.defaultAction_.play();
            this.jumpAction_ = new Action(this.mixer_.clipAction(geometry.animations[1]), 0, false);
            this.walkAction_ = new Action(this.mixer_.clipAction(geometry.animations[3]), 0, true);
            resolve(this.meth_);
        });
    })
}

このActionオブジェクトのweight(0〜1)が大きいほど、アニメーションへの影響力が強くなるので、これを利用しながらアニメーションをミックスしていきます。今回のサンプルでは、defaultAction(静止状態)とwalkAction(歩行アニメーション)のweightを反比例させながらトゥイーンさせる事で、静止〜歩行へのスムーズなアニメーションが行われています。今回のサンプルでは使用していません(※1)が、ActionオブジェクトにはfadeInfadeOutcrossFadeTocrossFadeFromといったweightをトゥイーンさせるための便利なメソッドがありますので、こちらを利用してもいいかもしれません。

※1 よりスムーズなアニメーションのために、イージング関数を利用してより細かなトゥイーンの調整が必要だったのと、weightの情報をレゴメッシュの移動距離に反映させるために、トゥイーン実行中や完了時に、コールバック関数が必要だったので、jQueryのAnimate関数を利用しました。

/src/babel/animation/mixer/LegoMan.es6 Line:219

class Action {

    constructor(action, weight = 0, isRoop = true) {
        this.action_ = action;
        this.weight_ = weight;
        if (!isRoop) this.action_.setLoop(THREE.LoopOnce, 0);
        this.isRunning_ = false;
        this.weight = this.weight_;
    }

    play() {
        if (this.action_) this.action_.play();
    }

    reset() {
        if (this.action_) this.action_.reset();
    }

    toWeight(target, duration, easing = "linear", step) {
        return new Promise((resolve, reject)=> {
            $(this).stop().animate({
                weight: target
            }, {
                duration: duration,
                easing: easing,
                step: ()=>{
                    step(this.weight);
                },
                complete: ()=>{
                    resolve();
                }
            });
        });
    }

    setAction(val) {
        if (this.action_) this.action_.setEffectiveWeight(val);
    }

    set weight(val) {
        if (val < 0) val = 0;
        if (1 < val) val = 1;
        this.weight_ = val;
    }

    get weight() {
        return this.weight_;
    }

    set isRunning(flg) {
        this.isRunning_ = flg;
    }

    get isRunning() {
        return this.isRunning_;
    }

}

まとめ

これでアニメーションの幅が広がったかなと思います。より自然なアニメーションには、Weightの扱いに工夫が必要かと思いますが、うまくつながった時には、キャラクターがより活きてくることになります。とはいえ、何よりBlenderでのモデリングとアニメーションの設定ができないと、何も始まりません。極めるのは大変かもしれませんが、WebGLを使って3D表現をしていきたいと考えるのなら、最低限一通りの流れが出来るようになっておく必要があるのかもしれません。個人的な話ですが、いい課題を見つけました。