はじめに
イケメンテックラボで 「王子様のささやき朗読VR」の メインエンジニアを務めている、茨田と申します。
第3回目になる今回は、キャラクターを実際の人間のように、音声と同期して口を動かせるようになる、Oculus Lipsync Unity(OVRLipSync)の導入について、ご紹介させていただきたいと思います。
▼以前の記事はこちらから
「王子様のささやき朗読VR」で、最初にプランナーから貰ったオーダーは、「2人きり」で、「雰囲気ある」場所で、「囁かれたい」というものでした。
キャラクターの音声が聞こえるのに、口が動いていないと、現実との差が大きくコンテンツに集中できません。そこで OVRLipSync を使用してどのように「囁かれる」体験を実現したのかをご紹介します。
今回説明で扱うキャラクターは王子様ではなく、どなたでも比較的入手しやすい、ユニティちゃん(© UTJ/UCL)を使って説明させていただきます。
そもそもリップシンクってなんだろう?
すごく簡単に言うと、口が音とシンクロするからリップシンクと言います。
音声にどんな成分(母音など)が含まれているかを解析して、音に合わせて最初に設定してあった口の形状を合成しながら切り替える技術で、今回使う、OVRLipSyncでは、全部で15形状設定ができます。
日本語の場合は「あかさたなはまやらわ」は「あ」「いきしちに・・・」の列は「い」「うくすつぬ・・・」の列は「う」みたいな形で、アイウエオの母音分の形状を5つ作って適応するとかなり喋っているような口の動きを再現できます。

試した環境とダウンロード方法
今回はUnity2018.2.12を使用して動作検証を行っています。
※「王子様のささやき朗読VR」開発時には当時の最新のUnityのバージョンを追っていた為、Unity2017.4.0f1とUnity2018.1.0f2にて動作検証を行っていました。
まず最初に必要なアセットなどをダウンロードしてきます。
今回はLipSyncを3Dのキャラクターに適応するためのunitypackageと、実際に喋らせるキャラクターのユニティちゃんをDLします。
Oculus Lipsync Unity(OVRLipSync)の準備
Oculus Lipsync Unityを以下からver1.30.0をダウンロードします。
※開発時は1.16.0を利用していました
https://developer.oculus.com/downloads/package/oculus-lipsync-unity/

ダウンロードしたファイルを解凍すると、UnityPluginの中にOVRLipSync.unitypackageが入っています。

このファイルをAsset > ImportPackage > CustomPackageからインポートしましょう。

Asset配下が以下の画像の様になれば成功です。

ユニティちゃんの準備
次にユニティちゃんを下記URLからダウンロードします。
http://unity-chan.com/contents/guideline/
ダウンロードするには、ユニティちゃんライセンス同意が必要なので、内容を確認した上で、データをダウンロードするボタンを押しましょう。

「ユニティちゃん 3Dモデルデータ」から最新(検証時ver1.2.1)のデータをダウンロードします。

ダウンロードした、UnityChan_1_2_1.unitypackageをUnityで、Asset > ImportPackage > CustomPackageからインポートすることで、ユニティちゃんを使えるようになります。

Asset配下が上記の画像の様になれば成功です。
ユニティちゃんにLipSyncを適応する適応手順
インポートが終わったユニティちゃんにOVRLipSyncを適応して喋らせて見ましょう。
OVRLipSyncの配置
まず最初に、Hierarchyを右クリックして、Create Emptyを選択し、空のGameObjectを作成します。

作成したGameObjectを選択してInspectorからAddComponentを押すと、ウインドウが出るので、OVRと検索をかけて、OVRLipSyncを選択して、コンポーネントをアタッチします。

あとで見た時に分かりやすい様にGameObjectの名前もOVRLipSyncに変更しておきます。

ユニティちゃんプレハブをシーンに追加する
ProjectのAssets/UnityChan/Prefabs/の中にある、unitychan.prefabをHierarchyにドラックアンドドロップ(以降D&Dと呼びます)します。

これでシーン上の原点にユニティちゃんが表示されました。

ユニティちゃんへのコンポーネントの適応
ユニティちゃんの口のパーツは、
Character1_Reference > Character1_Hips > Character1_Spine > Character1_Spine1 > Character1_Spine2 > Character1_Neck > Character1_Head の下にあるMTH_DEFになります。

口のパーツあたりから音を鳴らしたいので、Character1_Headを右クリックしてCreateEmptyで空のGameObjectを作ります。

作ったGameObjectにOVRLipSyncContextとOVRLipSyncContextMorphTargetをAddComponentします。
※OVRLipSyncContextを追加すると自動で音源になるAudioSourceも追加されます
GameObjectの名前もLipSyncSoundに変更しておきます。

OVRLipSyncとモデルの口の動きを繋ぎ込む
OVRLipSyncContextMorphTargetのSkinnedMeshRendererに動かす口のパーツを設定します。
先ほども言った通り、口のパーツは、
Character1_Reference > Character1_Hips > Character1_Spine > Character1_Spine1 > Character1_Spine2 > Character1_Neck > Character1_Head の下にある MTH_DEF を使います。
※ユニティちゃん以外を使う場合、顔(口)のSkinned Mesh Rendererを探して設定すること
HierarchyにあるMTH_DEFをOVRLipSyncContextMorphTargetのSkinnedMeshRendererにD&Dします。

次にOVRLipSyncContextMorphTargetのVisemeToBlendTargetsを設定していきます。
VisemeToBlendTargetsは口の形状と解析した音声をつなぎ合わせる部分になります。
日本語の母音は下の方にある[aa]=あ、[E]=え、[ih]=い、[oh]=お、[ou]=うと設定することで実現できます。
初期では下の画像のように順番に数字が入っているだけなので、ユニティちゃんのBlendShapesに合わせて設定する必要があります。

BlendShapesは口の動きや表情などの各形状に対して、0から100の数値が入力できるようになっています。
複数の形状を合成することで、口の動きや表情などの複雑な形状を再現できるようになります。
各形状のインデックス番号はSkinned Mesh RendererのBlendShapesの一番上を0として下の画像の赤字の様に順番に振られいます。

ユニティちゃんの場合、インデックス番号の6がa、7がi、8がu、9がe、10がoとなっているため、VisemeToBlendTargetsは[aa]=6、[E]=9、[ih]=7、[oh]=10、[ou]=8を設定します。他の数値は0に設定しておきます。

これでOVRLipSyncとモデルの口の動きの繋ぎこみは完了です。
再生したい音声の設定
再生したい音声を先ほど作ったLipSyncSoundに自動的に追加されたAudioSourceに設定します。
ユニティちゃんの場合、Assets/UnityChan/Voice/の中に音声ファイルが同梱されています。その中にある、unity-chan_voice_list.txtを参考に好きな音声を再生して見ましょう。

今回は口の動きがわかりやすいように、長めの音声、univ1341を設定して見ます。
AudioSourceのAudioClipにuniv1341をD&Dします。

次に上記で設定したAudioSourceをOVRLipSyncContextの中にあるAudioSourceにD&Dします。

これで音声が設定できました。
再生するための問題対処
このまま再生すると以下のようなエラーが出てしまいます。
Array index (15) is out of bounds (size=11)
UnityEngine.SkinnedMeshRenderer:SetBlendShapeWeight(Int32, Single)
OVRLipSyncContextMorphTarget:SetLaughterToMorphTarget(Frame) (at Assets/Oculus/LipSync/Scripts/OVRLipSyncContextMorphTarget.cs:191)
OVRLipSyncContextMorphTarget:Update() (at Assets/Oculus/LipSync/Scripts/OVRLipSyncContextMorphTarget.cs:129)
これはOVRLipSyncContextMorphTargetのLaughterBlendTargetに設定されている数値に対して、SkinnedMeshRendererの形状が少なかったため起きているエラーです。
ユニティちゃんの場合、LaughterBlendTargetを10に設定すると起きなくなります。
※恐らくSkinned Mesh RendererのBlendShapesが0から10のため、最大値より大きい数字を指定するとエラーが起きる

次にスクリプトの修正をします。
Assets/Oculus/LipSync/Scripts/にある、OVRLipSyncContextMorphTarget.csの119行前後(OVRLipSyncのバージョンによって変わります)にある、Update()をLateUpdate()に変更します。
void Update ()
{
if((lipsyncContext != null) && (skinnedMeshRenderer != null))
{
// get the current viseme frame
OVRLipSync.Frame frame = lipsyncContext.GetCurrentPhonemeFrame();
・・・
void LateUpdate ()
{
if((lipsyncContext != null) && (skinnedMeshRenderer != null))
{
// get the current viseme frame
OVRLipSync.Frame frame = lipsyncContext.GetCurrentPhonemeFrame();
・・・
LateUpdate関数はUpdate関数の後に実行される関数で、Update関数で必要なデータを作り、LateUpdate関数で、口の形状を変化させる流れが作れます。
表情のアニメーションをUpdateで行なっている場合、リップシンクをLateUpdateで行うことで、表情変化後に口の動きを適応できるようになります。
次に、AudioSourceにPlayOnAwake以外のチェックが入っていないかを確認しましょう。筆者の環境では、BypassEffectsにチェックが入っていると、音声は流れますが、口が動きませんでした。

これで音を解析して(音を鳴らさずに)口が動くようになりました。
口の動きと同時に音を鳴らす場合は、OVRLipSyncContextのAudioLoopbackにチェックを入れましょう。

カメラの位置調整
最後にユニティちゃんの口元が見やすい位置にカメラを移動しましょう。
HierarchyからMainCameraを選択して、InspectorのTransformを変更します。
PositionをX:0 Y:1.3 Z:0.45に設定してカメラを移動させます。
RotateをX:0 Y:180 Z:0に設定してカメラを回転させます。

これでユニティちゃんの顔周辺にカメラが近づきました。

実際に再生している様子
開発時に起きた問題と解決方法
「王子様のささやき朗読VR」を開発時(OVRLipSync1.16.0とUnity2017.4.0f1を使用)に起きた問題と解決方法を記載しておきます。
※最新版を使うと解決されているものが殆どです。理由があって昔のものを使う場合、参考にしてください
最初の問題と解決方法
問題:
再生時にシーンに最初からユニティちゃんのプレハブが置いてあると、音声だけ再生されて、口パクが発生しない。
解決方法:
根本原因は不明ですが、以下を行うことで回避できるようになります。
空のGameObjectに一つスクリプトを挟んで、スクリプトのStart関数でInstantiateを行い、ユニティちゃんを生成すると口パクするようになりました。
根本原因を少し詳しく調べたメモ
OVRLipSync.Frame frame = lipsyncContext.GetCurrentPhonemeFrame();
上記のframeに値が入っていない。
本来はframeの値が毎フレーム更新されるはずで、値が全て0なのはおかしい。
もしかすると、初期化の順番などのタイミングでうまく行っていないのかもしれない。
原因が判明した時メモ
原因はOVRPhonemeContext.Start ERROR: Could not create Phoneme context.というエラーが出ていたせいだった。
エラーなのになぜかワーニングで出力されており、なかなか気がつかなかったが、OVRLipSyncが初期化に失敗していたようで、一度RemoveComponentして、再度OVRLipSyncスクリプトをアタッチすることで口パクするようになった。
Unityを起動するたびに起きる可能性があるため、注意が必要。
起動時も回避する方法(最終的な解決方法)
根本原因はOVRLipSyncとOVRLipSyncContextのスクリプトの実行順番だった。
プロジェクト設定からスクリプトの実行順番を設定することで回避することができるようになります。
Edit > ProjectSettings > ScriptExecutionOrderで設定を開いて、Inspectorの+ボタンからOVRLipSyncとOVRLipSyncContextを追加します。
OVRLipSyncは-1、OVRLipSyncContextは100に設定することで実行順番を制御できます。
※数字が小さいほうが先に実行されるので-1と1などでも大丈夫です
この設定により、最初からシーン上にモデルを置いて、LipSyncがつけてあっても、ちゃんとLipSyncが動作するようになりました。
おわりに
今回は、ユニティちゃんで音声に合わせて口を動かすために、OVRLipSyncを設定する方法と、開発時に実際に起きた問題や解決方法を説明させていただきました。
このように、3Dのキャラクターや音声などを入手できれば、個人でもキャラクターを自由に喋らせることができます。
今回の記事では扱いませんでしたが、OVRLipSyncでは、マイクから入力した音声を使ったLipSyncや、2DキャラクターでもLipSyncさせることが可能です。
是非、自分好みのキャラクターを自由に喋らせてみてください。
次回以降も、「王子様のささやき朗読VR」で使った技術やTipsなどを紹介させていただきますので、よろしくお願いいたします。