この記事はUnreal Engine (UE) Advent Calendar 2023のシリーズ2、18日目の記事です。
キャラの半透明処理
最近……でも無いですがカメラの邪魔になるキャラとか背景オブジェクトを透過させる処理をよく見ます。割りと当たり前に実装されているこの機能ですが、レンダリングプログラマーにははっきりいって敵です。本当にそれ必要なのか?透けなくてもええやん、カメラで頑張れや、ゴッド・オブ・ウォーとか透過無いぞ?とか文句を言う日々ですが、まあ現場は聞いてはくれません。
半透明がなんでそんなに嫌なん?
レンダリングプログラマーが半透明を嫌うにはちゃんと理由があります。むやみに嫌がってるわけではないんです。以下その理由です。
描画が重い
単純に描画負荷が高いです。半透明で描画するためには、すでに描画された結果を読み込んで半透明で演算して書き戻す必要があります。早期カリングといった様々な描画負荷軽減の仕組みも半透明描画に対しては機能しません。半透明ポリゴンが多重に重なったりすると激重です。
描画処理が特殊になる
今やディファードレンダリングが主流ですが、ディファードレンダリングでは半透明を処理できないので、半透明だけフォワードで別途描画したり、別バッファに描画したものを後で合成したりする必要があります。描画パスが別になるし、ディファードとは別の描画方法になるので描画に一貫性がなくなります。ライティングもディファードと同じようには適用されないので、少し品質が低いライティングになったりもします。最近流行りのNaniteとかLumenとかも使用できません。
被写界深度やブラーと相性が悪い
これはなんでかと言うと、半透明描画は深度情報を書き込まないので深度をもとに計算される被写界深度が適用できません。ブラーも同様です。なので被写界深度が半透明にかかったりかからなかったりします。これは半透明が被写界深度の前に描かれるか後に描かれるかの問題で、前に描くとその半透明の奥の描画と同じボケかたをしますし、後に書くと被写界深度は一切かかりません。
ブラーに関しても深度でぼかす度合いを調整する場合に、深度を持たない半透明には適用できないという問題があるので、カメラを激しく動かしたり、半透明オブジェクトが激しく動くといい感じにブラーがかからなかったりします。
半透明マテリアルにも色々オプションがありますが、Output Depth and Velocityを有効にすることでモーションブラーの描画を改善することができたりします。
描画順が厳密に制御できない
これは前段の深度情報を持たないという事に起因しますが、半透明が重なった場合の前後関係を判定するのが困難なので、正しい順番で描画できない場合があります。半透明の描画時は奥から順番に描画することである程度の前後関係は保たれますが、そのためにソートが必要で負荷の増加に繋がりますし、めり込んだ半透明同士を正しく描画することはできません。透明なグラスの中の液体とか特殊な描画処理が必要ですし難易度も高い。
最近はOIT(Order-independent transparency)という半透明を正しく描く技術も使われることがありますが、描画負荷が高かったりメモリ使用量が増えたり、実装難易度が高かったりするのであまり普及はしていません。比較的実装が楽なOITもあるので、もんしょさんの記事など参考にすると良いかと思います。
とはいえ、フレームレート重視のゲームに使うにはまだ厳しいかなと思ってしまいます。
まあ、そんなわけで半透明描画はできればやりたくないというのがレンダリングプログラマーの正直な気持ちです。
UE5での半透明描画
まあ、長々と愚痴を書いたのですが、それでも半透明が嫌だでは許してもらえません。ということで半透明と付き合っていくわけですが、UEで半透明マテリアルを作成する際にはだいたい2つの選択肢のどちらかを選択することになります。
Translucent
いわゆる半透明です。マテリアルのBlend ModeをTranslucentにすると半透明描画するマテリアルになります。 GPUの半透明描画が使用されますが、UEではほとんどの描画が終わった後に描画されます。
Dither TemporalAA
こちらは疑似半透明です。Blend ModeはMaskedでディザパターンで描画されます。ディザなのでドットが見えそうですが、TemporalAAによる時間方向の補完を利用して比較的きれいに半透明描画になります。
TemporalAAを簡単に説明すると、ポリゴンのエッジのギザギザなどを時間方向。つまり過去のフレームの情報を利用してなめらかに補完する技術です。
Translucentは描画ハードウェアによる半透明なので、ある意味まっとうな半透明ですが、前述のように半透明ゆえの様々な問題が発生する可能性があります。特にキャラクターを半透明化すると
- ライティングが正しくない
- 影が描画されない
- 重なったポリゴンが汚く見えたり、キャラの内部が透けたりする。特に顔や髪はかなり悲惨なことになる場合がある。
- AOやSSRなど適用されない描画が多数
などなど。
Dither TemporalAAによる半透明はけっこうキレイだし、ディファードレンダリングとして他のポリゴンと同じように描画されるので描画の一貫性も保てます。欠点といえば
- 半透明の濃さが6段階に制限される
- TemporalAAありきなので、これが使えない、使いたくないデバイスではただのディザになってしまう。
- カメラやアニメーションで動いたときにジッターが出やすい。
- Dither TemporalAAが多重に重なった場合に奥のオブジェクトが消えたり汚い重なりになったりする場合がある。
といったところ。
どちらを使っても完璧な半透明描画にはならないので結局のところ妥協するしかないのが現状です。
実際にキャラを透過させよう(やりたくないけど)
実際問題としてキャラにTranslucentを使うのはあまり現実的では無いと思います。理由のいくつかは前述でも触れましたが
- 透過遷移時にマテリアルを入れ替える必要がある
- 描画がしょぼくなる
- 半透明どうしが重なったところが汚いし、顔の中の目とか歯とかが透けてグロい
- 影が描画できない
などなど。
なのでまあ、オーソドックスにDither Temporal AAで透過させることにします。
マテリアルの設定
マテリアルはMaskedにします。
Opacity MaskにDitherTemporalAAノードを接続して、Alpha ThresholdにScalar Parameterを接続する。
これでマテリアルのOpacityパラメーターを0-1で変化させれば半透明っぽく描画できます。
アクターの設定
ということでまあ普通に使うにはこんな感じで透過させることになるのかと思います。
しかし、影をよく見ると、影にもディザがかかってしまい、不自然に見えてしまうかもしれません。対策としては一定以上透明に近づくと影を消してしまうというのもひとつの手です。こんな感じでノードを組むと、Opacityが0.333以下だと影が消えるようにできます。
ポイントはShadow Pass Switchです。シャドウマップへの描画のときのOPacity Maskを別途計算することで制御しています。
自然に影を薄くできると良いのですが、そこそこ面倒なエンジン改造が必要なので今回はやりません。
なぜ影はきれいにディザがかからないかというと、シャドウマップにはTemporalAAがかからないし、スクリーンに投影されるときにディザパターンも歪んでしまうからです。
Shadow Pass Switchの処理を入れるとこんな感じです。
まあ実用上問題ないレベルで半透明らしくなります。UEで開発されている多くのタイトルでは同じように実装されてるんじゃないかと思います。
ちょっと捻ってみよう
Temporal AAが使えない場合などに次善の策として、透過時は単色の半透明で塗りつぶすという対応をしてみようと思います。シルエットが残ってれば良いでしょって場合の方策です。ナントカの夜っぽい感じ。
アクターの設定
アクターの設定をいじります。この記事ではThirdPersonテンプレートを触っているので、BP_ThirdPersonCharacterに手を入れます。
まずMesh/Render in Main Passを無効にします。これで何が起きるかというと、プレイヤーのメッシュが描画されなくなります。
次にMesh/Render CustomDepth Passを有効にして、CustomDepth Stencil Valueに1を入れます。
プロジェクトの設定
プロジェクト設定を開いてエンジン-レンダリングのポストプロセス、カスタム深度ステンシルパスの設定をEnabled with Stencilに変更します。
ここまでで何が起きるかというと、プレイヤーのメッシュは描画されないが、プレイヤーのメッシュが描画される領域のCustom Stencilに1の値が書き込まれます。
このようにアクターのメッシュは描画されなくなりますが、影は描画されています。
メッシュは描画されませんがCustom Stencilは描画されています。
この状態で何ができるかというと、Custom DepthやCustom Stencilの情報を使ってキャラの描画をカスタムしようという趣旨です。
ポストプロセスマテリアル
ポストプロセスマテリアルを作成して適用します。Blendable LocationはBefore Translucencyにしてみました。ノードはこんな感じです。
簡単です。Stencilが1のところに背景にShilhoutte Colorを乗算したものを描画、それ以外は背景をそのまま出力しているだけです。
結果はこんな感じ。
ただし、このままだとステンシル部分が背景を貫通してしまいます。
そこでCustom DepthがDepthより奥にある部分だけ描画するようにマスクします。
ifを使って1.0とCustom Stencilの比較を行っていますが、これは悪い手です。浮動小数点値を直接比較しても誤差によって同じ値とみなされないことがあるからです。ここではわかりやすくするために行っていますし、計算結果ではなくあくまでも定数値だから正しく判定できていますが、なんらかの計算結果をイコールで比較するのは危険なので注意しましょう。16bit浮動小数が混ざったりしても簡単に破綻します。
TemporalAAが使えないデバイスだったり、シルエットだけ残れば良い場合はこういう手段もあるという例でした。Custom Depth描画が追加されたりポストプロセス描画が必要なので必ずしも負荷が軽くなるということも無いですが、TemporalAAよりは軽いはず?
この手法で輪郭線だけ描画するとかアイディア次第では色々面白いこともできるかも。
光学迷彩にしてみよう
ということでせっかくなのでもうちょい凝った表現をやってみましょう。
みんな大好き光学迷彩風。一見半透明&ディストーションっぽいですが、安心してください不透明ですよ!
何をやってるかというと、これも前段と同じようにポストプロセスでCustom Depth/Custom Stencilが描画された部分にだけ特殊描画をしているのですが、Custom Depthの深度の勾配から大雑把な法線を計算して、屈折ベクトルを作成してサンプリングすることでメッシュの法線にそって屈折しているように見せています。
ノードはこんな感じです。
スクリーンスペースと深度情報という全くスケールが違うパラメーターを混ぜているし、頂点法線しか反映されないのでかなり大雑把で不正確な法線ですが、それっぽくはなります。サンプリング点を増やしたりノイズを混ぜたりするともうちょい良い感じになりそうです。
まとめ
結局毎度のマテリアル芸になってしまいました。まあ実際の話自キャラくらい透過処理入れたところで全体の描画負荷に大きな影響は無いのですが、「邪魔だから透過させてしまえ」と安易な透過処理を入れるのが個人的には好きでは無いです。自キャラだけならともかく背景やプロップ、敵キャラなどなどなんでもかんでも透過処理を入れ始めると描画負荷にも処理負荷にも影響が出てくるので、よく考えて使って欲しいなと思うのであります。
レイトレ、Nanite、Lumenの時代なのに古臭いことやってるなあと思わなくも無いですが2023年のアドカレ記事でした。