【アーカイブ】VRMモデルをOVRLipSyncに対応させる方法

## この記事は　アーカイブ　です

**<a href="https://twitter.com/kohack_v" target="_blank">坪倉さん</a>**が非破壊的にVRMにOVRLipSyncを適用させるライブラリを公開しました。
こちらを使用した方がスマートです。
**<a href="https://github.com/TsubokuLab/VRMLipSyncContextMorphTarget" target="_blank">VRMLipSyncContextMorphTarget</a>**

ちゃんとリファレンス読みましょうという話…
**<a href="https://developer.oculus.com/documentation/audiosdk/latest/concepts/book-audio-ovrlipsync/" target="_blank">Oculus Lipsync Guide</a>**

#### 何故残すのか？

こうなった原因なのですが、**リファレンスを読む**という発想が抜けて、なんとなくOVRLipSyncのコード読んで（とりあえずここの数字ぶっこぬけばいいんだな！）と判断して、あとは安易にグローバル変数作るというやっつけ実装をしたのが原因です。
今後は**リファレンス読む(当たり前)**という事を心がけようと思います。

####準備
１．[OVRLipSync 1.28.0](https://developer.oculus.com/downloads/package/oculus-lipsync-unity/) のインポートを行う。
２．[UniVRM](https://github.com/dwango/UniVRM/releases) のインポートを行う。
（この記事を書いた時点では v0.42 を使用しています。
　本記事の内容的に最新版を使用して問題ないかと思います。）
３．**セットアップ済みの**VRMアバターをインポートする。
（[dwango on GitHub](https://dwango.github.io/vrm/) を参考にしてVRMモデルを準備してください。）
４．[VRMLipSync](https://github.com/NekomimiMaster/VRMLipSync_CorrespondOVRLipSync)のScriptをインポートする。

####作業の概要
１．シーンの準備(スクリプト・テスト用モデルの配置)
２．OVRLipSyncContextMorphTargetを書き換える。
　・メッシュの設定がnullだと動かないので、メッシュのnullを無視するようにする。
　・LipSynkの処理を外部に渡すようにフィールド変数を用意する。
　　(この記事ではPublicで作りますが、必要であればプロパティを設定してください)
　・メッシュにウェイト値を割り当ててる箇所を、フィールド変数に値を渡す処理に書き換える。
３．実行して確認。

####シーンセットアップ
![unity.png](https://qiita-image-store.s3.amazonaws.com/0/149449/2ef4b1ef-6748-033e-ada2-6d15a9fb8279.png)
１．空のゲームオブジェクトを作り「LipSyncScripts」等名前をつける。
２．そのゲームオブジェクトに「OVRLipSync」と「VRMLipSync」をアタッチする。

![unity.png](https://qiita-image-store.s3.amazonaws.com/0/149449/d8aa8d68-a2d2-1521-7693-9867440cb943.png)
３．テスト用にhierarchyにVRMアバターを配置します。
※**実行中にローダー等で読み込む場合は３の作業は必要ありませんが、デフォルトだとVRMのGameObjectの名前が VRM である必要があります。**
※VRMアバターのobject名を VRM にするか、VRMLipSyncのLipSyncTargetNameを読み込むVRMアバターの名前に変更してください。

####OVRLipSync完全に理解した(分かってない)
別のクラスからOVRLipSyncを読み取る場合、OVRLipSyncContextMorphTargetに変更を加える必要があります。
１つは読み取り用のフィールド変数の作成と、LipSyncの処理の変更。
２つめはLipSyncのターゲットとなるskinnedMeshRendererの参照が外れてても無視して実行するようにする変更。

**１．読み取り用のフィールド変数を作り、LipSyncを変化させる箇所の書き換え**

```c#:OVRLipSyncContextMorphTarget
    //VRMLipSync用フィールド変数
    public float[] VRMLipValue = new float[15];
```

上記のフィールド変数を**追記**してください。

```c#:OVRLipSyncContextMorphTarget
/*
 * 下記処理をコメントアウト
 * 
   skinnedMeshRenderer.SetBlendShapeWeight(
      visemeToBlendTargets[i],
      frame.Visemes[i]);
*/
   //下記の処理を追記
   VRMLipValue[i] = frame.Visemes[i];
```
大体、150行目～170行目あたりにある **skinnedMeshRenderer.SetBlendShapeWeight** をコメントアウトし、その直後に**VRMLipValue[i] = frame.Visemes[i];** を追記してください。
この箇所はfor文で配列に割り当てられたモーフ全てに対してウェイトを与える処理を行っています。
今回はその処理結果をフィールド変数に代入するように書き換える事で、VRMLipSyncから参照できるようにします。

**２．本来のLipSyncのターゲットとなるskinnedMeshRendererの参照が外れてても、無視して実行するように変更する。**

```c#:OVRLipSyncContextMorphTarget
/*
 * メッシュが割り当てられてなくても無視する / メッシュのnullを無視
 *
   if (skinnedMeshRenderer == null)
   {
    Debug.LogError("LipSyncContextMorphTarget.Start Error: " +
    "Please set the target Skinned Mesh Renderer to be controlled!");
    return;
    }
*/
```
「skinnedMeshRenderer」で検索して、だいたい80行目～90行目(Start関数の直後)あたりにあるif文をコメントアウトします。
ここはモーフを割り当てるメッシュの参照が外れてた場合エラーで停止する処理なのですが、**今回はメッシュに対して直接命令するのではなく、一度フィールド変数を経由してLipSyncに結果を渡す**必要があるのでskinnedMeshRendererの参照が外れてても無視する必要があります。

```c#:OVRLipSyncContextMorphTarget
void Update ()
   {
      if((lipsyncContext != null) /* && (skinnedMeshRenderer != null) */ )
      {
         ※中略
      }
   }
```
「skinnedMeshRenderer」で検索して、だいたい110行目～120行目(Update関数の直後)あたりにある「&& (skinnedMeshRenderer != null)」をコメントアウトします。
ここの条件式も「skinnedMeshRendererが参照されてない場合は実行しない」という処理なので、コメントアウトする必要があります。
OVRLipSync完全に理解した。

####実行して確認

<script type="application/javascript" src="https://embed.nicovideo.jp/watch/sm33940463/script?w=640&h=360"></script><noscript><a href="http://www.nicovideo.jp/watch/sm33940463">LipSyncの確認用動画</a></noscript>

実行中にVRMを読み込んだ場合は「L」キーを押すと、処理がはじまります。
(もしくわ、VrmLipSyncSetup()をUIなどから呼んでください。)

####補足とか
**実行中のUnityのGameタブをクリックすると自分の声がループバックするようになる問題**
OVRLipSyncにはループバックする機能が標準でついてます。
OVRLipSyncContext のScriptを開いて ToggleAudioLoopback(); で検索するとループバックを有効にする処理が書かれている場所が分かります。この部分をコメントアウトするとループバックしなくなります。
もしくわ、キーバインドを変更するなどして対応。

**OVRLipSyncのアクティブが外れると(他のウインドウを選択すると)リップシンクが停止する問題**
OVRLipSyncはアクティブが外れた場合はデフォルトでリップシンクを停止する処理が入ってます。
OVRLipSyncMicInput のScriptを開いて StopMicrophone(); で検索するとマイクのアクティブを切り替える処理が書かれている場所が分かります。
この部分をコメントアウトするとウインドウが非アクティブ時にマイクが停止しなくなります。

![comment.png](https://qiita-image-store.s3.amazonaws.com/0/149449/041408b9-c913-3b2f-509c-62b4af1b37ed.png)

#参考になるリンク
<a href="https://developer.oculus.com/documentation/audiosdk/latest/concepts/audio-ovrlipsync-viseme-reference/">Viseme Reference Images</a>
公式リファレンス。OVRLipSyncのモーフをどのように作ればよいか載っています。

<a href="http://greety.sakura.ne.jp/redo/2018/01/how-to-vtuber.html">バーチャルYouTuberのやり方 #VTuber</a>
ようてんさんの記事。OVRLipSyncに関する細かい点について書かれています。

<a href="http://tips.hecomi.com/entry/2016/02/16/202634">Unity でリップシンクができる OVRLipSync を試してみた</a>
凹みチップスを食べてつよつよになりたい。

<a href="https://docs.unity3d.com/ja/current/ScriptReference/MonoBehaviour.OnApplicationFocus.html">MonoBehaviour.OnApplicationFocus(bool)</a>
アプリケーションが非アクティブになると実行される命令のリファレンス