学園アイドルマスターの眉毛のShaderをUnityで再現してみた

ここ最近、学園アイドルマスター(学マス)の鼻や眉毛のシェーダーがTwitter(現X)で話題になっていました。

鼻のアウトラインがカメラの角度で消える実装は容易に思いつくのですが(カメラのViewベクトルと頭のforwardベクトルの内積からディゾルブ等)、

眉毛が角度でフェードする処理(正面から見ると眉毛が前髪より手前に、横顔に近づくと眉毛がフェードアウトする処理)の実装はすぐには思いつきませんでした。

技術的にも面白そうなテーマだと思ったので、Unityで再現することにしました。

mayu.gif

Unity URP上の再現

Stencilを使うパターンと、Stencilを使わずにDepth Offsetするパターンの2つをUnity URP上で実装しました。

プロジェクトファイルはGitHubでも公開しました。

モデルの準備

UnityちゃんシリーズのSDトーコちゃんの3Dモデルを使わせていただきました。

眉毛のメッシュが顔のメッシュとマージされていたので、Blenderの練習も兼ねて眉毛を独立したメッシュとして分割しました。

Meshを独立させることで、Unity上で独立したパスとして描画ができます。

Stencilを使うパターン

まずはStencilを使うパターンを実装しました。

眉毛と前髪を特殊なシェーダーにして、前髪は2Passで描画しています。

  • 眉毛でStencil(今回はStencil値を2)を書き込む
  • 前髪の1Pass目で眉毛に重ならない領域(Comp NotEqual)を不透明描画
  • 前髪の2Pass目で眉毛に重なった領域(Comp Equal)を半透明描画してアルファを制御

前髪の2Pass目を省略すると、眉毛が常に不透明度100%で描画されます。2Pass目によって前髪を眉毛の上からアルファブレンドすることで、眉毛を半透明に見せています。 2Pass目の前髪のアルファ値が眉毛の透明度、つまり 1.0-眉毛の不透明度 になります。

Depth Offsetするパターン

Stencilは使わずにDepth Offsetするパターンでも実装しました。

眉毛だけ2Passで描画しています。

  • 眉毛1Pass目:普通に不透明で描画
  • 眉毛2Pass目:View空間上でDepth Offsetして前髪より手前に移動した状態でアルファブレンドで描画。アルファを顔の角度でフェード

非常にシンプルな実装です。

眉毛の2Pass目を省略すると通常の描画(前髪に隠れた眉毛は描画されない)になります。

2Pass目はDepth Offsetをして3D空間上で眉毛を前髪よりも手前に移動することで、実質的にZ Testの無効化と同じ効果があります。

Z Testを無効化しても同じ効果を得られますが、Z Testを無効化してしまうと背景や他のキャラクターまで眉毛が貫通して描画してしまうため、Depth Offsetの方が利点が多いように思います。

DepthのOffset量についてはパラメーターなどで調整可能にして、前髪より眉毛が手前になるべく小さい値にしています。

Depth Offsetのアプローチについては、こちらの中国語の記事で知りました。

Depth Offsetは頂点シェーダーでこのような処理をしています。

クリッピング空間上のZにしか影響を与えないように実装したので、深度情報のみが変化し、メッシュのシルエットは変化しません。

// View空間上でDepth Offset
// https://zhuanlan.zhihu.com/p/696515379
float3 positionWS = TransformObjectToWorld(input.positionOS.xyz);
float3 positionVS = mul(UNITY_MATRIX_V, float4(positionWS, 1.0)).xyz;

// View空間上でDepth Offset
positionVS.z += _DepthOffset;

float4 positionHCS = TransformWViewToHClip(positionVS);
float depth = positionHCS.z / positionHCS.w;
output.positionHCS = TransformObjectToHClip(input.positionOS.xyz);

// クリッピング空間上でオフセットされた深度を適用
output.positionHCS.z = depth * output.positionHCS.w;

この記事だけを読むと、前髪に被っている/被っていないで、眉毛の透明度が変化するのが不思議に思ったのですが、こちらのツイートで疑問が解決しました。

シェーダー側では何か特別な処理をしなくても、眉毛をDepth Offset(もしくはZ Test無効)してアルファブレンドすれば自然に意図した結果になります。

  • 前髪に被ってるところ
    • 髪+眉のブレンドなので、眉の不透明度は眉のアルファ値で変化
  • 前髪に被ってないところ
    • 眉+眉のブレンドなので、眉のアルファ値がいくつでも眉の色がそのままの色になる(眉の不透明度は100%で固定)

色々と頭を捻りましたが、最終的にはこんなシンプルな仕組みでも同じ効果が得られて、おもしろいなと思いました。

考察とまとめ

2パターン実装してみましたが、Depth Offsetするパターンの方が使い勝手の面でも性能面でも優位性がありそうだと思いました。

Stencilを使うアプローチでは、キャラクターが複数になったときに、顔が重なると破綻してしまいます。たとえば、キャラクターの頭が重なったときに、奥側のキャラクターの眉毛が手前のキャラクターの頭を貫通するということが起こり得ます。

Depth Offsetするパターンでは、こうした問題を回避できます。

描画負荷の面でも、眉毛の面積は前髪よりも小さいので、眉毛の方を2PassにするDepth Offsetの方がGPU負荷が低いと予想できます。

特殊な眉毛のキャラクターを描画をする機会があれば、DetphOffsetを使ってみたいと思いました。

ちなみに、目(眼球)のようにZ Test無効にすると眼球全体が最前面になって見た目が破綻する要素については、Depth Offsetでは難しいので、Stencilを使うしかないと考えています。

comments powered by Disqus

gam0022.net's Tag Cloud