【Unity】MediaPipeのポーズトラッキングでゲームを作ろう

# はじめに
※この記事は、[JOKEN Advent Calender 2023](https://adventar.org/calendars/9105)の参加記事（OB枠）です。

こんにちは！　株式会社トルクスでエンジニアをしているオガワです。
今回は、とあるイベントの出展用にMediaPipeのUnityプラグインを使ってゲームを作ったので、そのプラグインをゲーム制作に使う方法を解説していきます。

# MediaPipeとは
Googleが提供しているオープンソースのライブラリです。画像・動画から顔、手、姿勢などの識別を行えます。

# 作ったゲーム

https://twitter.com/i/status/1730487150008004984


# 環境
Windows 11 Home
Unity 2021.3.18f1
MediaPipeUnityPlugin v0.12.0

# MediaPipeUnityPluginの導入
MediaPipeをゲームエンジンのUnityで使えるようにしてくれるプラグインがGitHubで公開されているので、ありがたく使わせてもらいます。

https://github.com/homuler/MediaPipeUnityPlugin

このプロジェクトをダウンロードしてそのまま使いたいところですが、Unityで開くとパッケージが足りていない旨のエラーが大量に出てきます。

どうやら必要なパッケージを自分で導入する必要があるみたいですが、リリースにてパッケージを導入済みのUnityプロジェクトが配布されているのでそちらを使います。

以下のリンクから`MediaPipeUnityPlugin-all.zip`をダウンロードしてきて解凍してください。

[Releases/v0.12.0](https://github.com/homuler/MediaPipeUnityPlugin/releases/tag/v0.12.0 "MediaPipeUnityPlugin")

# サンプルの実行
解凍した`MediaPipeUnityPlugin-all`をUnityで開きます。試しにサンプルを実行してみましょう。
`Assets/MediaPipeUnity/Samples/Scenes/Start Scene`を開いて実行します。


実行画面右上のメニューボタンをクリックするとトラッキングの種類を選択できます。
![スクリーンショット 2023-11-23 170803.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/d135c8c6-edd1-a6ce-b85c-810acebd6ff2.png)

ハンドトラッキング
![MediaPipeHandTrackingTest.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/3c00c63a-daf9-6d55-f3ae-4caf93687ac4.gif)

ポーズトラッキング
![MediaPipePoseTrackingTest2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/6fd2979a-8d1b-72f3-a75f-513c4947b2fe.gif)

# ゲーム制作
サンプルの動作確認ができたところで、ゲーム制作に移っていきます。今回使うのはポーズトラッキングなので`Assets/MediaPipeUnity/Samples/Scenes/Pose Tracking/Pose Tracking`を開きます。

### ゲームシーンの用意
まずはヒエラルキーウィンドウでシーン名を選択して**シーンを別名で保存**しましょう。名前は何でもいいですが、この記事では`Pose Tracking Game`として保存することにします。以降はこのシーンを編集していきます。

![MediaPipeQiita1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/16c45710-7cb8-965f-1df9-ea13dc5bb0f3.gif)

### トラッキングポジションの取得
トラッキングポジションを取得して追従するオブジェクトを作っていきます。
ランドマークモデルは[Pose landmark detection guide](https://developers.google.com/mediapipe/solutions/vision/pose_landmarker/ "Pose landmark detection guide")から確認できます。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/4620c3fa-3444-4ff4-38c3-bc78d7d848ec.png">

`PointListAnnotation`オブジェクトについている`PointListAnnotation`スクリプトを編集しましょう。
ここでは左右の手の人差し指のトラッキングポジションを取得しています（ランドマークモデルの19,20番が対応）。

![スクリーンショット 2023-12-13 211327.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/3bcf7af5-bee0-c7cc-b8d1-566057fe7446.png)

```c#:PointListAnnotation.cs
public class PointListAnnotation : ListAnnotation<PointAnnotation>
{
    //追加
    public GameObject RightIndex;
    public GameObject LeftIndex;
    //追加ここまで

    [SerializeField] private Color _color = Color.green;
    [SerializeField] private float _radius = 15.0f;
    //...

    public void Draw(IList<NormalizedLandmark> targets, bool visualizeZ = true)
    {
      if (ActivateFor(targets))
      {
        CallActionForAll(targets, (annotation, target) =>
        {
          if (annotation != null) { annotation.Draw(target, visualizeZ); }
        });
      }

      //追加
      if (RightIndex != null && LeftIndex != null)
      {
        RightIndex.transform.position = children[20].transform.position;
        LeftIndex.transform.position = children[19].transform.position;
      }
      //追加ここまで
    }
    //...
}
```

スクリプトを編集できたらトラッキングポジションに追従するオブジェクトを用意します。以下のGIFのようにして`RightIndexSphere`オブジェクトと`LeftIndexSphere`オブジェクトを作成してください。

![MediaPipeQiita2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/7b9c0828-3fe8-9083-15f8-cef8b0d2f4cd.gif)

分かりやすいようにインスペクターウィンドウでサイズを10倍にしておきます。

![スクリーンショット 2023-12-14 123859.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/808c238e-1d58-12f8-fc40-ec16c2d2cd36.png)

作成したオブジェクトを`PointListAnnotation`の枠にドラック＆ドロップして関連付けましょう。

![MediaPipeQiita3-2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/6929faad-ea60-03af-fe6c-5ed3ebf18e06.gif)

これでオブジェクトが手の位置に追従するようになりました。オブジェクトを剣や盾に変えれば構えたり振ったりと、もう遊べますね。

![スクリーンショット 2023-12-16 103341.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/0b4e4fa4-b03b-72f5-6a96-113ff5a0b3f4.png)

### グリーンバックの変更
今のままだと背景が緑一色なので変えたいと思います。
背景画像を用意して以下のGIFのようにインポートからの設定を行ってください。
![MediaPipeQiita4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/c64d98fb-eb91-1135-c3f3-80e6c946d1d8.gif)

`MaskAnnotation`オブジェクトを選択し、インスペクターウィンドウの`マスクテクスチャ`欄に先ほどの背景画像をプロジェクトウィンドウからドラック＆ドロップして関連付けしてください。

![スクリーンショット 2023-12-16 105330.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/e995115d-8f77-adb6-0739-e49089986c72.png)

![スクリーンショット 2023-12-16 105344.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/292cb341-eec3-e829-d8c8-f4c574224e5f.png)

これで背景が自分の用意したものに変わりました。

![スクリーンショット 2023-12-16 105515.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/b5df1ba0-2076-2745-cf49-8de96b9b9e32.png)


### ボーン、サブスクリーンの非表示
身体の前に表示されているボーンを非表示にしましょう。

一度実行してみると分かりますが、ボーンは`PointListAnnotation`オブジェクトと`ConnectionListAnnotation`オブジェクトの子として生成されています。

以下のGIFを参考にそれぞれのオブジェクトに設定されている`AnnotationPrefab`の`Renderer`を非アクティブにしましょう。

![MediaPipeQiita5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/cc2f9c55-6fc4-b5cb-e1e5-cd73bf4aa5bf.gif)

これでボーンが非表示になったはずです。加えて、画面右下に表示されているサブスクリーンも非表示にしておきましょう。
おそらくボーン確認用に用意されているのだと思いますが、今は必要ないので非表示にします。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/d99f8680-cdb0-2fa0-7361-f47ab4354b13.png">

`SubScreen`オブジェクトと`Room`オブジェクトを、それぞれインスペクターウィンドウで名前の横のチェックマークを外して非アクティブにしましょう。

![スクリーンショット 2023-12-16 115339.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/805093bc-ed73-e669-7d81-7cd0e67b8e67.png)

![スクリーンショット 2023-12-16 115937.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/af0e0410-2f31-f1e8-7bda-24fbc8369aae.png)

これでボーンとサブスクリーンが非表示になりました。

![スクリーンショット 2023-12-16 120304.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/8dda3753-f27c-d476-4294-f2393df35f00.png)


### ハンドボタンの実装
せっかくトラッキングができるのに、ボタンを押すためにキーボードの前に戻るのは味気ないので、トラッキングを使って押せるボタンを作りましょう。

まず、`MainCanvas`の子として`Image`オブジェクトを作り、名前を`HandButton`とします。
さらにその子として`Image`オブジェクトを作り、名前を`FillImage`としましょう。`FillImage`には適当な画像を設定して画像タイプを**塗りつぶし**に変えます。

![MediaPipeQiita7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/e60d534a-5c0b-0e8e-4459-cdf10cd9cf65.gif)

次に`HandButton`スクリプトを作ります。
プロジェクトウィンドウでスクリプトを新規作成して名前を`HandButton`としてください。

![MediaPipeQiita6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/892c2535-03f7-c634-b759-ce09da0119c4.gif)

`HandButton`スクリプトを開いて内容を以下のように書き換えて下さい。

```c#:HandButton.cs
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class HandButton : MonoBehaviour
{
    // カスタムイベントを定義
    [System.Serializable]
    public class CustomEvent : UnityEvent { }
    
    // カスタムイベントを公開
    public CustomEvent onCustomEvent;
    
    public Transform handObject; // ハンドオブジェクトのTransformコンポーネントを設定
    public GameObject FillImage;
    
    private float timer = 0f;
    public float MAX_timer = 1f;
    
    // Update is called once per frame
    void Update()
    {
        // ハンドオブジェクトが目標地点にあるかを確認
        bool objectAtTarget = Vector3.Distance(handObject.position, transform.position) < 1f;
    
        // ハンドオブジェクトが目標地点にあって、かつMAX_timer秒経過したらカスタムイベントを実行
        if (objectAtTarget)
        {
            timer += Time.deltaTime;
            if (timer >= MAX_timer)
            {
                timer = 0f;
        
                // カスタムイベントを実行
                if (onCustomEvent != null)
                {
                    onCustomEvent.Invoke();
                }
            }
        }
        else
        {
            // オブジェクトが目標地点から移動した場合、タイマーをリセット
            timer = 0f;
        }
    
        FillImage.GetComponent<Image>().fillAmount = timer / MAX_timer;
    }
}

```

`HandButton`オブジェクトに`HandButton`スクリプトをアタッチして`RightIndexSphere`と`FillImage`を設定します。

`OnCustomEvent()`の`＋`マークを押してイベントを追加することで通常のUnityボタンと同じように使えます。

![MediaPipeQiita8.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/005c02fe-0530-5cd9-d362-f7b33aeb5ff4.gif)

これでハンドボタンができました。右手を1秒間かざすことで登録されたイベントを実行します。

![MediaPipeQiita9.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/7a9f1800-400b-d72c-1fe6-391cd12e0e54.gif)

### 制作時の注意点
あとはそれぞれにオリジナルのゲームを作っていけばOKなのですが、いくつか注意点があります。

#### ビルドの注意点
ビルド時には`Start Scene`を含むようにしてください。これが無いとゲームが正常に動作しません。

シーン遷移の設定などは必要なく、もとからある`Start Scene`をBuildSettingsで一番上に置けばOKです。

![スクリーンショット 2023-12-16 121048.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/31745828-1154-101f-5c80-b82a198d1062.png)

#### エディタ上でシーン遷移するときの注意点
ゲームを制作する上でシーンを複数用意することがあると思いますが、エディタ上で`SceneManager.LoadScene("SceneName");`などを使ってシーン遷移をすると、**Unityが落ちます**。

これはおそらく`Start Scene`からゲームを始めていないためMediaPipeの設定がされていないのが原因だと思います。なので、シーン遷移のテストがしたいときはまず`Start Scene`から始めてください。

:::note warn
シーン遷移を行うのに`Start Scene`から始めていなくて保存していないデータが飛ぶ...ということが何度もありました。
:::

### （おまけ）トラッキング方式の変更
ゲームを作っていると、他のトラッキング方式で作っていればよかった...。と思うことがあります。

ここではトラッキング方式を変更する方法を解説していきます。

<details><summary>トラッキング方式の変更方法</summary>

例として、ポーズトラッキングからホリスティック（ポーズに加えてハンド、フェイストラッキングもできる）形式に変えていきます。

まず`Assets/MediaPipeUnity/Samples/Scenes/Holistic/Holistic`を開きます。
ここで`AnnotatableScreen`オブジェクトと`Solution`オブジェクトをコピーしましょう。

![スクリーンショット 2023-12-16 152954.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/05d89691-ca49-b8f7-d1a6-adc29cb4aa86.png)

`Pose Tracking Game`シーンに戻ってきて元からあったオブジェクトと置き換えます（削除して、コピーしてきた方を代わりに置く）。

`Solution`オブジェクトの`[トラッキング名] Solution`スクリプトを画像のように設定します。
設定するものは`AnnotatableScreen`、`AnnotationArea`、`AnnotationLayer`の三つです。

+ `AnnotatableScreen`：先ほどコピーしてきた`AnnotatableScreen`
+ `AnnotationArea`：`Room`の子オブジェクト
+ `AnnotationLayer`：`AnnotatableScreen`の子オブジェクト

![スクリーンショット 2023-11-20 121057.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621034/58d26aba-fe03-99a3-a6e0-67a898f97df1.png)

これで変更完了です。

</details>

# おわりに
最後まで読んでいただきありがとうございました！

MediaPipeUnityPluginを使えばWebカメラだけでトラッキングを使ったゲームを作れます。いろいろと応用が利きそうで面白いので、この記事を読んでいいなと思った方はぜひ作ってみてください！

ではまたどこかで('ω')ノ