パーフェクトシンク対応(VRM0)

Blender で パーフェクトシンク用のシェイプキー(Shape Key)を追加して、Unity で VRM0 のブレンドシェイプに設定する手順について説明する。VRM1 に関しては、「パーフェクトシンク対応(VRM1)」という記事を参照してほしい。

前提

VRoid で作成したモデルは想定していない。

パーフェクトシンクとは

パーフェクトシンクは、ARKit[1] で取得出来る BlendShapeLocation[2] 全52点を VRM の同名 BlendShapeClip に適用させる設定です。(中略)
パーフェクトシンクを使う事で、実際のユーザーの表情を直接アバターに適用する事が出来ます。これによって、非常に表情豊かな表現をすることが出来ます。(中略)
「パーフェクトシンク」はARKitの特徴を利用してモーフィングする挙動を指す造語です

パーフェクトシンクについて
  1. Apple 社が開発した iOS デバイスで動作する AR フレームワーク、API
  2. 検出された表情における顔の部位の位置(動き)を係数として記述したもの

スクリプト①

import bpy
def add_shape_keys():
    
    keys = [
        "Basis",
        "BrowDownLeft",
        "BrowDownRight",
        "BrowInnerUp",
        "BrowOuterUpLeft",
        "BrowOuterUpRight",
        "CheekPuff",
        "CheekSquintLeft",
        "CheekSquintRight",
        "EyeBlinkLeft",
        "EyeBlinkRight",
        "EyeLookDownLeft",
        "EyeLookDownRight",
        "EyeLookInLeft",
        "EyeLookInRight",
        "EyeLookOutLeft",
        "EyeLookOutRight",
        "EyeLookUpLeft",
        "EyeLookUpRight",
        "EyeSquintLeft",
        "EyeSquintRight",
        "EyeWideLeft",
        "EyeWideRight",
        "JawForward",
        "JawLeft",
        "JawOpen",
        "JawRight",
        "MouthClose",
        "MouthDimpleLeft",
        "MouthDimpleRight",
        "MouthFrownLeft",
        "MouthFrownRight",
        "MouthFunnel",
        "MouthLeft",
        "MouthLowerDownLeft",
        "MouthLowerDownRight",
        "MouthPressLeft",
        "MouthPressRight",
        "MouthPucker",
        "MouthRight",
        "MouthRollLower",
        "MouthRollUpper",
        "MouthShrugLower",
        "MouthShrugUpper",
        "MouthSmileLeft",
        "MouthSmileRight",
        "MouthStretchLeft",
        "MouthStretchRight",
        "MouthUpperUpLeft",
        "MouthUpperUpRight",
        "NoseSneerLeft",
        "NoseSneerRight",
        "TongueOut"
    ]
    if bpy.context.mode != 'OBJECT':
        bpy.ops.object.mode_set(mode='OBJECT')
    
    for key in keys:
        bpy.context.active_object.shape_key_add(name=key)
    
add_shape_keys()

空のシェイプキーを追加する

  1. ワークスペースを Scripting に変更する
  2. シェイプキーを追加したいオブジェクトを選択する
  3. 上記のスクリプト①をコピペする
  4. スクリプトを実行する

VRChat や VRM で使用する空のシェイプキーも追加する場合は、「VRChat と VRM で使用する空のシェイプキーを追加する」という記事を参考にしてほしい。

表情を作成する

はいぬっかさんの以下の記事を参考にして、Blender で表情を作成する。
パーフェクトシンク対応モデルをつくろう! iPhone顔トラッキング用 52BlendShapes制作メモ – はいぬっかメモ (hatenablog.com)

ブレンドシェイプを手動で設定したくない

Unity にて、52 個の BlendShapeClip を作成して、その全てに Weight をセットする作業は、非常に時間がかかるので手動では決してやりたくない。スクリプトを用いて設定する。

スクリプト②

using System.IO;
using UnityEditor;
using UnityEngine;

namespace VRM
{
    public class AddPerfectSyncBlendShapeClip : MonoBehaviour
    {
        // AssetsフォルダのVRMプレハブをセットしてもらう
        public GameObject VRMPrefab;
        // 縦の三点リーダーから関数を実行してもらう
        [ContextMenu("AddPerfectSyncBlendShapeClip")]
        public void AddBlendShapeClip()
        {
            // パスを特定する
            var vrmPrefabPath = AssetDatabase.GetAssetPath(VRMPrefab);
            var ext = ".prefab";
            var blendShapePath = vrmPrefabPath.Replace(ext, ".BlendShapes/BlendShape.asset");

            // アバターをロードする
            var blendShapeAvatar = AssetDatabase.LoadAssetAtPath<BlendShapeAvatar>(blendShapePath);

            // BlendShapeがあるメッシュを特定する
            Mesh mesh;
            int blendShapeCount;
            int transformCount = VRMPrefab.transform.childCount;
            for (int i = 0; i < transformCount; i++)
            {
                var transform = VRMPrefab.transform.GetChild(i);
                var skinnedMeshRenderer = transform.GetComponent<SkinnedMeshRenderer>();
                if (skinnedMeshRenderer != null)
                {
                    blendShapeCount = skinnedMeshRenderer.sharedMesh.blendShapeCount;
                    if (blendShapeCount != 0)
                    {
                        mesh = skinnedMeshRenderer.sharedMesh;

                        // Clipを追加してゆく
                        for (int j = 0; j < blendShapeCount; j++)
                        {
                            var name = mesh.GetBlendShapeName(j);
                            var assetName = name + ".asset";
                            var clipPath = blendShapePath.Replace("BlendShape.asset", assetName);
                            var DefaultClipPath = blendShapePath.Replace(".asset", "." + assetName);
                            if (!File.Exists(clipPath) && !File.Exists(DefaultClipPath))
                            {
                                var clip = ScriptableObject.CreateInstance<BlendShapeClip>();
                                clip.name = name;
                                clip.BlendShapeName = name;
                                clip.Preset = BlendShapePreset.Unknown;
                                AssetDatabase.CreateAsset(clip, clipPath);
                                if (blendShapeAvatar.GetClip(name) == null)
                                {
                                    blendShapeAvatar.Clips.Add(clip);
                                }
                            }
                        }

                        // Weightを設定してゆく
                        var proxy = VRMPrefab.GetComponent<VRMBlendShapeProxy>();
                        var meshName = mesh.name.Replace(".baked", "");
                        for (int j = 0; j < blendShapeCount; j++)
                        {
                            for (int k = 0; k < proxy.BlendShapeAvatar.Clips.Count; k++)
                            {
                                var clip = proxy.BlendShapeAvatar.Clips[k];
                                if (mesh.GetBlendShapeName(j) == clip.BlendShapeName)
                                {
                                    var value = new BlendShapeBinding
                                    {
                                        RelativePath = meshName,
                                        Index = j,
                                        Weight = 100.0f
                                    };
                                    clip.Values = null;
                                    clip.Values = new BlendShapeBinding[1] { value };
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

ブレンドシェイプをスクリプトで設定する

  1. Assets フォルダの適当な場所に、AddPerfectSyncBlendShapeClip という名前の C# Script を作成する
    (Project タブで右クリック > Create > C# Script)
  2. スクリプトをダブルクリックしてエディタで開き、上記のスクリプト②をコピペして、上書き保存する
    (Unity にアタッチする必要がある場合もある)
  3. GameObject を新規に作成する(Hierarchy タブで右クリック > Create Empty)
  4. GameObeject にスクリプトをドラッグ&ドロップでアタッチする
  5. VRM のプレハブをスクリプトの変数にセットする(Project タブからドラッグ&ドロップ)
  6. 関数(AddPerfectSyncBlendShapeClip)を実行する

BlendShapeProxy にパーフェクトシンク用の BlendShape が追加されたことを確認する。

最後に、作成したスクリプトとゲームオブジェクトを削除する。

ドキュメント

Apple Developer – Arkit / ARFaceanchor / BlendShapeLocationARKit
Unity Documentation – Enum ARKitBlendShapeLocation | ARKit Face Tracking | 1.0.14 (unity3d.com)

所感

  • VRoid で作成したモデルは、頂点データが同じであるため、自動でパーフェクトシンクを設定してくれるツールが存在する。フルスクラッチで作成したモデルは、52 個の表情を手動で作成するしかない。骨の折れる作業だ。
  • スクリプトには必要最低限のコードしか書いていないので、適宜修正して使ってほしい。
  • パーフェクトシンクという名前は大げさだと思う。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です