11
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

QualiArtsAdvent Calendar 2024

Day 10

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

Last updated at Posted at 2024-12-09

QualiArts Advent Calendar 2024 の10日目の記事になります。

はじめに

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

補助骨とは何かの説明や、Maya標準ノードを使用した手首/肘補助骨の一例を以下の記事に記載しています。
Mayaによる補助骨作成の基本

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

AnimationC#Jobsとは

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

本記事で実現したいこと

肩と前腕に鎧のような硬い装飾がついているモデルを想定します。

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

バインドポーズ 稼働
肩鎧モデル 肩鎧稼働
前腕鎧モデル 前腕鎧稼働

計算

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

回転の曲げ、捻り分解

基礎骨の回転を、曲げと捻りの2つに分解します。

曲げの取得

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

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

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

捻りの取得

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

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

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

任意の曲げ重みをかける/任意の捻り重みをかける

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

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

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

回転合成

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

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

\displaylines{
resultRotation = weightedRoll * weightedBend; \\
or \\
resultRotation = weightedBend * weightedRoll;
}

実装

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

コンポーネント実装

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コンポーネントを付与します。
これにより、各リグコンポーネントが動作します。

リグルート設定 リグコンポーネント設定
肩鎧駆動 前腕鎧駆動

結果

肩鎧、前腕鎧にそれぞれ設定して動かします。
よさそうですね、軸ごとにウェイトが異なりつつ、変なフリップもなさそうです。

肩鎧駆動 前腕鎧駆動
肩鎧駆動 前腕鎧駆動

まとめ

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

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

また、より発展的な内容を弊社技術ブログに記載しています。
ご興味ある方はぜひご覧ください。

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

おまけ

Mayaでの実現方法を、以下の記事で公開しました。
MayaのBifrostを使用した曲げ捻り補助骨の実装例

11
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?