はじめに
モーキャプでとったアニメーションをAnimation Riggingでプロシージャルに修正する必要があったのですが、用意されているコンストレイン トだけでは上手くいかなかったのでカスタムコンストレイン トを実装してみました。
Animation Riggingとは
アニメーション作成時やランタイムで使えるリグを組めるやつです(Animation Rigging でアニメーションをレベルアップさせる方法を学ぼう | Unity Blog )。独自のコンストレイン トを作れるなど拡張性も売りにしています。
似たような使い方をされるFinalIKと比較すると、アニメーション編集時でもIKやAimコンストレイン トなどを使えるのが明確な利点だと思います。あと無料です。ただ、必要最小限のコンポーネント しか提供されていないため、例えばFinalIKのVRIKに相当するものをAnimation Riggingで実装しようとすると手間がかかります。
また、Animation RiggingはAnimation C# Jobs(Animation C# Jobs | Unity Blog )を使用していてパフォーマンスが良い一方で、コンストレイン トの処理の間にMonobehaviorスクリプト の処理は挟めないようになっています(Question - Script Execution Order - Unity Forum )。ここらへんはちょっとややこしかったです。
UnityフォーラムにAnimation Riggingの情報まとめ(Official - Compilation of resources for Animation Rigging - Unity Forum )が投稿されていたのを今見つけたのですが、もっと早く見つけたかった……
使用場面
モーキャプのデータでキャラク ターを動かす必要があったのですが、このように腕が机を貫通する問題が発生していました。(このgifで使ってるモーションはMixamo のやつで、実際に使ったやつとは違いますが)
モーションを入れただけ
前から
モーションを撮り直したり手作業で修正する手もありましたが、実際に動かすキャラク ターの総数は数十体で、かつ仕様もはっきりと決まっていなかったため、Animation Riggingのコンストレイン トを使ってプロシージャルに貫通を防ぐリグを作ろうと考えました。
しかし、用意されているコンストレイン トの組み合わせでは上手くいかず、なぜかメモリリーク が発生するリグさえ出来上がってしまいました。そこで、専用のカスタムコンストレイン トを作りました。
TwoBoneIKコンストレイン トを拡張した、HeightLimitedTwoBoneIKConstraintというコンストレイン トを実装しました。次のような処理を行っています。
TwoBoneIKのtargetが制限高より低くならないように位置を修正する
TwoBoneIKの処理を行う
midが制限高より高くなるように、tipの位置を維持したまま(tip-rootを軸にして)rootを回転させる
横から見た図
コード(最後に載せています)の説明は省略しますが、カスタムコンストレイン トの書き方はこの動画で説明されています。
VIDEO youtu.be
作ったカスタムコンストレイン トを使ってリグを組みます。
リグの構成
HeightLimitedTwoBoneIKConstraintのプロパティは、Limit Heightを机と同じくらいの高さにし、RootやTarget等はTwoBoneIKConstraintと同じように設定します。ただし、このTargetはMulti-PositionConstraintでHandボーンと同じ位置になるように拘束しています。
コンストレイン トの実行順は上から下の順で処理が行われるので、Multi-PositionConstraintを上に、HeightLimitedTwoBoneIKConstraintを下にすることで、(Animatorで計算された手の位置を取得)→(手の位置の修正)という風に適切に実行できます。
という感じで、腕が机を貫通しないリグを組むことができました。
カスタムコンストレイン トを使った場合
前から
体格の違うキャラク ターであっても、異なるアニメーションであってもこのリグを流用するだけでいいので便利です。
おわりに
最初はAnimation RiggingをFinalIKのようなものと考えていたので苦戦しましたが、使いこなせたらかなり強いツールだと感じました。
参考
Animation Rigging でアニメーションをレベルアップさせる方法を学ぼう | Unity Blog
Animation C# Jobs | Unity Blog
Question - Script Execution Order - Unity Forum
(79) Extending the Animation Rigging package with C# - Unite Copenhagen - YouTube
Official - Compilation of resources for Animation Rigging - Unity Forum
Mixamo
おまけ(コード)
汚くて恥ずかしいですが、一応コードを載せときます。
rootの回転角度の計算はもっと賢いやり方がありそうですが思いつきませんでした。
using UnityEngine ; using UnityEngine . Animations ; using UnityEngine . Animations . Rigging ; [ DisallowMultipleComponent , AddComponentMenu ( "Animation Rigging/Height Limited Two Bone IK Constraint" )] public class HeightLimitedTwoBoneIKConstraint : RigConstraint < HeightLimitedTwoBoneIKConstraintJob , HeightLimitedTwoBoneIKConstraintData , HeightLimitedTwoBoneIKConstraintJobBinder < HeightLimitedTwoBoneIKConstraintData > > { protected override void OnValidate () { base . OnValidate () ; m_Data . hintWeight = Mathf . Clamp01 ( m_Data . hintWeight ) ; m_Data . targetPositionWeight = Mathf . Clamp01 ( m_Data . targetPositionWeight ) ; m_Data . targetRotationWeight = Mathf . Clamp01 ( m_Data . targetRotationWeight ) ; } } [ Unity . Burst . BurstCompile ] public struct HeightLimitedTwoBoneIKConstraintJob : IWeightedAnimationJob { public ReadWriteTransformHandle root ; public ReadWriteTransformHandle mid ; public ReadWriteTransformHandle tip ; public ReadOnlyTransformHandle hint ; public ReadOnlyTransformHandle target ; public AffineTransform targetOffset ; public FloatProperty targetPositionWeight ; public FloatProperty targetRotationWeight ; public FloatProperty hintWeight ; public FloatProperty limitHeight ; public bool isLeftArm ; public FloatProperty jobWeight { get ; set ; } public void ProcessRootMotion ( AnimationStream stream ) { } public void ProcessAnimation ( AnimationStream stream ) { float w = jobWeight . Get ( stream ) ; if ( w > 0f ) { HeightLimitedSolveTwoBoneIK ( stream, root , mid , tip , target , hint , targetPositionWeight . Get ( stream ) * w, targetRotationWeight . Get ( stream ) * w, hintWeight . Get ( stream ) * w, targetOffset , limitHeight . Get ( stream ) , isLeftArm ) ; } else { AnimationRuntimeUtils . PassThrough ( stream, root ) ; AnimationRuntimeUtils . PassThrough ( stream, mid ) ; AnimationRuntimeUtils . PassThrough ( stream, tip ) ; } } public static void HeightLimitedSolveTwoBoneIK ( AnimationStream stream, ReadWriteTransformHandle root, ReadWriteTransformHandle mid, ReadWriteTransformHandle tip, ReadOnlyTransformHandle target, ReadOnlyTransformHandle hint, float posWeight, float rotWeight, float hintWeight, AffineTransform targetOffset, float limitHeight, bool isLeftArm ) { Vector3 aPosition = root. GetPosition ( stream ) ; Vector3 bPosition = mid. GetPosition ( stream ) ; Vector3 cPosition = tip. GetPosition ( stream ) ; target. GetGlobalTR ( stream, out Vector3 targetPos, out Quaternion targetRot ) ; //target の高さが指定よりも低ければ調整 if ( targetPos. y < limitHeight ) { targetPos. y = limitHeight; } //TwoBoneIK の処理 Vector3 tPosition = Vector3 . Lerp ( cPosition, targetPos + targetOffset. translation , posWeight ) ; Quaternion tRotation = Quaternion . Lerp ( tip. GetRotation ( stream ) , targetRot * targetOffset. rotation , rotWeight ) ; bool hasHint = hint. IsValid ( stream ) && hintWeight > 0f ; Vector3 ab = bPosition - aPosition; Vector3 bc = cPosition - bPosition; Vector3 ac = cPosition - aPosition; Vector3 at = tPosition - aPosition; float abLen = ab. magnitude ; float bcLen = bc. magnitude ; float acLen = ac. magnitude ; float atLen = at. magnitude ; float oldAbcAngle = TriangleAngle ( acLen, abLen, bcLen ) ; float newAbcAngle = TriangleAngle ( atLen, abLen, bcLen ) ; Vector3 axis = Vector3 . Cross ( ab, bc ) ; float k_SqrEpsilon = 1e-8f ; if ( axis. sqrMagnitude < k_SqrEpsilon ) { axis = hasHint ? Vector3 . Cross ( hint. GetPosition ( stream ) - aPosition, bc ) : Vector3 . zero ; if ( axis. sqrMagnitude < k_SqrEpsilon ) axis = Vector3 . Cross ( at, bc ) ; if ( axis. sqrMagnitude < k_SqrEpsilon ) axis = Vector3 . up ; } axis = Vector3 . Normalize ( axis ) ; float a = 0.5f * ( oldAbcAngle - newAbcAngle ) ; float sin = Mathf . Sin ( a ) ; float cos = Mathf . Cos ( a ) ; Quaternion deltaR = new Quaternion ( axis. x * sin, axis. y * sin, axis. z * sin, cos ) ; mid. SetRotation ( stream, deltaR * mid. GetRotation ( stream )) ; cPosition = tip. GetPosition ( stream ) ; ac = cPosition - aPosition; root. SetRotation ( stream, QuaternionExt . FromToRotation ( ac, at ) * root. GetRotation ( stream )) ; if ( hasHint ) { float acSqrMag = ac. sqrMagnitude ; if ( acSqrMag > 0f ) { bPosition = mid. GetPosition ( stream ) ; cPosition = tip. GetPosition ( stream ) ; ab = bPosition - aPosition; ac = cPosition - aPosition; Vector3 acNorm = ac / Mathf . Sqrt ( acSqrMag ) ; Vector3 ah = hint. GetPosition ( stream ) - aPosition; Vector3 abProj = ab - acNorm * Vector3 . Dot ( ab, acNorm ) ; Vector3 ahProj = ah - acNorm * Vector3 . Dot ( ah, acNorm ) ; float maxReach = abLen + bcLen; if ( abProj. sqrMagnitude > ( maxReach * maxReach * 0.001f ) && ahProj. sqrMagnitude > 0f ) { Quaternion hintR = QuaternionExt . FromToRotation ( abProj, ahProj ) ; hintR. x *= hintWeight; hintR. y *= hintWeight; hintR. z *= hintWeight; hintR = QuaternionExt . NormalizeSafe ( hintR ) ; root. SetRotation ( stream, hintR * root. GetRotation ( stream )) ; } } } tip. SetRotation ( stream, tRotation ) ; //mid が制限高より低い場合に、 tip の位置を維持して root を回転させる if ( mid. GetPosition ( stream ) . y >= limitHeight ) return ; Vector3 rootPosition = root. GetPosition ( stream ) ; Vector3 midPosition = mid. GetPosition ( stream ) ; Vector3 tipPosition = tip. GetPosition ( stream ) ; Vector3 mPosition = ( rootPosition + tipPosition ) * 0.5f ; Vector3 normal = ( tipPosition - rootPosition ) . normalized ; Vector3 rVector = midPosition - mPosition; float radius = rVector.magnitude ; Vector3 xVector = rVector; Vector3 yVector = xVector. magnitude > Mathf . Epsilon ? Vector3 . Cross ( normal, xVector ) : Vector3 . Cross ( normal, Vector3 . up ) ; xVector. Normalize () ; yVector. Normalize () ; float rotateDirection = isLeftArm ? - 1 : 1 ; float angle = 0 ; Vector3 point = mPosition + radius * xVector; while ( point. y < limitHeight && angle <= 180f ) { angle += 0.1f ; float theta = rotateDirection * Mathf . Deg2Rad * angle; point = mPosition + radius * ( xVector * Mathf . Cos ( theta ) + yVector * Mathf . Sin ( theta )) ; } Quaternion rotation = Quaternion . AngleAxis ( rotateDirection * angle, normal ) ; root. SetRotation ( stream, rotation * root. GetRotation ( stream )) ; Quaternion inverse = Quaternion . Inverse ( rotation ) ; tip. SetRotation ( stream, inverse * tip. GetRotation ( stream )) ; } private static float TriangleAngle ( float aLen, float aLen1, float aLen2 ) { float c = Mathf . Clamp (( aLen1 * aLen1 + aLen2 * aLen2 - aLen * aLen ) / ( aLen1 * aLen2 ) / 2.0f , - 1.0f , 1.0f ) ; return Mathf . Acos ( c ) ; } } public interface IHeightLimitedTwoBoneIKConstraintData { Transform root { get ; } Transform mid { get ; } Transform tip { get ; } Transform target { get ; } Transform hint { get ; } bool maintainTargetPositionOffset { get ; } bool maintainTargetRotationOffset { get ; } string targetPositionWeightFloatProperty { get ; } string targetRotationWeightFloatProperty { get ; } string hintWeightFloatProperty { get ; } string limitHeightFloatProperty { get ; } bool isLeftArm { get ; } } [ System . Serializable ] public struct HeightLimitedTwoBoneIKConstraintData : IAnimationJobData , IHeightLimitedTwoBoneIKConstraintData { [ SerializeField ] Transform m_Root ; [ SerializeField ] Transform m_Mid ; [ SerializeField ] Transform m_Tip ; [ SyncSceneToStream , SerializeField ] Transform m_Target ; [ SyncSceneToStream , SerializeField ] Transform m_Hint ; [ SyncSceneToStream , SerializeField , Range ( 0f , 1f )] float m_TargetPositionWeight ; [ SyncSceneToStream , SerializeField , Range ( 0f , 1f )] float m_TargetRotationWeight ; [ SyncSceneToStream , SerializeField , Range ( 0f , 1f )] float m_HintWeight ; [ NotKeyable , SerializeField ] bool m_MaintainTargetPositionOffset ; [ NotKeyable , SerializeField ] bool m_MaintainTargetRotationOffset ; [ SyncSceneToStream , SerializeField ] float m_LimitHeight ; [ SerializeField ] private bool m_IsLeftArm ; public Transform root { get => m_Root ; set => m_Root = value ; } public Transform mid { get => m_Mid ; set => m_Mid = value ; } public Transform tip { get => m_Tip ; set => m_Tip = value ; } public Transform target { get => m_Target ; set => m_Target = value ; } public Transform hint { get => m_Hint ; set => m_Hint = value ; } public float targetPositionWeight { get => m_TargetPositionWeight ; set => m_TargetPositionWeight = Mathf . Clamp01 ( value ) ; } public float targetRotationWeight { get => m_TargetRotationWeight ; set => m_TargetRotationWeight = Mathf . Clamp01 ( value ) ; } public float hintWeight { get => m_HintWeight ; set => m_HintWeight = Mathf . Clamp01 ( value ) ; } public bool maintainTargetPositionOffset { get => m_MaintainTargetPositionOffset ; set => m_MaintainTargetPositionOffset = value ; } public bool maintainTargetRotationOffset { get => m_MaintainTargetRotationOffset ; set => m_MaintainTargetRotationOffset = value ; } public bool isLeftArm { get => m_IsLeftArm ; set => m_IsLeftArm = value ; } string IHeightLimitedTwoBoneIKConstraintData . targetPositionWeightFloatProperty => ConstraintsUtils . ConstructConstraintDataPropertyName ( nameof ( m_TargetPositionWeight )) ; string IHeightLimitedTwoBoneIKConstraintData . targetRotationWeightFloatProperty => ConstraintsUtils . ConstructConstraintDataPropertyName ( nameof ( m_TargetRotationWeight )) ; string IHeightLimitedTwoBoneIKConstraintData . hintWeightFloatProperty => ConstraintsUtils . ConstructConstraintDataPropertyName ( nameof ( m_HintWeight )) ; string IHeightLimitedTwoBoneIKConstraintData . limitHeightFloatProperty => ConstraintsUtils . ConstructConstraintDataPropertyName ( nameof ( m_LimitHeight )) ; bool IAnimationJobData . IsValid () => ( m_Tip != null && m_Mid != null && m_Root != null && m_Target != null && m_Tip . IsChildOf ( m_Mid ) && m_Mid . IsChildOf ( m_Root ) ) ; void IAnimationJobData . SetDefaultValues () { m_Root = null ; m_Mid = null ; m_Tip = null ; m_Target = null ; m_Hint = null ; m_TargetPositionWeight = 1f ; m_TargetRotationWeight = 0f ; m_HintWeight = 1f ; m_MaintainTargetPositionOffset = false ; m_MaintainTargetRotationOffset = false ; m_LimitHeight = 0f ; m_IsLeftArm = true ; } } public class HeightLimitedTwoBoneIKConstraintJobBinder < T > : AnimationJobBinder < HeightLimitedTwoBoneIKConstraintJob , T > where T : struct , IAnimationJobData , IHeightLimitedTwoBoneIKConstraintData { public override HeightLimitedTwoBoneIKConstraintJob Create ( Animator animator, ref T data, Component component ) { var job = new HeightLimitedTwoBoneIKConstraintJob () ; job. root = ReadWriteTransformHandle . Bind ( animator, data. root ) ; job. mid = ReadWriteTransformHandle . Bind ( animator, data. mid ) ; job. tip = ReadWriteTransformHandle . Bind ( animator, data. tip ) ; job. target = ReadOnlyTransformHandle . Bind ( animator, data. target ) ; if ( data. hint != null ) job. hint = ReadOnlyTransformHandle . Bind ( animator, data. hint ) ; job. targetOffset = AffineTransform . identity ; if ( data. maintainTargetPositionOffset ) job. targetOffset . translation = data. tip . position - data. target . position ; if ( data. maintainTargetRotationOffset ) job. targetOffset . rotation = Quaternion . Inverse ( data. target . rotation ) * data. tip . rotation ; job. targetPositionWeight = FloatProperty . Bind ( animator, component, data. targetPositionWeightFloatProperty ) ; job. targetRotationWeight = FloatProperty . Bind ( animator, component, data. targetRotationWeightFloatProperty ) ; job. hintWeight = FloatProperty . Bind ( animator, component, data. hintWeightFloatProperty ) ; job. limitHeight = FloatProperty . Bind ( animator, component, data. limitHeightFloatProperty ) ; job. isLeftArm = data. isLeftArm ; return job; } public override void Destroy ( HeightLimitedTwoBoneIKConstraintJob job ) { } }