UnityのAnimation Riggingで位置を回転に転送するシンプルなコンストレイントを作成する
はじめに
Animation Riggingの勉強のためにかんたんなコンストレイントを作ったのでメモします。
- Unity: 2019.3.0b11
- Animation Rigging: 0.2.3-preview
Animation Riggingとは
Animation Riggingは2019.1からプレビューパッケージとして提供されている、プロシージャルアニメーション及びアニメーションオーサリングに使えるリグを構築するためのパッケージです。
Animation Rigging | Package Manager UI website
Unity 2019.1 の Animation Rigging プレビューパッケージの概要 – Unity Blog
高度な Animation Rigging:キャラクターとプロップのインタラクション – Unity Blog
このパッケージは拡張性が高く作られており、C#で独自のコンストレイントを構築できることが売りのひとつのようです。 そこで今回は、独自のコンストレイントの作成に挑戦してみます。
Animation Riggingのインストール
あたらしくUnityのプロジェクトを立ち上げたら「Window > Package Manager」でパッケージマネージャを開きます。
「Advanced > Show preview packages」でプレビューのパッケージを表示します。
Animation Riggingを選択して「install」を実行します。
位置を回転に転送するコンストレイントのスクリプトを作成する
新しく「PositionToRotationConstraint」というC#のスクリプトを作成します。
スクリプトファイルに次のように記述します。
using Unity.Burst;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Animations.Rigging;
[DisallowMultipleComponent, AddComponentMenu ("Animation Rigging/Custom/Position To Rotation Constraint")]
public class PositionToRotationConstraint : RigConstraint<PositionToRotationConstraintJob, PositionToRotationConstraintData, PositionToRotationConstraintBinder> { }
[BurstCompile]
public struct PositionToRotationConstraintJob : IWeightedAnimationJob {
public ReadWriteTransformHandle constrained;
public ReadWriteTransformHandle source;
public FloatProperty jobWeight { get; set; }
public void ProcessRootMotion (AnimationStream stream) { }
public void ProcessAnimation (AnimationStream stream) {
float w = jobWeight.Get (stream);
var sourcePos = source.GetLocalPosition (stream);
sourcePos = new Vector3 (sourcePos.x, sourcePos.y, 0);
source.SetLocalPosition (stream, sourcePos);
if (w > 0f) {
var rot = constrained.GetLocalRotation (stream);
rot *= Quaternion.AngleAxis (sourcePos.y * Mathf.PI * Mathf.Rad2Deg, Vector3.forward);
rot *= Quaternion.AngleAxis (sourcePos.x * Mathf.PI * Mathf.Rad2Deg, Vector3.right);
constrained.SetLocalRotation (
stream,
Quaternion.Lerp (constrained.GetLocalRotation (stream), rot, w)
);
}
}
}
[System.Serializable]
public struct PositionToRotationConstraintData : IAnimationJobData {
public Transform constrainedObject;
[SyncSceneToStream] public Transform sourceObject;
public bool IsValid () => !(constrainedObject == null || sourceObject == null);
public void SetDefaultValues () {
constrainedObject = null;
sourceObject = null;
}
}
public class PositionToRotationConstraintBinder : AnimationJobBinder<PositionToRotationConstraintJob, PositionToRotationConstraintData> {
public override PositionToRotationConstraintJob Create (Animator animator, ref PositionToRotationConstraintData data, Component component) {
var job = new PositionToRotationConstraintJob ();
job.constrained = ReadWriteTransformHandle.Bind (animator, data.constrainedObject);
job.source = ReadWriteTransformHandle.Bind (animator, data.sourceObject);
return job;
}
public override void Destroy (PositionToRotationConstraintJob job) { }
}
リグを構築する
シーンに空のGameObjectを作成します。
AnimatorとRig Builderをアタッチします。
GameObjectの下にCylinderを作成します。 このオブジェクトをリグで回転させようと思います。
RootのGameObjectの下に空のGameObjectを作成し「ControlRig」と名付けます。
CntrlRigにRigコンポーネントをつけます。
ControlRigのRigコンポーネントをルートのRigBuilderにアタッチします。
ControlRigの下に空のGameObjectを作成し、「PositionToRotation」と名付けます。
適当にz座標をずらしておきます。ここではzに1.5を入れました。
PositionToRotationに先ほど作成したPositionToRotationConstraintをアタッチします。
PositionToRotationの下にGameObjectを作成してCNTRLと名付けます。
PositionToRotationConstraintスクリプトのConstrained ObjectにCylinderを、Source ObjectにCNTRLをアタッチします。
CNTRLを選択してシーンビュー右下からEffectorを追加します。
Sizeを0.5に、ShapeをBallEffectorにしました。
動かしてみる
Animation Riggingで組んだリグを動作させるにはPlayモードに入るかAnimation WindowでPreviewするなどが必要です。 ここではPlayモードに入ることにします。
Effectorを選択して移動させるとそれに応じてシリンダーが回転するのがわかります。
スクリプト解説
スクリプトは4つの部分からなっています。
最初にPositionToRotationConstraintです。
[DisallowMultipleComponent, AddComponentMenu ("Animation Rigging/Custom/Position To Rotation Constraint")]
public class PositionToRotationConstraint : RigConstraint<PositionToRotationConstraintJob, PositionToRotationConstraintData, PositionToRotationConstraintBinder> { }
これはGameObjectに貼り付けるコンポーネントとなっています。
RigConstraint<TJob, TData, TBinder>
を継承します。
次にPositionToRotationConstraintJob
です。
[BurstCompile]
public struct PositionToRotationConstraintJob : IWeightedAnimationJob {
public ReadWriteTransformHandle constrained;
public ReadWriteTransformHandle source;
public FloatProperty jobWeight { get; set; }
public void ProcessRootMotion (AnimationStream stream) { }
public void ProcessAnimation (AnimationStream stream) {
float w = jobWeight.Get (stream);
var sourcePos = source.GetLocalPosition (stream);
sourcePos = new Vector3 (sourcePos.x, sourcePos.y, 0);
source.SetLocalPosition (stream, sourcePos);
if (w > 0f) {
var rot = constrained.GetLocalRotation (stream);
rot *= Quaternion.AngleAxis (sourcePos.y * Mathf.PI * Mathf.Rad2Deg, Vector3.forward);
rot *= Quaternion.AngleAxis (sourcePos.x * Mathf.PI * Mathf.Rad2Deg, Vector3.right);
constrained.SetLocalRotation (
stream,
Quaternion.Lerp (constrained.GetLocalRotation (stream), rot, w)
);
}
}
}
これはさきほどのRigConstraint
のTJob
として渡すものです。
IWeightedAnimationJob
を実装します。
ProcessAnimation (AnimationStream stream)
が今回の実装のメイン部分です。
最初にjobWeight
を受け取っています。
float w = jobWeight.Get (stream);
次にsource
のローカル位置を取得しz座標を0にしています。
var sourcePos = source.GetLocalPosition (stream);
sourcePos = new Vector3 (sourcePos.x, sourcePos.y, 0);
source.SetLocalPosition (stream, sourcePos);
ウエイトが0より大きいとき、constraindに回転を与えています。 適当にsourceのxy座標から回転を作って渡しています。
if (w > 0f) {
var rot = constrained.GetLocalRotation (stream);
rot *= Quaternion.AngleAxis (sourcePos.y * Mathf.PI * Mathf.Rad2Deg, Vector3.forward);
rot *= Quaternion.AngleAxis (sourcePos.x * Mathf.PI * Mathf.Rad2Deg, Vector3.right);
constrained.SetLocalRotation (
stream,
Quaternion.Lerp (constrained.GetLocalRotation (stream), rot, w)
);
}
次にPositionToRotationConstraintData
です。
[System.Serializable]
public struct PositionToRotationConstraintData : IAnimationJobData {
public Transform constrainedObject;
[SyncSceneToStream] public Transform sourceObject;
public bool IsValid () => !(constrainedObject == null || sourceObject == null);
public void SetDefaultValues () {
constrainedObject = null;
sourceObject = null;
}
}
これはRigConstraint
にTData
として渡します。
IAnimationJobData
を実装し[System.Serializable]
をくっつけています。
public
で2つのTransformをシリアライズして保持しています。
sourceObject
にくっつけている[SyncSceneToStream]
は、多分その名のとおりのものです。
これをつけない場合sourceObjectが動かせなくなります。
シーンの位置がStreamに適用されずにJobの方のGetLoacalPosition
に渡ってしまい、それのzを0にしてSetLocalPosition
にわたすことになってしまうのでそのような挙動になると思われます。
シーンの位置をstream適用したい場合は[SyncSceneToStream]
が必要になるようです。
IsValid
ではこのコンストレイントが有効かどうかを判定し、SetDefaultValues()
では2つのTransformにnull
を与えています。
最後にPositionToRotationConstraintBinder
です。
public class PositionToRotationConstraintBinder : AnimationJobBinder<PositionToRotationConstraintJob, PositionToRotationConstraintData> {
public override PositionToRotationConstraintJob Create (Animator animator, ref PositionToRotationConstraintData data, Component component) {
var job = new PositionToRotationConstraintJob ();
job.constrained = ReadWriteTransformHandle.Bind (animator, data.constrainedObject);
job.source = ReadWriteTransformHandle.Bind (animator, data.sourceObject);
return job;
}
public override void Destroy (PositionToRotationConstraintJob job) { }
}
これはその名のとおりDataとJobをバインドする役割を果たしています。
コンポーネントとなるPositionToRotationConstraint
だけはファイル名と一致している必要がありますが、それ以外については同一ファイル内に詰め込んでも問題がないようです。
参考
おわりに
かんたんなコンストレイントを自作してみました。 2軸で回っている部分の挙動が若干怪しいですが、適当なサンプルなので気にしないということで……。
https://github.com/MatchaChoco010/UnityAnimationRiggingPositionToRotation