質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Unity3D

Unity3Dは、ゲームや対話式の3Dアプリケーション、トレーニングシュミレーション、そして医学的・建築学的な技術を可視化する、商業用の開発プラットフォームです。

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

Q&A

解決済

2回答

3184閲覧

Animator内の遷移にイージーイン・アウトは可能でしょうか?

Lovedelic_VR

総合スコア24

Unity3D

Unity3Dは、ゲームや対話式の3Dアプリケーション、トレーニングシュミレーション、そして医学的・建築学的な技術を可視化する、商業用の開発プラットフォームです。

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

0グッド

0クリップ

投稿2020/06/11 09:21

あるアニメーションから、もう一方(1フレーム)のアニメーションに遷移させるとき、Animator内では動きに自動で補間がききますが、
その動きを一定速でなく、変化のあるものにしたいです。(イージーイン・アウト)
アニメーションクリップ同士の遷移にイージーイン・アウトさせることは可能でしょうか?

画像内の、IdleクリップとShootposeクリップ(1フレーム)を遷移させる際にイージーイン・アウトをさせたいです。
イメージ説明

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

パート2

そしてプロジェクト内にEditorフォルダを作り、コンポーネント編集用の下記スクリプトを入れておきました。

C#

1using System.Collections.Generic; 2using System.Linq; 3using UnityEditor; 4using UnityEditor.Animations; 5using UnityEngine; 6 7[CustomEditor(typeof(TransitionModifier))] 8public class TransitionModifierEditor : Editor 9{ 10 private readonly Dictionary<AnimatorState, string> stateFullPaths = new Dictionary<AnimatorState, string>(); 11 12 public override void OnInspectorGUI() 13 { 14 var targetModifier = this.target as TransitionModifier; 15 if (targetModifier == null) 16 { 17 EditorGUILayout.HelpBox("No component.", MessageType.Error); 18 return; 19 } 20 21 var animator = targetModifier.GetComponent<Animator>(); 22 if (animator == null) 23 { 24 EditorGUILayout.HelpBox("No Animator.", MessageType.Warning); 25 return; 26 } 27 28 if (EditorApplication.isPlaying) 29 { 30 EditorGUILayout.HelpBox("Running.", MessageType.Info); 31 return; 32 } 33 34 var animatorController = animator.runtimeAnimatorController as AnimatorController; 35 if (animatorController == null) 36 { 37 EditorGUILayout.HelpBox("No Controller.", MessageType.Warning); 38 return; 39 } 40 41 this.stateFullPaths.Clear(); 42 foreach (var layer in animatorController.layers) 43 { 44 this.GatherStateFullPaths(layer.stateMachine); 45 } 46 47 var assets = AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(animatorController)); 48 var states = assets.OfType<AnimatorState>(); 49 var transitionsWithSource = 50 states.SelectMany(state => state.transitions.Select(transition => (transition, state))).ToArray(); 51 var transitionsWithoutSource = assets.OfType<AnimatorStateTransition>() 52 .Except(transitionsWithSource.Select(transition => transition.transition)) 53 .Select(transition => (transition, (AnimatorState)null)); 54 this.serializedObject.Update(); 55 var equationKeys = this.serializedObject.FindProperty("equationKeys"); 56 var equationValues = this.serializedObject.FindProperty("equationValues"); 57 var equationWithoutSourceKeys = this.serializedObject.FindProperty("equationWithoutSourceKeys"); 58 var equationWithoutSourceValues = this.serializedObject.FindProperty("equationWithoutSourceValues"); 59 foreach (var (transition, fromState) in transitionsWithSource.Concat(transitionsWithoutSource)) 60 { 61 var displayName = transition.GetDisplayName(fromState); 62 var path = fromState == null 63 ? this.stateFullPaths[transition.destinationState] 64 : $"{this.stateFullPaths[fromState]} -> {this.stateFullPaths[transition.destinationState]}"; 65 var hash = Animator.StringToHash(path); 66 var previousValue = targetModifier.GetEquation(hash, fromState != null, out var index); 67 var newValue = (TransitionModifier.Equation)EditorGUILayout.EnumPopup(displayName, previousValue); 68 if (newValue != previousValue) 69 { 70 var keys = fromState == null ? equationWithoutSourceKeys : equationKeys; 71 var values = fromState == null ? equationWithoutSourceValues : equationValues; 72 if (index < 0) 73 { 74 index = keys.arraySize; 75 keys.InsertArrayElementAtIndex(index); 76 values.InsertArrayElementAtIndex(index); 77 } 78 79 keys.GetArrayElementAtIndex(index).intValue = hash; 80 values.GetArrayElementAtIndex(index).intValue = (int)newValue; 81 } 82 } 83 84 this.serializedObject.ApplyModifiedProperties(); 85 } 86 87 private void GatherStateFullPaths(AnimatorStateMachine stateMachine, string parentPath = "") 88 { 89 parentPath += $"{stateMachine.name}."; 90 foreach (var state in stateMachine.states.Select(childState => childState.state)) 91 { 92 this.stateFullPaths.Add(state, $"{parentPath}{state.name}"); 93 } 94 95 foreach (var subMachine in stateMachine.stateMachines.Select(childMachine => childMachine.stateMachine)) 96 { 97 this.GatherStateFullPaths(subMachine, parentPath); 98 } 99 } 100}

実験用のアニメーションは「左下で円を描く動き(Motion1)」→「中央上で正方形を描く動き(Motion2)」→「右下で逆五芒星を描く動き(Motion3)」を繰り返すもので、各クリップは長さ4秒、下図のように2反復(結果としてステート滞在時間は8秒)したのち次のステートへ2秒かけて遷移...となっています。

図1

通常通りAnimationControllerによる制御を行った場合は下図のようになります。

図2

図の下半分にPlayableGraphの構造を表示しました。中央付近のAnimationMixerの先が三つ叉に分岐しており、遷移タイミングでブレンド比がじわっと変化しています。
当初の目論見では、このミキサーのブレンド比を操作してイージングできるのではないかと思ったのですが、残念ながらAnimatorController(エメラルドグリーンのノード)より先は外部からの操作が許可されていないようです。
仕方ないのでAnimationClipノードを二つ追加して本来のアニメーションクリップと同一のクリップをセット、AnimationMixerを追加してそれらを混合、出力するようにしてみました。本来のミキサーのブレンド比を監視し、その比率にイージング処理を施してから新しいミキサーに適用することでイージングさせようと考えました。

コンポーネントの設定は下図のようにMotion1からMotion2はEaseInOutQuad(二次関数のカーブを描きながらじわっと加速、じわっと減速)、Motion2からMotion3はEaseOutCirc(最初は急激、そこから円弧を描くカーブでじわっと減速)、Motion3からMotion1はEaseInOutBounce(ボールが跳ねるような動きで高さを増しながら中間点まで移動、そこから逆に高さを減らしながら跳ねて最終点まで移動)を選択しました。

図3

実行するとAnimatorの「Controller」はNoneになり、代わってTransitionModifierが作ったPlayabeGraphによってアニメーションがコントロールされます。

図4

動きは下図のようになりました。比較のため、白いボールに重ねて表示している赤い半透明のボールに本来の線形の動きをさせています。

図5

グラフ右下にAnimationController由来のツリーがあり、そこのミキサーがクリップを切り替えるのと同期して、左上に追加されたミキサーがクリップを切り替えています。
最初に申し上げたように機能としては不完全で、たとえば本来のツリーにあるAnimationPoseとかいう黄色いノードは何をしているのかわからず、完全に無視しました(IK関連の機能?)。他にも、複数のレイヤーを持つアニメーションの場合はAnimationLayerMixerから枝分かれが発生するようですが、それも考慮せずベースレイヤーしか見ていません。また、おそらくノードの評価はAnimationOutputを起点に末端へ向かって進行するだろうと思われ、本来のミキサーがブレンド比を決定するよりも先にTransitionModifierが追加ミキサーのブレンド比を決定するという処理順になっていると想像されます。つまり、TransitionModifierは1フレーム前のグラフの姿を見ているっぽく、アニメーションがわずかに遅れてしまいます。

投稿2020/06/14 14:35

Bongo

総合スコア10807

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

ベストアンサー

どうやら標準機能としては提供されていないみたいですね。AnimationControllerによる制御はあきらめて、自前でアニメーションシステムを構築するべきかもしれません。

複雑なステート制御が不要なら、【Unity】AnimationControllerを用意せずAnimatorによるアニメーションを使う - テラシュールブログの実装例のように比較的簡単に作れそうです。
スクリプト末尾のコルーチン内にアニメーションのブレンド比を決めている部分がありますが、そこにイージング処理を挟むことでお好みの緩急をつけられるんじゃないでしょうか?

Unity TechnologiesからもSimpleAnimationなんてものが出ており、ブレンド機能(参考:【Unity】Simple Animationを使ってAnimator Controllerなしでお手軽モーション再生 - LIGHT11)をうまく使えばイージング機能付きクロスフェードを実現できそうに思います。

ああいったシンプルなアニメーションシステムでまかなえそうならばそちらをおすすめしたいところですが、やはりUnity標準のステート管理機能を使いたい...と言われたらどうしようと思い、なにか手はないか検討してみました。
後述しますが、レイヤーを考慮していないなど不完全な出来です...あくまでもアプローチ検討の参考ということでお願いします。

まずAnimatorによってアニメーションするオブジェクトに、下記スクリプトをアタッチしました。

C#

1using System; 2using System.Collections.Generic; 3using System.Linq; 4using UnityEngine; 5using UnityEngine.Animations; 6using UnityEngine.Playables; 7 8[RequireComponent(typeof(Animator))] 9public class TransitionModifier : MonoBehaviour 10{ 11 // 0~1の値を0~1の値に変換するイージング処理を記述する 12 // Robert Penner's Easing Functionsをベースに簡略化したもの 13 // http://robertpenner.com/easing/ 14 public enum Equation 15 { 16 Linear, 17 EaseInOutQuad, 18 EaseOutCirc, 19 EaseInOutBounce 20 } 21 private static readonly Dictionary<Equation, Func<float, float>> TransitionFunctions = 22 new Dictionary<Equation, Func<float, float>> 23 { 24 {Equation.Linear, t => t}, 25 {Equation.EaseInOutQuad, t => t < 0.5f ? (t *= 2.0f) * t * 0.5f : 1.0f - ((t = (1.0f - t) * 2.0f) * t * 0.5f)}, 26 {Equation.EaseOutCirc, t => Mathf.Sqrt(1.0f - ((t -= 1.0f) * t))}, 27 {Equation.EaseInOutBounce, t => t < 0.5f ? (1.0f - EaseOutBounce(1.0f - (t * 2.0f))) * 0.5f : (EaseOutBounce((t * 2.0f) - 1.0f) * 0.5f) + 0.5f} 28 }; 29 private static float EaseOutBounce(float v) 30 { 31 if (v < (1 / 2.75f)) 32 { 33 return 7.5625f * v * v; 34 } 35 36 if (v < (2 / 2.75f)) 37 { 38 return (7.5625f * (v -= 1.5f / 2.75f) * v) + 0.75f; 39 } 40 41 if (v < (2.5 / 2.75)) 42 { 43 return (7.5625f * (v -= 2.25f / 2.75f) * v) + 0.9375f; 44 } 45 46 return (7.5625f * (v -= 2.625f / 2.75f) * v) + 0.984375f; 47 } 48 49 // どのトランジションに対してどのイージングを適用するかを記憶しておくためのフィールド 50 // 本来ならDictionaryのようなコレクションを使いたいところではあるが、シリアライズするために 51 // キー(トランジションまたはステートのフルパスハッシュ)と値をそれぞれListとして保持する 52 // リストは2組用意し、前者はステート間トランジション、後者はAny Stateからの場合のような 53 // ソースステートを持たないトランジションに使う 54 // ソースなしトランジションでもAnimatorTransitionInfo.fullPathHashはそれっぽい値を返すので、 55 // ソースなしの場合のハッシュの元となる文字列の作り方がわかればリストは1組ですむはずなのだが... 56 // リファレンスには載っておらず、勘で試してもハズレばかりであきらめた 57 [SerializeField] private List<int> equationKeys = new List<int>(); 58 [SerializeField] private List<Equation> equationValues = new List<Equation>(); 59 [SerializeField] private List<int> equationWithoutSourceKeys = new List<int>(); 60 [SerializeField] private List<Equation> equationWithoutSourceValues = new List<Equation>(); 61 62 private PlayableGraph animationGraph; 63 private Animator animator; 64 private RuntimeAnimatorController runtimeAnimatorController; 65 66 // ハッシュからEquationを引っ張ってくるのに使うヘルパーメソッド 67 // TransitionModifierEditorが使う 68 public Equation GetEquation(int hash, bool hasSource, out int index) 69 { 70 index = (hasSource ? this.equationKeys : this.equationWithoutSourceKeys).FindIndex(i => i == hash); 71 return index < 0 ? Equation.Linear : (hasSource ? this.equationValues : this.equationWithoutSourceValues)[index]; 72 } 73 74 private void Start() 75 { 76 // まずPlayableGraphを新規作成する 77 this.animator = this.GetComponent<Animator>(); 78 this.runtimeAnimatorController = this.animator.runtimeAnimatorController; 79 this.animationGraph = PlayableGraph.Create("Animation"); 80 this.animationGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime); 81 var animatorOutput = AnimationPlayableOutput.Create(this.animationGraph, "AnimatorOutput", this.animator); 82 var animatorController = AnimatorControllerPlayable.Create( 83 this.animationGraph, 84 this.runtimeAnimatorController); 85 86 // ハッシュ値とイージングファンクションを対応づけたDictionaryを作り... 87 var transitionFunctions = new Dictionary<int, Func<float, float>>(); 88 foreach (var (key, value) in this.equationKeys.Zip(this.equationValues, (key, value) => (key, value))) 89 { 90 transitionFunctions.Add(key, TransitionFunctions[value]); 91 } 92 var transitionWithoutSourceFunctions = new Dictionary<int, Func<float, float>>(); 93 foreach (var (key, value) in this.equationWithoutSourceKeys.Zip( 94 this.equationWithoutSourceValues, 95 (key, value) => (key, value))) 96 { 97 transitionWithoutSourceFunctions.Add(key, TransitionFunctions[value]); 98 } 99 100 // メインの処理を担当するTransitionModifierPlayableを作成、必要なデータを投入する 101 var modifierPlayable = ScriptPlayable<TransitionModifierPlayable>.Create(this.animationGraph, 1); 102 modifierPlayable.GetBehaviour().Initialize( 103 modifierPlayable, 104 animatorController, 105 this.animator, 106 transitionFunctions, 107 transitionWithoutSourceFunctions); 108 109 // AnimatorのruntimeAnimatorControllerはnullにして黙らせ、代わりに 110 // できあがったPlayableGraphを起動してアニメーションを行わせる 111 animatorOutput.SetSourcePlayable(modifierPlayable); 112 this.animator.runtimeAnimatorController = null; 113 this.animationGraph.Play(); 114 } 115 116 private void OnEnable() 117 { 118 this.runtimeAnimatorController = this.animator.runtimeAnimatorController; 119 this.animator.runtimeAnimatorController = null; 120 121 if (this.animationGraph.IsValid()) 122 { 123 this.animationGraph.Play(); 124 } 125 } 126 127 private void OnDisable() 128 { 129 if (this.animationGraph.IsValid()) 130 { 131 this.animationGraph.Stop(); 132 } 133 134 if ((this.animator != null) && (this.runtimeAnimatorController != null)) 135 { 136 this.animator.runtimeAnimatorController = this.runtimeAnimatorController; 137 } 138 } 139 140 private void OnDestroy() 141 { 142 if (this.animationGraph.IsValid()) 143 { 144 // 生成したPlayableGraphは明示的に破棄しないとクラッシュするらしい...! 145 // http://tsubakit1.hateblo.jp/entry/2017/07/30/032008#%E6%B3%A8%E6%84%8F%E7%82%B9 146 this.animationGraph.Destroy(); 147 } 148 } 149 150 private class TransitionModifierPlayable : PlayableBehaviour 151 { 152 private Dictionary<int, Func<float, float>> transitionFunctions; 153 private Dictionary<int, Func<float, float>> transitionWithoutSourceFunctions; 154 private Animator animator; 155 private PlayableGraph animationGraph; 156 private AnimationMixerPlayable referenceMixer; 157 private AnimationMixerPlayable actualMixer; 158 private AnimationClipPlayable mirrorClip0; 159 private AnimationClipPlayable mirrorClip1; 160 161 public void Initialize( 162 ScriptPlayable<TransitionModifierPlayable> self, 163 AnimatorControllerPlayable animatorController, 164 Animator animator, 165 Dictionary<int, Func<float, float>> transitionFunctions, 166 Dictionary<int, Func<float, float>> transitionWithoutSourceFunctions) 167 { 168 // AnimatorControllerによるノードツリー内の、トランジション前後のモーション混合を 169 // 行っているミキサーを監視し、そのブレンド比をイージングファンクションで変化させ、 170 // それを別途用意したミキサーに適用する作戦 171 // AnimatorControllerのノードツリーは編集不許可のようなので、こんな回りくどい方法をとった 172 this.transitionFunctions = transitionFunctions; 173 this.transitionWithoutSourceFunctions = transitionWithoutSourceFunctions; 174 this.animator = animator; 175 this.referenceMixer = (AnimationMixerPlayable)animatorController.GetInput(0).GetInput(0); 176 this.animationGraph = this.referenceMixer.GetGraph(); 177 var referenceClip0 = (AnimationClipPlayable)this.referenceMixer.GetInput(0).GetInput(0); 178 var referenceClip1 = (AnimationClipPlayable)this.referenceMixer.GetInput(1).GetInput(0); 179 this.mirrorClip0 = AnimationClipPlayable.Create(this.animationGraph, referenceClip0.GetAnimationClip()); 180 this.mirrorClip1 = AnimationClipPlayable.Create(this.animationGraph, referenceClip1.GetAnimationClip()); 181 this.actualMixer = AnimationMixerPlayable.Create(this.animationGraph, 3); 182 this.actualMixer.ConnectInput(0, this.mirrorClip0, 0, 1.0f); 183 this.actualMixer.ConnectInput(1, this.mirrorClip1, 0); 184 this.actualMixer.ConnectInput(2, animatorController, 0); 185 self.ConnectInput(0, this.actualMixer, 0, 1.0f); 186 } 187 188 public override void PrepareFrame(Playable playable, FrameData info) 189 { 190 // アニメーションクリップが変わっていたらノードを差し替え、その後再生位置を同期させる 191 this.UpdateMirror( 192 (AnimationClipPlayable)this.referenceMixer.GetInput(0).GetInput(0), 193 ref this.mirrorClip0, 194 0); 195 this.UpdateMirror( 196 (AnimationClipPlayable)this.referenceMixer.GetInput(1).GetInput(0), 197 ref this.mirrorClip1, 198 1); 199 200 // 遷移中ならミキサーの混合比を同期させる 201 // このときイージング処理を施し緩急をつける 202 var transitionInfo = this.animator.GetAnimatorTransitionInfo(0); 203 var functions = transitionInfo.anyState 204 ? this.transitionWithoutSourceFunctions 205 : this.transitionFunctions; 206 var transitionHash = transitionInfo.anyState 207 ? this.animator.GetNextAnimatorStateInfo(0).fullPathHash 208 : transitionInfo.fullPathHash; 209 if (transitionHash != 0) 210 { 211 var weight0 = this.referenceMixer.GetInputWeight(0); 212 var weight1 = this.referenceMixer.GetInputWeight(1); 213 var weightSum = weight0 + weight1; 214 if (weightSum > 0.0f) 215 { 216 var normalizedWeight1 = weight1 / weightSum; 217 if (functions.TryGetValue(transitionHash, out var function)) 218 { 219 normalizedWeight1 = function(normalizedWeight1); 220 } 221 222 var normalizedWeight0 = 1.0f - normalizedWeight1; 223 this.actualMixer.SetInputWeight(0, normalizedWeight0); 224 this.actualMixer.SetInputWeight(1, normalizedWeight1); 225 } 226 } 227 228 base.PrepareFrame(playable, info); 229 } 230 231 private void UpdateMirror(AnimationClipPlayable reference, ref AnimationClipPlayable mirror, int port) 232 { 233 if (mirror.GetAnimationClip() != reference.GetAnimationClip()) 234 { 235 mirror.Destroy(); 236 mirror = AnimationClipPlayable.Create(this.animationGraph, reference.GetAnimationClip()); 237 this.actualMixer.ConnectInput(port, mirror, 0, port == 0 ? 1.0f : 0.0f); 238 } 239 240 mirror.SetTime(reference.GetTime()); 241 } 242 } 243}

(パート2へ続く...)

投稿2020/06/14 14:30

編集2020/06/14 15:45
Bongo

総合スコア10807

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Lovedelic_VR

2020/06/25 05:02 編集

ご丁寧に様々な角度から実装のイメージをアドバイスいただきまして、ありがとうございます。 Unityの仕様把握やAnimationController内部のシステム構築の理解につながりました。 自身のプロジェクトでは、キャラ制御をAnimationController依存でレイヤーを使用した実装で進めていましたので、Bongo様のコードを参考にPlayabeGraphでの実装を試してみたいと思います。 自身のプロジェクトで解決せずとも、これからのシステム設計や実装の方針を決める際に考慮するべき知見を収集できた点からみても大変有意義な質問となりました。ご回答ありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問