【Unity】VRMの表情をTimelineで制御する

こんにちは🌟八ツ橋まろんですっ‼

VRMの表情をTimelineで制御するスクリプトを作りました。

(スクリプトは [FANBOX](https://www.fanbox.cc/@maron/posts/4840242) および[GitHub](https://github.com/MaronVtuber/VRMBlendShapeOverride)で配布しています)


2018年に『VRM』という規格が作られて、これがアバターの統一規格のひとつとして使いやすく、様々なアプリ制作で使われていますね。

UnityではHumanoid構造な3Dモデルは既存のダンスモーション(Humanoidアニメーション)なんかを共通で使いまわすことができます。VRMモデルは全てHumanoid構造なので、どんなVRMモデルをダウンロードしてきても既存のHumanoidアニメーションがそのまま使えます。


一方で、Unityでキャラクターの表情や口パクなどのブレンドシェイプを動かすアニメーションはHumanoid構造から外れて名前参照のGenericアニメーションになるため、他のアバターと共有はできません。(3Dモデル毎にパラメータの名前が違うため)

[画像：表情アニメーションは共有できない]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478685/a7453bc7-3cc3-af79-ae55-660beaac0b57.png)


VRMモデルには「A,I,U,E,O,Fun,Blink」などの表情が定義されていて、これはVRMアバター共通で動かすことができます。
[画像：VRMモデルは同じ命令で同じ表情を作れる]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478685/248d1426-e5e7-1e75-79f0-57fefc804f63.png)


VRMの表情操作は便利ですが、VRMのアバターに付いている『VRM Blend Shape Proxyコンポーネント』にスクリプトで命令して操作しないといけないので、プログラミングの知識が必須で、初心者には難易度が高いです。

この敷居を下げて、いろんな人が表情操作できるようになると良いなと思い、
 **『TimelineでVRMの表情を操作できるようになるTimeline拡張スクリプト』** 
を作成しました。

Timelineの拡張スクリプトなので、以下の4つを作成しています。
①～Behaviour.cs
②～Clip.cs
③～Track.cs
④～Mixer.cs

①～③は大したオリジナリティはなく、お手本通りに作成し、必要事項だけ変更しています。④は処理の記述があるのでしっかり書いています。

```csharp:① VrmBlendShapeBehaviour.cs
using UnityEngine;
using UnityEngine.Playables;
using VRM;

[System.Serializable]
public class VrmBlendShapeBehaviour : PlayableBehaviour
{
    public BlendShapePreset blendShapePreset = BlendShapePreset.A;
    public float blendShapeValue = 1f;
}

```
①ではTimelineに置いたときの設定値を定義します。TimelineでVRMのBlensShapeのプリセットを呼び出すため、BlendShapePresetとそのウェイト(float)の2点を書きました。
デフォルトでプリセットの"A"を指定し、ウェイトは1.0を指定しています。


```csharp:② VrmBlendShapeClip.cs
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

public class VrmBlendShapeClip : PlayableAsset, ITimelineClipAsset
{
    public VrmBlendShapeBehaviour behaviour = new VrmBlendShapeBehaviour();

    public ClipCaps clipCaps
    {
        get
        {
            return ClipCaps.Blending | ClipCaps.SpeedMultiplier;
        }
    }

    public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    {
        return ScriptPlayable<VrmBlendShapeBehaviour>.Create(graph);
    }
}
```
②はclipですが、特殊なことはせずお手本通りのスクリプトを使っています。


```csharp:③ VrmBlendShapeTrack.cs
using System.Linq;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
using VRM;

[TrackColor(0.5f, 0.7f, 0.5f)]
[TrackBindingType(typeof(VRMBlendShapeProxy))]
[TrackClipType(typeof(VrmBlendShapeClip))]
public class VrmBlendShapeTrack : TrackAsset
{
    public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
    {
        var mixer = ScriptPlayable<VrmBlendShapeMixerBehaviour>.Create(graph, inputCount);
        mixer.GetBehaviour().Clips = GetClips().ToArray();
        mixer.GetBehaviour().Director = go.GetComponent<PlayableDirector>();

        // 名前変更
        foreach (TimelineClip clip in m_Clips)
        {
            var playableAsset = clip.asset as VrmBlendShapeClip;
            clip.displayName = playableAsset.behaviour.blendShapePreset.ToString();
        }
        return mixer;
    }
}
```
③はTrackです。Timeline上での色や、Trackに置くことのできるClipの指定、VRM Blend Shape Proxyを対象に取ること、Timeline上でのClipの名前をBlendShapePreset名に変更することなどを記述しています。

```csharp:④ VrmBlendShapeMixerBehaviour.cs
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
using System.Collections.Generic;
using VRM;

public class VrmBlendShapeMixerBehaviour : PlayableBehaviour
{
    public TimelineClip[] Clips { get; set; }
    public PlayableDirector Director { get; set; }

    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        var proxy = playerData as VRMBlendShapeProxy;
        if (proxy == null)
        {
            return;
        }

        var time = Director.time;

        // 「あいうえお」
        var value_A = 0f;
        var value_I = 0f;
        var value_U = 0f;
        var value_E = 0f;
        var value_O = 0f;

        // 「喜怒哀楽」
        var value_Angry = 0f;
        var value_Blink = 0f;
        var value_Blink_L = 0f;
        var value_Blink_R = 0f;
        var value_Fun = 0f;
        var value_Joy = 0f;
        var value_Sorrow = 0f;
        var value_Netural = 0f;
        var isLipSync = false;
        var isFacial = false;


        for (int i = 0; i < Clips.Length; i++)
        {
            var clip = Clips[i];
            var clipAsset = clip.asset as VrmBlendShapeClip;
            var behaviour = clipAsset.behaviour;
            var clipWeight = playable.GetInputWeight(i);
            var clipProgress = (float)((time - clip.start) / clip.duration);

            if (clipProgress >= 0.0f && clipProgress <= 1.0f)
            {
                switch (behaviour.blendShapePreset)
                {
                    // 「あいうえお」
                    case BlendShapePreset.A:
                        value_A += clipWeight * behaviour.blendShapeValue;
                        isLipSync = true;
                        break;
                    case BlendShapePreset.I:
                        value_I += clipWeight * behaviour.blendShapeValue;
                        isLipSync = true;
                        break;
                    case BlendShapePreset.U:
                        value_U += clipWeight * behaviour.blendShapeValue;
                        isLipSync = true;
                        break;
                    case BlendShapePreset.E:
                        value_E += clipWeight * behaviour.blendShapeValue;
                        isLipSync = true;
                        break;
                    case BlendShapePreset.O:
                        value_O += clipWeight * behaviour.blendShapeValue;
                        isLipSync = true;
                        break;
                    case BlendShapePreset.Unknown:
                        isLipSync = true;
                        break;

                    // 「喜怒哀楽」
                    case BlendShapePreset.Angry:
                        value_Angry += clipWeight * behaviour.blendShapeValue;
                        isFacial = true;
                        break;
                    case BlendShapePreset.Blink:
                        value_Blink += clipWeight * behaviour.blendShapeValue;
                        isFacial = true;
                        break;
                    case BlendShapePreset.Blink_L:
                        value_Blink_L += clipWeight * behaviour.blendShapeValue;
                        isFacial = true;
                        break;
                    case BlendShapePreset.Blink_R:
                        value_Blink_R += clipWeight * behaviour.blendShapeValue;
                        isFacial = true;
                        break;
                    case BlendShapePreset.Fun:
                        value_Fun += clipWeight * behaviour.blendShapeValue;
                        isFacial = true;
                        break;
                    case BlendShapePreset.Joy:
                        value_Joy += clipWeight * behaviour.blendShapeValue;
                        isFacial = true;
                        break;
                    case BlendShapePreset.Sorrow:
                        value_Sorrow += clipWeight * behaviour.blendShapeValue;
                        isFacial = true;
                        break;
                    case BlendShapePreset.Neutral:
                        value_Netural += clipWeight * behaviour.blendShapeValue;
                        isFacial = true;
                        break;
                }
            }
        }

        // 「あいうえお」と「喜怒哀楽」どちらを操作するか判断して適用する
        if (isLipSync)
        {
            proxy.SetValues(new Dictionary<BlendShapeKey, float>{
                {BlendShapeKey.CreateFromPreset(BlendShapePreset.A), value_A },
                {BlendShapeKey.CreateFromPreset(BlendShapePreset.I), value_I },
                {BlendShapeKey.CreateFromPreset(BlendShapePreset.U), value_U },
                {BlendShapeKey.CreateFromPreset(BlendShapePreset.E), value_E },
                {BlendShapeKey.CreateFromPreset(BlendShapePreset.O), value_O },
            });
        }
        else if(isFacial)
        {
            proxy.SetValues(new Dictionary<BlendShapeKey, float>{
                {BlendShapeKey.CreateFromPreset(BlendShapePreset.Angry), value_Angry },
                {BlendShapeKey.CreateFromPreset(BlendShapePreset.Blink), value_Blink },
                {BlendShapeKey.CreateFromPreset(BlendShapePreset.Blink_L), value_Blink_L },
                {BlendShapeKey.CreateFromPreset(BlendShapePreset.Blink_R), value_Blink_R },
                {BlendShapeKey.CreateFromPreset(BlendShapePreset.Fun), value_Fun },
                {BlendShapeKey.CreateFromPreset(BlendShapePreset.Joy), value_Joy },
                {BlendShapeKey.CreateFromPreset(BlendShapePreset.Sorrow), value_Sorrow },
                {BlendShapeKey.CreateFromPreset(BlendShapePreset.Neutral), value_Netural },
            });
        }
    }
}
```

④では、Timelineの進行に応じて表情適用の処理を行います。
映像制作では目の表情と口パクを分けて使いたいので、配置されたプリセットが目の表情なのか口パクなのかを判断して適用しています。

たとえば「A」の口をさせたい時、すでに「O」の口をしている状態に追加して「A」を適用すると口を開きすぎるので、「A」の口の時は他の「I」「U」「E」「O」は0%にする処理をしています。

「Angry」を適用したいとき、これは口パクには関係ないので、「A」「I」「U」「E」「O」には触らず「Fun」や「Joy」などを0%にして、「Angry」を適用します。

これにより、Timelineで表情操作を2行使うことで1行目で目の表情操作、2行目で口パクの操作ができます。
(キャラクターによってはFunやJoyに口を開ける操作が入っていることがあるので、絶対に口パクに干渉せず操作できるわけではありませんが)


このスクリプトを使えば、UnityのTimelineを使ってダンスアニメーションに合わせたVRMアバターの表情づけや口パクが簡単に作成できます。

実際にダンスモーションと組み合わせたサンプルは[こちら](https://maron.fanbox.cc/posts/4838662)で配布しています。


使い方の動画はYouTubeにアップしています。
https://youtu.be/JP2CstQHIvk



VRMの表情操作のTimeline拡張を作りました。従来のダンスモーションのみの配布から、ダンス+表情・口パクのセットで配布できるようになります。よければ使ってみてください。

それでは🌟