Unity
OVRLipsync
VRM

VRMのBlendShapeをAnimationClipに記録し、どのアバターでも使える表情モーションを作成する

概要

・通常ではAnimationClipに記録できないVRMのBlendShapeを記録、再生できるようにする。
・ついでに、VRMでOVRLipSyncをそのまま利用できるようにする。

VRMのBlendShapeは、AnimationClipで制御できない

VRMは、アバターの情報を共通化されたフォーマットにまとめてくれているナイスな規格です。
そのため、これまではアバターの差し替えによって色々と調整する必要があったところが、およそ無調整で対応できるようになりました。

BlendShapeも共通化されていて、あらゆるアバターのBlendShapeは「VRMBlendShapeProxy」を使い、共通のスクリプトで操作できるようになっています。
clip1.png
しかし、このVRMBlensShapeProxy、悲しいことに(現時点では)AnimationClipで制御できないんですよ……。下図のとおり、AnimationClipに設定できるプロパティがありません。
clip2.png
そのため、どのアバターでも共通で使えるモーションをAnimationClipで作りたくても、BlendShapeを組み込むことができないという悩みがあります。

空のSkinnedMeshRendererを組み込み、VRMBlendShapeProxyと連動させる

解決策は、AnimationClipで設定できるプロパティを作成して、その値をVRMBlendShapeProxyと連動させることです。
単なるスクリプトの変数でもいいと思いますが、SkinnedMeshRendererであればOVRLipSyncなどの表情操作ツールにも適用できるようになるので、一石二鳥です。
新たにSkinnedMeshRendererを作成し、VRMと同構成のBlendShapeを組み込む方法を採ります。

そのスクリプトが以下のとおり。これを、VRMアバターのルートオブジェクト(=VRMBlendShapeProxyが存在するオブジェクト)に追加します。
VRMアバターのオブジェクトに手を加えたくなければ、別のスクリプトから動的に組み込んでください。(私はそうしました)

(2019/1/11 AnimatorControllerを貼りなおす処理を追加しました)

BlendShapeAnimationController.cs
using System.Collections.Generic;
using UnityEngine;
using VRM;

public class BlendShapeAnimationController : MonoBehaviour {

    public bool isActive = true;
    public bool lipSyncTarget = false;

    VRMBlendShapeProxy proxy;
    SkinnedMeshRenderer smr;
    List<string> shapes = new List<string>();

    void Start ()
    {
        proxy = GetComponent<VRMBlendShapeProxy>();

        //VRMのBlendShape情報を取得
        BlendShapeAvatar bsa = GetComponent<VRMBlendShapeProxy>().BlendShapeAvatar;

        //VRMのBlendShapeClipリストからBlendShape名を取り出し、新規Meshに登録する
        Mesh smr_mesh = new Mesh();
        foreach (BlendShapeClip clip in bsa.Clips)
        {
            string sn = clip.BlendShapeName;
            smr_mesh.AddBlendShapeFrame(sn, 1, new Vector3[0], new Vector3[0], new Vector3[0]);
            shapes.Add(sn);
        }

        //新たなSkinnedMeshRendererをVRMBlendShapeProxyのあるGameObjectに追加
        smr = transform.gameObject.AddComponent<SkinnedMeshRenderer>();

        //SkinnedMeshRendererにMeshを登録
        smr.sharedMesh = smr_mesh;

        //LipSyncを適用させる
        if (lipSyncTarget)
        {
            SetLipSyncTarget();
        }

        //AnimatorControllerがあれば貼りなおす
        Animator anim = GetComponent<Animator>();
        if (anim)
        {
            RuntimeAnimatorController rac = anim.runtimeAnimatorController;
            if (rac)
            {
                anim.runtimeAnimatorController = null;
                anim.runtimeAnimatorController = rac;
            }
        }
    }

    void Update ()
    {
        if (isActive)
        {
            for (int i = 0; i < shapes.Count; ++i)
            {
                //Shape値を取得
                float weight = smr.GetBlendShapeWeight(i) / 100;

                //VRMBlendShapeProxyに反映
                proxy.ImmediatelySetValue(shapes[i], weight);
            }
        }
    }

    public void SetLipSyncTarget()
    {
        OVRLipSyncContextMorphTarget target = FindObjectOfType<OVRLipSyncContextMorphTarget>();
        target.skinnedMeshRenderer = smr;
        target.enabled = true;
    }
}

実行すると、VRMと同じBlendShapeキーを持ったSkinnedMeshRendererコンポーネントが姿を現します。
clip3.png
BlendShape値を変更すると、連動してVRMBlendShapeProxyのBlendShapeも変更され、モデルにも反映されることを確認してください。
ちなみに、SkinnedMeshRendererのBlendShapeは100が最大値ですが、VRMBlendShapeProxyは1が最大値となるため、値を100分の1にしています。

実行中にAnimationClipを編集する

こうなれば、BlendShapeの情報をAnimationClipに登録できますね!
実行中でなければ登録できないというのは少し難点ですが。
clip4.png
もちろん、Animationを再生するときも上記スクリプトが必須です。
スクリプトさえ組み込んでおけば、アバターを切り替えても同じAnimationClipが使えるようになります。
clip5.png

オマケ:OVRLipSyncを使えるようにする

先に少し触れましたが、このスクリプトを使うことでOVRLipSyncをそのまま使用できるようにもなります。
方法は、このスクリプトを静的に組み込んでいる場合、InspectorでlipSyncTargetをtrueにしてください。
動的に組み込んでいる場合、組み込んだ直後にlipSyncTargetをtrueにしてください。

そして、OVRLipSyncContextMorphTargetを非アクティブにしておきます。
image.png
これは、OVRLipSyncContextMorphTargetがStart()で設定するようになっているため、開始時点でアクティブになっているとSkinnedMeshRendererの設定のタイミングを損なうためです。

なお、これはあくまでも副産物なので、VRMにOVRLipSyncを対応させる目的だけであれば、坪倉さんのVRMLipSyncContextMorphTargetで十分です。

注意点

VRMBlendShapeProxyを常時連動させているため、これが生きている間は逆に他のスクリプトからVRMBlendShapeProxyを制御できなくなる可能性があります。
その場合、AnimationClipを再生しないときは連動処理を停止させる(isActive=false)などしてください。