North Detail / ノースディテール

BLOG ブログ

ブログ
CATEGORY
TECH

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

※本記事は、NorthDetail Advent Calendar 2020の一環として投稿しています

成果物

トップVTuber間違いなし😉

作業環境

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

・Unity : 2019.4.16f1
google/MediaPipe : v0.8.0
MediaPipeUnityPlugin : v0.2.1

※2020/12/19現在 本記事の内容はLinux, macOS, Androidのみ対応
また、GPU対応はLinux, Androidのみで、MacではCPUモードのみなので注意
詳しくは上記プラグインの「Platforms」を参照

(余談)Unityでその他フェイス・トラッキング

以下のような技術で、深度カメラ搭載デバイス(iPhone Xなど)向けにビルドや、
デバイスからUnityに顔データを送信する方法などがあります
ARKit Face Tracking
Facial AR Remote

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

Unity単体で簡単に実装できるAssetなどもあります
OpenCV for Unity : $95
Dlib FaceLandmark Detector : $40
CV VTuber Example : 無料 (別途 上の2アセットが必要)

実際に動かすとこんな感じ


© Unity Technologies Japan/UCL

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

・・・が、値段がめちゃくちゃ高い。。。(私はバンドルセールで購入しました)
そして動作が結構重かったり横顔を認識できなかったり
視線検出にも対応していないため、カメラ目線固定などの対応となります
Pythonで実現している人はいるので頑張ればできそう?)

各種インストール

1. Unity

言わずと知れたゲームエンジン

ダウンロードページからUnity Hubをダウンロード
2019.4.10f1以上をインストール
(2020では動かないかもしれないので2019の近いバージョンがよいかも)

2. Google/MediaPipe

MediaPipeとは?

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

昨年のアドカレでも記事にしているので興味ある方は是非↓
スマホカメラで手のモーションを記録してUnityでピアノ演奏したかった

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

インストール手順

インストールページの手順に従ってセットアップ

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

バージョンは古いですが昨年の記事も参考になるかも

3. MediaPipeUnityPlugin

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

インストール手順

1. 事前準備

以下をインストール
・MediaPipe
・OpenCV
・.NET Core

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

2. リポジトリをクローン
$ git clone https://github.com/homuler/MediaPipeUnityPlugin.git
$ cd MediaPipeUnityPlugin
3. CPUモードでビルド
$ make cpu

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

# Androidで動かしたい場合
$ make android_arm
4. ビルドしたファイルをアセットフォルダに配置
# ライブラリ・モデルファイルをアセットフォルダに配置してくれる
$ make install

これで準備完了

UnityでMediaPipeを実行

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

UnityでMediaPipeが実行できました!

3Dモデルを動かす

3Dモデルを動かすには、

  1. ボーンを動かす
  2. メッシュを動かす
  3. アニメーションを実行する

などの方法があります

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

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

1. 3Dモデルを用意

クローンしたMediaPipeリポジトリに3Dモデルが用意されています

mediapipe/mediapipe/modules/face_geometry/data/canonical_face_model.obj

Blenderで開くとこんな感じ

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

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

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

2. Unityにインポート

スクリーンショット 2020-12-20 17.20.05.png

UnityのProjectウィンドウにファイルをドラッグ&ドロップ
さらにProjectウィンドウからHierarchyウィンドウにドラッグ&ドロップ

Unityは3Dオブジェクトを読み込むと自動でMeshが最適化されます

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

Projectウィンドウからオブジェクトを選択し、
InspectorウィンドウのModelタブから以下を修正

  1. Read/Write Enabled → ON
  2. Optimize Mesh → Nothing
  3. Smoothing Angle → 180
  4. Applyボタンをクリック

3. 顔を動かす

以下のファイルを修正していきます
Assets/MediaPipe/Examples/Scripts/IrisTracking/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);
    }
    /* ------------------------------------------- */

これで[▶]実行すると

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

4. 目を動かす

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

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

こんな感じ↓

先程のコードを修正します

    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;
    }
    /* ------------------------------------------- */

これで[▶]実行すると

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

まとめ

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

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

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

YAMATO
WRITER:YAMATO
主な記事 一覧へ

一覧へ

IS 501383 / ISO 27001