【FaceMash】vrm使ってVtuberになれる仕組みを作ってみた（顔編）

# 前書き
MediaPipeを勉強がてらVrmの表情操作を行えるようにしてみようと思って作りました
顔編と題しているものの、体編など作るかは未定です。。。（期待してたかたすいませんmm
また、コードの全文は記載しません
FaceMeshの頂点データの番号は記載します（調べるの面倒だったので少しでも助けになれば

　
# 完成品
![tes.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370747/4c3e7120-3aae-e31c-6d33-b73224b15fdc.gif)

右上が自分の顔の動きです
ちなみに、細かい部分の動きはかなり誤魔化してます

# 使用技術
・Unity2022.3.20f1
・vrm1.0
・https://github.com/homuler/MediaPipeUnityPlugin
・UniTask

# 苦戦したこと
・頂点のIndex調べる
・vrmのパラメーターにfaceMeshの値を補正する
正直このぐらいでした
それ以外はすんなりできた

# 参考資料
↓の記事を参考に補正を作ってみたり
https://note.com/reality_eng/n/ndaddbaf3aaae
https://note.com/reality_eng/n/nb36f85b8123e

↓の記事から顔の向きを作ってみたり
https://zenn.dev/ykesamaru/articles/025b0eabf24e20

# コード

## 頂点のIndex（2024/04/27時点
```Csharp
public static class FaceLandmarkIndex
{
    // 鼻の頭
    public const int NoseTop = 1;
    
    //　唇
    public const int UpperLipBottom = 13;
    public const int LowerLipTop = 14;
    public const int UpperLipTop = 0;
    public const int LowerLipBottom = 17;
    
    //口角
    public const int MouthCornerRight = 61;
    public const int MouthCornerLeft = 291;

    //輪郭
    public const int ContourBottom = 152;
    public const int ContourRight = 93;
    public const int ContourLeft = 366;
    public const int ContourTop = 10;

    //まぶた
    public const int LeftEyelidLower = 374;
    public const int LeftEyelidUpper = 386;
    public const int RightEyelidUpper = 159;
    public const int RightEyelidLower = 144;

    //眉
    public const int LeftBrowCenterUpper = 336;
    public const int LeftBrowCenterLower = 285;
    public const int LeftBrowOuterUpper = 293;
    public const int LeftBrowOuterLower = 283;
    public const int RightBrowCenterUpper = 107;
    public const int RightBrowCenterLower = 55;
    public const int RightBrowOuterUpper = 63;
    public const int RightBrowOuterLower = 53;

    //目
    public const int RightEye = 469;
    public const int LeftEye = 474;
}
```

## 全体のコード

```CSharp
public class VrmFace : MonoBehaviour
{
    [SerializeField] private Vrm10Instance vrmInstance;
    
    /* 補正値などを入れておくと便利 */
    
    private Vrm10RuntimeExpression vrm10RuntimeExpression;

    //
    private bool isInitialized = false;
    private bool isDestroyed = false;
    private NormalizedLandmarkList defaultFaceLandmarks;
    private NormalizedLandmarkList tempFaceLandmarks;
    
    private Quaternion neckDefaultRotation;
    private Quaternion chestDefaultRotation;
    
    
    private void Start()
    {
        //初期化処理を登録
        initializeButton.onClick.AddListener(InitializeFace);
        
        //VRMの表情を取得
        vrm10RuntimeExpression = vrmInstance.Runtime.Expression;
        
        //初期化
        /* 補正値 */
    }

    private void OnDestroy()
    {
        isDestroyed = true;
    }

    public void InitializeFace()
    {
        //デフォルトの表情を取得
        defaultFaceLandmarks = tempFaceLandmarks.Clone();
        isInitialized = true;
    }

    //自分でカスタマイズしたGraphRunnerとImageSourceSolutionから呼び出している
    public async UniTaskVoid SyncFace(NormalizedLandmarkList resultFaceLandmarksWithIris)
    {
        tempFaceLandmarks = resultFaceLandmarksWithIris;
        if (!isInitialized) return;

        // mediaPipe側でコールバックで呼び出される際はMainThreadにいない時があるため戻しておく
        await UniTask.SwitchToMainThread();
        if(this == null || isDestroyed) return;
        
        //表情の変更処理
        if (tempFaceLandmarks?.Landmark == null) return;
        
        var lipLength = GetPureFaceLandmarkDistance(FaceLandmarkIndex.UpperLipBottom, FaceLandmarkIndex.LowerLipTop);
        var mouseWidth = GetPureFaceLandmarkDistance(FaceLandmarkIndex.MouthCornerRight, FaceLandmarkIndex.MouthCornerLeft);
        var leftEyelidLength = GetPureFaceLandmarkDistance(FaceLandmarkIndex.LeftEyelidLower, FaceLandmarkIndex.LeftEyelidUpper);
        var rightEyelidLength = GetPureFaceLandmarkDistance(FaceLandmarkIndex.RightEyelidLower, FaceLandmarkIndex.RightEyelidUpper);
        
        /*非公開　口、口角、瞬きの処理*/
        
        /* ここから目の位置を変更する処理
         むずいのでこのままだと動かないためコメントアウト（代用としてLookAtを使用する
        /*非公開　目のトラッキング、調整がむずいため断念*/
        */
        
        //顔の回転
        /*非公開*/
        
        //体の向き
        /*非公開*/
        
        
        
        debugText.text = $"Lip: {lipValue}\n" +
                        $"Mouth: {mouthValue}\n" +
                        $"Blink: {leftBlinkValue} {rightBlinkValue}\n" +
                        /*$"Eye: {leftEyeMovement} {rightEyeMovement}"*/
                        $"Angle: {angleY} {angleX}";
        
        
    }
    
    private (float, float) GetPureMovableVector(int index)
    {
        /*非公開*/
    }
    
    private float GetPureFaceLandmarkDistance(int index1, int index2)
    {
        /*非公開　各Landmarkの座標の距離などを計算する関数*/
    }
    
    private float GetValue(float current, float higher, float defaultValue)
    {
        /*非公開　瞬きや口の補正値関数*/
    }
}


```