Pose Mirror開発で詰まった5つの問題と解決策

投稿日: 2026年5月7日 対象読者: Unity WebGL × AI連携アプリを作ろうとしているエンジニア

この記事はこんな方向け:

  • 似たようなアーキテクチャ(MediaPipe × Unity WebGL)で詰まっている方
  • Unity WebGL開発の落とし穴を事前に知っておきたい方
  • 開発者の失敗談・解決策が知りたい方

TL;DR

  • MediaPipeとUnityの座標系の違いでポーズが逆さになった(Y軸反転忘れ)
  • Unity WebGLのBrotliヘッダー未設定で画面が真っ白になった
  • JsonUtilityDictionary非対応——シリアライズできるクラスを定義する必要がある
  • スマホのSafariでメモリ不足によるタブクラッシュが発生した
  • MediaPipeは照明が暗い画像に弱い——エラーメッセージで事前案内するのが有効

はじめに

「MediaPipeとUnity WebGLを連携させる記事が少なくて、どこで詰まるか分からない……」そんな不安を抱えながら開発を始めていませんか?

Pose Mirrorは「写真1枚でポーズを3D化する」というシンプルなコンセプトのアプリですが、いざ作り始めると予想外の詰まりポイントが続々と出てきました。

この記事では、開発中に実際に詰まった5つの問題と、その解決策をまとめます。同じような構成(MediaPipe × Unity WebGL)で開発している方に、1つでも詰まった時間を短くできれば幸いです。

アーキテクチャ全体の解説は 【Unity WebGL】MediaPipeで3Dポーズを動かす をご覧ください。

問題①:3Dモデルのポーズが上下逆になる

症状

MediaPipeのランドマーク座標をUnityにそのまま送ったら、3Dモデルが上下逆のポーズをとった。「頭」の位置に足が来て、「足」の位置に頭が来る。

原因

MediaPipeとUnityでY軸の向きが逆です。

X軸 Y軸 Z軸
MediaPipe 右が正 下が正 手前が正
Unity 右が正 上が正 奥が正

さらにX軸は「画像の左端=0, 右端=1」の正規化値なので、Unityの中心基準に合わせる変換も必要です。

解決策

JavaScriptでUnityに渡す前に座標を変換します。

// 送信前に座標系を変換する
payload[name] = {
  x:  lm.x - 0.5,    // 中心基準に(0〜1 → -0.5〜0.5)
  y: -(lm.y - 0.5),  // Y軸を反転(これを忘れると上下逆になる)
  z: -lm.z,           // Z軸反転(奥行き方向を合わせる)
  visible: lm.visibility > 0.5
};

教訓:異なるSDKを組み合わせるときは、座標系を軸ごとに図に書いて確認する。

問題②:Firebaseにデプロイしたら画面が真っ白

症状

firebase deploy は成功したのに、ブラウザでアクセスすると画面が真っ白のまま。Consoleには Failed to load resource エラー。

原因

Unity WebGLのBrotli圧縮ファイル(.br)を配信する際、Content-Encoding: br ヘッダーがないとブラウザはファイルを展開しません。Firebase HostingはデフォルトでこのヘッダーをBrotliファイルに付与しません。

また .wasm ファイルにも Content-Type: application/wasm が必要で、これがないとWebAssemblyとして認識されません。

解決策

firebase.json に明示的にヘッダーを設定します。

{
  "hosting": {
    "headers": [
      {
        "source": "**/*.wasm.br",
        "headers": [
          { "key": "Content-Encoding", "value": "br" },
          { "key": "Content-Type", "value": "application/wasm" }
        ]
      },
      {
        "source": "**/*.framework.js.br",
        "headers": [
          { "key": "Content-Encoding", "value": "br" },
          { "key": "Content-Type", "value": "application/javascript" }
        ]
      },
      {
        "source": "**/*.data.br",
        "headers": [
          { "key": "Content-Encoding", "value": "br" },
          { "key": "Content-Type", "value": "application/octet-stream" }
        ]
      }
    ]
  }
}

診断方法: Chrome DevToolsのNetworkタブで .wasm.br のResponse Headersを確認し、Content-Encoding: br が付いているかどうかチェックするのが最速です。

教訓:WebGLビルドをどこかにデプロイするときは、wasmとBrotliのヘッダーを必ず確認する。

詳しいデプロイ手順は 【Firebase】Unity WebGLを無料で公開する手順 を参照してください。

問題③:JsonUtility.FromJson でデータが全部0になる

症状

JavaScriptから送ったJSONをC#で受け取り JsonUtility.FromJson<T>() でパースしたが、全フィールドが 0 または null になる。

原因

JsonUtility には2つの制約があります:

  1. パースするクラスに [Serializable] 属性が必要
  2. Dictionary<K, V> はシリアライズに非対応

最初は以下のように書いていました:

// NG: Dictionary は JsonUtility で扱えない
var data = JsonUtility.FromJson<Dictionary<string, Vector3>>(json);
// → 全て 0 になる

解決策

専用のシリアライズクラスを定義してから FromJson に渡します。

// OK: [Serializable] を付けた専用クラスを定義する
[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;
    // ... 全関節を定義
}

// これで正しくパースできる
var data = JsonUtility.FromJson<PoseData>(json);

JavaScript側でも、Dictionary的な可変キーではなく固定キー(関節名)を送るように統一することで、クラス定義との一致が保証されます。

教訓:JsonUtility はシンプルなクラス専用。複雑な構造には Newtonsoft.Json などを使う。

問題④:iOSのSafariでタブが突然クラッシュする

症状

iPhoneでPose Mirrorを使っていると、数分後にタブが突然リロードされる。使っているうちに重くなっていき、最終的にクラッシュする。

原因

iOSのSafariにはタブあたりのメモリ上限があります(機種によって異なるが概ね1〜2GB)。UnityのWebGLビルドはデフォルトのメモリ確保量が大きく、これが上限を超えるとクラッシュします。

解決策

createUnityInstancememorySize を明示的に制限します。

createUnityInstance(canvas, {
  dataUrl:      "Build/app.data.br",
  frameworkUrl: "Build/app.framework.js.br",
  codeUrl:      "Build/app.wasm.br",
  // スマホ向けは256MBに制限する(デフォルトより小さく設定)
  memorySize: 256 * 1024 * 1024,
});

加えて、IL2CPP + Managed Stripping Level: High を設定してビルドサイズを削減しました。スマホブラウザのメモリ節約は、ビルドサイズとランタイムメモリの両面から対処するのが有効です。

教訓:スマホ向けWebGLはメモリ消費量を意識した設計が必要。最初から memorySize を設定する習慣をつける。

問題⑤:暗い写真でポーズが全く検出されない

症状

ユーザーから「ポーズが検出されない」という報告が来た。確認すると、薄暗い室内で撮影した写真だった。アプリ側で何もエラーメッセージを出していなかったため、ユーザーはなぜ動かないか分からない状態だった。

原因

MediaPipe Poseは照明の影響を受けやすく、暗い画像では人物の輪郭を正確に認識できません。また、複数人が写っている写真や人物が小さすぎる写真でも同様に検出できません。

解決策(2段階)

① 検出失敗時のエラーメッセージを具体的にする

pose.onResults((results) => {
  if (!results.poseLandmarks) {
    // NG: 何も表示しない、または「エラーが発生しました」だけ
    // OK: 具体的な対処法を案内する
    showMessage(
      '人物が検出できませんでした。\n' +
      '・全身が写った写真を使ってください\n' +
      '・明るい場所で撮影した写真を試してください\n' +
      '・1人だけが写っている写真を使ってください'
    );
    return;
  }
  // 成功時の処理...
});

② 画面上に「ヒント」を常時表示する

<p class="upload-hint">
  ヒント:全身が写った、明るい写真を使うと精度が上がります
</p>

技術的な解決(モデルの差し替え等)は工数が大きいため、まずUIで案内するアプローチをとりました。AIの限界を伝えるUIは、精度改善と同じくらい重要です。

教訓:AIが失敗する条件をユーザーに事前に伝えるUIを作る。エラーメッセージは「何が原因か」「どうすればいいか」を具体的に書く。

まとめ

問題 原因 解決策
ポーズが上下逆 MediaPipe/Unity 座標系の違い Y軸反転して送信
画面が真っ白 Brotliヘッダー未設定 firebase.json にヘッダー追加
JSON全部0 Dictionary非対応 [Serializable]クラスを定義
iOSでクラッシュ メモリ上限超過 memorySize を明示的に制限
検出されない 暗い画像 具体的なエラーメッセージを表示

どれも「知っていれば30分で解決できる」問題ですが、知らないと丸1日溶かすような罠でした。同じ轍を踏まないための参考になれば幸いです。

ぜひ実際に Pose Mirror を動かして確認してみてください!


関連記事:

参考資料

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