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を使用した曲げ捻り補助骨の実装例