UnityのAnimation Riggingで良い感じの足のコントローラを作成する

はじめに

Animation Riggingで足のコントローラを作成したのでメモします。

2019 11 22 22 39 00

  • Unity: 2019.3.0b12
  • Animation Rigging: 0.2.3-preview

Animation Riggingをインポートする

前回までと同様にしてPackage ManagerからAnimation Riggingをインストールします。

2019 11 22 20 29 17

スクリプトを作成する

RotationBlendSliderConstraint.csを作成します。

2019 11 22 20 30 41

スクリプトには次のように記述します。

using Unity.Burst;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Animations.Rigging;

[DisallowMultipleComponent, AddComponentMenu ("Animation Rigging/Custom/Rotation Blend Slider")]
public class RotationBlendSliderConstraint : RigConstraint<RotationBlendSliderConstraintJob, RotationBlendSliderConstraintData, RotationBlendSliderConstraintBinder> { }

[BurstCompile]
public struct RotationBlendSliderConstraintJob : IWeightedAnimationJob {

    public ReadWriteTransformHandle Target;

    public ReadOnlyTransformHandle SourceA;
    public ReadOnlyTransformHandle SourceB;

    public ReadWriteTransformHandle Slider;

    public FloatProperty jobWeight { get; set; }

    public void ProcessRootMotion (AnimationStream stream) { }

    public void ProcessAnimation (AnimationStream stream) {
        float w = jobWeight.Get (stream);

        if (w > 0f) {
            var sliderPos = Slider.GetLocalPosition (stream);
            var t = Mathf.Clamp01 (sliderPos.y);
            Slider.SetLocalPosition (stream, new Vector3 (0, t, 0));

            var rot = Quaternion.Lerp (
                SourceA.GetRotation (stream),
                SourceB.GetRotation (stream),
                t
            );

            var targetRot = Target.GetRotation (stream);
            Target.SetRotation (stream, Quaternion.Lerp (targetRot, rot, w));
        }
    }

}

[System.Serializable]
public struct RotationBlendSliderConstraintData : IAnimationJobData {

    public Transform Target;

    [SyncSceneToStream] public Transform SourceA;
    [SyncSceneToStream] public Transform SourceB;

    [SyncSceneToStream] public Transform Slider;

    public bool IsValid () => !(Target == null || SourceA == null || SourceB == null || Slider == null);

    public void SetDefaultValues () {
        Target = null;
        SourceA = null;
        SourceB = null;
        Slider = null;
    }

}

public class RotationBlendSliderConstraintBinder : AnimationJobBinder<RotationBlendSliderConstraintJob, RotationBlendSliderConstraintData> {

    public override RotationBlendSliderConstraintJob Create (Animator animator, ref RotationBlendSliderConstraintData data, Component component) {
        var job = new RotationBlendSliderConstraintJob ();

        job.Target = ReadWriteTransformHandle.Bind (animator, data.Target);

        job.SourceA = ReadOnlyTransformHandle.Bind (animator, data.SourceA);
        job.SourceB = ReadOnlyTransformHandle.Bind (animator, data.SourceB);

        job.Slider = ReadWriteTransformHandle.Bind (animator, data.Slider);

        return job;
    }

    public override void Destroy (RotationBlendSliderConstraintJob job) { }

}

FootIKConstraint.csを作成します。

2019 11 22 20 31 33

スクリプトには次のように記述します。

using Unity.Burst;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Animations.Rigging;

[DisallowMultipleComponent, AddComponentMenu ("Animation Rigging/Custom/Foot IK")]
public class FootIKConstraint : RigConstraint<FootIKConstraintJob, FootIKConstraintData, FootIKConstraintBinder> { }

[BurstCompile]
public struct FootIKConstraintJob : IWeightedAnimationJob {

    public ReadWriteTransformHandle FootIKTarget;

    public ReadOnlyTransformHandle FootControllerBase;
    public ReadWriteTransformHandle FootRollCursor;

    public ReadOnlyTransformHandle ToeEnd;
    public ReadOnlyTransformHandle FootHeel;
    public ReadOnlyTransformHandle FootLeftSide;
    public ReadOnlyTransformHandle FootRightSide;

    public FloatProperty jobWeight { get; set; }

    public void ProcessRootMotion (AnimationStream stream) { }

    public void ProcessAnimation (AnimationStream stream) {
        float w = jobWeight.Get (stream);

        if (w > 0f) {
            var cursorPos = FootRollCursor.GetLocalPosition (stream);
            FootRollCursor.SetLocalPosition (stream, new Vector3 (cursorPos.x, cursorPos.y, 0));

            var pos = FootControllerBase.GetPosition (stream);
            var rot = FootControllerBase.GetRotation (stream);

            if (cursorPos.x >= 0f) {
                var footRightSidePos = FootRightSide.GetPosition (stream);
                var axisZ = rot * Vector3.forward;
                var rotZ = Quaternion.AngleAxis (180 * cursorPos.x, axisZ);
                pos = footRightSidePos + rotZ * (pos - footRightSidePos);
                rot = rotZ * rot;

                if (cursorPos.y >= 0f) {
                    var toePos = footRightSidePos + rotZ * (ToeEnd.GetPosition (stream) - footRightSidePos);
                    var axisX = rot * Vector3.right;
                    var rotX = Quaternion.AngleAxis (180 * cursorPos.y, axisX);
                    pos = toePos + rotX * (pos - toePos);
                    rot = rotX * rot;

                } else {
                    var heelPos = footRightSidePos + rotZ * (FootHeel.GetPosition (stream) - footRightSidePos);
                    var axisX = rot * Vector3.right;
                    var rotX = Quaternion.AngleAxis (180 * cursorPos.y, axisX);
                    pos = heelPos + rotX * (pos - heelPos);
                    rot = rotX * rot;
                }
            } else {
                var footLeftSidePos = FootLeftSide.GetPosition (stream);
                var axisZ = rot * Vector3.forward;
                var rotZ = Quaternion.AngleAxis (180 * cursorPos.x, axisZ);
                pos = footLeftSidePos + rotZ * (pos - footLeftSidePos);
                rot = rotZ * rot;

                if (cursorPos.y >= 0f) {
                    var toePos = footLeftSidePos + rotZ * (ToeEnd.GetPosition (stream) - footLeftSidePos);
                    var axisX = rot * Vector3.right;
                    var rotX = Quaternion.AngleAxis (180 * cursorPos.y, axisX);
                    pos = toePos + rotX * (pos - toePos);
                    rot = rotX * rot;

                } else {
                    var heelPos = footLeftSidePos + rotZ * (FootHeel.GetPosition (stream) - footLeftSidePos);
                    var axisX = rot * Vector3.right;
                    var rotX = Quaternion.AngleAxis (180 * cursorPos.y, axisX);
                    pos = heelPos + rotX * (pos - heelPos);
                    rot = rotX * rot;
                }
            }

            var footIKTargetPos = FootIKTarget.GetPosition (stream);
            var footIKTargetRot = FootIKTarget.GetRotation (stream);
            FootIKTarget.SetPosition (stream, Vector3.Lerp (footIKTargetPos, pos, w));
            FootIKTarget.SetRotation (stream, Quaternion.Lerp (footIKTargetRot, rot, w));
        }
    }

}

[System.Serializable]
public struct FootIKConstraintData : IAnimationJobData {

    public Transform FootIKTarget;

    [SyncSceneToStream] public Transform FootControllerBase;
    [SyncSceneToStream] public Transform FootRollCursor;

    [SyncSceneToStream] public Transform ToeEnd;
    [SyncSceneToStream] public Transform FootHeel;
    [SyncSceneToStream] public Transform FootLeftSide;
    [SyncSceneToStream] public Transform FootRightSide;

    public bool IsValid () => !(FootIKTarget == null || FootControllerBase == null || FootRollCursor == null || ToeEnd == null || FootHeel == null || FootLeftSide == null || FootRightSide == null);

    public void SetDefaultValues () {
        FootIKTarget = null;
        FootControllerBase = null;
        FootRollCursor = null;
        ToeEnd = null;
        FootHeel = null;
        FootLeftSide = null;
        FootRightSide = null;
    }

}

public class FootIKConstraintBinder : AnimationJobBinder<FootIKConstraintJob, FootIKConstraintData> {

    public override FootIKConstraintJob Create (Animator animator, ref FootIKConstraintData data, Component component) {
        var job = new FootIKConstraintJob ();

        job.FootIKTarget = ReadWriteTransformHandle.Bind (animator, data.FootIKTarget);
        job.FootControllerBase = ReadOnlyTransformHandle.Bind (animator, data.FootControllerBase);
        job.FootRollCursor = ReadWriteTransformHandle.Bind (animator, data.FootRollCursor);
        job.ToeEnd = ReadOnlyTransformHandle.Bind (animator, data.ToeEnd);
        job.FootHeel = ReadOnlyTransformHandle.Bind (animator, data.FootHeel);
        job.FootLeftSide = ReadOnlyTransformHandle.Bind (animator, data.FootLeftSide);
        job.FootRightSide = ReadOnlyTransformHandle.Bind (animator, data.FootRightSide);

        return job;
    }

    public override void Destroy (FootIKConstraintJob job) { }

}

モデルを読み込む

前回と同様にしてMake Humanで作成したモデルを読み込みます。 読み込んだモデルをシーンに配置しました。

2019 11 22 20 26 08

2019 11 22 20 25 59

リグを構築する

モデルのルートにAnimatorとRig Builderコンポーネントをつけます。

2019 11 22 22 01 55

モデルのルートの下にGameObjectを配置しControlRigと名付けます。

2019 11 22 20 33 11

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

2019 11 22 22 02 37

LeftLegのRigコンポーネントをControlRigのRig Builderに登録します。

2019 11 22 22 02 58

ControlRigの下にGameObjectを追加してLeftLegと名付けます。

2019 11 22 20 34 42

LeftLegの下にGameObjectを追加してLeftLegFKと名付けます。

2019 11 22 20 38 36

LeftLegFKの下にGameObjectを追加してPelvisと名付けます。

2019 11 22 20 41 12

Pelvisを選択し、Ctrlキーを押しながら骨盤のボーン(この場合はpelvis)を選択します。 選択する順番も重要です。

2019 11 22 20 42 22

「Animation Rigging > Align Transform」を実行します。

2019 11 22 20 42 44

これでPelvisが骨盤のボーンの位置に揃いました。

2019 11 22 20 43 20

Pelvisを骨盤のボーンに追従させます。

PelvisにMulti-Parent Constraintをつけます。

2019 11 22 20 44 13

Constrained ObjectにPelvisを、Source Objectに骨盤のボーン(この場合はpelvis)を指定します。

2019 11 22 20 45 41

これで骨盤のボーンにリグのPelvisが追従するようになります。

Pelvisの下にGameObjectを追加してUpperLeg_pivotと名付けます。

2019 11 22 20 48 21

UpperLeg_pivotを選択し、Ctrlキーを押しながら左の太もものボーンを選択します。 選択する順番も重要です。

2019 11 22 20 49 21

Align Transformします。

2019 11 22 20 49 34

UpperLegpivotの下にGameObjectを追加してUpperLegCNTRLと名付けます。

2019 11 22 20 51 02

UpperLegCNTRLの下にGameObjectを追加してLowerLegpivotと名付けます。

2019 11 22 20 52 47

LowerLeg_pivotを選択し、Ctrlキーを押しながらふくらはぎのボーンを選択し、Align Transformします。

2019 11 22 20 54 01

2019 11 22 20 54 14

LowerLegpivotの下にGameObjectを追加してLowerLegCNTRLと名付けます。

2019 11 22 20 55 00

LowerLegCNTRLの下にGameObjectを作成しFootFKpivotと名付けます。

2019 11 22 20 56 12

足のボーンとAlign Transformします。

2019 11 22 20 56 38

2019 11 22 20 56 53

FootFKpivotの下にGameObjectを作成しFootFKCNTRLと名付けます。

2019 11 22 20 57 37

FootFKCNTRLの下にGameObjectを追加してToeFKpivotと名付けます。

2019 11 22 20 59 06

つま先のボーンとAlign Transformします。

2019 11 22 20 59 29

2019 11 22 20 59 39

2019 11 22 20 59 53

ToeFKpivotの下にGameObjectを追加してToeFKCNTRLと名付けます。

2019 11 22 21 00 37

ここまででFK用の階層が作成できました。

次に、LeftLegの下にGameObjectを追加してLeftLegIKと名付けます。

2019 11 22 21 03 56

LeftLegIKの下にGameObjectを追加してPelvisと名付けます。

2019 11 22 21 04 47

骨盤のボーンとAlign Transformします。

2019 11 22 21 05 09

PelvisにMulti-Parent Constraintをつけます。 Constrained Objectに自身を、Source Objectとして骨盤のボーンを指定します。 これで骨盤のボーンにリグのPelvisが追従するようになります。

2019 11 22 21 06 50

Pelvisの下にGameObjectを追加してUpperLegと名付けます。 UpperLegを太もものボーンにAlign Transformします。

2019 11 22 21 07 46

UpperLegの下にGameObjectを追加してLowerLegと名付けます。 LowerLegをふくらはぎのボーンにAlign Transformします。

2019 11 22 21 08 59

LowerLegの下にGameObjectを追加してFootと名付けます。 Footを足のボーンにAlign Transformします。

2019 11 22 21 10 32

Footの下にGameObjectを追加してToeIKpivotと名付けます。 ToeIKpivotをつま先のボーンにAlign Transformします。

2019 11 22 21 11 46

ToeIKpivotの下にGameObjectを追加してToeIKCNTRLと名付けます。

2019 11 22 21 12 45

次にLeftLegIKの下にGameObjectを追加してFootControllerBase_CNTRLと名付けます。

2019 11 22 21 14 09

FootControllerBase_CNTRLを選択し、Ctrlキーを押しながら足のボーンを選択します。 選択する順番も重要です。

2019 11 22 21 15 08

「Animation Rigging > Align Position」を実行し位置を揃えます。

2019 11 22 21 15 19

2019 11 22 21 15 50

FootControllerBaseCNTRLの下にGameObjectを追加しCursorpivotと名付けます。

2019 11 22 21 17 47

適当に足のちょっと後ろに配置します。

2019 11 22 21 18 06

適当にScaleを小さくしておきます。

Cursorpivotの下にGameObjectを追加してCursorCNTRLと名付けます。

2019 11 22 21 18 54

FootControllerBase_CNTRLの下にGameObjectを追加しToeEndと名付けます。

2019 11 22 21 20 51

ToeEndを次のようにつま先の位置に配置します。

2019 11 22 21 21 27

2019 11 22 21 21 39

FootControllerBase_CNTRLの下にGameObjectを追加しHeelと名付けます。

2019 11 22 21 21 59

Heelを次のようにかかとの位置に配置します。

2019 11 22 21 22 18

2019 11 22 21 22 34

FootControllerBase_CNTRLの下にGameObjectを追加しLeftSideと名付けます。

2019 11 22 21 22 55

LeftSideを次のように足の左側の位置に配置します。

2019 11 22 21 23 15

2019 11 22 21 24 00

FootControllerBase_CNTRLの下にGameObjectを追加しRightSideと名付けます。

2019 11 22 21 24 22

RightSideを次のように足の右側の位置に配置します。

2019 11 22 21 24 49

2019 11 22 21 24 57

次にLeftLegIKの下にGameObjectを追加してIKTargetと名付けます。

2019 11 22 21 26 02

IKTargetを足のボーンにAlign Positionします。

2019 11 22 21 26 41

2019 11 22 21 26 49

LeftLegIKの下にGameObjectを追加してIKHint_CNTRLと名付けます。

2019 11 22 21 27 50

膝の前の方に配置します。

2019 11 22 21 28 24

次にLeftLegIKに作成したFootIKコンポーネントをつけます。

2019 11 22 21 29 34

Dataに次の図のように設定します。

2019 11 22 21 30 20

LeftLegIKにTwo Bone IK Constraintをつけて次のように設定します。

2019 11 22 21 31 30

Maintain Target OffsetはRotationに設定します。

2019 11 22 21 42 55

これでLeftLegIKの下のPelvisから下の階層がIKで動くようになりました。

FKの階層とIKの階層をブレンドするリグを作ります。

LeftLegの下にGameObjectを作成しIKFKSliderと名付けます。

2019 11 22 21 34 26

IKFKSliderの下にGameObjectを追加してSlider_pivotと名付けます。

2019 11 22 21 35 31

Slider_pivotを適当に左脚の近くに配置します。

2019 11 22 21 35 37

適当にScaleを小さく変更しておきます。

Sliderpivotの下にGameObjectを追加してSLiderCNTRLと名付けます。

2019 11 22 21 36 27

IKFKSliderの下にGameObjectを4つ追加してそれぞれUpperLeg、LowerLeg、Foot、Toeと名付けます。

2019 11 22 21 38 23

IKFKSliderの下のUpperLegに作成したRotationBlendSliderをつけます。

2019 11 22 21 39 06

DataのTargetには太もものボーンを、SourceAにはFKの階層のUpperLegCNTRLを、SourceBにはIKの階層のUpperLegを、SliderにはSliderCNTRLをそれぞれアタッチします。

2019 11 22 21 41 00

同様にしてLowerLeg、Foot、ToeについてもIKとFKをブレンドするよう設定します。


Effector用のMeshをいくつかBlenderで作成しました。

2019 11 22 21 51 44

2019 11 22 21 51 55

2019 11 22 21 52 06

2019 11 22 21 55 18

前回のスクリプトを利用してobjファイルからLine状のMeshにしておきます。

CNTRLオブジェクトにEffectorを設定していき、次のようになりました。

2019 11 22 22 30 04

青がIKで赤がFKにしてあります。

動かしてみる

IKモードのカーソルを使って足をぐりぐりするのがよい感じです。

おわりに

足のリグの作成でした。 手順で書くと長くなりましたがやってることは大した作業量じゃないので、そんな時間かからずに構築できると思います。

今回のプロジェクトは次のリポジトリにまとめてあります。

https://github.com/MatchaChoco010/UnityAnimationRiggingLegRig