Spine runtime - 02.Animation

f:id:Ko-Ta:20191028181359p:plain

UnityやUnrealEngineのコンポーネント使わない、生のSpineランタイムの取り扱います第二回。コンポーネントを使えばらくちんですが、隠されている便利な機能もあります。役に立たないようで役に立つかもしれませんよ。

Spine runtime アニメーション編

次にアニメーションの説明をしたいとおもいます。アニメーション制御は ver3.6 あたりからほぼ変更はありません。使える機能が増えたことと、最初のkeyより前の扱いが明確に定義されました。

ちょうど公式にサンプルがあるので、軽く見てから読むとわかりやすいかもしれません。

ja.esotericsoftware.com

あまりプログラム側からは直接変更を加えず、SpineのIKなどの機能を使って、出来るだけアニメーション内で完結させるのが、開発者の想定している理想の開発モデルみたいです。 アニメーションを新規に生成する命令などを保有していますが、最終手段と言って良いでしょう。

アニメーショントラック

Spine は複数のアニメーションを合成できます。アニメーションにアニメーションを重ねるイメージです。

track[0] : idle
track[1] : walk
track[2] : aim
track[3] : empty

こんな感じで、この層を トラック(track) と言います。重ねると、デフォルトでは、アニメーションがある箇所のボーンやメッシュの値が上書きされます。なので、パーツごとに動きを作って重ねるのが基本になります。

トラックは特に気にする必要はありません。例えば、AnimationStateData クラスを介して以下のようにアニメーションを設定します。

animationState->setAnimation(0, "idle", true);
animationState->setAnimation(1, "walk", true);

第一引数がトラック番号です。何の下準備もなく指定して構いません。内部で勝手にトラック数を増やしたり、不要になったら減らしたりしてくれます。

かといって以下のようなことは止めましょうね。

animationState->setAnimation(0, "idle", true);
animationState->setAnimation(100, "walk", true);

この場合、1~99番目が無駄な領域になってしまいます。

TrackEntry の取得

アニメーションの操作は、TrackEntry を介して行われます。例えば、setAnimation 命令も、実は TrackEntry クラスが返されます。

auto track = animationState->setAnimation(0, "idle", true);

もちろん、現在再生中のアニメーションの情報も取得できます。

auto track = animationState->getCurrent(0);

が、タイミングによってどちらも同じ idel の情報が取得できるとは限らないので注意が必要です。

setAnimation

setAnimation はアニメーションを切り替えます。蓄えてあるキューを消してCurrentに置き換える とあるので、この命令の時点で切り替わっています。 直後に getCurrent で取得しても切り替わったアニメーション情報が取得できます。

...
setAnimation(0,idle)    track0:idle    result:idle
getCurrent(0)           track0:idle     result:idle
...
apply()                 track0:idle

が、後記しますが、getCurrent で変更を行う手法は危険な場合があるので、出来るだけ setAnimation の時点で変更を終了させましょう。

addAnimation

addAnimation は、アニメーションの終了(ループの終端)を待ってから、次のアニメーションに切り替える命令です。この場合は、切り替わるタイミングが不定期なため、getCurrent で操作することは基本的に出来ないと考えて良いでしょう。

addAnimation(0,walk)    track0:idle     result:walk
getCurrent(0)           track0:idle     result:idle
apply()                 track0:idle
getCurrent(0)           track0:idle     result:idle

基本的に、アニメーションを操作したい場合、setAnimation,addAnimation の段階でほぼ済ませましょう。getCurrent で操作するのは、トラックのブレンド率など、一部のパラメータ用とすべきです。

短いアニメーションの場合

TrackEntry ですが、生存時間がアニメーションによって様々なので、保持することは辞めましょう。 取得したら、次のSpineの命令を使うまでに変更を完了させないとクラッシュする恐れがあります。 それは、短いアニメーションであれば apply() で消えてしまう可能性があるからです。

...
setAnimation(0,stop)    track0:Stop    result:stop
apply()                 track0:null
getCurrent(0)           track0:null     result:null

このような場合、トラックは自動的に削除、解放されています。 参照するとメモリ読み込み違反です。

TrackEntry の操作

TrackEntry はプロパティを持っており、その値を操作することでアニメーションを制御します。

github.com

c++ なので getter,setter で書かれています。他の言語だと get,set は不要です。 主要なものを説明していきます。

アニメーション

        Animation* getAnimation();

現在のトラックで再生中のアニメーションのデータを参照できます。 名前を調べるにはこれを使ってください。

Loop

        bool getLoop();
        void setLoop(bool inValue);

ループ設定です。setAnimation でも指定できますが、ここでも指定できます。 再生中、途中でloopを外したりできます。

Delay

        float getDelay();
        void setDelay(float inValue);

delay は addAnimation のときに使用します。アニメーションが切り替わる時間を指定します。

TrackTime

        float getTrackTime();
        void setTrackTime(float inValue);
        float getAnimationTime();

トラックの再生経過時間です。アニメーション上の時間ではありません。 3秒のループアニメーションの場合、9秒などループ以上の値を返します。

ループを考慮した値は getAnimationTime を使ってくださいとのことです。

この値は自由に変更することができます。後の TimeScale=0 を組み合わせると、タイムストレッチが可能です。また逆再生も可能です。ただし、その場合はイベントを使用しないでください。

Track alpha

        float getAlpha();
        void setAlpha(float inValue);

アニメーションの合成率を設定できます。0~1.0のみならず、5.0や-1.0の負の値も許容します。もっとも使う要素だと思います。

        float getMixDuration();
        void setMixDuration(float inValue);

前回のアニメーションとのブレンド、遷移時間です。この値は setDefaultMix などで事前に定義するのが一般的ですが、ここで乗っ取ることができます。

これは非常に便利で、0.0を指定すれば即切り替わります。覚えておいて損はないでしょう。

Loop time

        float getAnimationStart();
        void setAnimationStart(float inValue);
        float getAnimationEnd();
        void setAnimationEnd(float inValue);
        float getAnimationLast();
        void setAnimationLast(float inValue);

なんとループ範囲を任意に設定できます。StartとEndをせっていします。 アニメーションを超えた時間を指定することもできます。

・・・あれ?3項目ありますね。 setAnimationLast が特殊なもので、説明は以下のようになっています。

The time in seconds this animation was last applied. Some timelines use this for one-time triggers. 
Eg, when this animation is applied, event timelines will fire all events between the animation last time (exclusive) and animation time(inclusive). 
Defaults to -1 to ensure triggers on frame 0 happen the first time this animation is applied.

このアニメーションが最後に適用された時間。一部のタイムラインは、1回限りのトリガーとして使用します。
例えば、このアニメーションを反映したとき、イベントは、AnimationLast(を含まない)から AnimationTime(含む)までのイベントを発生させます。
デフォルトでは、このアニメーションが適用されたときに、フレーム 0 のイベントが発生するように、-1が設定されています。

When changing the animation start time, it often makes sense to set TrackEntry.AnimationLast to the same value to
prevent timeline keys before the start time from triggering.

AnimationStart を変更する場合、AnimationLast を同じ値に設定して、
開始時間より前のイベントが発生しないようにします。

ふーん。ループの開始時をいじる場合、開始より前のイベントを発生させたい?その場合は AnimationLastを-1にしてね。そうじゃない場合は開始時間と同じにしてね。ということだそうです。

含む、含まない が逆なような気がしますが、合ってるのかな?

Time scale

        float getTimeScale();
        void setTimeScale(float inValue);

アニメーションの再生速度を変更できます。 0にすると停止します。

再生速度はほかの場所にもありますが、これはトラック(アニメーション)単位で出来ますよーというものです。主な使い方としては、0で停止させて、時間(setTrackTime)をいじって、タイムストレッチできます。

MixBlend

        MixBlend getMixBlend();
        void setMixBlend(MixBlend blend);

version3.7 の途中から正式にサポートされた機能です。アニメーションとアニメーションをどのように合成するかを選択できます。

mode ---
mixReplace 値を上書きします。
mixAdd 値を加算します。

デフォルトは mixReplace です。TrackAlpha が 100% なら上書き、50% なら半分ずつ合成されます。

A : animationA
B : animationB

out = A * (1.0-alpha) + B * alpha

alpha = 1.0 : out = A * 0.0 + B * 1.0 = B
alpha = 0.0 : out = A * 1.0 + B * 0.0 = A

では mixAdd とは何でしょう? これは単純にアニメーションを加算します。

out = A + B * alpha

alpha = 1.0 : out = A + B * 1.0 = A + B
alpha = 0.0 : out = A + B * 0.0 = A

イマイチ想像しにくいですね。ボーンの角度で考えてみましょう。

out = A.bone(30) + B(10) = 40

もとのアニメーション(A)を損なわずに、違うアニメーション(B)が合成できるモードです。もう一例、位置で考えてみましょう。

out.xy = A.bone.xy(20,0) + B.bone.xy(0,20) = xy(20,20)

右に移動するアニメーションと上に移動するアニメーションの合成です。結果は右上に移動します。

公式サンプルではこれになります。

ja.esotericsoftware.com

この手法は、3Dで昔は MorphTarget などで呼ばれたりもしていました。ボーンよりもメッシュの頂点の合成で使われ、主に表情の合成に使われます。

最後に言っておきます。まだ公式ランタイムではバグ持ちの機能です。そのうち治るといいですね。

EmptyAnimation

これを説明しなければなりません。Spine 特有のモーションです。

empty の名を冠する通り、空を意味します。Spine ではアニメーションの削除には `clearTrack' があります。

animationstate->setAnimation(0,"idle",true);
...
animationstate->clearTrack(0);

アニメーションの削除の意味するところは、元の形状(setup pose)に戻ることではありません。Spineのskeletonでも説明したように、boneはアニメーションがない限り、値の状態をキープし続けます。つまり、最後のアニメーションの状態を保って、動かなくなります。

そこで、元の形状に戻す命令がこの setEmptyAnimation , addEmptyAnimationです。

animationstate->setEmptyAnimation(0.25f);

説明するまでもありませんね。

empty 後

empty の後、トラックはどのようになるか理解しておく必要があります。setEmptyAnimation の後、そのトラックは empty という特殊なアニメーションに切り替わります。そしてボーンの姿勢が戻ったのち、削除され、そのトラックは nullptr になります。

setEmptyAnimation       track[0] : walk
...                     track[0] : empty
...
...                     track[0] : nullptr

この nullptr にちょっとした癖があるので注意してください。

nullptr 時の扱い

最初に setAnimation でアニメーションをセットしたとき、アニメーションがどうなったかよーく見てください。前のアニメーションと滑らかにミックスされましたか? 最初(nullptr) なのでされませんよね。

そうなんです、nullptr になると、アニメーションミックスされないんです。

これはバグではなく仕様であり、別に変な仕様でもありません。が、Spineのアニメーションミックスの機能を過信していると、empty からの繋ぎで、アニメーションが飛ぶような現象が発生することがあります。それはこの nullptr の仕業です。

実際に遭遇するのは先になるかと思いますが、先に解決だけ書いておきます。

解決策は簡単で、empty の代わりに、何も無い 空アニメーションをエディタで作っておきます。それを使えば解決されます。 空アニメーションがずっと再生されつづけるという状態になりますが、処理負荷はほぼ増えないので安心してください。

最後に

表示もまだの状態で込み入った話になってしまいましたが、spine のアニメーションはこんな感じになっています。今はふーんぐらいにとどめて、あとで必要になったら思い出してみてください。