こちらは Unreal Engine 4 (UE4) Advent Calendar 2018 17日目の記事です。
UE4でVRMのマテリアルを作っていて思った。
「Unityでは可愛く描画できてるのに… UE4のコレジャナイ感はなんなの?」
#UE4のUnlitでトゥーンレンダリングをキメる!
Unlitでマテリアルを組むと、想定よりも色味が薄く出力されます。
トゥーンレンダリングに限らず、UE4のNPRは大概この問題にぶち当たるように思います。
この原因を探り、根本的な対処をすることが今回の目的です。
###先に結果
左:コレジャナイ。Unlitでテクスチャを貼ったもの。
右:キマったもの。FilmToneMapInverse利用。
シーンはデフォルトマップです。
https://3d.nicovideo.jp/alicia/ |
https://vroid.pixiv.net/ |
これらを踏まえて、ベースをLITにしてライトを適用、PostProcessで調整したもの
https://vroid.pixiv.net/ |
想定に近い色が出るようになりました。PostProcessも素直に反映されています。
なおキャッチー(モデラーさんに感謝)なスクショはこれだけです。
後は地味で分かりづらい内容が続きます。。
検証にはUE4.20.3 WindowsでSDRモニタを利用しています。
おそらくHDRモニタでは結果が異なります(私は未確認です)
色空間やモニタについては以下のEpicさんの資料を合わせて読むと 理解が深まると思います。
https://www.slideshare.net/EpicGamesJapan/ue4-v11
一部 理解が浅く、解説がふわふわした箇所があります。気になる方はご自分の手でご確認ください。
間違いあったらすみません。ツッコミをお願いします。
###方針:テクスチャの値をそのまま出す
大方針は「テクスチャの値や手入力値」と「出力値」を一致させることです。
また汎用的に利用したいので、既存の描画機能と共存できるもので、エンジン改造なし、マテリアルで完結するものを探ります。
###コレジャナイの原因:FilmicTonemapper
先に話してしまいますが、原因はこいつです。
https://docs.unrealengine.com/en-us/Engine/Rendering/PostProcessEffects/ColorGrading
UE4.15でしれっと有効化されたこの機能、バージョンアップで苦しんだ方も多いのではないでしょうか。私は苦しかったです
それなりな見栄えを求める場合、トゥーンレンダリング向けの決定打はなさそうです。
正直なところ改造して真面目に実装するか、PostProcessで頑張る方が早そうな感じはします… が今回はマテリアルで頑張ります。
先に私見をまとめておきます。アイデア次第では、全ての場面で破綻しない実装方法がある… かもしれません。
対処方法 | 入力と出力が一致する | PostProcess使える | 背景に影響しない | モバイルで利用できる | 補足・懸念 |
---|---|---|---|---|---|
なにもしない | × | ◎ | ◎ | ◎ | |
r.TonemapperFilm=0 | ○ | ◎ | ○ | ◎ | 将来的に廃止されるらしい? |
自前のTonemap またはShowFlag.Tonemapper=0 |
◎ | × | ○ | ◎ | |
Lit+PostProcess | ◎ | × | △ | × | ForwardShadingで利用できない |
カラーグレーディングやLUTで調整 | △ | ◎ | △ | ◎ | 手間がかかる |
FilmToneMapInverseを利用 | △ | ◎ | ◎ | ◎ | 正しい処理か不明 |
◎:問題なし ○:厳密には違うが許容範囲 △:許容できるか微妙 ×:全然ダメ
説明は省きますが、MToonの機能を阻害しないことが前提です。
以前にまとめた記事があるので、ご興味ある方はどうぞ。
順を追って検証していきます。
#検証
##検証1 入力値をそのまま出す
###vector3で(入力=出力)を目指す
Unlitでこんなマテリアルを組んでみます。
出力されてほしいのは、
255 * 0.25 = 63.75
255 * 0.50 = 127.5
255 * 0.75 = 191.25
ですが、それより大きな値(158,191,209)が出ました。
これはTonemapの影響です。ひとまず無効化しましょう。
このようなPostProcessMaterialを作成しTonemapを置き換えます。
|
(64,127,191)で、ほぼ一致しました。
おそらくですが、「Replacing the Tonemapper」では、Tonemapに加えてその後段のDisplayGammaまで置き換わるようです。
余談ですが、今回の色の確認にはWindows標準のペイントブラシのスポイト機能を利用します。
UE4のスポイトは、LinearColorに変換した値が格納されます。
試しにエディタで上記の色を取得すると、
|
(0.0512, 0.2122, 0.5209)という異なる値が出ました。
UE4のsRGBからLinearへの変換は、以下のようなコードを利用していますので、、
half3 sRGBToLinear( half3 Color )
{
Color = max(6.10352e-5, Color); // minimum positive non-denormal (fixes black problem on DX11 AMD and NV)
return Color > 0.04045 ? pow( Color * (1.0 / 1.055) + 0.0521327, 2.4 ) : Color * (1.0 / 12.92);
}
これをcol=(0.25, 0.5, 0.75)にあてはめると、、
R=0.25 | G=0.5 | B=0.75 | |
---|---|---|---|
スポイトした値 | 0.0512 | 0.2122 | 0.5209 |
sRGBToLinear変換した値 | 0.0508 | 0.2140 | 0.5225 |
まだ差があって気になりますが… 本題ではないので深追いしません… |
###テクスチャ値で(入力=出力)を目指す
このTonemapでUnlitのテクスチャを出力してみます
マテリアル |
利用したテクスチャ |
レンダリング結果 |
値が小さく(絵的には暗く)なりました。
これはテクスチャの値がsRGBからLinearColorへ変換されているからです。
これを無効化するため、テクスチャのsRGBチェックを外します。同時にサンプラはLinearColorにします。
元テクスチャと完全に一致しました。
同じ値は出ましたが、このままでは全体的に暗く、HDRを活かした絵が出ません。続きます。
##検証2 TonemapなしDisplayGamma有り を再現する
コンソールコマンドより ShowFlag.Tonemapper=0 を入力すると、Tonemapが無効化されます。
手始めにPostProcessでこの見た目を再現してみます。
中身は簡単で、ざっくりと、
OutColor = pow(InColor, 1/2.2);
とすれば完成です。
それっぽい見た目になりました。
##Tonemapなしで vector3の(入力=出力)を目指す
DisplayGammaを打ち消すために、pow(x, 2.2)を追加します。
(64,127,191) で元と一致しました。
###Tonemapなしで テクスチャ値の(入力=出力)を目指す
先程のPostProcessで テクスチャ(sRGBチェック入り)をUnlitで出してみます。
レンダリング結果とテクスチャを交互に表示したもの |
だいたい合っていますが、厳密な数値では差(256段階で1~2程度)が出ています。
これはおそらく、sRGBToLinear(col)とpow(col,2.2)の差です。この2つは近似に利用されるくらい似たようなカーブを描きます。
厳密に値を合わせるならば、テクスチャのsRGBチェックを外して、自前でpow(col,2.2)を追加すればOKです。
元テクスチャと完全一致します。
これでHDRを活かしつつ、値をそのまま出すことができました。大抵はこの手法による対処で問題ないと思います。
※ただし、PostProcessは効きません。
※PostProcessを使う場合は、差異を許容してr.TonemapperFilm=0で上記設定を利用するか、r.TonemapperFilm=0自体を打ち消すマテリアルを作成する必要があります。
それではいよいよ、UE4のデフォルト設定と馴染ませやすい方法を探ってみます。続きます。
##検証3 FilmicTonemapper上で(入力=出力)を目指す
本題です。
テクスチャの値をFilmicTonemapperで出力してみましょう。
初期状態に戻すため コンソールコマンドで r.TonemapperFilm=1、ShowFlag.Tonemapper=1 として、FilmicTonemapperを有効化します。
レンダリング結果とテクスチャを並べたもの |
矢印で示した帯はテクスチャ画像です。
だいぶ差があります。特に目立つのは、左下の白や左上の肌、青系全般も気になります。
UE4の素のUnlitで白色や肌色がクッキリ出ないのは この影響でしょう。
###対策1:FilmicTonemapperのパラメータで頑張る
結論だけ言ってしまえば、この方法では調整は厳しそうです。
そもそも入力値1をそのまま出力1とするためには、slopeの値を1以上にするか、他のパラメータ(DisplayGammaやExposure、マテリアル自体)をいじる必要がありそうです。
なんとなく近似できる組み合わせはありそうなのですが…
下図は初期パラメータのグラフで、入力1に対して、出力は約0.75 となっています。
つまり真っ白を想定して1を入力しても、実際はx0.75された灰色が出力されます。
UE4ToneMapper:https://www.desmos.com/calculator/h8rbdpawxj
こちらを参考にさせていただきました:https://technorgb.blogspot.com/2018/11/ue4.html?spref=tw
###対策2:FilmToneMapInverse を利用する
なんとか出来ないかシェーダを眺めていると、FilmToneMapInverse という関数を見つけました。
※ちゃんと読めていません。関数名から察するに、FilmicTonemapperの逆変換をしてくれるのでしょう… が、パラメータが固定されています。特定の条件下での簡易的な逆変換のように見えます。
藁にもすがる思いで組み込みます
家弓メソッドです |
そこそこ合いました。が、この差を許容できるかは微妙なラインですね…
##検証4 LitのBaseColorを利用して(入力=出力)を目指す
ここまでの検証は「Unlitのマテリアル内でTonemapを打ち消す手法」に注力してきました。
今回はアプローチを変えて、値をLitのBaseColorに格納し、それをPostProcessで拾う手法を試してみます。
マテリアル |
ポストプロセスマテリアル |
レンダリング結果 |
値が小さく(絵的には暗く)なりました。
検証1でやった通り、画像のsRGBチェックを外せば一致します。
毎回sRGBチェックを外すのも手間なので、今回はLinear2srgbを利用してマテリアルで対処してみます。
中身はGammaCorrectionCommon.ush の LinearToSrgbBranchingChannelにあります。
テクスチャと色があいました。
当然ながら、シーン全体がBaseColorで描画されてしまっています。空はUnlitなのでBaseColorを参照すると真っ黒になっています。
対処する場合は、CustomDepth等を利用したマスク処理が必要でしょう。
また、TemporalAAを利用している場合はジラ付きが発生します。
FXAAを利用しましょう。(MSAAが利用できない理由は後述します)
マスクを利用すると、、
対象のモデルのみテクスチャ値が出力され、
他はFilmicTonemapperが効いている絵が出ました。
ただしこれはGBufferを利用しているためForwardShadingでは利用できません(=MSAAも利用できない)
また対象のモデルにはPostProcessが効きません。
##検証おまけ UE4とUnityで同じ値を出力する
以下は素のUE4とUnityそれぞれで、Unlitのマテリアルに(0.25,0.5,0.75)を入力した結果です
同じ入力値でもUE4の方が大きな値に(絵的には明るく)なっています。
「カラーを入力してグレイスケールのテクスチャと乗算する」ような場合、同じ数値をそのまま使うと結果がずれます。
見た目を素のUnityに寄せるならば、UE4側の入力にsRGBToLinear(col)が必要です。
(環境や設定によって結果は異なることがあります)
#まとめ
これであなたのUnlitもトゥーンレンダリングがキマったでしょうか。
ただ解説途中でも触れたように、対処方法によっては既存のPostProcessが使えなかったり、他モデルと並べて表示すると破綻する場合があります。
ゲームであれば、エフェクトや半透明との相性、描画負荷の問題もあるでしょう。
デバイスによってはForwardShadingで破綻ないか確認する必要もあります(モバイルやスタンドアロンのVRデバイス)
十分吟味の上、適した手法を探ってみてください。
###宣伝:冒頭のスクショはVRM4Uを利用しました
冒頭のスクショはVRM4U(VRMローダプラグイン)を利用したものです。
もし興味あれば下記リンクからお試しください。近々VRoid Hubと連携可能になる… 予定です。
https://github.com/ruyo/VRM4U/
以上です。