Unity でリップシンクができる OVRLipSync を試してみた - 凹みTips

凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity でリップシンクができる OVRLipSync を試してみた

はじめに

Oculus 社が Unity 向けのリップシンクライブラリ、OVRLipSync をリリースしました。

オーディオストリーム(録音音源、マイク入力)から口形素Visemes、ビジーム)と呼ばれる特定の発音に対応する唇や顔の形状を求め、それを予めモデルに仕込まれたモーフのブレンド率に反映させるプラグインです。利用している口形素は 15 種類の「sil(無音), PP, FF, TH, DD, kk, CH, SS, nn, RR, aa, E, ih, oh, ou」で、例えば「PP」は「ポップコーン」と発音するときの「ポ」の最初の形状、「FF」は「フィッシュ」と発音するときの「フィ」の最初の形状になります。これらの記号は MPEG-4 の国際標準規格で定められているようですが、このあたりの詳細は勉強してもアレなので、取り敢えずはこれを一通り抑えればそれっぽい口の動きをする発音-唇形状の便利な関係、と覚えておけば良いと思います。

プラグインの形式としては、コア部分がネイティブで作られていて、それをラップしたスクリプト群が .unitypackage に収められています。対応プラットフォームとしては、Win 32/64bit、Mac OS XAndroid(Gear VR)で、ライセンスは Audio SDK と同様のものです。

サードパーティ製ライブラリおよびライセンスについては同梱の THIRD_PARTY_NOTICES.txt をご参照下さい。

ダウンロード

以下よりダウンロードできます。2016/02/15 現在バージョンは 1.0.1 になっています。

解凍するとライセンスやドキュメントの PDF、.unitypackage が含まれています。.unitypackage をインポートすればサンプルシーンを含む一連のアセットがインポートされます。

サンプルシーン

v.1.0.0 時では 3 種類のサンプルシーンがありましたが、v.1.0.1 のタイミングでアセットの整理がなされ、サンプルシーンは LipSync_Demo の 1 つのみとなりました。

LipSync_Demo

モーフが仕込まれた唇を動かすデモです。

  • キーマッピング
    • 1: 唇 - マイク
    • 2: ロボット - マイク
    • 3: 唇 - 録音データ
    • 4: ロボット - 録音データ
    • L: マイク音声のループバック
    • ←: ゲイン小
    • →: ゲイン大
    • D: 認識した口形素のデバッグ表示

MorphTest_MorphTarget_Girl (v.1.0.0 のみ)

Twitter でよく見かけたこの子(head_girl ちゃん)は v.1.0.1 でいなくなってしまいました、残念。

MorphTest_TextureFlip (v.1.0.0 のみ)

テクスチャベースで作成されたロボットです。こちらは v.1.0.1 でも引き続き LipSync_Demo シーンで見ることが出来ます。

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

AudioSource で再生しているオーディオを OnAudioFilterRead() で引っ張ってきて DLL 側の認識のアルゴリズムに渡し、得られた結果を 3D モデルなら skinnedMeshRendererBlendShape に突っ込み、2D テクスチャならテクスチャを変更する、という感じです。

スクリプト個別概要

同時にリップシンクするモデルが複数体いても良いように、コンテキストという単位で ID を割り振って認識を分割しています。呼ばれるヘルパや操作用のスクリプトもあるので思ったより沢山ファイルがありますが、重要なのは以下の 5 つです。

OVRLipSync

  • DLL のラッパースクリプト
  • Awake() を使ったシングルトン形式でシーンに 1 つだけ配置
  • static なメンバを通じてコンテキストの生成・破棄やデータを渡して認識を行う

OVRLipSyncContext

  • 内部ではコンテキストの生成・破棄、そして OnAudioFilterRead() を使って再生中のオーディオのデータを取得して DLL へ投げたりするコア部分
    • コンテキストは C# 側へは uint の ID で返ってきて、これを通じて DLL 側で操作を行う
  • モーフまたはテクスチャでリップシンクを行う対象の GameObject 1 つにつき 1 つずつアタッチ
  • GetCurrentPhonemeFrame() で口形素の情報などを含む ovrLipSyncFrame を取ってこれる

OVRLipSyncContextMorphTarget

  • GetCurrentPhonemeFrame() で取ってきた口形素を skinnedMeshRenderer.SetBlendShapeWeight() に与える
  • OVRLipSyncContext と同じ GameObject へアタッチ

OVRLipSyncContextTextureFlip

  • 最も近い(値の大きい)口形素のテクスチャにマテリアルを変更
  • OVRLipSyncContext と同じ GameObject へアタッチ

OVRLipSyncMicInput

  • AudioSource にファイルを与える代わりに Microphone クラスを用いて得られたマイク入力を利用する
  • OVRLipSyncContext と同じ GameObject へアタッチ

ざっくりとこんな感じです。

ユニティちゃんで利用してみる

実際にモデルに適用して使い方を見ていきましょう。本来ならば全ての口形素のモーフを仕込んだモデルを用意するのが望ましいですが、未だそういったモデルはないと思うので、取り敢えず母音(あいうえお)のみ対応したモデルということでユニティちゃんを例に見ていこうと思います。

ユニティちゃんアセットと OVRLipSync アセットを同じプロジェクトにインポートしておきます。ユニティちゃんは Skinned Mesh Renderer が分割されているため、階層の下の方にある MTH_DEF をセットする必要があります。また、標準のアニメーションにモーフが含まれているので、これを排除するためにちょっとスクリプトをいじります。

デモ

© UTJ/UCL

セットアップ

  1. 空の GameObject を作成して OVRLipSync コンポーネントをアタッチ
  2. unitychan Prefab をシーンに追加
  3. unitychan GameObject に以下のコンポーネントをアタッチ
    • OVRLipSyncContext
    • OVRLipSyncMorphTarget
  4. OVRLipSyncMorphTargetSkinned Mesh Renderer フィールドに Reference > Hips > Spine > Spine1 > Spine2 > Neck > Head 下にある MTH_DEF をセット
  5. OVRLipSyncMorphTargetViseme To Blend Targets の母音部分を対象のモーフのインデックス番号を指定(スクショ参照)
  6. OVRLipSyncMorphTargetUpdate()LateUpdate() に変更(下記コード参照)
  7. AudioSourceAudioClip に適当な音声を指定
  8. OVRLipSyncContextAudio Mute のチェックを外す

コンポーネント配置

f:id:hecomi:20160216163244p:plain

OVRLipSyncMorphTarget のパラメタ

口形素 aa, ih, ou, E, oh を、あいうえおのモーフのインデックスにします。子音に対応するモーフはないので取り敢えず適当なインデックスを割り当てています(ここでは SMILE1)。aa(10), E(11), ih(12), oh(13), ou(14) となっています。

f:id:hecomi:20160216154649p:plain

ちなみにユニティちゃんの口(MTH_DEF)のメッシュの BlendShape は以下のようになっています(0 以外の数値は笑みを消すために適当に私の方で調整した値を入れています)。

f:id:hecomi:20160216155651p:plain

あいうえおが 6 ~ 10 のモーフに割り当てられているので、その値を代入しているわけです。この値が直接 SkinnedMeshRenderer.SetBlendShapeWeight() に与えるインデックス番号になります。

スクリプトの変更

アニメーション後に OVRLipSync の結果を反映させるために LateUpdate() タイミングに変更します。

// 変更前(95 行目)
void Update () 
{
    if((lipsyncContext != null) && (skinnedMeshRenderer != null))
        ...

// 変更後
void LateUpdate () 
{
    if((lipsyncContext != null) && (skinnedMeshRenderer != null))
        ...

マイク入力

OVRLipSyncMicInput コンポーネントをアタッチするだけで動作します。Mic ControlConstant Speak にしておくとずっと認識し続けるので分かりやすいと思います。口が余り動かない場合は Gain パラメタを大きくしてみてください。また、ハウリングが起こる場合は OVRLipSyncContextAudio Mute をチェックして自分の声が出力されないようにすると良いです。

f:id:hecomi:20160216163451p:plain

上記例では利用例の紹介のために unitychanAudioSource を割り当てていましたが、口の位置から音が聞こえるように本来ならば口もしくは口の下の階層に空の GameObject を作成し、そこに諸々のコンポーネントを割り当てた方が良いと思います。

MMD4Mecanim で利用してみる

MMD4Mecanim を利用したモデルでも勿論出来ます。表情豊かなTda式Appendミクさんをお借りしてやってみます。

デモ

セットアップ

基本的には先程のユニティちゃんと同じです。モーフが多いのが素晴らしいですね。モーフは MMD4Mecanim の Morph タブから確認し、SkinnedMeshRenderer コンポーネントBlendShapes からインデックス番号を確認、そして OVRLipSyncMorphTargetViseme To Blend Targets にそのインデックス番号を書き込みます。順番は「sil(無音), PP, FF, TH, DD, kk, CH, SS, nn, RR, aa, E, ih, oh, ou」です。子音の口形素は「ん」などのモーフに適当に割り当てています。

f:id:hecomi:20160216201448p:plain

その他気になる点

ライブラリと提供しているにも関わらず、コアのコンポーネント群にデバッグ用の機能(キー押下やクリックで音声のループバックの切り替え等)が含まれているのは結構面倒で一番気になります。

後、Editor スクリプトを使った UI でなくプレーンなスクリプトから作られた UI のため、結構面倒な所が多いのでこの辺りも今後改善されることに期待したいです。

おわりに

私も Unity 向けのリップシンクライブラリを作っていてコア部分以外は大体同じことをしていたのですが、OVRLipSync の方がキャリブも不要で認識精度も高く、また子音に対応する形状にも対応していることから、はるかに自然なリップシンクになります。。

VR のコミュニケーション手段としてだけではなく、幅広く応用できそうで素晴らしいアセットを公開して下さった Oculus 社に感謝です。