Unityでリップシンク機能を実装してみる

はいどーもこんにちは、えもふりに夢中の無職です。
本日はリップシンクを実装してみたいと思いますよ。
この記事は前回の記事で作成したものを前提として書いていますので、えもふり自体の準備ができていないという方は前回の記事を参考にして導入しておいてください。

リップシンクってなんやねん

僕は2Dモーション系のツールに疎いのですが、リップシンクって一般的な言葉なんですかね。
LipThinkか? と思っていたのですが、どうもLipSynch(唇同期)のようですね。
要は再生するSE(音声)に合わせて、えもふりのオブジェクトの唇をパクパクと動かす機能です。

簡単な実装方法

Unityでリップシンクを実装する最も簡単な方法は、同梱のEmoteLipSynchControlスクリプトを使うことでしょう。
このスクリプトの使い方は「E-mote Unity SDK マニュアル.pdf」に詳しく書いているのでそちらを見てください。

もう一つの実装方法

上記の方法はとてもお手軽なので、通常はこれで問題ないと思います。
ただ公式の回答に以下のような一文がありました。
「一つ目の手法(注*EmoteLipSynchControlスクリプトを使う方法)はリアルタイムに音声の解析をしているため、環境によってはCPU負荷が問題になる可能性があります」
便利ですしテストプレイ中はこのリアルタイム音声の解析でも問題ないと思いますが、実際に配布するゲームでは二番目の方法、すなわち解析済みのデータに合わせて口パクさせる方がよいでしょう。
ただ実際にどの程度の負荷がかかるのかは調査していないので、他の機能との兼ね合いを考えても許容できる範囲の負荷なら一番目の方法でも大丈夫だと思います。

静的な解析を実装する前準備

先ほどの公式回答をもう一度見てください。
静的な解析の実装について以下のように書かれています。

「そうした場合、E-moteエディタの「ボイスボリュームを解析する」機能を使います。
これによりwavファイルに対応した、時系列での口パクの大きさを指定するcsvファイルを出力出来ます。
この内容を何らかの方法でUnityに読み込み、音声再生時に Time.deltaTime の進行に合わせて EmotePlayer.setVariable() 関数で の E-moteキャラの face_talk 変数に値を設定することで口パクを行わせる事が出来ます。」

ちゅーわけで、まずはcsvファイルを出力しましょう。
僕は自分で素材ファイルを用意することができないもとい面倒だったので、同梱のファイル(EmotePlayer/Audioフォルダのやつ)に対してボイスボリュームの解析をおこないました。
入力フォルダは正常に認識したのですが、出力フォルダの設定がなぜか無視され、入力フォルダにcsvファイルが出力された以外は特に問題なく進めました(この辺は僕がえもふりの機能に慣れてないので、理由はよくわかりませんw)。
みなさんも適当にcsvファイルを作ったあと、EmotePlayer/Audioフォルダに出力してあげてください(つまりwavファイルと同じ場所にcsvファイルを配置)。

スクリプトを作成していく

ここまでで解析済みのファイルをUnityに取り込むことができたので、お次はこの解析済みのファイルをゲーム実行時に参照しなければなりません。
最も簡単な方法は、csvファイルの中身(生成したcsvファイルを見てください。よくわからん数字が羅列されていて、それらがカンマで区切られているはずです)をコピペして、ハードコーディングするやり方でしょう。
まずはこれで作成してみます(ただし後で説明するように、この方法は全く実用的ではありません)。

それではUnityの編集画面を開いてください。

1.ヒエラルキーに「SubCharacter」という名の空オブジェクトを作成
2.EmotePlayerスクリプトをアタッチ
3.PSBFileをvr_boyに変更
4.ScaleをX10Y10にする(口の動きを見やすくするため)
5.位置を適当に調整
6.SubCharacterの子オブジェクトとして「Auido」という名の空オブジェクトを作成
7.AudioSourceコンポーネントをアタッチ
8.AudioClipをsample_voice_001に変更
9.PlayOnAwakeのチェックを外す

これで緑色の髪をした男の画像が表示されているはずです。
次はスクリプトを組み立てていきます。

まずはILipSynchCsvLoaderインターフェイスを作成しましょう。
このインターフェイスを通じてcsvファイルを生成します。

さてインターフェイスだけでは何もできませんから、ILipSynchCsvLoaderを実装するクラスも作成しましょう。
それがLipSynchDirectLoadクラスです。
このクラスは受け取った文字列をカンマで区切り、それらをfloat形式のキューに格納していきます。

このクラスはコンストラクタでCSV形式の文字列を受け取ります。
例えば僕の生成したsample_voice_001のcsvファイルは以下のような内容になっていたので、それをそのまま渡します。
0.0497314007952809, 0.124328501988202, 0.211358453379944, 0.304604829871096, 0.45107063051546, 0.677870201907353, 0.92635132932628, 0.92635132932628, 0.92635132932628, 0.92635132932628, 0.92635132932628, 2.42773862795048, 3.02015257878907, 3.5866347708162, 3.92479946999543, 3.99789676886442, 3.81954661609349, 3.51034141925414, 3.2331464784391, 3.0849222856281, 3.07555596137873, 3.20463088336587, 3.43539502618608, 3.72176566305617, 3.99338084256679, 4.23956809560427, 4.42733205934703, 4.5429508608038, 4.6045591300279, 4.60893105288833, 4.58180965009227, 4.47814613846941, 4.26437054950496, 4.06809303224647, 3.92387866475616, 3.82293818174034, 3.80127042017383, 3.84422216741801, 3.90252887368006, 3.99201260028451, 4.06701089738936, 4.12779559585176, 4.15863699854053, 4.16286722227959, 4.14549096017789, 4.07104060484731, 3.91480567114772, 3.69435667902502, 3.44754957770307, 3.21458462186292, 3.07842990576097, 3.0631159604, 3.1435403633287, 3.31153820152569, 3.53498996258891, 3.76833246941639, 3.98819762982362, 4.16783633172367, 4.2609255292537, 4.2302053315364, 4.09946894368808, 3.91745954351646, 3.70938464881053, 3.41581606009543, 3.04300345869072, 2.64359752792365, 2.22995132675892, 1.83245661659007, 1.46473068012247, 1.16513903802167, 0.960684968591445, 0.843882971351256, 0.813151655551882, 0.854387848939663, 0.9195492295051, 0.9195492295051, 0.9195492295051, 0.9195492295051, 0.9195492295051, 0.9195492295051, 0.9195492295051, 0.9195492295051, 0.996674638118412, 0.931667183022794, 0.875918900540832

そしてCreateCsvQueueメソッドを用いると、コンストラクタで受け取ったCSV形式の文字列をもとにキューを作成します。
このキューを使って「音声再生時に Time.deltaTime の進行に合わせて EmotePlayer.setVariable() 関数で の E-moteキャラの face_talk 変数に値を設定することで口パクを行わせる」のですね。
今回は特に使いませんが、SetCsvでリップシンクさせたい値を変更することも可能です。

というわけで最後はLipSynchDirectLoadクラスを使用するクラスEmoTestLipSynchクラスの作成です。

このスクリプトをCharacterControllerオブジェクト(前回の記事で作成しているはずですが、なければ別のオブジェクトでも可)にアタッチしてください。
それからインスペクタのEmotePlayerをSubCharacterに変更してください。
これで準備は完了です。
あとはStartVoiceActionメソッドを呼べば、sample_voice_001に合わせたリップシンクがおこなわれます(ただし「音声再生時に Time.deltaTime の進行に合わせて~」という部分の意味がよくわからなかったので、Update関数で毎フレームキューを更新しているだけです)。
というわけで試してみましょう。

1.Canvasにボタンを追加
2.OnClickイベントにStartVoiceActionメソッドを設定
3.ゲーム実行で試す

音声の再生と同時に口がパクパク動きましたか?
動いたなら成功です。

ファイルからCSVファイルを読み込みたい

実際に使ってみればわかると思うのですが、LipSynchDirectLoadはかなり不便です。
せめてファイル名を指定してCSVファイルを読み込みたいと思うのが人情でしょう。
というわけでそれを作ろうとしたのですが……。

今回の記事が思ったより長くなった&この記事で予定していたResoucesフォルダからのロードも僕自身が使わないなと思ったので止めにしました。
ただここで終わってはさすがにアレなので、もう少し実用的なバージョンを次回の記事であげたいと思いますよ。
まあインターフェイスを用意したのは完全に無駄になりましたが……(本当はResourcesからロードするバージョンでも同じインターフェイスを使う予定だった)。

終わりに

リップシンクの機能を使えばUnityで使う簡易的なADV的な表示も映えるんじゃないかなあと思いました。
とりあえず次回までしばし待たれよ!

ほなそんな感じでまた。

フォローする