Akatsuki Games Advent Calendar 3日目の記事です。
AnimationMixerPlayableとは
早い話がAnimationControllerをスクリプトベースで作るAPIの一つです。
AnimationControllerはノーコードで設計可能なステートマシンであり、グラフベースで記述できる何やら便利そうな機能なのですが・・・
本格的なゲームのキャラクター制御などを行えるほど複雑なステートマシンを保守できるほど人類は賢くありません。
そうなってくるとスクリプトベースのアニメーション制御システムが必要になるわけですが、そこで使われるAPIがAnimationMixerPlayableやAnimationClipPlayableなどのPlayableAPIです。
さてこのAPIたちの使い方を解説しようとするとこのブログ記事の余白では到底足りないので、その解説はUnity公式リファレンスやChatGPTにお任せするとして、AnimationMixerPlayableを使う上で遭遇する致命的な不具合を解説します。
ブレンドの振る舞いがルートモーションのRotationだけ異なる
直感的な期待動作として、クリップAとクリップBをブレンドした時、片方にしか存在しないキーフレームはWeightが1で適用されて欲しいわけですが、以下のようなプログラムでテストしてみましょう。
public class TestAnimationPlayable : PlayableBehaviour { private PlayableGraph _graph; private AnimationMixerPlayable _baseMixer; private AnimationClipPlayable[] _playables = new AnimationClipPlayable[2]; public override void OnPlayableCreate(Playable playable) { base.OnPlayableCreate(playable); _graph = playable.GetGraph(); _baseMixer = AnimationMixerPlayable.Create(_graph, 2); _baseMixer.SetInputWeight(0, 1f); _graph.Connect(_baseMixer, 0, playable, 0); playable.SetInputWeight(0, 1f); } public void Play(AnimationClip[] clips) { for (int i = 0; i < clips.Length; ++i) { _baseMixer.DisconnectInput(i); _playables[i] = new AnimationClipPlayable(); _playables[i] = AnimationClipPlayable.Create(_graph, clips[i]); _playables[i].SetDuration(clips[i].length); _playables[i].SetApplyFootIK(false); _playables[i].SetApplyPlayableIK(false); _playables[i].SetSpeed(1f); _baseMixer.ConnectInput(i, _playables[i], 0); _baseMixer.SetInputWeight(i, 1.0f); } } }
Playに渡されたアニメーションクリップリストを全部Weight1でブレンドするシンプルなPlayableBehaviourです。(雑なコードなのでリストは2個しか受け付けてなさそうです)
さて、このPlayableBehaviourにこんな感じでクリップを渡してみます。
public class TestComponent : MonoBehaviour { private Animator _animator; private PlayableGraph _graph; private TestAnimationPlayable _animationPlayable; [SerializeField] private AnimationClip _clip; private void Awake() { _animator = GetComponent<Animator>(); _animator.cullingMode = AnimatorCullingMode.AlwaysAnimate; _animator.updateMode = AnimatorUpdateMode.Normal; _animator.applyRootMotion = true; _graph = PlayableGraph.Create(_animator.gameObject.name + ".Animation"); _graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime); var playable = ScriptPlayable<TestAnimationPlayable>.Create(_graph, new TestAnimationPlayable(), 1); _animationPlayable = playable.GetBehaviour(); var output = AnimationPlayableOutput.Create(_graph, "Output", _animator); output.SetSourcePlayable(playable); } public void Play() { _animationPlayable.Play(new [] { _clip, new AnimationClip() }); _graph.Play(); } private void OnDestroy() { _graph.Destroy(); } }
PlayableGraphを生成し、AnimationPlayableOutputでAnimatorに出力先を設定 Play関数でシリアライズフィールドで受け取ったクリップと空のクリップを先ほどのPlayableBehaviourに渡しています。
つまりこれはあるアセットのクリップと空のクリップをブレンドするだけの特に意味のないテスト用コンポーネントです。
まず、検証用に作成したアニメーションクリップを上記のシステムを使わずに単体で再生してみましょう。
くるっと90度回転しながら直進するアニメーションです。
さて、このアニメーションクリップを上記のシステムを使い、すなわち空のクリップとブレンドして再生してみましょう
うおおおどこいくねーん!!!
これ、Positionの変化量は変わってないんですが、Rotationの変化量だけ謎にLerpされているんですよね。今回の場合両方のクリップをWeight1でブレンドしたので、「無」とLerpした結果、変化量が半分になってます。Rotationだけですね。
これ何が問題が起こるかというと、普通のモーションとフェイシャルモーションなどをブレンド再生した時に、普通のモーションのルートモーションのRotationだけおかしくなるわけです。 AvatarMaskをフェイシャル用とボディ用と分けて管理すれば解決するかもしれませんが、弊社ではその方式をとっていませんでした。
また、AnimationLayerMixierPlayableを使って加算アニメーションとして再生してもこの問題は起こりませんが、加算アニメーションをうまく使うためには0F目のキーの状態がモデルの参照Poseと一致している必要があります。弊社ではフェイシャルアニメーションがこの制約を満たしていないためやはり採用できず。
この問題はすでにUnity社に報告済みで、なんと 2021/2022/6000すべてのバージョンで現在再現します。
プロダクトではとんでもねぇワークアラウンドを使ってこの問題を回避しましたが、皆さんはちゃんとAvatarMaskを使いましょう! では。