【Unity×MediaPipe】脚や前腕が180°ねじれる原因|Cross積の符号と前方基準
投稿日: 2026年6月23日(2026年10月5日更新)
対象読者: MediaPipe の推定結果を Unity のボーン回転に反映していて、特定のポーズで関節がねじれるエンジニア
不具合内容
MediaPipe で推定したポーズを Unity のボーンの回転に反映すると、特定のポーズだけ、骨が長軸まわりに 180° 近くねじれました。PoseMirror(写真から3Dデッサン人形のポーズを作るアプリ)で、2026年5月から7月にかけて次の症状を直しています。
- 深くしゃがむ、床に座る、脚を蹴り上げるポーズで、太ももが最大 180° ねじれる
- 左の上腕だけが、約 180° 回った状態で付く
- 腕を伸ばしたポーズで、前腕が約 160° 回り、肘の境目でねじれる
原因
- 骨の「上方向」を、外積や射影で作っていた。外積の向きは、2本のベクトルの位置関係や、左右の鏡写しで反転する
- 反転が起きる角度(膝の屈曲 90° 超、股関節の屈曲 90° 超など)を境に、骨の上方向が裏返っていた
- 手のひらの面を作る3点(手首・小指・人差し指)が一直線に近いと、外積の向きが定まらなかった
解決方法
- 太ももとすねの上方向を、どちらも膝の曲げ平面から決める(膝は蝶番の関節なので、ねじれが0になる)
- 膝が伸びていて曲げ平面が無いときは、直立の姿勢から今の脚の向きまで回した「体の前方」を使う
- 左腕では外積の符号を反転し、鏡写しの関係に合わせる
- 手のひらの面が不安定なときは使わず、肘の曲げ平面から引き継ぐ
- 方向とねじれを数値で判定する回帰テストを用意し、ポーズを足して再発を防ぐ
動作環境
| 項目 | バージョン |
|---|---|
| Unity | 6000.0.58f2 |
| MediaPipe | Tasks Vision 0.10.0 (pose_landmarker_heavy) |
| 対象のボーン | 大腿・下腿・上腕・前腕 |
解決方法の詳細:骨の上方向の決め方
MediaPipe は33個の関節の位置を返しますが、骨の向きやねじれは返しません。骨の向き(肩→肘など)は2点から決まりますが、骨がその向きを軸にどれだけ回っているかは、別に「上方向」を決めないと定まりません。PoseMirror では、骨ごとに上方向を計算して Quaternion.LookRotation(向き, 上方向) で回しています。ねじれの不具合は、すべてこの上方向の作り方から起きていました。
手順1:太ももとすねの上方向を、同じ膝の曲げ平面から決める
膝は、ほぼ1方向にしか曲がらない蝶番の関節です。すねのねじれは、太ももに従います。そこで、太ももとすねの上方向(膝頭の側)を、どちらも膝の曲げ平面から求めます。
// LegPoseCalculator.cs(抜粋)
Vector3 kneeBend = Vector3.ProjectOnPlane(lowerDir, upperDir); // すねが太ももの軸から逸れた向き
Vector3 thighUp = Vector3.ProjectOnPlane(-kneeBend, upperDir); // 太もも:その逆=膝頭の側
Vector3 shinUp = Vector3.ProjectOnPlane(upperDir, lowerDir); // すね:同じ平面で膝頭の側
2つの上方向が同じ平面から出るので、膝の関節でのねじれは0になります。
太ももの上方向をすねの軸に射影して引き継ぐ方法もありますが、引き継いだ値は「膝の屈曲角の cos × 膝平面の基準」になります。屈曲が 90° を超えると cos が負になり、符号が反転するので使いません。
手順2:深く折った膝でも、曲げ平面を使い続ける
膝の曲げ平面を使うかどうかは、「すねが太ももの軸からどれだけ逸れているか」で判定しています。逸れが小さいと平面が定まらないからです。
ところが、この値(屈曲角の sin の2乗)は、膝が伸びているときだけでなく、深く折りたたんだときにも小さくなります。判定を左右対称にしていた頃は、屈曲 147° を超える深いしゃがみが「膝が伸びている」扱いになり、太ももが最大 180° ねじれていました。
深く折りたたんだ膝の曲げ平面は、むしろ安定しています。そこで、屈曲 90° を超えるときは、平面が完全につぶれる屈曲 174° 以上だけを除外するようにしました。
手順3:膝が伸びているときは、直立から運んだ「体の前方」を使う
膝がほぼ伸びていると、曲げ平面が定まりません。このときは体の前方を使います。
以前は、体の前方をそのまま脚の軸に射影していました。この方法では、股関節を 90° 以上曲げて脚を前に振り上げると、膝頭が真下を向き、太ももが約 160° ねじれました(脚を 130° 蹴り上げた写真で発生)。
今は、直立の姿勢(脚が真下)から今の脚の向きまで、最短の回転で体の前方を運んでいます。膝の曲げ平面が使える212本の脚を正解にして比べると、股関節の屈曲 90〜120° での誤差の中央値は、射影の 83° から 15° に減りました。誤差が 90° を超える破綻も3本から1本になっています。
手順4:左腕では外積の符号を反転する
PoseMirror の人形は、左右の上腕と前腕で、骨の前方向が同じ向き(下向き)に作られています。一方、外積で作った上方向は、鏡に映すと符号が反転します。左右で同じ式を使うと、左の上腕だけが元の向きから約 180° 回った状態で付きました。
// ArmPoseCalculator.cs(抜粋)
float sideSign = isRight ? 1f : -1f;
Vector3 bendPlaneUp = Vector3.Cross(upper, lower) * sideSign; // 左腕は符号を反転
左右対称のポーズで、左の上腕のねじれは 180° から 0° になりました。手のひらの面を作る外積も、左右で引数の順序を入れ替えています。
Vector3 rawHandUp = isRight
? Vector3.Cross(pPinky - pWrist, pIndex - pWrist)
: Vector3.Cross(pIndex - pWrist, pPinky - pWrist);
手順5:手のひらの面が不安定なときは使わない
前腕の上方向は、手のひらの面(手首・小指・人差し指の3点)から決めています。ところが、腕を伸ばしたポーズでは、小指と人差し指の点が手首から見てほぼ同じ方向に並ぶことがあります。このとき外積の向きが定まらず、前腕が約 160° 回りました。
そこで、次のどれかに当てはまるときは手のひらの面を捨て、肘の曲げ平面から上方向を引き継ぐようにしました。
- 2本の指へのベクトルのなす角の sin が 0.30 未満(実測で、破綻した例は 0.229、正常に物を握る手は 0.387)
- 指へのベクトルが、前腕の長さに比べて短すぎる
- 3点の信頼度(visibility)が低い(ただし、PoseMirror が今読み込んでいる tasks-vision 0.10.0 は visibility を出力しないので、この条件はまだ働いていません)
確認1:方向とねじれを数値で判定する回帰テスト
このねじれは、目で見て気づきにくい形で出ることがあります。そこで、Unity の PlayMode テストで、サンプルのポーズごとに骨の向きとねじれの角度を計算し、閾値を超えたら失敗にしています。
| 検証項目 | 計算 | 閾値 |
|---|---|---|
| 方向 | 骨の up と、ランドマークから求めた骨の向きのなす角 |
15° |
| ねじれ | 骨の forward と、膝頭の向き(期待値)のなす角 |
30° |
最初は4つのポーズ(T ポーズ、ヨガ、床に座るポーズなど)で、合わせて0.1秒ほどで終わるテストでした。不具合を直すたびに、その原因になった写真のポーズを追加しています。
原因詳細:外積の向きは位置関係と鏡写しで反転する
再現1:T ポーズで左脚が 168° ねじれる
膝の向きを太ももとすねの外積で作っていた頃、T ポーズで左脚が 168° ねじれていました。外積 Cross(a, b) は、a と b を入れ替えると符号が反転します。左右の脚は鏡写しの関係なので、同じ式のままだと片方だけ向きが逆になりえます。体の右方向を基準に符号をそろえて直しました。
再現2:床に座るポーズで、太ももが 164° 裏返る
体の前方を太ももの軸に射影して上方向を作っていた頃、座る、後ろに倒れる、床に寝るといった、太ももが前上方を向くポーズで射影が裏返りました。写真から作ったポーズの1つでは、太ももが 164° 回っていました。
なぜ「特定のポーズだけ」なのか
外積も射影も、ある角度(屈曲 90° など)を境に符号が反転します。立ちポーズや歩くポーズはその境を越えないので、正しく付きます。深く曲げる、振り上げる、左右を鏡写しにする、といった条件がそろったときだけ裏返るので、「全部おかしい」ではなく「特定のポーズだけおかしい」という症状になります。この症状が出たら、上方向を作る式のどこで符号が反転しうるかを探すのが近道でした。
まとめ
骨が 180° 近くねじれるのは、上方向を作る外積や射影の符号が、ある角度や左右の鏡写しを境に反転しているサインです。PoseMirror では、膝は曲げ平面から、伸びた脚は直立から運んだ前方から、腕は左右で符号をそろえた外積から上方向を決め、不安定なときは隣の関節から引き継ぐ形に落ち着きました。どれも、壊れたポーズを回帰テストに足しながら見つけた直し方です。
ボーン回転の基礎(ローカル軸まわりのひねり)はローカル座標で関節を回転する実装、MediaPipe の受け渡しはMediaPipeで3Dポーズを動かすで扱っています。これらを組み込んだ実物は PoseMirror で触れます。