【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#)を連携させるという、あまり例のない構成を実装しました。この記事では、その仕組みを全体像から実装の詳細まで解説します。

作ったもの

→ Pose Mirror を使ってみる

  • 写真をアップロードすると、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 を動かしてみてください!


関連記事:

参考資料

← ポートフォリオ TOP へ戻る