【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つのベクトルの位置関係で反転します。脚をまっすぐ前に出すか、深く折り畳むかで upperDirlowerDir の関係が変わり、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 で触れます。