ぼっちプログラマのメモ

UE4とかUE5とかについて書いたり書かなかったり。

揺れ骨用自作AnimNode「Kawaii Physics」の内部実装解説的なもの その1

はじめに

疑似物理プラグインであるKawaii Physicsを先日リリースしました。髪、スカート、胸などの揺れものを「かんたんに」「かわいく」揺らすことができます。
f:id:pafuhana1213:20190726145840j:plain
f:id:pafuhana1213:20190726144035g:plain
f:id:pafuhana1213:20190726144050g:plain
github.com

エンジン標準のPhysics AssetやAnim Dynamicsは高性能かつ多機能なのですが…慣れるまでは調整大変といった声を聞こえてきたので、自作AnimNodeの勉強も兼ねて作った感じです。趣味プロジェクトです(ここ大事)。

せっかくなので、AnimNodeの自作やKawaii Pyhscisの内部実装に関して単にですがまとめようと思います(未来の自分用メモという意味合いもあります)。
これで皆さん自由に拡張・不具合修正できますね!

なお、本記事ではKawaii Physicsの使い方に関する説明はしません。そちらに関しては、Githubで公開しているサンプルと簡易ドキュメントをご確認くださいまし…。

参考

www.unrealengine.com
qiita.com
qiita.com
あと、エンジンコード

各クラスに関して

まずは全体像を…ということでプラグインに含まれる各クラスについてざっくり説明します。

AnimNode_KawaiiPhysics

アニメーションの制御に関する実装(主に初期化、更新)は全てこのクラスで行っています。といっても、大部分は EvaluateSkeletalControl_AnyThread 関数で行われています。

実際にAnimNodeを自作したり、FAnimNode_AnimDynamics における実装を見ると分かるのですが…AnimNodeのInitialize_AnyThreadでは初期化に関する処理全てを行うのが難しいケースがあるためです。そのため、EvaluateSkeletalControl_AnyThread の初回実行時にボーン階層情報の収集・構築(InitModifyBones)を行ったりしています。

あとは、物理に関する処理をしているSimulateModfyBones関数、その結果をボーンに反映するApplySimuateResult関数が重要な働きを担っています。

AnimGraphNode_KawaiiPhysics

f:id:pafuhana1213:20190726153125p:plain
AnimGraphに配置されたノードのタイトルや一部デバッグ表示をここで行っています。
実際のランタイム上で使われる処理はここでは書かれていません。

KawaiiPhysicsEditMode

f:id:pafuhana1213:20190726153051g:plain
KawaiiPhysicsはPersonaのプレビュー画面でコリジョンの調整をできるようにしているのですが、その処理をここで行っています。また、左下にデバッグ表示している部分に関してもここで行っています。
AnimGraphNode_KawaiiPhysicsと同じく、実際のランタイム上で使われる処理はここでは書かれていません。

後述しますが、このEditModeに関する情報が世になく…そこそこ難産だった記憶があります。

KawaiiPhysicsEditModeBase

KawaiiPhysicsEditModeの基底クラスです。

FModifyBoneEditModeなどエンジン標準のAnimGraphはFAnimNodeEditModeクラスを継承したクラスでプレビュー画面における制御・デバッグ表示を行っているのですが…FAnimNodeEditModeが外部モジュールで使用することを想定していない作りになっていたので、丸ごとコピペしました。FAnimNodeEditModeの実装が今後大きく変化しない事を祈ります。


以降はそれぞれのクラスに関して深掘りしようと思います。ただ全部解説すると終わらないので、要点に絞って説明しようと思います。コードが仕様書です。

AnimNode_KawaiiPhysics

FAnimNode_SkeletalControlBaseについて

AnimNode_KawaiiPhysicsクラスはボーン制御に関する基本的な処理が用意されているFAnimNode_SkeletalControlBaseを継承しています。ボーン制御に関する流れは以下の通り。

  1. AnimNodeの入力ピンに指定したポーズをEvaluateSkeletalControl_AnyThreadのOutput.Poseから取得
  2. Output.Poseに含まれる各ボーンのTransformをコネコネ
  3. OutBoneTransformsに制御する骨と処理後のTransformを渡す

f:id:pafuhana1213:20190726154622p:plain

見ての通り扱いやすい作りになっているので、ボーンを制御するAnimNodeを実装したいときは、とりあえずFAnimNode_SkeletalControlBaseを継承しておけば問題ないのではないかと思います。

一つ注意すべきなのは、Output.PoseやOutBoneTransformsで扱うTransformの座標系が Component座標系なことです。そのため、ボーンの親子関係を考慮した制御などの処理を行いたい場合は各座標系に変換する必要があります。ただご安心ください。各座標系への変換用の関数が用意されています。

//  Component座標系から引数で指定した座標系への変換
FAnimationRuntime::ConvertCSTransformToBoneSpace
// 引数で指定した座標系からComponent座標系への変換
FAnimationRuntime::ConvertBoneSpaceTransformToCS

例えば、KawaiiPhysicsでは各ボーンに紐づくコリジョンに対してのオフセット計算をボーン座標系でする必要があったため、以下のようなコードを組んでいます。

FAnimationRuntime::ConvertCSTransformToBoneSpace(ComponentTransform, Output.Pose, BoneTransform, CompactPoseIndex, BCS_BoneSpace);
BoneTransform.SetRotation(Capsule.OffsetRotation.Quaternion() * BoneTransform.GetRotation());
BoneTransform.AddToTranslation(Capsule.OffsetLocation);
FAnimationRuntime::ConvertBoneSpaceTransformToCS(ComponentTransform, Output.Pose, BoneTransform, CompactPoseIndex, BCS_BoneSpace);

この辺りに関しては、Transform(Modify)Boneノードの実装を行っているFAnimNode_ModifyBoneクラスが非常に参考になります。
f:id:pafuhana1213:20190726155924p:plain

骨の階層構造の収集に関して

KawaiiPhysicsはRootBoneに指定したボーンとそれ以下の階層にある各ボーンを制御対象するようにしています。そして、その階層構造を収集中にExclude Bonesに指定したボーンに到達した場合はそれ以下は収集しない作りにしてます。
f:id:pafuhana1213:20190726163710g:plain

実際に収集しているのは CollectChildBones関数なのですが…ここで問題になるのが、各ボーンは親のボーンのインデックス情報しか持っていないことです。そのため、FReferenceSkeleton::GetDirectChildBonesの処理を参考、というか丸パクリしてます。

int32 FReferenceSkeleton::GetDirectChildBones(int32 ParentBoneIndex, TArray<int32> & Children) const
{
	Children.Reset();

	const int32 NumBones = GetNum();
	for (int32 ChildIndex = ParentBoneIndex + 1; ChildIndex < NumBones; ChildIndex++)
	{
		if (ParentBoneIndex == GetParentIndex(ChildIndex))
		{
			Children.Add(ChildIndex);
		}
	}
	return Children.Num();
}

全探索なので場合によっては結構重たい処理になるかと思います。しかも、現状AnimNode1個毎にこの処理が1回が走るので…(;´∀`)
ちゃんと実装するなら、AnimInstance側で収集して各AnimGraphはそれを参照する形がいいのかなと思います。が、Kawaii Physicsは手軽さも重視してるので先送りしてます。

物理処理について

SimulateModfyBonesで行ってます。で、Adjust~という名前の関数でコリジョンとの当たり判定や角度制限などを行っています。

コリジョン以外の物理アルゴリズムgithubに記載している通り、以下のCEDEC講演で解説されている内容をほぼそのまま使ってます。20行ぐらいの超シンプルな作りです。もし物理挙動をカスタマイズしたい場合はこの辺りを編集することになります。
cedil.cesa.or.jp

物理周りで特に他に説明することはないのですが…強いて言うなら、Adjust~関数にて使用している以下の関数が便利でした。FMathやFVectorには良い機能沢山あるので見るのおすすめです。

点Aから線分B上の最も近い点までの距離の2乗を取得。
FMath::PointDistToSegmentSquared | Unreal Engine
点Aから線分B上の最も近い点を取得
FMath::ClosestPointOnSegment | Unreal Engine
線分Aが平面Bと交差しているかを取得
FMath::SegmentPlaneIntersection | Unreal Engine
点Aを平面Bに投影した際の座標を取得
FVector::PointPlaneProject | Unreal Engine

STATコマンドで負荷を計測できるように

stat animで各負荷を確認したかったので、調査したい処理の部分に SCOPE_CYCLE_COUNTER を埋め込んでます。画像の通り、KawaiiPhysicsの処理が何回呼ばれて合計処理時間はどの程度かをエディタ・実機上で確認できます。
f:id:pafuhana1213:20190726170333p:plain

DECLARE_CYCLE_STAT, SCOPE_CYCLE_COUNTERを使うだけで簡単にstat対応できるのでおすすめです。実際、Kawaii Physicsの最適化においてとても役に立ちました。

実際にあった話。デフォルト設定の場合、カーブアセットが指定されていると毎回値を読みに行くのですが…その負荷が全体の約1/3を占めてました。

// Damiping
Bone.PhysicsSettings.Damping = PhysicsSettings.Damping;
if (TotalBoneLength > 0 && DampingCurve && DampingCurve->GetCurves().Num() > 0)
{
	Bone.PhysicsSettings.Damping *= DampingCurve->GetFloatValue(LengthRate);
}
Bone.PhysicsSettings.Damping = FMath::Clamp<float>(Bone.PhysicsSettings.Damping, 0.0f, 1.0f);

アセットにアクセスしに行くのである程度オーバーヘッドが発生した結果かと思います。とはいえ、動的に値を反映したいケースもあるかと思うので、フラグで動的更新するかをON・OFFできるようにしときました( Update Physics Settings in Game)。
f:id:pafuhana1213:20190726170825p:plain

こういった気付きがあるので、システムを自作する際はSTAT系のスコープを仕込んでおくことを強くおすすめします!


長くなってきたので一旦ここまで。続きはまた後日