Edited at

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)などしてください。