プログラミング
逆運動学とフットロッキング
Inverse Kinematics and Foot Locking (theorangeduck.com)
要約
この記事は、アニメーションにおける逆運動学(IK)とフットロッキングの技術について解説しています。フットスライディングはアニメーションの品質に大きく影響する問題であり、著者はこの問題を解決するためのいくつかのレシピを紹介しています。具体的には、足のチェーンを目標位置に配置する方法、ランタイムでのフットロッキング、アニメーションデータへのコンタクト注釈付け、オフラインでのフットスライディング除去といった手法が説明されています。
全文翻訳
プロジェクト . 出版物 . 文章 . 自己紹介 . アーカイブ . マイクロブログ . Github . RSS
0xf39c . 0x81a9 . 0x23ed . 0xd095 . 0x2ffe . 0xd19e . 0xc27f . 0x380d . 0x3ce6
逆運動学とフットロッキング
2026/07/30
私の最もリクエストの多いブログ記事の一つは、常に逆運動学とフットロッキングに関するものでした。これは驚くことではありません。フットスライディングは、ほぼすべての С анимация システムに影響を与える問題であり、適切なフットロッキングと逆運動学は、正しく実装するのが難しいものの、結果として得られるアニメーションの視覚品質に大きなプラスの影響を与えることができます。しかし、アニメーションプログラマーに二人話を聞けば、フットスライディングを解決する最善の方法について、それぞれ全く異なる答えを聞くことになるでしょう。間違いなく、このトピックは科学というより芸術であり、それがリソースを見つけるのが常に容易ではない理由かもしれませんし、学術的な分野でなぜそれほど注目されないのかを説明しているのかもしれません。
そうは言っても、フットスライディングを解決するための小さなレシピのセレクションを共有したいと思います。これらがこの問題に対する証明された最良の解決策ではないことは確かですが(そして、おそらくそれよりも優れた方法で実装しているゲームも数多くあるでしょう)、長年にわたって私には役立ってきましたし、このトピックに入門する人にとっては良い出発点となるはずです。
消化しやすくするために、トピックをいくつかのパートに分けました。まず、つま先を目標位置に配置するための足のチェーンを解決する方法を説明します。次に、ランタイムでコンタクト中に慣性化を使用してつま先の位置をロックするソリューションをカバーします。3番目に、アニメーションデータでコンタクトを自動的に注釈付けするテクニックです。4番目に、アニメーション全体とサイクルに余裕がある場合のフットスライディング除去のためのオフラインメソッドです。そして最後に、この全体についていくつかの哲学的な考察で締めくくります。
記事全体でコードを提供しますが、この記事の完全なソースコードはここにあります。
足のチェーンを解決する
フットロッキング
コンタクトタイム
オフラインフットロッキング
結論
足のチェーンを解決する
概要
問題の設定は以下の通りです。キャラクターの脚のジョイントチェーンがあり、特定の構成でポーズされています。既存のポーズを可能な限り維持しつつ、つま先が目標位置に来るようにローカル回転を調整したいと考えています。
これを行うために、以下の手順を実行します。
目標つま先位置から目標かかと位置を計算する
2つのジョイントのIK問題を解いて、かかとを目標位置に配置する
つま先が目標方向を向くように、かかとジョイントを回転させる
地面との衝突を解決するために、オプションでつま先エンドを回転させる
1. かかとターゲットの検索
目標かかと位置の計算は簡単です。既存の入力ポーズにおけるつま先からかかとへのベクトルを計算し、それを目標つま先に追加するだけです。
*targetHeel = Vector3Add(*targetToe, Vector3Subtract( globalTransforms[heelBoneIndex].translation, globalTransforms[toeBoneIndex].translation));
これは次のように見えます。
2. かかとターゲットの解決
かかとターゲットが得られたら、脚のチェーンを正しく配置するように、股関節と膝関節のローカル回転を解決できます。これを行うために、以前共有した2ボーン逆運動学コードのわずかに変更されたバージョンを使用します(QuaternionFromScaledAngleAxis関数の導出については、このページを参照してください)。
```c
static inline Quaternion QuaternionExp(Vector3 v) {
float halfangle = sqrtf(v.x*v.x + v.y*v.y + v.z*v.z);
if (halfangle < 1e-4f) {
return QuaternionNormalize((Quaternion){ v.x, v.y, v.z, 1.0f });
} else {
float c = cosf(halfangle);
float s = sinf(halfangle) / halfangle;
return (Quaternion){ s * v.x, s * v.y, s * v.z, c };
}
}
static inline Quaternion QuaternionFromScaledAngleAxis(Vector3 v) {
return QuaternionExp(Vector3Scale(v, 0.5f));
}
static inline void TwoBoneInverseKinematics( Quaternion *localHip, Quaternion *localKnee, Transform globalPelvis, Transform globalHip, Transform globalKnee, Transform globalHeel, Vector3 targetHeel, Vector3 sideVector, float maxExtension, float softening) {
// maxExtension によって与えられた距離に基づいて、ターゲットをソフトにクランプします
Vector3 targetClamp = targetHeel;
float targetLength = Vector3Distance(targetHeel, globalHip.translation);
if (targetLength > maxExtension - softening) {
// maxExtension から softening 距離内に入ったときにスムーズにクランプします
float saturation = 1.0f - expf( -Max(targetLength - maxExtension + softening, 0.0f) / softening);
targetClamp = Vector3Add(
globalHip.translation,
Vector3Scale(Vector3Subtract(targetHeel, globalHip.translation), (maxExtension - softening + softening * saturation) / targetLength)
);
}
// 回転軸を、提供された膝のサイドベクトルに最も近い平面に垂直なベクトルに基づいて計算します
Vector3 axisDwn = Vector3Normalize( Vector3Subtract(globalHeel.translation, globalHip.translation));
Vector3 axisFwd = Vector3Normalize(Vector3CrossProduct(axisDwn, sideVector));
Vector3 axisRot = Vector3Normalize(Vector3CrossProduct(axisDwn, axisFwd));
Vector3 a = globalHip.translation;
Vector3 b = globalKnee.translation;
Vector3 c = globalHeel.translation;
Vector3 t = targetClamp;
// コサインルールを使用して、2ボーンIKを解決するために必要な回転角度の変更を計算します
float lab = Vector3Distance(b, a);
float lcb = Vector3Distance(b, c);
float lat = Vector3Distance(t, a);
float lca = Vector3Distance(a, c);
float acab0 = acosf(Clamp(Vector3DotProduct( Vector3Scale(Vector3Subtract(c, a), 1.0f / lca), Vector3Scale(Vector3Subtract(b, a), 1.0f / lab)), -1.0f, +1.0f));
float babc0 = acosf(Clamp(Vector3DotProduct( Vector3Scale(Vector3Subtract(a, b), 1.0f / lab), Vector3Scale(Vector3Subtract(c, b), 1.0f / lcb)), -1.0f, +1.0f));
float acab1 = acosf(Clamp( (lab * lab + lat * lat - lcb * lcb) / (2.0 * lab * lat), -1.0f, +1.0f));
float babc1 = acosf(Clamp( (lab * lab + lcb * lcb - lat * lat) / (2.0 * lab * lcb), -1.0f, +1.0f));
// 2ボーンIKを解決するために必要な3つのワールドスペース回転を計算します
Quaternion r0 = QuaternionFromScaledAngleAxis(Vector3Scale(axisRot, acab1 - acab0));
Quaternion r1 = QuaternionFromScaledAngleAxis(Vector3Scale(axisRot, babc1 - babc0));
Quaternion r2 = QuaternionNormalize(QuaternionBetween( Vector3Subtract(globalHeel.translation, globalHip.translation), Vector3Subtract(targetClamp, globalHip.translation)));
// 股関節と膝関節のローカルスペース回転を更新します
*localHip = QuaternionMultiply(QuaternionMultiply(QuaternionMultiply( QuaternionInvert(globalPelvis.rotation), r2), r0), globalHip.rotation);
*localKnee = QuaternionMultiply(QuaternionMultiply( QuaternionInvert(globalHip.rotation), r1), globalKnee.rotation);
}
```
ここでの2つの追加点はシンプルです。まず、ユーザーが最大伸長長 `maxExtension` を提供できるようにし、それを `softening` 距離に基づいてソフトにクランプします。これは、四肢が提供された `maxExtension` に指数関数的に近づくだけであることを意味し、過度の伸長を防ぐのに役立ちます。
```c
Vector3 targetClamp = targetHeel;
float targetLength = Vector3Distance(targetHeel, globalHip.translation);
if (targetLength > maxExtension - softening) {
// maxExtension から softening 距離内に入ったときにスムーズにクランプします
float saturation = 1.0f - expf( -Max(targetLength - maxExtension + softening, 0.0f) / softening);
targetClamp = Vector3Add(
globalHip.translation,
Vector3Scale(Vector3Subtract(targetHeel, globalHip.translation), (maxExtension - softening + softening * saturation) / targetLength)
);
}
```
次に、膝関節のサイドベクトルを使用して、回転が安定する軸を見つけます。これにより、ポールベクターなどを必要としなくなります。
```c
Vector3 axisDwn = Vector3Normalize( Vector3Subtract(globalHeel.translation, globalHip.translation));
Vector3 axisFwd = Vector3Normalize(Vector3CrossProduct(axisDwn, sideVector));
Vector3 axisRot = Vector3Normalize(Vector3CrossProduct(axisDwn, axisFwd));
```
ジョイントのローカルおよびグローバルトランスフォームがすべて入力された2つのバッファ `localTransforms` と `globalTransforms` があると仮定すると、関数は次のように呼び出すことができます。
// 2ボーンIKを解決する