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」でパッケージマネージャを開きます。

2019 11 20 16 27 09

「Advanced > Show preview packages」でプレビューのパッケージを表示します。

2019 11 20 16 28 22

Animation Riggingを選択して「install」を実行します。

2019 11 20 16 28 50

位置を回転に転送するコンストレイントのスクリプトを作成する

新しく「PositionToRotationConstraint」というC#のスクリプトを作成します。

2019 11 20 16 30 25

スクリプトファイルに次のように記述します。

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を作成します。

2019 11 20 17 10 42

AnimatorとRig Builderをアタッチします。

2019 11 20 17 12 10

GameObjectの下にCylinderを作成します。 このオブジェクトをリグで回転させようと思います。

2019 11 20 17 12 58

RootのGameObjectの下に空のGameObjectを作成し「ControlRig」と名付けます。

2019 11 20 17 14 16

CntrlRigにRigコンポーネントをつけます。

2019 11 20 17 14 45

ControlRigのRigコンポーネントをルートのRigBuilderにアタッチします。

2019 11 20 17 15 36

ControlRigの下に空のGameObjectを作成し、「PositionToRotation」と名付けます。

2019 11 20 17 16 04

適当にz座標をずらしておきます。ここではzに1.5を入れました。

2019 11 20 17 17 06

2019 11 20 17 17 13

PositionToRotationに先ほど作成したPositionToRotationConstraintをアタッチします。

2019 11 20 17 18 10

PositionToRotationの下にGameObjectを作成してCNTRLと名付けます。

2019 11 20 17 18 48

PositionToRotationConstraintスクリプトのConstrained ObjectにCylinderを、Source ObjectにCNTRLをアタッチします。

2019 11 20 17 19 47

CNTRLを選択してシーンビュー右下からEffectorを追加します。

2019 11 20 17 21 44

2019 11 20 17 22 34

Sizeを0.5に、ShapeをBallEffectorにしました。

2019 11 20 17 23 12

2019 11 20 17 23 22

動かしてみる

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)
            );
        }
    }
}

これはさきほどのRigConstraintTJobとして渡すものです。 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;
    }
}

これはRigConstraintTDataとして渡します。

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だけはファイル名と一致している必要がありますが、それ以外については同一ファイル内に詰め込んでも問題がないようです。

参考

Unity-Technologies/animation-rigging-workshop-siggraph2019: Repository for SIGGRAPH 2019 Animation Rigging workshop

おわりに

かんたんなコンストレイントを自作してみました。 2軸で回っている部分の挙動が若干怪しいですが、適当なサンプルなので気にしないということで……。

https://github.com/MatchaChoco010/UnityAnimationRiggingPositionToRotation