【Unity×MediaPipe】脚や前腕が180°ねじれる原因|Cross積の符号と前方基準
投稿日: 2026年6月23日
対象読者: MediaPipeの推定結果をUnityのボーン回転に反映していて、特定ポーズで関節がねじれるエンジニア
この記事はこんな方向け:
- 写真からポーズを反映すると、特定のポーズだけ脚や前腕が180°裏返る方
- 見た目は「少しおかしい」程度で、どこがバグか特定できずに困っている方
- ボーン回転の正しさを毎回目視で確認していて、回帰を防ぎたい方
TL;DR
- 関節が180°ねじれる典型原因は、曲がり方向を出す
Cross積の符号が暴走すること - ワールド軸を基準にすると、外開きポーズ(ヨガ・正座など)で符号が反転する
- 基準を人体相対(
bodyRight = Cross(hipsForward, bodyUp))に統一すると安定する - T-poseで「ねじれ(twist)」が非ゼロなら即バグ。これを自動テストの判定軸にできる
はじめに
PoseMirror(写真からポーズを3D化する無料ツール)で一番粘り強く戦ったのが、このねじれバグです。多くのポーズは正しく反映されるのに、ある写真だけ両脚が180°逆を向く。前腕でも、左右のどちらか一方だけが裏返る。やっかいなのは、パッと見では「ちょっと不自然かな」くらいにしか見えず、明確に壊れて見えないことでした。原因はボーン回転を組み立てるときの Cross(外積)の符号にありました。
症状:特定ポーズだけ関節が裏返る
MediaPipeは33個のランドマーク(関節点)の位置を返してくれますが、関節の「向き」や「ねじれ」までは直接くれません。そこで、隣り合う関節ベクトルの外積から「曲がる方向」を作ってボーンの回転を決めます。
このとき、外積の結果が想定と逆を向くポーズがあると、その軸まわりに180°回ってしまいます。具体的には次のような症状でした。
- 立ちポーズや歩行は正常なのに、ヨガや正座のように脚を大きく折ると膝が外側に開いて裏返る
- 前腕では、右腕は正しいのに左腕だけがねじれる(またはその逆)
「全部おかしい」のではなく「特定ポーズ・片側だけ」というのが、符号バグ特有のサインでした。
原因①:脚の曲がり方向の符号がワールド基準で暴走する
膝の曲がる向き(kneeSideDir)は、大腿の方向ベクトルと下腿の方向ベクトルの外積で求めます。
// 膝の曲がり方向。外積の向きが「どちらに膝が出るか」を決める
Vector3 kneeSideDir = Vector3.Cross(upperDir, lowerDir);
外積の向きは2つのベクトルの位置関係で反転します。脚をまっすぐ前に出すか、深く折り畳むかで upperDir と lowerDir の関係が変わり、kneeSideDir の符号が裏返ることがあります。ここでワールドのX軸や characterRoot.right のような固定軸を基準に符号を補正していると、人体が向きを変えたり脚を畳んだ瞬間に基準とズレて、膝が外開き=ねじれとして現れます。
解決:符号の基準を人体相対にそろえる
固定軸ではなく、人体そのものの右方向を基準にして符号を統一します。人体の右は、体幹の前方向と上方向の外積で得られます。
// 人体の右方向(体幹基準)。これを符号の判定基準に使う
Vector3 bodyRight = Vector3.Cross(hipsForward, bodyUp);
// kneeSideDir が bodyRight と逆を向いていたら反転させて符号を統一
if (Vector3.Dot(kneeSideDir, bodyRight) < 0f)
kneeSideDir = -kneeSideDir;
// 脚がほぼ一直線で外積が定義できないときはフォールバック
if (kneeSideDir.sqrMagnitude < 0.0001f)
kneeSideDir = isRight ? fallbackRight : -fallbackRight;
基準を bodyRight に統一すると、ヨガ・正座位のような非T-poseでも膝が外開きせず、向きが安定します。ポイントは「ワールド固定の軸ではなく、その人体の向きに追従する基準を使う」ことです。
原因②:左右で外積の引数順序が食い違う
前腕(ForeArm)のねじれは、もっと単純で見落としやすいミスでした。手の上方向(rawHandUp)を作る外積の引数順序が、左腕と右腕で食い違っていたのです。外積は引数を入れ替えると符号が反転するため、一方は正しく、もう一方だけが180°裏返るという非対称な症状になります。
// NG例:左右で引数順序が違うと、片側だけ符号が反転する
// 右: Cross(a, b)
// 左: Cross(b, a) ← これだけ裏返る
// OK:左右で順序を統一する
Vector3 rawHandUp = Vector3.Cross(handDir, fingerDir);
「左右どちらか一方だけおかしい」ときは、まず左右で計算式が鏡像になっていないか(引数順序が揃っているか)を疑うのが近道です。
再発防止:T-poseのtwistを自動テストの判定軸にする
このバグの最大の難点は「目視で見つけにくい」ことでした。そこで、見た目ではなく数値で機械的に判定できるようにしました。
基準にしたのが「ねじれ(twist)」の角度です。ボーンの前方向と、体幹前方を骨軸へ射影したベクトルの角度差で定義します。
twist = Angle(bone.forward, ProjectOnPlane(-hipsForward, boneAxis))
ここで効くのが「T-poseならtwistは0であるべき」という不変条件です。基準姿勢のT-poseで脚のtwistが非ゼロになっていれば、それは確実にこの符号バグです。PoseMirrorでは、複数のサンプルポーズ(T-pose・ヨガ・正座など)について、ボーンのdirection(延伸方向)とtwist(ねじれ)を数値で検証するPlayMode回帰テストを用意し、閾値を超えたら失敗するようにしました。
| 検証項目 | 計算 | 閾値 | 主な原因 |
|---|---|---|---|
| direction | Angle(bone.up, (childLm - parentLm).normalized) |
15° | Cross順序・軸ミスマッチ |
| twist | Angle(bone.forward, ProjectOnPlane(-hipsForward, boneAxis)) |
30° | kneeSideDir符号バグ・ねじれ反転 |
これで「特定ポーズだけ裏返る」回帰を、目視に頼らず数秒の自動テストで捕まえられるようになりました。
まとめ
- 関節が180°ねじれるのは、曲がり方向を出す外積の符号が反転しているサイン
- 符号の基準はワールド固定軸ではなく、人体相対(
bodyRight = Cross(hipsForward, bodyUp))に統一する - 「片側だけ」おかしいときは、左右で外積の引数順序が食い違っていないか確認する
- T-poseでtwistが非ゼロなら即バグ。これを自動テストの判定軸にすると回帰を機械的に防げる
ボーン回転の基礎(ローカル軸まわりのひねり実装)はローカル座標で関節を回転する実装、MediaPipeのランドマーク構造はMediaPipeの33ランドマーク解説で扱っています。これらを組み込んだ実物は写真からポーズを3D化する Pose Mirror で触れます。