Posted at

UE4のUnlitでトゥーンレンダリングをキメる!

こちらは Unreal Engine 4 (UE4) Advent Calendar 2018 17日目の記事です。

UE4でVRMのマテリアルを作っていて思った。

「Unityでは可愛く描画できてるのに… UE4のコレジャナイ感はなんなの?」


UE4のUnlitでトゥーンレンダリングをキメる!

Unlitでマテリアルを組むと、想定よりも色味が薄く出力されます。

トゥーンレンダリングに限らず、UE4のNPRは大概この問題にぶち当たるように思います。

この原因を探り、根本的な対処をすることが今回の目的です。


先に結果

左:コレジャナイ。Unlitでテクスチャを貼ったもの。

右:キマったもの。FilmToneMapInverse利用。

シーンはデフォルトマップです。

https://3d.nicovideo.jp/alicia/
image.png

https://vroid.pixiv.net/
image.png

これらを踏まえて、ベースをLITにしてライトを適用、PostProcessで調整したもの

https://vroid.pixiv.net/
image.png

想定に近い色が出るようになりました。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でこんなマテリアルを組んでみます。

image.png

image.png

出力されてほしいのは、

255 * 0.25 = 63.75

255 * 0.50 = 127.5

255 * 0.75 = 191.25

ですが、それより大きな値(158,191,209)が出ました。

これはTonemapの影響です。ひとまず無効化しましょう。

このようなPostProcessMaterialを作成しTonemapを置き換えます。

image.png

image.png

(64,127,191)で、ほぼ一致しました。

おそらくですが、「Replacing the Tonemapper」では、Tonemapに加えてその後段のDisplayGammaまで置き換わるようです。

余談ですが、今回の色の確認にはWindows標準のペイントブラシのスポイト機能を利用します。

UE4のスポイトは、LinearColorに変換した値が格納されます。

試しにエディタで上記の色を取得すると、

image.png

(0.0512, 0.2122, 0.5209)という異なる値が出ました。

UE4のsRGBからLinearへの変換は、以下のようなコードを利用していますので、、


GammaCorrectionCommon.ush

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のテクスチャを出力してみます

マテリアル
image.png

利用したテクスチャ
image.png

レンダリング結果
image.png

値が小さく(絵的には暗く)なりました。

これはテクスチャの値がsRGBからLinearColorへ変換されているからです。

これを無効化するため、テクスチャのsRGBチェックを外します。同時にサンプラはLinearColorにします。

image.png

image.png

image.png

元テクスチャと完全に一致しました。

同じ値は出ましたが、このままでは全体的に暗く、HDRを活かした絵が出ません。続きます。


検証2 TonemapなしDisplayGamma有り を再現する

コンソールコマンドより ShowFlag.Tonemapper=0 を入力すると、Tonemapが無効化されます。

手始めにPostProcessでこの見た目を再現してみます。

中身は簡単で、ざっくりと、

OutColor = pow(InColor, 1/2.2);

とすれば完成です。

image.png

image.png

それっぽい見た目になりました。


Tonemapなしで vector3の(入力=出力)を目指す

DisplayGammaを打ち消すために、pow(x, 2.2)を追加します。

image.png

image.png

(64,127,191) で元と一致しました。


Tonemapなしで テクスチャ値の(入力=出力)を目指す

先程のPostProcessで テクスチャ(sRGBチェック入り)をUnlitで出してみます。

image.png

レンダリング結果とテクスチャを交互に表示したもの
01.gif

だいたい合っていますが、厳密な数値では差(256段階で1~2程度)が出ています。

これはおそらく、sRGBToLinear(col)とpow(col,2.2)の差です。この2つは近似に利用されるくらい似たようなカーブを描きます。

厳密に値を合わせるならば、テクスチャのsRGBチェックを外して、自前でpow(col,2.2)を追加すればOKです。

image.png

元テクスチャと完全一致します。

これでHDRを活かしつつ、値をそのまま出すことができました。大抵はこの手法による対処で問題ないと思います

※ただし、PostProcessは効きません。

※PostProcessを使う場合は、差異を許容してr.TonemapperFilm=0で上記設定を利用するか、r.TonemapperFilm=0自体を打ち消すマテリアルを作成する必要があります。

それではいよいよ、UE4のデフォルト設定と馴染ませやすい方法を探ってみます。続きます。


検証3 FilmicTonemapper上で(入力=出力)を目指す

本題です。

テクスチャの値をFilmicTonemapperで出力してみましょう。

初期状態に戻すため コンソールコマンドで r.TonemapperFilm=1、ShowFlag.Tonemapper=1 として、FilmicTonemapperを有効化します。

image.png

image.png

レンダリング結果とテクスチャを並べたもの
image.png

矢印で示した帯はテクスチャ画像です。

だいぶ差があります。特に目立つのは、左下の白や左上の肌、青系全般も気になります。

UE4の素のUnlitで白色や肌色がクッキリ出ないのは この影響でしょう。


対策1:FilmicTonemapperのパラメータで頑張る

結論だけ言ってしまえば、この方法では調整は厳しそうです。

そもそも入力値1をそのまま出力1とするためには、slopeの値を1以上にするか、他のパラメータ(DisplayGammaやExposure、マテリアル自体)をいじる必要がありそうです。

なんとなく近似できる組み合わせはありそうなのですが…

下図は初期パラメータのグラフで、入力1に対して、出力は約0.75 となっています。

つまり真っ白を想定して1を入力しても、実際はx0.75された灰色が出力されます。

image.png

UE4ToneMapper:https://www.desmos.com/calculator/h8rbdpawxj

こちらを参考にさせていただきました:https://technorgb.blogspot.com/2018/11/ue4.html?spref=tw


対策2:FilmToneMapInverse を利用する

なんとか出来ないかシェーダを眺めていると、FilmToneMapInverse という関数を見つけました。

※ちゃんと読めていません。関数名から察するに、FilmicTonemapperの逆変換をしてくれるのでしょう… が、パラメータが固定されています。特定の条件下での簡易的な逆変換のように見えます。

藁にもすがる思いで組み込みます

家弓メソッドです
image.png

image.png

image.png

そこそこ合いました。が、この差を許容できるかは微妙なラインですね…


検証4 LitのBaseColorを利用して(入力=出力)を目指す

ここまでの検証は「Unlitのマテリアル内でTonemapを打ち消す手法」に注力してきました。

今回はアプローチを変えて、値をLitのBaseColorに格納し、それをPostProcessで拾う手法を試してみます。

マテリアル
image.png

ポストプロセスマテリアル
image.png

レンダリング結果
image.png

値が小さく(絵的には暗く)なりました。

検証1でやった通り、画像のsRGBチェックを外せば一致します。

毎回sRGBチェックを外すのも手間なので、今回はLinear2srgbを利用してマテリアルで対処してみます。

中身はGammaCorrectionCommon.ush の LinearToSrgbBranchingChannelにあります。

image.png

image.png

テクスチャと色があいました。

当然ながら、シーン全体がBaseColorで描画されてしまっています。空はUnlitなのでBaseColorを参照すると真っ黒になっています。

対処する場合は、CustomDepth等を利用したマスク処理が必要でしょう。

また、TemporalAAを利用している場合はジラ付きが発生します。

FXAAを利用しましょう。(MSAAが利用できない理由は後述します)

マスクを利用すると、、

image.png

対象のモデルのみテクスチャ値が出力され、

他はFilmicTonemapperが効いている絵が出ました。

ただしこれはGBufferを利用しているためForwardShadingでは利用できません(=MSAAも利用できない)

また対象のモデルにはPostProcessが効きません。


検証おまけ UE4とUnityで同じ値を出力する

以下は素のUE4とUnityそれぞれで、Unlitのマテリアルに(0.25,0.5,0.75)を入力した結果です

image.png

image.png

同じ入力値でもUE4の方が大きな値に(絵的には明るく)なっています。

「カラーを入力してグレイスケールのテクスチャと乗算する」ような場合、同じ数値をそのまま使うと結果がずれます。

見た目を素のUnityに寄せるならば、UE4側の入力にsRGBToLinear(col)が必要です。

(環境や設定によって結果は異なることがあります)


まとめ

これであなたのUnlitもトゥーンレンダリングがキマったでしょうか。

ただ解説途中でも触れたように、対処方法によっては既存のPostProcessが使えなかったり、他モデルと並べて表示すると破綻する場合があります。

ゲームであれば、エフェクトや半透明との相性、描画負荷の問題もあるでしょう。

デバイスによってはForwardShadingで破綻ないか確認する必要もあります(モバイルやスタンドアロンのVRデバイス)

十分吟味の上、適した手法を探ってみてください。


宣伝:冒頭のスクショはVRM4Uを利用しました

冒頭のスクショはVRM4U(VRMローダプラグイン)を利用したものです。

もし興味あれば下記リンクからお試しください。近々VRoid Hubと連携可能になる… 予定です。

https://github.com/ruyo/VRM4U/

以上です。