リターゲティング:ウェブカメラでVRMアバターを動かす — アバタートーク #3(MediaPipe → three.js)

動くと、アバターも動く。パート3はその集大成です。パート1でウェブカメラから取り出した骨格が、パート2のVRMアバターをついに動かします——ブラウザだけで、トラッキングスーツも深度カメラもなし。多くのチュートリアルが飛ばすところ、リターゲティング(ランドマークをボーンの回転へ)です。コードはretarget.tsにあり、GitHubで公開しています。(動画・コードの画面表示は英語です。)

分割画面:左のウェブカメラ映像(MediaPipeの姿勢ランドマーク付き)が、右の3D VRMアバターをリアルタイムに動かし、鏡のようにポーズを再現する。
パート3——ウェブカメラの姿勢ランドマーク(左)が、VRMアバター(右)をリアルタイムに動かす。ブラウザだけで、トラッキングスーツも深度カメラもなし。

落とし穴:位置ではなく回転

リターゲティングを見た目より難しくしている落とし穴があります。MediaPipeが返すのは位置です——「ここが肘、ここが手首」。しかし骨格は位置ではなく回転でできています。ボーンをある点へ移動させることはできません。回転させることしかできず、子ボーンはそれについてきます。だから、すべてのランドマークを角度に変換する必要があります。retarget.tsの全体は、実のところ1つの考えの繰り返しです——ボーンが向くべき方向を取り、そこへ回す回転を求める。

2つの座標系(と鏡映し)

2つの世界は食い違っています。MediaPipeはメートル単位で、Xは画像の右、Yは下、Zはカメラから遠ざかる方向。VRM 1.0はYが上、Zは手前(見ている側)が正面です。YとZの反転はX軸まわりの180°回転——ここを間違えると、アバターが上下逆さや裏返しになります。さらにXも反転すると、ポーズが鏡映しになります。右手を上げるとアバターも画面の同じ側の手を上げる。鏡なので、自分の右側のランドマークがアバターののボーンを動かす——意図的な入れ替えです。

// MediaPipe(x右・y下・z奥)→ VRM(y上・z手前)、
// さらにxを反転して鏡のように。zTrust は深度を縮める。
private toAvatarSpace(lm: Landmark, out: Vector3, zTrust = Z_TRUST): Vector3 {
  return out.set(-lm.x, -lm.y, -lm.z * zTrust);
}

// 鏡映し:人物の「右」肩がアバターの「左」になる
const avatarLeftShoulder  = this.toAvatarSpace(world[LM.rightShoulder], a);
const avatarRightShoulder = this.toAvatarSpace(world[LM.leftShoulder],  b);

基準の向き → 目標回転

いよいよ本題のリターゲティングです。どのボーンにも、正規化リグ上の基準の向きがあります——左腕は+X方向。ボーンが向くべき方向(肩→肘)を取り、基準を目標へ回す回転を求めます。three.jsなら setFromUnitVectors 1回で計算できます。この単一のプリミティブ——方向がクォータニオンになる——が、このファイルの心臓部です。

// 上腕:基準の向きを 肩→肘 ベクトルへ回す
c.subVectors(elbow, shoulder).normalize();
target.setFromUnitVectors(restDir, c);   // 方向 → クォータニオン、1行

ワールド空間 → ローカル:チェーンをたどる

この回転はワールド空間のものですが、ボーンの回転は親からの相対で保存されます。そこで、チェーンを上へたどり、そのボーンの上位の回転を掛け合わせ、それを割り戻します(逆回転で premultiply)。正規化ヒューマノイドリグでは、どのボーンも基準姿勢でワールドと軸が揃っているので、チェーンに沿ってローカル回転を掛けるだけで、行列を触らずにその関節のワールド回転が得られます。前腕が巧いところ:親に今設定した上腕が含まれるため、出てくるのは肘の曲がりそのもの——腕に対する相対値です。

// 関節のワールド回転 = その上位のローカル回転の積
private chainQuaternion(chain: VRMHumanBoneName[], out: Quaternion): Quaternion {
  out.identity();
  for (const name of chain) {
    const node = this.vrm?.humanoid.getNormalizedBoneNode(name);
    if (node) out.multiply(node.quaternion);
  }
  return out;
}

// ワールド → ローカル:親の回転を剥がす
this.chainQuaternion(CHAINS.leftArm, parent);
target.premultiply(parent.invert());
this.driveBone(upper, target, alpha);

イージングと復帰

生のランドマークは揺れ、揺れるアバターは壊れて見えます。そこでボーンは目標へ一気に飛ばず滑らかに近づけます——1 - exp(-rate·dt) のスムージングにより、30fpsでも144fpsでも同じ感触です。そして手が画面から出たら、その腕は空中で固まらず、アバターの基準姿勢へ自然に戻ります。

const FOLLOW_RATE = 14;   // 追跡中に目標を追う速さ
const RELEASE_RATE = 4;   // 追跡を失ったとき自然位置へ戻る速さ

// フレームレート非依存のスムージング:30/144fpsで同じ感触
private ease(rate: number, delta: number): number {
  return 1 - Math.exp(-rate * delta);
}

// 目標回転への球面補間として適用:
node.quaternion.slerp(target, this.ease(FOLLOW_RATE, delta));

深度の落とし穴(Zを信用しすぎない)

もう1つ——これで一晩溶かしました。両手を上げているのに、アバターが片腕を横へ投げ出し続けたのです。計算は正しかった。間違っていたのは入力でした。カメラ1台では奥行きを測れず、MediaPipeは推測します。左右対称のポーズなのに、片方の肘をもう片方の2倍も手前にあると返してきました。対策はZをXやYほど信用しないこと——Z_TRUST係数で縮めると、手足はカメラが実際に見たシルエットへ戻ります。そこがカメラの得意な部分です。

深度の落とし穴:両手をまっすぐ上げているのに、MediaPipeの推測した深度をそのまま信用すると、アバターが片腕を前へ振り出してしまう。
深度の落とし穴——両手を上げているのに、Zをそのまま信用すると片腕が前へ振り出される。Zを縮める(Z_TRUST)と、手足はカメラが実際に見たシルエットへ戻る。
/**
 * カメラ1台ではzを測れず、モデルは推測する——そしてそれは大きくずれる:
 * 左右対称のポーズで片方の肘をもう片方の2倍も手前と報告することがある。
 * zを縮めると、手足はカメラが実際に見たシルエットへ戻る。
 */
const Z_TRUST = 0.45;   // out.set(-lm.x, -lm.y, -lm.z * Z_TRUST)

1つ注意点:だけは深度をそのまま使います。耳→鼻のベクトルはほぼZ方向を向くので、ここでZを縮めると、頭の向きが安定するどころか、あらゆる首振りが誇張されてしまいます。深度が当てにならないのは、ポーズがカメラに対しておおむね平ら(正面)なときだけです。

よくある質問

なぜアバターのボーンをランドマークの位置へそのまま動かせないのですか?

骨格は自由に浮かぶ点の集まりではなく、回転の階層だからです。ボーンは親の関節まわりにしか回転できず、子はそれに従います。だから各ランドマークは、テレポートする位置ではなく、ボーンが向くべき方向——つまり回転——へ変換する必要があります。

なぜアバターは自分をそのまま真似ず、鏡映しになるのですか?

X軸を反転すると鏡のように見え、画面上では自然だからです——右手を上げるとアバターの同じ側が上がる。副作用として、自分の右側のランドマークがアバターの左側のボーンを動かすため、左右の対応はコード上で意図的に入れ替えています。

なぜカメラ1台の深度はそれほど当てにならないのですか?

2Dカメラ1台には真の奥行き情報がなく、モデルがZを推測するためです。その推測が信号の中で最も弱い部分で、左右対称のポーズでは片方の手足を大きく前へ置いてしまうことがあります。カメラが実際に見ているX/Yのシルエットより Z を信用しない——これが安価で効果的な補正になります。

次回 — パート4

これがリターゲティングです——ウェブカメラがアバターを動かす、ブラウザだけで、トラッキングスーツも深度カメラもなく。まだ荒さは残ります——指のトラッキングも、顔からの表情もなく、スムージングもわざと単純なまま。それがパート4:スムージング・手・顔で、パート2で用意したVRMの表情システムを動かします。コードはGitHubにあります(パート3は v3-retargeting でタグ付け)。はじめての方は、パート1:MediaPipeによるブラウザ・モーションキャプチャ、続いてパート2:VRMアバターの読み込みからどうぞ。


福島 崇(Takashi Fukushima) — Sports Science & Pose Estimation.
YouTubeで登録  ·  GitHub  ·  ウェブサイト  ·  お問い合わせ

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール