Shaderで顔を動かそう

久々の更新ですが今回はいつも以上に利用機会が少なそうな話です.

f:id:FriendSea:20190615030833g:plain

製作中のゲームでようやくキャラクタの表情を動かせるようになりました.
ざっくり紹介します.
つまりこれの続き

friendsea.hateblo.jp

Blend Shapeでの表現がちょっと難しかったので代わりにシェーダを使っています.

Blend Shapeによる表情

3Dのキャラモデルの顔を動かす方法としてBlend Shapeが利用されることが多いですね.
私が使っているBlenderでもシェイプキーを作ってUnityにインポートすることができます.

ただ当然ですがBlend Shapeを使うとメッシュの頂点数が変わる編集ができなくなります.
やろうと思うとシェイプキーをすべて作り直すことになる.
また,ミラーモディファイアも利用できなくなるのでモディファイアなしで左右対称な編集を行う必要があります.

この殆ど取り返しがつかない感じがとても嫌だ.

目を動かす

目をシェーダ内で合成

今回は顔のパーツを描いたテクスチャを用意して合成します.
昔から使われている手法のようです.

スクウェア・エニックス モバイルオープンカンファレンス | SQUARE ENIX (1つ目の資料)

上の資料の例では目を作るために瞼,眉,瞳,ハイライトなどを別々に描いていますが,
今回はテクスチャ内でも形が見えるようにしたかったので瞳以外は重ねて描いてます.

f:id:FriendSea:20190615113719p:plain

チャンネル毎にパーツの形の領域を指定しています.

領域
R-1
G-1 上まぶた
B-1

値がひっくり返っているのは白目部分を白く描きたかったからです.

UV座標について

瞼や眉のテクスチャと瞳のテクスチャで,それぞれ別のUV座標を使ってサンプリングしています.
瞼はキャラクタ毎のテクスチャの一部として描いているのに対して,瞳は共通で1つのテクスチャを利用しています.
瞳のUV座標はUV2として書き出してもいいのですが,今回は頂点カラーのRGチャンネルを使ってます.
左右のどちらの目かをメッシュデータに持たせる必要があるので,そこでBチャンネルを使うためです.

ハイライト

瞳のハイライトはテクスチャには描かずに,Shader内で生成しています. 光源方向に応じた位置にハイライトを乗せることが可能になります.
また,半目状態でもハイライトが隠れてしまわないように移動させたりなんかもできます.

f:id:FriendSea:20190615134522g:plain

テクスチャの変形

表情を作るためにテクスチャを変形します.
チャンネル毎にサンプリングする際にUV座標をオフセットすることでテクスチャをズラします.
alphaは基本的に乗算ですが上瞼の領域は必ず描画されるようにしています.

f:id:FriendSea:20190615140548p:plain

瞳の位置,つまり視線もこの方法で動かしています.

また,瞳用のUVを利用して適当な関数を利用すればもう少し複雑な変形も可能です.
例えば,瞬きでは円弧の関数をv座標に足すことで上に凸の瞼を下に凸に変えています.

f:id:FriendSea:20190615142142p:plain

口を動かす

口の開閉

口を動かす場合,口角の上下などは目と同様に変形させれば良いですが,
口の開閉はあまりうまく表現できなさそうです.
そこでアルファクリップの閾値の変化によって動かすことにしました.

f:id:FriendSea:20190615143533p:plain

口の閉じた形と開いた形を決め,その間をグラデーションでつなぎます.
このテクスチャをブレンドを行わずにアルファクリップで描画し,
しきい値を[0, 1]で動かすことで口が開閉します.

f:id:FriendSea:20190615144643g:plain

口が横から見えるようにしたい

アニメとかでよくある表現です.よく見ると怖いですね.
単純に口のポリゴンを視線方向に少し回して表現しました.

f:id:FriendSea:20190615145555p:plain

割と強引なことをしているので角度によっては稀に変な見え方になっちゃいます.
もともと立体として破綻しているものを表現しようとしているので
仕方ないかもしれないけどそのうちなんとかしたい.

パラメータの制御

Shaderで顔が動かせるようになりました.マテリアルのパラメータを動かすことで顔が動きます.

f:id:FriendSea:20190615150755p:plain

でもコレをランタイム中に動かすわけには行きません.
動かすと異なるパラメータを持った新たなマテリアルとして別インスタンスができてしまいます.
そのため,Material Property Blockを利用してRenderer毎にパラメータを指定しています.

UnityEngine.MaterialPropertyBlock - Unity スクリプトリファレンス

パラメータは個別に動かすとしんどそうなのでScriptableAssetとして表情のセットを作っています.
これを呼び出す処理をStateMachineBehaviourやアニメーションイベントから呼び出すことでモーションに応じた表情に遷移可能です.

f:id:FriendSea:20190615151729p:plain

おしまい

というわけで顔が動くようになりました.
シェイプキーを作りたくないという理由でこのような方法をとっていますが,
正直あんまり変わったことをすると思わぬところでトラブりそうな不安もあります.

組んだコードはかなり汎用性が低くなったので公開などできませんが,
ブレンドシェイプに親を殺された方やアレルギーのある方は同じようなことをしてもいいかもしれません.