
こんにちは、Unityエンジニアのrioil(@rioil_dev)です。 Mirrativの3Dアバター(エモモ)の描画には、Universal Render Pipeline(URP)をベースに、カスタムのシェーダーやレンダーパスを組み合わせて使用しています。
今回、あるコラボ企画をきっかけに「眉毛を髪の上に描画する」機能を実装しました。普段のエモモは眉毛が前髪に隠れますが、キャラクターによっては前髪の上に眉毛が見えているデザインがあり、これを再現したいという要望でした。
一見すると「眉毛を最前面に描けばよいだけ」に思えますが、実際には前後関係や半透明の扱いに工夫が必要で、いくつかの実装方式を試すことになりました。この記事では、いくつかの試作品とそれぞれの問題点、そして最終的にたどり着いた実装方式までの過程を紹介します。
エモモの眉毛のメッシュ構造
まず前提として、エモモの眉毛がどのように描画されているかを説明します。
眉毛は目のメッシュの一部ですが、専用のシェーダーで描画されています。眉毛のテクスチャは、眉の形の周囲が透明になっているだけでなく、眉のフチなどにアルファ値が中間の半透明な部分を含んでいます。メッシュの見た目上の「眉毛の形」とポリゴンが占める領域(下の画像の緑の部分)が一致しないこと、そして特に眉毛が完全な不透明ではなく半透明であることが、この後の実装で課題になりました。

通常のエモモでは、眉毛は頭部メッシュと同じように深度(Zバッファ)に従って描画されるため、前髪の方が手前にある場合は眉毛が前髪に隠れます。今回はこの前後関係を、特定の条件下で意図的に上書きするのが目的です。
試作品1:ZTestを無効化して常に前面に描く
最初に試したのは、最もシンプルな方法です。眉毛のシェーダーには以前から、深度テスト(ZTest)の比較モードを変更する仕組みが用意されていました。ZTestの比較モードを Always にすると、深度に関係なく常に描画されるため、眉毛が必ず手前に描かれるようになります。
実際に試すと、確かに眉毛は前髪の上に描画されるようになりました。しかし、Always は「髪より手前」ではなく「あらゆるメッシュより手前」に描画してしまうという問題があります。
そのため、たとえば顔の前に要素が来るような衣装を着用すると、本来隠れるべき場面でも眉毛が衣装の上に浮き出てしまいました。

この方式は実装こそ単純ですが、髪以外との前後関係を破壊してしまうため採用を見送りました。
試作品2:眉毛のメッシュをマスクにして髪を消す
次に検討したのが、ステンシルバッファを使う方法です。考え方としては、「眉毛が描かれる領域だけ、その手前にある髪を描かないようにする」ことで、結果的に眉毛が髪の上に見えるようにする、というアプローチです。
ステンシルを使う場合、眉毛メッシュの形でマスクを書き込み、髪の描画時にそのマスク領域を避ける、という流れになります。ただしここで前述の「眉毛メッシュには透明領域が含まれる」という性質が問題になりました。素直にステンシルを書き込むと、眉毛の透明な部分までマスクされてしまい、眉の周囲の四角い領域ごと髪が欠けてしまいます。
これは、シェーダーの clip() で透明ピクセルを破棄することで解決できました。clip() で破棄されたピクセルはステンシル値の書き込みも行われないため、眉の形に沿ったマスクだけを残せます。これにより、少なくとも眉毛の不透明な部分については、意図通り髪の上に見えるようになりました。
しかし、この「髪を消す」アプローチには別の課題が残りました。
- 半透明部分の色:眉毛のフチなど半透明な部分では、本来は下にある髪の色とブレンドされてほしいのに、髪を消してしまうと下地の肌の色と合成されてしまいます。髪が暗いデザインなどでは眉頭が肌色っぽく浮いてしまい、不自然になります。これを正しく見せようとすると、ステンシルの書き込みを不透明部分と半透明部分で分け、髪色を一度描画してからアルファブレンドする、といった多段階の描画が必要になり、実装が一気に複雑化しました。
- 髪以外との前後関係:髪の深度を書き込まない領域ができるため、ヘアオーナメントや帽子、メガネといった頭部周辺のオブジェクトと重なったときに、前後関係が崩れる懸念がありました。発生条件は限定的とはいえ、破綻のリスクを抱えたまま進めるのは避けたいところでした。
複雑さと前後関係のリスクを天秤にかけた結果、この方式も最終的には採用しませんでした。
最終案:髪が見えている領域をマーキングして眉毛側でテストする
ここまでの試作品は、いずれも「眉毛側を起点に、その手前の髪を消す」という発想でした。最終案では発想を逆転させ、「実際に髪が見えている領域を記録しておき、眉毛はその領域でだけ前面に描く」という方式に切り替えました。
処理の流れは次のようになります。
- すべての不透明メッシュを通常どおり描画する(髪も含めて常にZバッファへ書き込む)
- 髪とアウトラインが実際に画面に見えている領域をステンシルにマーキングする
- このために専用のレンダーパスを追加
- ZTestを
Equalにして、深度バッファと一致するピクセル(=最終的に表示されている髪)だけをステンシル値でマーキング
- 眉毛を2パスで描画する
- パス1(髪が見えている領域):ZTestなしで常に描画し、髪の上に眉毛を載せる
- パス2(髪が見えていない領域):通常どおりZTest
LEqualで描画する
このアプローチの利点は次のとおりです。
- 前後関係が正確:髪は常に深度を書き込むため、衣装やアクセサリーなど他の不透明メッシュとの前後関係が破綻しません。試作品1・2で問題になっていた点が原理的に解消されました。
- 可視判定が正確:髪が「実際に見えている」領域だけをマーキングするため、髪が他のものに隠れている部分まで眉毛が浮くことがありません。

また、眉毛を髪の上に描画する設定が無効なアバターしかいない場合には、マーキングのレンダーパスや前面描画パス自体をスキップするようにし、不要な描画コストが発生しないようにしています。
まとめ
「眉毛を髪の上に描画する」という一見シンプルな要望でしたが、
- ZTestを無効にするだけでは髪以外との前後関係が壊れる
- 眉毛を起点に髪を消す方式は半透明や前後関係で複雑さとリスクが残る
という問題を経て、最終的には「髪が見えている領域をマーキングし、眉毛をその領域で前面描画する」方式に落ち着きました。発想を「眉毛から消す」から「髪が見えている所に載せる」へ転換したことで、前後関係と半透明を無理なく両立できました。レンダーパスは増えるものの、パフォーマンスへの影響は小さく抑えられています。
We are hiring!
ミラティブでは一緒に開発してくれるエンジニアを募集しています!少しでも興味を持っていただいた方は、ぜひ気軽にエントリーください。 また、ミラティブの技術関連の情報は公式Xアカウント(@mirrativ_tech)にて随時発信していますので、ぜひフォローいただけると嬉しいです。