Unity
音声認識
Vtuber

レイテンシの軽減とリミテッドアニメーションを実現するAniLipSyncを使ってみる

まず、AniLipSyncとは?

AniLipSyncとは、Unite Tokyo 2018にてGOROman(@GOROman )氏による講演「AniCast!東雲めぐちゃんの魔法ができるまで」にて発表されたOVRLipSyncの拡張ライブラリです。OVRLipSyncとは?という方は凹さんのこちらの記事が参考になると思います。OVRLipSyncとの違いは、音声入力から口を動かすまでの遅延が軽減されていること、リミテッドアニメーションっぽいリップシンクを実現していることです。MITライセンスとなっており、こちらのリポジトリで公開されています。

百聞は一見に如かず、ということでOvrLipSyncとAniLipSyncを見比べてみましょう。

(音声は効果音ラボからお借りしました。)

いかがでしょうか、AniLipSyncの方が口がパクパクと動いて、キャラクターが生き生きとしているように見えませんか?AniLipSyncはあえてフレートを落としたり、口の形を母音に絞ったりすることで、実際のアニメのような表現を実現しているのです。アニメはコマ割りで作られており、1秒あたり何枚と制限された中で表現されています。AniLipSyncはそのアニメ特有のコマ割りの表現を再現したような感じですね。また、音声入力から口を動かすまでの遅延に関しても大幅な改善が施されており、AniLipSyncはOvrLipSyncと比べて約150msも遅延を軽減しています

AniLipSyncのざっくりとした処理の流れ

LowLatencyLipSyncContext.csMicrophone.Start()でAudioClipを生成し、そのAudioClipからAudioClip.GetData()で音声データを取得し、コンテキストに渡します。そしてその音声データをOVRLipSync.csにインポートされているDLLの認識アルゴリズムに渡し、音素などの解析を行います。その結果を元にAnimMorphTarget.csがskinnedMeshRendererのBlendshapeを変更し、口の動きに反映させています。また、このAnimMorphTarget.csにはリミテッドアニメーションを再現する機能も備わっており、各パラメータを元に口の動きを制御しています。

AniLipSyncImage.png

遅延の軽減について

OVRLipSyncではMicrophone.Start()OnAudioFilterRead()の組み合わせによって音声を取得しており、AniLipSyncはMicrophone.Start()AudioClip.GetData()の組み合わせによって音声を取得しています。遅延に差が出ているのは主にこの部分によるものです。詳しい内容は @TyounanMOTI さんによる Unityでのマイク録音3種盛り:レイテンシ比較にあるので、ぜひ読んでみてください。

動作環境

自分が使用した動作環境

  • Windows 10 Version 1709 Build 16299.431
  • OVRLipSync Version 1.25.0
  • Unity2017.1.2f1
  • UnityChan_1_2_1
  • AniLipSync Ver.1.0.0

AniLipSyncのREADMEに記載されている動作環境

  • Windows 10 Version 1709 Build 16299.371
  • OVRLipSync Version 1.25.0
  • Unity 2018.1.0f2

Unity2017.1.~とUnity2017.2.~以降ではVR関係で大きな変更がありますが、OvrLipSync,AniLipSyncは特にVRに関係したライブラリを使用していないので、特に問題なく使えるかと思います。

導入

公式のREADMEが非常に優れているので、説明する必要があるかどうかも謎ですが、せっかくなので補完する形として画像多めで解説しようと思います。

  1. OVRLipSyncをインポート
  2. AniLipSync.unitypackage をインポート
  3. Assets/Oculus/LipSync/Prefabs/LipSyncInterface プレハブをシーンに配置
  4. Assets/AniLipSync/Prefabs/AniLipSync プレハブをシーンに配置
  5. AniLipSync GameObjectの AnimMorphTarget の各プロパティをインスペクタで編集(とくにSkinned Mesh RendererとViseme To Blend Shapeは変更が必要です)

    AniLipSyncのREADMEから引用(https://github.com/XVI/AniLipSync/blob/master/README.md)

1.OVRLipSyncをインポート

公式からファイルをダウンロードします。
ovr-min.PNG

ダウンロードした「ovr_audio_lipsync_unity_~.zip」を解凍します。その後、Unityから「Assets→Import Packages→Custom Package」を選び、「ovr_audio_lipsync_unity_~\LipSync\UnityPlugin\OVRLipSync.unitypackage」を選択してインポートを行います。

2.AniLipSync.unitypackage をインポート

AniLipSync.unitypackageをダウンロードします。

AniLipSync.PNG

ダウンロードができたら、Unityから「Assets→Import Packages→Custom Package」を選び、「AniLipSync.unitypackage」を選択してインポートします。この時点で、UnityProjectのAssets配下にAniLipSyncとOculusディレクトリがあれば、とりあえずAniLipSyncを使うためのファイル準備は完了です。「AniLipSync/Examples/Scenes」に「AniLipSync」というサンプルシーンがあるので、ここで一回動くかどうか試してみるのも良いと思います。

3.Assets/Oculus/LipSync/Prefabs/LipSyncInterface プレハブをシーンに配置

Assets/Oculus/LipSync/Prefabs/LipSyncInterface プレハブをシーンに配置しましょう。もし、サンプルシーンを試していた方は別のシーンに移動してからこれ以降の作業を行いましょう。

図1.png

4.Assets/AniLipSync/Prefabs/AniLipSync プレハブをシーンに配置

Assets/AniLipSync/Prefabs/AniLipSync プレハブをシーンに配置しましょう。
AniPreb.png

5. AniLipSync GameObjectの AnimMorphTarget の各プロパティをインスペクタで編集

さて、AniLipSyncを動かす下準備は整ったので、口パクさせたいキャラクターにセットアップしていきます。今回は口パクするキャラクターとしてユニティちゃんを例として説明しようと思います。
まず、ユニティちゃんをダウンロードします。ダウンロードができたらUnityから「Assets→Import Packages→Custom Package」を選び、「UnityChan_~.unitypackage」を選択してインポートを行います。インポートができたら、Assets/UnityChan/Modelsにあるunitychan Prefabをシーンに配置します。注意ですが、アセットストアのユニティちゃんにはBlendShapeに母音が用意されていないため、そちらからインポートはしないようにしましょう。

UnitychanThis.png

では、ユニティちゃんも配置できましたし、AniLipSync GameObjectのセットアップをしていきましょう。
まずは、Skinned Mesh Rendererにユニティちゃんの表情のBlendShapeがあるオブジェクトを選択して入れます。ユニティちゃんの場合、MTH_DEFというものがそれにあたります。MTH_DEFはとても深い層(unitychan>Character1_Reference>Character1_Hips>Character1_Spine>Character1_Spine1>Character1_Spine2>Character1_Neck>Character1_Head>MTH_DEF)にあって見つけにくいのでHierarchyビューから検索をかけて探すことをおススメします。

MTHDEF.png

次に、Viseme To Blend Shapeに上からaa, E, ih, oh, ouの順でモーフのインデックス番号を指定していきます。ユニティちゃんのMTH_DEFを見てみると、aa:6、E:9、ih:7、oh:10、ou:8というように対応していることが分かります。なので、Viseme To Blend ShapeのElement 0に6、Element 1に9、Element 2に7、Element 3に10、Element 4に8を入力します。

morf.png

image.png

これでセットアップは終了です。マイクに向かって話しかけてみてください。ユニティちゃんも一緒に口パクしてくれるでしょうか?

音声と口パクの遅延合わせについて

AniLipSyncを使っても完全に遅延を消せるわけではありません。なので、配信ソフトや動画編集ソフトを使ってそちら側で合わせることをUniteの講演では推奨していました。AniLipSyncだと250msほどズラすと丁度いいそうです。
OBSを使っている場合は、左上の 編集>オーディオの詳細プロパティ と進み、そこからマイクの同期オフセットに入力するとよいです。

2018-06-13 (1).png
2018-06-13 (2).png

AnimMorphTargetの各プロパティについて

これでキャラクターの口を動かせるようになりましたね。ただ、このままではモデルや環境によっては綺麗に動いて見えないかもしれませんし、リミテッドアニメーションも自分で調節したいということもあるでしょう。AnimMorphTargetの各プロパティを調整することでそのあたりの調整をすることができます。

以下、公式のREADMEの引用です。

Transition Curves

aa, E, ih, oh, ou のそれぞれの音素へ遷移する際に、BlendShapeの重みを時間をかけて変化させるためのカーブです。

例えば、黙っている状態から aa の音素を検知した場合、Element 0 のカーブに従って時々刻々とBlendShapeの重みを変化させます。aa の状態で ih の音素を検知した場合、Element 2 のカーブに従います。

徐々に重みを増やすことで、ゆるやかに口の形が変化するような表現が可能です。

Curve Amplifier

Transition Curveの縦軸の値に倍率をかけます。

Transition Curveの縦軸を 0.0 ~ 1.0 の範囲にしておいて 100 の倍率をかけることで、カーブ編集時の煩雑な操作を省略できます。

Weight Threashold

この値より小さな音素の重みを無視します。

小さなノイズ等で口が開いてしまう現象を回避できます。

Frame Rate

1秒間にBlendShapeの重みを更新する頻度の設定です。

リミテッドアニメ風の効果を得ることができます。

Skinned Mesh Renderer

唇のBlendShapeを持ったSkinnedMeshRendererを指定してください。

Viseme To Blend Shape

aa, E, ih, oh, ouの順でBlendShapeのIndexを指定してください。

Smooth Amount

OVRLipSyncのSmooth amountの値を設定できます。

よくありそうなバグ

よくありそうというか、自分が出会ったバグとその解決方法です。

アニメーションを設定すると動かなくなってしまった。

原因
AniLipSyncがアニメーションで上書きされてしまっている。

解決
AnimMorohTarget.csのvoid Update ()void LateUpdate ()に変更して、アニメーションの適応後にBlendShapeの変更を行うように変更した。

AnimMorohTarget.cs
//変更前(43行目)
void Update() {
        if (context == null || skinnedMeshRenderer == null) {
            return;
        }
AnimMorohTarget.cs
//変更後(43行目)
void LateUpdate() {
        if (context == null || skinnedMeshRenderer == null) {
            return;
        }

OVRPhonemeContext.Start ERROR: Could not create Phoneme context

原因
OVRLipSyncとAniLipSync共存できるか試そうとOVRLipSyncのスクリプトをシーン上に配置していたら出てきた。OVRLipSyncContext.csとLowLatencyLipSyncContext.csより先にOVRLipSyncがAwakeしてしまっており、うまくコンテキストが生成できていない。

OVRLipSyncContext.csとLowLatencyLipSyncContext.csはOVRLipSyncContextBase.csを継承しており、そのOVRLipSyncContextBase.csにあるコンテキストを生成するプログラムがエラーを吐いている。プログラムを編集し、何のエラーを返しているか試してみたところ"CannotCreateContext"が返ってきていた。コンテキストの生成に異常があることが分かる。

OVRLipSyncContextBase.cs
//変更前(78行目付近)
void Awake()
    {
        // Cache the audio source we are going to be using to pump data to the SR
        if (!audioSource)
        {
            audioSource = GetComponent<AudioSource>();
        }

        lock (this)
        {
            if (context == 0)
            {
                if (OVRLipSync.CreateContext(ref context, provider) != OVRLipSync.Result.Success)
                {
                    Debug.Log("OVRPhonemeContext.Start ERROR: Could not create Phoneme context.");
                    return;
                }
            }
        }
    }
OVRLipSyncContextBase.cs
//変更後(78行目付近)
void Awake()
    {
        OVRLipSync.Result rel;
        // Cache the audio source we are going to be using to pump data to the SR
        if (!audioSource)
        {
            audioSource = GetComponent<AudioSource>();
        }
        lock (this)
        {
            if (context == 0)
            {
                if ((rel = OVRLipSync.CreateContext(ref context, provider)) != OVRLipSync.Result.Success)
                {
                    Debug.Log("OVRPhonemeContext.Start ERROR: Could not create Phoneme context.");
                    Debug.Log(rel);
                    return;
                }
            }
        }
    }
OVRLipSync.cs
//OVRLipSync.CreateContext実行時に返ってくる値。(37行目付近)
public enum Result 
    {
        Success =               0,
        Unknown =               -2200,  //< An unknown error has occurred
        CannotCreateContext =   -2201,  //< Unable to create a context
        InvalidParam =          -2202,  //< An invalid parameter, e.g. NULL pointer or out of range
        BadSampleRate =         -2203,  //< An unsupported sample rate was declared
        MissingDLL =            -2204,  //< The DLL or shared library could not be found
        BadVersion =            -2205,  //< Mismatched versions between header and libs
        UndefinedFunction =     -2206   //< An undefined function 
    };

解決
OVRLipSync.csを一回外して付け直すか、スクリプトの実行順序を編集してOVRLipSync.csを優先にさせると直った。スクリプトの実行順序の編集に関しては【Unity】コンポーネントのイベント実行順についてのTipsに詳しくあるのでそちらを参考にしてみてください。

おわりに

遅延軽減やリミテッドアニメーションが簡単に実装できるほか、OVRLipSyncのゴチャゴチャ(デバッグ用の機能とか、セットアップのためのスクリプトとか)がかなり改善されていて、とても使いやすいです。あまりこの記事では触れていませんでしたが、セットアップという観点からみても非常に優れています。世のVtuberのみなさま、OVRLipSyncからこちらに乗り換えを検討してみてはいかがでしょうか。

参考記事

特に凸さんのUnity でリップシンクができる OVRLipSync を試してみたは記事構成において非常に参考にしました。素晴らしい記事をありがとうございます。

imageLicenseLogo.png
この作品はユニティちゃんライセンス条項の元に提供されています