【Unity】MediaPipeUnityPluginでFaceTrackingを実装してみた

## はじめに
UnityプロジェクトにMediaPipeUnityPluginを導入し、FaceTrackingしてLive2Dキャラクターを動かす方法を解説していきます。

| 完成図|
| ---- | 
| ![Videotogif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113615/68cc78cd-a3dd-f0fe-d86c-00816cd0d24e.gif)| 

## 環境
- Androidバージョン 12
- Unity 2022.3.39f1 (2020.3以上のverを使用しないとできないっぽいです！)
- jetBrains Rider2023.3.3
- MediaPipeUnityPlugin 0.14.4

## MediaPipeUnityPluginとは
MediaPipeUnityPluginはC++のMediapipeをネイティブプラグインとしてUnity上で使えるようにしたものです。公式のものではありませんが、チュートリアルやインストール方法などが詳細に書かれています。

https://github.com/homuler/MediaPipeUnityPlugin

## MediaPipeUnityPluginの導入・準備
サンプルプロジェクト内のMediaPipeUnityを既存のUnityプロジェクトのAssetsフォルダ配下にコピー。

![スクリーンショット 2024-09-09 19.42.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113615/7b36d585-ad48-d765-771b-1fce596578f8.png)

次にリリースノートからcom.github.homuler.mediapipe-(バージョン).tgzをダウンロードして既存のUnityプロジェクトのPackagesフォルダ配下にコピー。

![スクリーンショット 2024-09-09 19.46.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113615/8d137671-a4c7-0e8b-ec0f-566fbce8207a.png)

プロジェクト内のディレクトリ構成は以下のようなものになるかと思います。
![スクリーンショット 2024-09-09 19.48.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113615/9a91f071-971e-26df-4b14-52762396f979.png)

オブジェクトにAnnotation Layerを追加してFace LandMark Runnerと紐付ける。
Face LandMark Runnnerはフェイストラッキングの処理を開始するスクリプトです。（おそらく）

![スクリーンショット 2024-09-09 19.57.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113615/761c8a87-3066-5f05-9806-fba2e0dcafd6.png)

![スクリーンショット 2024-09-09 19.58.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113615/447c913c-80d1-bec6-f5c7-c81ee517555d.png)

InspectorにAddComponent後、スクリプト内でFaceLandMarkerRunnerを有効化してください。
```C#
// FaceLandmarkerRunnerを有効化
_faceLandMarkerRunner = gameObject.GetComponent<FaceLandmarkerRunner>();
_faceLandMarkerRunner.enabled = true;
``` 

デフォルトで内カメが起動するようにWebCamSouce.csを修正します。[0]が外カメ、[1]が内カメになります。
```C#
webCamDevice = availableSources[1];
```

## 実機でビルドしてみる
こんな感じのメッシュが出てきたらトラッキングできています。

| |
| ---- | 
| ![Screenshot_20240910-094819.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113615/095da2f0-7237-24bf-227a-71fa7fdaffa0.png)| 

ビルド時にAssets/StreamingAssetsの中身が足りずにビルドできない場合はエラー文を見て必要なものを追加してください。

## アバターにフェイストラッキングの情報を反映する
FaceLandMarkerRunnerのresultからフェイストラッキングの情報（ランドマーク、BlendShape型、顔の回転と位置）が取得できるため、他のスクリプトからも参照できるようにゲッターとセッターを作成します。

```C#
    public FaceLandmarkerResult CurrentFaceLandmarkerResult { get; set; }

    private void OnFaceLandmarkDetectionOutput(FaceLandmarkerResult result, Image image, long timestamp)
    {
      CurrentFaceLandmarkerResult = result;
      _faceLandmarkerResultAnnotationController.DrawLater(result);
    }
```

そのままLive2Dのアバターに渡しても期待した動きが再現できなかったため、計算を加えます。
表情はfaceBlendshapes。顔の回転や位置はfacialTransformationMatrixesを使用して値を出しています。

```C#
using UnityEngine;
using Mediapipe.Tasks.Vision.FaceLandmarker;
using Mediapipe.Unity.Sample.FaceLandmarkDetection;

public class FaceTrackingCalculator : MonoBehaviour
{
    private FaceLandmarkerRunner _faceLandmarkerRunner;

    // 顔全体のブレンドシェイプ情報をメンバ変数として保持
    public float FaceAngleX { get; private set; }
    public float FaceAngleY { get; private set; }
    public float FaceAngleZ { get; private set; }
    public float LeftEyeBlink { get; private set; }
    public float RightEyeBlink { get; private set; }
    public float MouthForm { get; private set; }
    public float MouthOpen { get; private set; }
    public float EyeBallX { get; private set; }
    public float EyeBallY { get; private set; }

    void Start()
    {
        // FaceLandmarkerRunnerのインスタンスを取得
        _faceLandmarkerRunner = FindObjectOfType<FaceLandmarkerRunner>();
    }

    private void Update()
    {
        // FaceLandmarkerResult から BlendShape 情報を取得
        if (_faceLandmarkerRunner.CurrentFaceLandmarkerResult.faceBlendshapes != null && 
            _faceLandmarkerRunner.CurrentFaceLandmarkerResult.faceBlendshapes.Count > 0)
        {
            var blendShapes = _faceLandmarkerRunner.CurrentFaceLandmarkerResult.faceBlendshapes[0].categories;

            // 必要な BlendShape を探して値を設定
            foreach (var blendShape in blendShapes)
            {
                // BlendShape のログを出力
                // Debug.Log($"BlendShape Name: {blendShape.categoryName}, Score: {blendShape.score}");
                switch (blendShape.categoryName)
                {
                    case "eyeBlinkLeft":
                        var eyeLeftScore = 1 - blendShape.score;
                        LeftEyeBlink = (eyeLeftScore > 0.5f) ? 1 : 0;
                        break;

                    case "eyeBlinkRight":
                        var eyeRightScore = 1 - blendShape.score;
                        RightEyeBlink = (eyeRightScore > 0.5f) ? 1 : 0;
                        break;

                    case "eyeLookInLeft":
                        EyeBallX = blendShape.score;
                        break;

                    case "eyeLookOutRight":
                        EyeBallX = -blendShape.score;
                        break;

                    case "eyeLookUpLeft":
                        EyeBallY = blendShape.score;
                        break;

                    case "eyeLookDownRight":
                        EyeBallY = -blendShape.score;
                        break;

                    case "mouthFunnel":
                        MouthForm = 0.5f;
                        break;

                    case "jawOpen":
                        MouthOpen = (blendShape.score * 1.8f < 0.04f) ? 0 : blendShape.score * 1.8f;
                        break;
                }
            }
        }
        // 顔の回転角度を計算
        UpdateFaceTransformFromMediapipe(_faceLandmarkerRunner.CurrentFaceLandmarkerResult);
    }

    private void UpdateFaceTransformFromMediapipe(FaceLandmarkerResult result)
    {
        if (result.facialTransformationMatrixes != null && result.facialTransformationMatrixes.Count > 0)
        {
            // 最初の顔に対する変換行列を取得
            var facialMatrix = result.facialTransformationMatrixes[0];
            
            // Matrix4x4からQuaternionに変換
            Quaternion faceRotation = MatrixToQuaternion(facialMatrix);

            // X, Y, Z軸の回転角度を取得
            // Yは大体正面が180、上に向けば値が小さくなり、下に向けば値が大きくなる
            Vector3 eulerAngles = faceRotation.eulerAngles;
            float x = NormalizeAngle(eulerAngles.x);
            float y = eulerAngles.y - 180;
            float z = NormalizeAngle(eulerAngles.z);
            
            // Z軸は首の横の傾き。基準が90で、左に行けば0度、右に行けば180度になるようになっている
            // Z軸の回転を基準0度、左に行けば-30度、右に行けば30度になるように調整
            float adjustedZ = CalculateAdjustedZ(z);

            // Live2Dに適用するために角度を調整
            FaceAngleX = x;
            FaceAngleY = -y;
            FaceAngleZ = adjustedZ;
        }
    }
    
    private float CalculateAdjustedZ(float z)
    {
        if (z >= 0 && z <= 90)
        {
            // 0度から90度の範囲: -30から0へ
            return Mathf.Lerp(-30, 0, z / 90f);
        }
        else if (z > 90 && z <= 180)
        {
            // 90度から180度の範囲: 0から30へ
            return Mathf.Lerp(0, 30, (z - 90) / 90f);
        }
        else
        {
            // 万が一、zが0〜180度以外の場合（安全のため）
            return 0;
        }
    }
    
    private Quaternion MatrixToQuaternion(Matrix4x4 matrix)
    {
        // Matrix4x4からQuaternionに変換するユーティリティ関数
        return Quaternion.LookRotation(matrix.GetColumn(2), matrix.GetColumn(1));
    }

    private float NormalizeAngle(float angle)
    {
        if (angle > 180)
        {
            return angle - 360;
        }
        return angle;
    }
}
```

キュビズムパラメーターに値を渡します。

```C#
    void LateUpdate()
    {
        UpdateFaceTrackingData(
            _faceTrackingToBlendShape.FaceAngleX,
            _faceTrackingToBlendShape.FaceAngleY,
            _faceTrackingToBlendShape.FaceAngleZ,
            _faceTrackingToBlendShape.LeftEyeBlink,
            _faceTrackingToBlendShape.RightEyeBlink,
            _faceTrackingToBlendShape.MouthForm,
            _faceTrackingToBlendShape.MouthOpen,
            _faceTrackingToBlendShape.EyeBallX,
            _faceTrackingToBlendShape.EyeBallY);
    }

        public void UpdateFaceTrackingData(float faceAngleX, float faceAngleY, float faceAngleZ, float leftEye,
        float rightEye, float mouthForm, float mouthOpen, float eyeBallX, float eyeBallY)
    {
        
        float smoothFactor = 0.9f; // この値を調整して滑らかさを制御

        if (_localFaceAngleX != null) _localFaceAngleX.Value = SmoothParameter(_localFaceAngleX.Value, faceAngleX, smoothFactor);
        if (_localFaceAngleY != null) _localFaceAngleY.Value = SmoothParameter(_localFaceAngleY.Value, faceAngleY, smoothFactor);
        if (_localFaceAngleZ != null) _localFaceAngleZ.Value = SmoothParameter(_localFaceAngleZ.Value, faceAngleZ, smoothFactor);
        if (_localLeftEye != null) _localLeftEye.Value = SmoothParameter(_localLeftEye.Value, leftEye, smoothFactor);
        if (_localRightEye != null) _localRightEye.Value = SmoothParameter(_localRightEye.Value, rightEye, smoothFactor);
        if (_localMouthForm != null) _localMouthForm.Value = SmoothParameter(_localMouthForm.Value, mouthForm, smoothFactor);
        if (_localMouthOpen != null) _localMouthOpen.Value = SmoothParameter(_localMouthOpen.Value, mouthOpen, smoothFactor);
        if (_localEyeBallX != null) _localEyeBallX.Value = SmoothParameter(_localEyeBallX.Value, eyeBallX, smoothFactor);
        if (_localEyeBallY != null) _localEyeBallY.Value = SmoothParameter(_localEyeBallY.Value, eyeBallY, smoothFactor);
    }
    
    private float SmoothParameter(float currentValue, float targetValue, float smoothFactor)
    {
        return Mathf.Lerp(currentValue, targetValue, smoothFactor);
    }
```

:::note warn
警告
:bulb: Live2Dのパラメータ更新はLateUpdate()内で行う
:::

実機でビルドができ、アバターにフェイストラッキングの情報が反映されれば完成です！

## 終わりに
私自身Androidでの開発経験はありましたが,Unityでの開発は初めてでした。
古い端末だとカクカク動いてしまうためパフォーマンスの改善が必要な状況です😭
コードの書き方や、オブジェクトの配置など不適切なところがあれば教えていただきたいです！

また、今回は必要最低限のパラメータのみ更新しているので、より多くのパラメータを更新することによりリッチなモーションを作成することも可能だと思います！

MediaPipeUnityPluginを用いてLive2Dアバターを動かしたいと思っている方に少しでも参考になれば幸いです:v: