#概要
・通常ではAnimationClipに記録できないVRMのBlendShapeを記録、再生できるようにする。
・ついでに、VRMでOVRLipSyncをそのまま利用できるようにする。
#VRMのBlendShapeは、AnimationClipで制御できない
VRMは、アバターの情報を共通化されたフォーマットにまとめてくれているナイスな規格です。
そのため、これまではアバターの差し替えによって色々と調整する必要があったところが、およそ無調整で対応できるようになりました。
BlendShapeも共通化されていて、あらゆるアバターのBlendShapeは「VRMBlendShapeProxy」を使い、共通のスクリプトで操作できるようになっています。
しかし、このVRMBlensShapeProxy、悲しいことに(現時点では)AnimationClipで制御できないんですよ……。下図のとおり、AnimationClipに設定できるプロパティがありません。
そのため、どのアバターでも共通で使えるモーションをAnimationClipで作りたくても、BlendShapeを組み込むことができないという悩みがあります。
#空のSkinnedMeshRendererを組み込み、VRMBlendShapeProxyと連動させる
解決策は、AnimationClipで設定できるプロパティを作成して、その値をVRMBlendShapeProxyと連動させることです。
単なるスクリプトの変数でもいいと思いますが、SkinnedMeshRendererであればOVRLipSyncなどの表情操作ツールにも適用できるようになるので、一石二鳥です。
新たにSkinnedMeshRendererを作成し、VRMと同構成のBlendShapeを組み込む方法を採ります。
そのスクリプトが以下のとおり。これを、VRMアバターのルートオブジェクト(=VRMBlendShapeProxyが存在するオブジェクト)に追加します。
VRMアバターのオブジェクトに手を加えたくなければ、別のスクリプトから動的に組み込んでください。(私はそうしました)
(2019/1/11 AnimatorControllerを貼りなおす処理を追加しました)
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コンポーネントが姿を現します。
BlendShape値を変更すると、連動してVRMBlendShapeProxyのBlendShapeも変更され、モデルにも反映されることを確認してください。
ちなみに、SkinnedMeshRendererのBlendShapeは100が最大値ですが、VRMBlendShapeProxyは1が最大値となるため、値を100分の1にしています。
#実行中にAnimationClipを編集する
こうなれば、BlendShapeの情報をAnimationClipに登録できますね!
実行中でなければ登録できないというのは少し難点ですが。
もちろん、Animationを再生するときも上記スクリプトが必須です。
スクリプトさえ組み込んでおけば、アバターを切り替えても同じAnimationClipが使えるようになります。
#オマケ:OVRLipSyncを使えるようにする
先に少し触れましたが、このスクリプトを使うことでOVRLipSyncをそのまま使用できるようにもなります。
方法は、このスクリプトを静的に組み込んでいる場合、InspectorでlipSyncTargetをtrueにしてください。
動的に組み込んでいる場合、組み込んだ直後にlipSyncTargetをtrueにしてください。
そして、OVRLipSyncContextMorphTargetを非アクティブにしておきます。
これは、OVRLipSyncContextMorphTargetがStart()で設定するようになっているため、開始時点でアクティブになっているとSkinnedMeshRendererの設定のタイミングを損なうためです。
なお、これはあくまでも副産物なので、VRMにOVRLipSyncを対応させる目的だけであれば、坪倉さんのVRMLipSyncContextMorphTargetで十分です。
#注意点
VRMBlendShapeProxyを常時連動させているため、これが生きている間は逆に他のスクリプトからVRMBlendShapeProxyを制御できなくなる可能性があります。
その場合、AnimationClipを再生しないときは連動処理を停止させる(isActive=false)などしてください。