UnityでMediaPipeを実行してVTuberになる

>:santa:この記事は、 [North Detail Advent Calendar 2020](https://qiita.com/advent-calendar/2020/northdetail) の21日目の記事です:christmas_tree:

# 成果物


![成果物.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/648c756a-b57d-a69d-031e-7165ba98f47b.gif)




トップVTuber間違いなし:wink:

# 作業環境

・MacBook Pro (Retina, 15-inch, Mid 2014) : Catalina 10.15.7 

・Unity : 2019.4.16f1
・[google/MediaPipe](https://github.com/google/mediapipe) : v0.8.0
・[MediaPipeUnityPlugin](https://github.com/homuler/MediaPipeUnityPlugin) : v0.2.1

※2020/12/19現在 本記事の内容は**Linux, macOS, Android**のみ対応
また、**GPU対応はLinux, Androidのみ**で、**MacではCPUモードのみ**なので注意
詳しくは[上記プラグインの「Platforms」](https://github.com/homuler/MediaPipeUnityPlugin#platforms)を参照

# （余談）Unityでその他フェイス・トラッキング

以下のような技術で、**深度カメラ搭載デバイス(iPhone Xなど)向けにビルド**や、
デバイスからUnityに顔データを送信する方法などがあります
・[ARKit Face Tracking](https://docs.unity3d.com/ja/2019.4/Manual/com.unity.xr.arkit-face-tracking.html)
・[Facial AR Remote](https://github.com/Unity-Technologies/facial-ar-remote)

・・・が、実装やテストが結構大変です。。。

Unity単体で簡単に実装できるAssetなどもあります
・[OpenCV for Unity](https://assetstore.unity.com/packages/tools/integration/opencv-for-unity-21088?locale=ja-JP) : $95
・[Dlib FaceLandmark Detector](https://assetstore.unity.com/packages/tools/integration/dlib-facelandmark-detector-64314) : $40
・[CV VTuber Example](https://assetstore.unity.com/packages/templates/tutorials/cv-vtuber-example-118186) : 無料 (別途 上の2アセットが必要)

実際に動かすとこんな感じ
![unity_chan.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/fb872634-1f22-3de6-6623-975235ac9033.gif)
© Unity Technologies Japan/UCL




OpenCV、DlibをUnityで使えるようにしたもので、画像認識とかいろいろできる
**3つのアセットを入れるだけで簡単にユニティちゃんを動かせます**

・・・が、**値段がめちゃくちゃ高い**。。。(私はバンドルセールで購入しました)
そして**動作が結構重かったり**、**横顔を認識できなかったり**...
**視線検出にも対応していない**ため、**カメラ目線固定**などの対応となります
（[Pythonで実現している人](https://towardsdatascience.com/real-time-eye-tracking-using-opencv-and-dlib-b504ca724ac6)はいるので頑張ればできそう？)

# 各種インストール

## 1. Unity
言わずと知れたゲームエンジン

[ダウンロードページ](https://unity3d.com/jp/get-unity/download)からUnity Hubをダウンロード
**2019.4.10f1以上**をインストール
(2020では動かないかもしれないので**2019の近いバージョン**がよいかも)

## 2. [Google/MediaPipe](https://github.com/google/mediapipe)
### MediaPipeとは？

Googleが開発している機械学習パイプライン構築フレームワーク
顔認識や物体の識別、ハンドトラッキングとかいろいろできる
**全体的に精度が高く**、フェイストラッキングでは**視線の検出**までできる

昨年のアドカレでも記事にしているので興味ある方は是非↓
・[スマホカメラで手のモーションを記録してUnityでピアノ演奏したかった](https://qiita.com/yamatohkd/items/0f5b9b031ec0976d872a)

昨年に比べて、CPUでもサクサク動いたり、WEBブラウザで実行できたり進化してる
でもまだまだ開発途上

### インストール手順
[インストールページの手順](https://google.github.io/mediapipe/getting_started/install)に従ってセットアップ

おそらく**エラー連発で手順通りに進まない**と思うので
各種バージョンを合わせたり、追加でnumpyをインストールしたりで解決しました

バージョンは古いですが[昨年の記事](https://qiita.com/yamatohkd/items/0f5b9b031ec0976d872a#mediapipe%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)も参考になるかも

## 3. [MediaPipeUnityPlugin](https://github.com/homuler/MediaPipeUnityPlugin)

**MediaPipeをUnityで実行できる**ようにする神プラグイン
日本人の方が作成しているっぽい、マジ感謝 (。-人-。)

### インストール手順

##### 1. 事前準備
以下をインストール
・MediaPipe
・OpenCV
・.NET Core

上２つは前述の手順でインストールされているはず
.NET Core は Visual Studio for Mac をインストールしていれば自動で入っています
手動で入れる場合はこちら↓
https://dotnet.microsoft.com/download

##### 2. リポジトリをクローン
```shell:
$ git clone https://github.com/homuler/MediaPipeUnityPlugin.git
$ cd MediaPipeUnityPlugin
```

##### 3. CPUモードでビルド
```shell:
$ make cpu

# ---------------------
# Linuxの場合はGPUモードも可
$ make gpu

# Androidで動かしたい場合
$ make android_arm
```

##### 4. ビルドしたファイルをアセットフォルダに配置
```shell:
# ライブラリ・モデルファイルをアセットフォルダに配置してくれる
$ make install
```

これで準備完了

# UnityでMediaPipeを実行
1. Unityで先程クローンした**MediaPipeUnityPluginフォルダを開く**
2. Projectウィンドウから**Assets/MediaPipe/Examples/Scenes/DesktopCPU** を開く
3. [▶]Playボタンで実行

![MediaPipe実行.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/9eb10c8a-0707-f54d-2c49-62092ae845a7.gif)


**UnityでMediaPipeが実行できました！**

# 3Dモデルを動かす
3Dモデルを動かすには、
1. ボーンを動かす
2. メッシュを動かす
3. アニメーションを実行する

などの方法があります

VTuberアプリなどはこれの**あわせ技**で、
口の動きはメッシュ、まばたきはアニメーションで表現したりします

今回は2の「**メッシュを動かす**」のみで実装します

### 1. 3Dモデルを用意
クローンしたMediaPipeリポジトリに3Dモデルが用意されています
`mediapipe/mediapipe/modules/face_geometry/data/canonical_face_model.obj`

Blenderで開くとこんな感じ
![スクリーンショット 2020-12-19 11.44.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/a36fdf2c-4234-7b23-12ca-36766b829e56.png)

**468個の頂点**で構成されたMeshになっています

先程Unityで実行した際の点 (**Landmark**) の数が**478個**でした
これは**顔468個**に加えて、**左目5個**、**右目5個**で構成されています

まずは顔のLandmarkに対応したMeshの頂点を同期させて動かします

### 2. Unityにインポート
UnityのProjectウィンドウにファイルを**ドラッグ&ドロップ**
さらにProjectウィンドウからHierarchyウィンドウに**ドラッグ&ドロップ**
![スクリーンショット 2020-12-20 17.20.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/600e1317-0fac-3218-22b8-36e2fa14c1ef.png)
Unityは3Dオブジェクトを読み込むと**自動でMeshが最適化**されます

これにより、**頂点の数が増えたり順番がバラバラになったり**するため設定を変更します

Projectウィンドウからオブジェクトを選択し、
Inspectorウィンドウの**Modelタブ**から以下を修正
1. Read/Write Enabled → ON
2. Optimize Mesh → Nothing
3. Smoothing Angle → 180
4. Applyボタンをクリック
<img width="400" alt="スクリーンショット 2020-12-20 17.23.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/5124b6a8-90da-703c-f6c6-3c406e64e5a3.png">

### 3. 顔を動かす
以下のファイルを修正していきます
`Assets/MediaPipe/Examples/Scripts/IrisTracking/IrisTrackingAnnotationController.cs`

```c#:IrisTrackingAnnotationController.cs
namespace Mediapipe {
  public class IrisTrackingAnnotationController : AnnotationController {
    [SerializeField] GameObject irisPrefab = null;
    [SerializeField] GameObject faceLandmarkListPrefab = null;
    [SerializeField] GameObject faceRectPrefab = null;
    [SerializeField] GameObject faceDetectionsPrefab = null;
    
    /* ------------------- 追加 ------------------- */
    private MeshFilter meshFilter; // オブジェクトのMeshFilter
    private Mesh faceMesh; // オブジェクトのMesh
    private List<Vector3> vertextList = new List<Vector3>(); // Meshの頂点の座標リスト
    /* ------------------------------------------- */

    /* ~ 省略 ~ */

    void Awake() {
      leftIrisAnnotation = Instantiate(irisPrefab);
      rightIrisAnnotation = Instantiate(irisPrefab);
      faceLandmarkListAnnotation = Instantiate(faceLandmarkListPrefab);
      faceRectAnnotation = Instantiate(faceRectPrefab);
      faceDetectionsAnnotation = Instantiate(faceDetectionsPrefab);

      /* ------------------- 追加 ------------------- */
      meshFilter = GameObject.Find("default").GetComponent<MeshFilter>(); // defaultオブジェクトからMeshFilterを取得
      faceMesh = meshFilter.mesh; // Meshをセット
      vertextList.AddRange(faceMesh.vertices); // Meshから頂点座標リストを取得
      /* ------------------------------------------- */

      /* ~ 省略 ~ */

    public void Draw(Transform screenTransform, NormalizedLandmarkList landmarkList,
        NormalizedRect faceRect, List<Detection> faceDetections, bool isFlipped = false)
    {
      if (landmarkList == null) {
        Clear();
        return;
      }

      UpdateFaceMesh(landmarkList); // 追加

      /* ~ 省略 ~ */

    /* ------------------- 追加 ------------------- */
    private int meshScale = -5; // サイズ調整用の変数
    private void UpdateFaceMesh(NormalizedLandmarkList landmarkList) {
      // 顔の頂点分だけ実行（478 - 10 = 468）
      for (var i = 0; i < landmarkList.Landmark.Count - 10; i++)
      {
          var landmark = landmarkList.Landmark[i];
          // 検出したLandmarkをMeshの頂点座標にセット
          vertextList[i] = new Vector3(meshScale*landmark.X, meshScale*landmark.Y, meshScale*landmark.Z);
      }
      // 座標リストをMeshに適用
      faceMesh.SetVertices(vertextList);
    }
    /* ------------------------------------------- */
```
これで[▶]実行すると

![顔トラッキング.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/3241cb0f-3202-21e6-ab4d-8b7cb118206c.gif)


**見事3Dモデルを動かすことができました！**

### 4. 目を動かす

**球体オブジェクトの目**を用意して、**傾きを計算して回転させる**のが理想なのですが
**大変なので同じくメッシュで**実装していきます

1. 適当な目（虹彩）の画像を用意
2. GameObject > Create Emptyで空のオブジェクトを2つ作成し、名前を**LeftEye, RightEye**に変更
3. LeftEye, RightEyeを**defaultオブジェクトの子**にセットする
4. LeftEye, RightEyeに**Mesh Filter**と**Mesh Renderer**を追加し、Materialに目の画像をセット

こんな感じ↓
![スクリーンショット 2020-12-20 18.53.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/5755a114-10a8-6855-f5cf-e7010497ca33.png)
先程のコードを修正します

```c#:IrisTrackingAnnotationController.cs
    
    private MeshFilter meshFilter;
    private Mesh faceMesh;
    private List<Vector3> vertextList = new List<Vector3>();

    /* ------------------- 追加 ------------------- */
    private MeshFilter leftEyeMeshFilter;
    private Mesh leftEyeMesh;
    private List<Vector3> leftEyeVertextList = new List<Vector3>();
    private List<Vector2> leftEyeUvList = new List<Vector2>();
    private List<int> leftEyeIndexList = new List<int>();

    private MeshFilter rightEyeMeshFilter;
    private Mesh rightEyeMesh;
    private List<Vector3> rightEyeVertextList = new List<Vector3>();
    private List<Vector2> rightEyeUvList = new List<Vector2>();
    private List<int> rightEyeIndexList = new List<int>();
    /* ------------------------------------------- */

    /* ~ 省略 ~ */

    void Awake() {
      /* ~ 省略 ~ */
      meshFilter = GameObject.Find("default").GetComponent<MeshFilter>();
      faceMesh = meshFilter.mesh;
      vertextList.AddRange(faceMesh.vertices);
 
      /* ------------------- 追加 ------------------- */     
      leftEyeMeshFilter = GameObject.Find("LeftEye").GetComponent<MeshFilter>();
      leftEyeMesh = CreateEyeMesh(leftEyeVertextList, leftEyeUvList, leftEyeIndexList, true);
      leftEyeMeshFilter.mesh = leftEyeMesh;

      rightEyeMeshFilter = GameObject.Find("RightEye").GetComponent<MeshFilter>();
      rightEyeMesh = CreateEyeMesh(rightEyeVertextList, rightEyeUvList, rightEyeIndexList, false);
      rightEyeMeshFilter.mesh = rightEyeMesh;
      /* ------------------------------------------- */

    /* ~ 省略 ~ */

    private void UpdateFaceMesh(NormalizedLandmarkList landmarkList) {
      for (var i = 0; i < landmarkList.Landmark.Count - 10; i++)
      {
          var landmark = landmarkList.Landmark[i];
          vertextList[i] = new Vector3(meshScale*landmark.X, meshScale*landmark.Y, meshScale*landmark.Z);
      }
      faceMesh.SetVertices(vertextList);

      /* ------------------- 追加 ------------------- */
      for (var i = landmarkList.Landmark.Count - 9; i < landmarkList.Landmark.Count - 5; i++)
      {
        var landmark = landmarkList.Landmark[i];

        leftEyeVertextList[i-469] = new Vector3(meshScale*landmark.X, meshScale*landmark.Y, meshScale*landmark.Z);
      }
      leftEyeMesh.SetVertices(leftEyeVertextList);

      for (var i = landmarkList.Landmark.Count - 4; i < landmarkList.Landmark.Count; i++)
      {
        var landmark = landmarkList.Landmark[i];

        rightEyeVertextList[i-474] = new Vector3(meshScale*landmark.X, meshScale*landmark.Y, meshScale*landmark.Z);
      }
      rightEyeMesh.SetVertices(rightEyeVertextList);
      /* ------------------------------------------- */
    }

    /* ------------------- 追加 ------------------- */
    private Mesh CreateEyeMesh(List<Vector3> eyeVertextList, List<Vector2> eyeUvList, List<int> eyeIndexList, bool isLeftEye)
    {
        var mesh = new Mesh();
        eyeVertextList.Add(new Vector3(1, 0, 0));
        eyeVertextList.Add(new Vector3(0, -1, 0));
        eyeVertextList.Add(new Vector3(-1, 0, 0));
        eyeVertextList.Add(new Vector3(0, 1, 0));

        eyeUvList.Add(new Vector2(1f, 0.5f));
        eyeUvList.Add(new Vector2(0.5f, 1f));
        eyeUvList.Add(new Vector2(0f, 0.5f));
        eyeUvList.Add(new Vector2(0.5f, 0f));

        if(isLeftEye){
          eyeIndexList.AddRange(new []{0,3,1,1,3,2});
        } else {
          eyeIndexList.AddRange(new []{2,3,1,1,3,0});
        }

        mesh.SetVertices(eyeVertextList);
        mesh.SetUVs(0,eyeUvList);
        mesh.SetIndices(eyeIndexList.ToArray(),MeshTopology.Triangles, 0);
        return mesh;
    }
    /* ------------------------------------------- */
```
これで[▶]実行すると

![目トラッキング.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/41a329ee-ffbc-41ab-44e6-e5a65fe6b3cd.gif)


**視線に合わせて目を動かすことができました！**

# まとめ
MediaPipe 昨年に比べてだいぶ進化していました
去年は**CPUモードではフリーズ**して使い物になりませんでしたから......

**Unityで利用できるプラグイン**まで作成され、
実用レベルもかなり上がって来ています

GPUモードでの実行やUnityとの連携など、更なる発展に期待が高まります
来年のアドベントカレンダーが楽しみですね〜笑