UnityのAnimationC#Jobsを使用した曲げ捻り補助骨の実装例

[QualiArts Advent Calendar 2024](https://qiita.com/advent-calendar/2024/qualiarts) の10日目の記事になります。

# はじめに
UnityのAnimationC#Jobsを使用した補助骨作成を行います。

補助骨とは何かの説明や、Maya標準ノードを使用した手首/肘補助骨の一例を以下の記事に記載しています。
[Mayaによる補助骨作成の基本](https://qiita.com/salt-k2t/items/e253725214d771b9c579)

前回に引き続き、本記事はテクニカルアーティストやリガーを対象とした初学者向けの内容となっています。
作例もシステマチックな内容になっており、見た目の良さを追求したものでは無い点をご了承ください。

# AnimationC#Jobsとは
AnimationStreamにアクセスして、任意の処理を実行するためのシステムです。
UpdateやLateUpdateなど他のゲームロジックと別の更新タイミングであるため、いかなるゲーム性であっても更新処理が干渉せず、ゲームエンジニアから見た扱い易さを担保してくれます。

```mermaid
block-beta

columns 4
Update space ゲームロジックなど

Update --> ゲームロジックなど

space:5

Update --> ProcessAnimation space

ProcessAnimation --> AnimationC#Jobs...補助骨など

space:5

ProcessAnimation --> LateUpdate space

LateUpdate --> カメラ操作など

```

# 本記事で実現したいこと
肩と前腕に鎧のような硬い装飾がついているモデルを想定します。

肩鎧はY軸とZ軸の曲げの動きには完全追従しますが、X軸の捻りの動きにはゆるく追従させたいです。
前腕鎧はX軸の捻りの動きにはゆるく追従しますが、Y軸とZ軸の曲げの動きには全く追従させません。

| バインドポーズ  | 稼働 |
| --- | --- |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878081/fda4b27e-0982-bd97-4d7d-acab7490c0fc.png" width="512" alt="肩鎧モデル"> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878081/e0f8c16a-ca73-cb89-5445-6901bc01a5d7.png" width="512" alt="肩鎧稼働"> |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878081/0a14b667-b73b-0261-f87a-23a923e8963a.png" width="512" alt="前腕鎧モデル"> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878081/fba9ac04-16c0-6b7f-bff7-068c42e6d69a.png" width="512" alt="前腕鎧稼働"> |

# 計算
今回の補助骨で実現したい計算は以下の流れになります。

```mermaid
flowchart TB
    A(基礎骨の回転) -->
    B[回転の曲げ,捻り分解] -- 曲げ回転 --> C[任意の曲げ重みをかける]
    B[回転の曲げ,捻り分解] -- 捻り回転 --> D[任意の捻り重みをかける]
    C[任意の曲げ重みをかける] -- 重み付き曲げ回転 --> E[回転合成]
    D[任意の捻り重みをかける] -- 重み付き捻り回転 --> E[回転合成]
    E[回転合成] --> G(補助骨の回転)
```

## 回転の曲げ、捻り分解
基礎骨の回転を、曲げと捻りの2つに分解します。
### 曲げの取得
基準となるベクトルを用意します。
基準ベクトルを基礎骨の回転角度で回転します。
基準ベクトルを回転後ベクトルに向ける回転を得ます。
ここで得た回転角度は2つのベクトルのなす角にあたり、元の基礎骨の回転から捻り成分を除いた曲げ回転になります。

いわゆるエイムを実装であり、別の計算方法でも構いません。
以下にプログラムで実装する場合の例を記載します。

```math
\displaylines{
float3 \ jointAxis = new float3(1, 0, 0); \\
float3 \ rotWay = math.mul(localRotation, jointAxis); \\
Quaternion \ bendRotation = Quaternion.FromToRotation(jointAxis, rotWay)
}
```

### 捻りの取得
基礎骨の回転と、前項で取得した曲げ回転を使用します。
捻り回転は、基礎骨の回転から曲げ回転を除いたものになります。

以下にプログラムで実装する場合の例を記載します。

```math
\displaylines{
Quaternion \ rollRotation = math.inverse(bendRotation) * localRotation;
}
```

## 任意の曲げ重みをかける/任意の捻り重みをかける
対象の回転を軸ベクトルとその軸周りの角度として得ます。
その軸周りの角度に重みをかけます。
軸ベクトルと重みを考慮した軸周りの角度から、重みを考慮した回転を得ます。

以下にプログラムで実装する場合の例を記載します。

```math
\displaylines{
bendRotation.ToAngleAxis(out \ float \ angle, out \ Vector3 \ axis); \\
Quaternion \  weightedBend = Quaternion.AngleAxis(angle * weight, axis);
}
```

## 回転合成
重みをかけた回転を合成します。
シンプルにQuaternionを合成するだけですが、合成する順番によって結果が異なります。
詳細は割愛しますが、両方試して見た目が良いほうを使用すればとりあえずOKです。

以下にプログラムで実装する場合の例を記載します。
```math
\displaylines{
resultRotation = weightedRoll * weightedBend; \\
or \\
resultRotation = weightedBend * weightedRoll;
}
```

# 実装
これまで上げた計算を、AnimationC#Jobsで実装します。
算術計算に関わる部分とは別に、Constraintとして扱うための形式的な部分も必要になります。
それらのプログラムをまとめて記載します。
実際のプロジェクトに取り入れる際には高速化/依存性解決/特殊な姿勢への対応なども行うのが望ましいですが、簡単のために省略させていただきます。

## コンポーネント実装
```csharp
using Unity.Burst;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Animations.Rigging;

public class HelperConstraintBendRoll :
    RigConstraint<
        HelperConstraintBendRollJob,
        HelperConstraintBendRollData,
        HelperConstraintBendRollBinder
    >
{
}

[BurstCompile]
public struct HelperConstraintBendRollJob : IWeightedAnimationJob
{
    public ReadWriteTransformHandle Source;
    public ReadWriteTransformHandle Target;

    public IntProperty ComposeType; // 同期のためにIndexとして使用する
    public FloatProperty BendWeight;
    public FloatProperty RollWeight;
    public Vector3Property JointAxis;

    public FloatProperty jobWeight { get; set; }

    public void ProcessRootMotion(AnimationStream stream)
    {
    }

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

        var localRotation = Source.GetLocalRotation(stream);
        BendRollComposeType composeType = (BendRollComposeType)ComposeType.Get(stream);
        float bendWeight = BendWeight.Get(stream);
        float rollWeight = RollWeight.Get(stream);
        Vector3 jointAxis = JointAxis.Get(stream);

        // Bend
        float3 rotWay = math.mul(localRotation, jointAxis);
        var bendRotation = Quaternion.FromToRotation(jointAxis, rotWay);

        // Roll
        Quaternion rollRotation = math.inverse(bendRotation) * localRotation;

        // BendWeight
        bendRotation.ToAngleAxis(out float bendAngle, out Vector3 bendAxis);
        Quaternion weightedBend = Quaternion.AngleAxis(bendAngle * bendWeight, bendAxis);

        // RollWeight
        rollRotation.ToAngleAxis(out float rollAngle, out Vector3 rollAxis);
        Quaternion weightedRoll = Quaternion.AngleAxis(rollAngle * rollWeight, rollAxis);

        Quaternion resultRotation;
        switch (composeType)
        {
            case BendRollComposeType.BendRoll:
                resultRotation = weightedRoll * weightedBend;
                break;
            case BendRollComposeType.RollBend:
                resultRotation = weightedBend * weightedRoll;
                break;
            default:
                throw new System.NotImplementedException();
        }

        Target.SetLocalRotation(stream, resultRotation);
    }
}

[System.Serializable]
public struct HelperConstraintBendRollData : IAnimationJobData
{
    [SyncSceneToStream]
    public Transform source;

    [SyncSceneToStream]
    public Transform target;

    [SyncSceneToStream]
    public int composeType; // 同期のためにIndexとして使用する

    [SyncSceneToStream]
    public float bendWeight;

    [SyncSceneToStream]
    public float rollWeight;

    [SyncSceneToStream]
    public Vector3 jointAxis;

    public bool IsValid() => !(source == null || target == null);

    public void SetDefaultValues()
    {
        source = null;
        target = null;

        composeType = (int)BendRollComposeType.BendRoll;
        bendWeight = default;
        rollWeight = default;
    }
}

public class HelperConstraintBendRollBinder : AnimationJobBinder<
    HelperConstraintBendRollJob, HelperConstraintBendRollData>
{
    public override HelperConstraintBendRollJob Create(Animator animator,
        ref HelperConstraintBendRollData data, Component component)
    {
        var job = new HelperConstraintBendRollJob();
        job.Source = ReadWriteTransformHandle.Bind(animator, data.source);
        job.Target = ReadWriteTransformHandle.Bind(animator, data.target);

        job.ComposeType = IntProperty.Bind(animator, component, "m_Data." + nameof(data.composeType));
        job.BendWeight = FloatProperty.Bind(animator, component, "m_Data." + nameof(data.bendWeight));
        job.RollWeight = FloatProperty.Bind(animator, component, "m_Data." + nameof(data.rollWeight));
        job.JointAxis = Vector3Property.Bind(animator, component, "m_Data." + nameof(data.jointAxis));

        return job;
    }

    public override void Destroy(HelperConstraintBendRollJob job)
    {
    }
}


public enum BendRollComposeType
{
    BendRoll = 0,
    RollBend = 1,
}
```

## コンポーネント設定
キャラクタルートなどの上位階層に、RigコンポーネントとRigBuilderコンポーネントを付与します。
キャラクタの各ジョイントに、今回作成したHelperConstraintBendRollコンポーネントを付与します。
これにより、各リグコンポーネントが動作します。

| リグルート設定 | リグコンポーネント設定 |
| --- | --- |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878081/d9ad0469-446b-d6bd-6370-927666496a82.png" width="512" alt="肩鎧駆動"> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878081/0f3808d8-b4da-f408-2dc5-f2ffccf1e993.png" width="512" alt="前腕鎧駆動"> |

# 結果
肩鎧、前腕鎧にそれぞれ設定して動かします。
よさそうですね、軸ごとにウェイトが異なりつつ、変なフリップもなさそうです。
| 肩鎧駆動 | 前腕鎧駆動 |
| --- | --- |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878081/16110e40-8535-3dab-323c-ca9d9d006ea2.gif" width="512" alt="肩鎧駆動"> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/878081/d5e22d34-f998-c250-0108-b53478ce845a.gif" width="512" alt="前腕鎧駆動"> |

# まとめ
本記事ではAnimationC#Jobsを使用した曲げ/捻り分解補助骨を作成しました。
UpdateやLateUpdateのような汎用的な使う更新タイミングとは異なるため、様々な環境でも扱いやすいと思います。

曲げ重みじゃなくて、縦曲げ横曲げそれぞれで分けたい...
可動域を制限したい...
もっと数式ベースでの解説が読みたい...
などの要望も出てきそうですが、それらはまた別の機会にとっておこうと思います。

また、より発展的な内容を[弊社技術ブログ](https://technote.qualiarts.jp/article/82/)に記載しています。
ご興味ある方はぜひご覧ください。

本記事が、3Dゲーム開発の助けになっていれば幸いです。

# おまけ
Mayaでの実現方法を、以下の記事で公開しました。
[MayaのBifrostを使用した曲げ捻り補助骨の実装例](https://qiita.com/salt-k2t/items/008500f7022f24ec7615)