【Unity WebGL】MediaPipeで3Dポーズを動かす
投稿日: 2026年5月6日 対象読者: UnityでWebGLアプリを作りたいエンジニア / MediaPipeをWebに組み込みたい方
この記事はこんな方向け:
- MediaPipeの解析結果をUnityに渡す方法が知りたい方
- Unity WebGLとJavaScriptの連携(SendMessage)で詰まっている方
- ブラウザ完結のAIアプリをゼロから作りたい方
TL;DR
- MediaPipe Pose で写真から33関節の座標を取得し、JSON文字列に変換してUnityへ送信
- Unity WebGLでは
SendMessageを使うことでJavaScriptからC#のメソッドを直接呼べる - C#側で座標から方向ベクトルを計算し、
Quaternion.LookRotationで各関節を回転させる - Firebase Hostingでは
.wasm/.brのヘッダー設定を忘れると画面が真っ白になる
はじめに
漫画やイラストを描いていると、「このポーズを別のアングルで見たい」という場面が必ずあります。写真を探しても、ぴったりの構図が見つかることはほとんどありませんよね。
「それなら、写真を1枚撮るだけで3D人形が同じポーズをとってくれれば解決では?」
そう考えて開発したのが Pose Mirror です。筆者はこのアプリの開発を通じて、MediaPipe(JavaScript)とUnity WebGL(C#)を連携させるという、あまり例のない構成を実装しました。この記事では、その仕組みを全体像から実装の詳細まで解説します。
作ったもの
- 写真をアップロードすると、AIが人物のポーズを自動検出
- 検出した33個の関節座標を3Dデッサン人形に反映
- ブラウザだけで完結(インストール不要)
- スマホ・PC両対応
精度の詳細は 【検証】Pose MirrorのAIポーズ解析精度を試した もあわせてご覧ください。
システム全体像
技術スタック
| レイヤー | 技術 | 役割 |
|---|---|---|
| AIポーズ推定 | MediaPipe Pose (JavaScript) | 写真から33関節の座標を取得 |
| 3D描画・制御 | Unity 6(WebGL出力) | 3Dモデルの表示と関節回転 |
| JS↔Unity連携 | SendMessage | 座標データをC#へ渡す |
| ホスティング | Firebase Hosting | ブラウザからアクセス可能に |
処理フロー
ユーザーが写真をアップロード
↓
MediaPipe Pose(JavaScript)
→ 33個のランドマーク座標を取得(x, y, z, visibility)
↓
JSON文字列に変換・Unity向けに座標系を変換
↓
SendMessage でUnityのC#メソッドを呼び出す
↓
Unity C# がJSONをパース
→ 各関節の方向ベクトルを計算
→ Quaternion で3Dモデルの関節を回転
↓
3Dモデルが同じポーズをとる
実装①:MediaPipeでポーズ取得
セットアップ
MediaPipe PoseはCDNから直接読み込めます。インストール不要でブラウザ上で動作するのが大きな利点です。
<!-- HTMLのheadに追加 -->
静止画からランドマークを取得する
Pose Mirrorでは静止画(アップロード画像)からポーズを1回だけ取得する構成です。pose.send() に画像要素を渡すだけで動きます。
const pose = new Pose({
locateFile: (file) =>
`https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`
});
pose.setOptions({
modelComplexity: 1, // 0=軽量 / 1=標準 / 2=高精度
smoothLandmarks: false, // 静止画なのでスムージングは不要
minDetectionConfidence: 0.5,
});
// 結果を受け取るコールバック
pose.onResults((results) => {
if (!results.poseLandmarks) return; // 人物が未検出の場合
sendLandmarksToUnity(results.poseLandmarks);
});
// 画像要素を渡して解析を1回実行
const imgElement = document.getElementById('upload-img');
await pose.send({ image: imgElement });
MediaPipeは33個のランドマークを返します。landmarks[11] が左肩、landmarks[12] が右肩...のように固定インデックスが割り当てられています。各座標は**画像サイズを1とした正規化値(0〜1)**で得られます。
実装②:JavaScript→Unity通信(SendMessage)
SendMessageの仕組み
ここがこのアーキテクチャの核心部分です。Unity WebGLビルドに付属する unityInstance は、JavaScriptから次のように呼び出せます。
// 書式
unityInstance.SendMessage('GameObjectName', 'MethodName', 'データ(文字列のみ)');
- 第1引数: Unity側のGameObject名(シーン上に存在する必要あり)
- 第2引数: そのGameObjectに付いているC#スクリプトのメソッド名
- 第3引数: 渡すデータ(文字列のみ — 数値や配列は文字列に変換が必要)
文字列しか渡せないため、座標データはJSON文字列に変換してから送ります。
座標変換とデータ送信
MediaPipeの座標系とUnityの座標系は異なります。そのまま渡すと上下が逆になるため変換が必要です。
function sendLandmarksToUnity(landmarks) {
// 使用する関節のみ抽出(インデックスはMediaPipeで固定)
const jointMap = {
leftShoulder: landmarks[11],
rightShoulder: landmarks[12],
leftElbow: landmarks[13],
rightElbow: landmarks[14],
leftWrist: landmarks[15],
rightWrist: landmarks[16],
leftHip: landmarks[23],
rightHip: landmarks[24],
leftKnee: landmarks[25],
rightKnee: landmarks[26],
leftAnkle: landmarks[27],
rightAnkle: landmarks[28],
};
const payload = {};
for (const [name, lm] of Object.entries(jointMap)) {
payload[name] = {
x: lm.x - 0.5, // 中心基準(0.5オフセットを除去)
y: -(lm.y - 0.5), // Y軸反転(MediaPipeは下向き正、Unityは上向き正)
z: -lm.z, // Z軸反転(奥行き方向を合わせる)
visible: lm.visibility > 0.5 // 信頼度フラグ
};
}
unityInstance.SendMessage('PoseReceiver', 'ReceivePoseData', JSON.stringify(payload));
}
visible フラグで信頼度の低い関節(例:後ろ向きで見えない肘)を識別し、Unity側で適用をスキップするために使います。
実装③:Unity C#で関節に反映
データクラスとJSONパース
JsonUtility でJSONを受け取るため、データ構造と一致するクラスを先に定義します。
[System.Serializable]
public class LandmarkPoint
{
public float x, y, z;
public bool visible;
}
[System.Serializable]
public class PoseData
{
public LandmarkPoint leftShoulder, rightShoulder;
public LandmarkPoint leftElbow, rightElbow;
public LandmarkPoint leftWrist, rightWrist;
public LandmarkPoint leftHip, rightHip;
public LandmarkPoint leftKnee, rightKnee;
public LandmarkPoint leftAnkle, rightAnkle;
}
SendMessageから呼ばれるメソッド
using UnityEngine;
public class PoseReceiver : MonoBehaviour
{
// Inspector で各関節の Transform をアサインしておく
[SerializeField] private Transform leftUpperArm, rightUpperArm;
[SerializeField] private Transform leftForeArm, rightForeArm;
[SerializeField] private Transform leftThigh, rightThigh;
[SerializeField] private Transform leftLeg, rightLeg;
// JavaScript の SendMessage から呼ばれる
public void ReceivePoseData(string json)
{
var data = JsonUtility.FromJson<PoseData>(json);
// 「肩→肘」の方向に上腕を向ける
ApplyBone(leftUpperArm, data.leftShoulder, data.leftElbow);
ApplyBone(rightUpperArm, data.rightShoulder, data.rightElbow);
// 「肘→手首」の方向に前腕を向ける
ApplyBone(leftForeArm, data.leftElbow, data.leftWrist);
ApplyBone(rightForeArm, data.rightElbow, data.rightWrist);
// 脚も同様
ApplyBone(leftThigh, data.leftHip, data.leftKnee);
ApplyBone(rightThigh, data.rightHip, data.rightKnee);
ApplyBone(leftLeg, data.leftKnee, data.leftAnkle);
ApplyBone(rightLeg, data.rightKnee, data.rightAnkle);
}
private void ApplyBone(Transform bone, LandmarkPoint from, LandmarkPoint to)
{
// どちらかが信頼度低なら適用しない
if (!from.visible || !to.visible) return;
Vector3 dir = new Vector3(to.x - from.x, to.y - from.y, to.z - from.z).normalized;
// Z軸を dir 方向に向かせる(Y軸を上向きに保つ)
bone.rotation = Quaternion.LookRotation(dir, Vector3.up);
}
}
回転制御の数学的な詳細(外積を使った軸の正確な決定方法)は 【Unity】オブジェクトをターゲット方向に向かせる方法 で解説しています。
実装④:Firebase Hostingへのデプロイ
# Firebase CLI をインストール(初回のみ)
npm install -g firebase-tools
# Google アカウントでログイン
firebase login
# プロジェクト初期化(public ディレクトリを指定)
firebase init hosting
# デプロイ
firebase deploy --only hosting
wasmとBrotli圧縮のヘッダー設定(必須)
UnityのWebGLビルドは .wasm(WebAssembly)と .br(Brotli圧縮)ファイルを使います。Firebaseはこれらのヘッダーを自動付与しないため、firebase.json に手動で設定する必要があります。
{
"hosting": {
"public": "public",
"headers": [
{
"source": "**/*.wasm",
"headers": [{ "key": "Content-Type", "value": "application/wasm" }]
},
{
"source": "**/*.br",
"headers": [{ "key": "Content-Encoding", "value": "br" }]
}
]
}
}
この設定なしだとブラウザがwasmを展開できず、画面が真っ白のままになります(筆者もここで1時間詰まりました)。
詰まった点・工夫した点
① WebGLではスレッド制約がある
通常のUnityと違い、WebGLではマルチスレッドが使えません。今回は「JSが主導権を持ち、処理完了後にSendMessageでUnityへ渡す」という一方向の設計にすることで、この制約を回避しています。C#→JS方向の呼び出しが必要な場合は jslib プラグインを使う方法もありますが、今回のユースケースでは不要でした。
② MediaPipeの座標系とUnityが違う
| 軸 | MediaPipe | Unity | 変換式 |
|---|---|---|---|
| X | 左端=0, 右端=1 | 中心=0(左が負) | x - 0.5 |
| Y | 上端=0, 下端=1 | 上が正 | -(y - 0.5) |
| Z | 手前が正 | 奥が正 | -z |
この変換を忘れると、ポーズが上下・前後逆になります。特にY軸の反転は見落としやすいので注意してください。
③ Brotli圧縮とFirebaseの相性
wasmとBrotliのヘッダー設定は確実に忘れます。テンプレートとして手元に控えておくことをお勧めします。症状は「画面が真っ白」なのでデバッグに時間がかかりがちです。Chrome DevToolsのNetworkタブで .wasm のレスポンスヘッダーを確認してみてください。
まとめ
- MediaPipe Pose を使えばJavaScriptだけでブラウザ上の写真から33関節の座標が取れる
- SendMessage でJavaScript→C#へ文字列(JSON)を渡せる。数値や配列は必ず文字列に変換すること
- MediaPipe→Unity間の座標系変換(Y軸反転など)を忘れずに
- Firebase Hostingでは
.wasm/.brのContent-Type・Content-Encodingヘッダーの設定が必須
この構成の最大の利点はブラウザだけで完結することです。ユーザーにアプリのインストールを求める必要がなく、リンク1つで使ってもらえます。
ぜひ実際に Pose Mirror を動かしてみてください!
関連記事:
- 【Unity】オブジェクトをターゲット方向に向かせる方法 — 関節回転の数学的な詳細
- 【検証】Pose MirrorのAIポーズ解析精度を試した — 実際の検出精度レポート