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



こちらは [Unreal Engine 4 (UE4) Advent Calendar 2018](https://qiita.com/advent-calendar/2018/ue4) 17日目の記事です。

UE4でVRMのマテリアルを作っていて思った。
**「Unityでは可愛く描画できてるのに… UE4のコレジャナイ感はなんなの？」**

#UE4のUnlitでトゥーンレンダリングをキメる！
Unlitでマテリアルを組むと、**想定よりも色味が薄く**出力されます。
トゥーンレンダリングに限らず、**UE4のNPRは大概この問題にぶち当たる**ように思います。

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


###先に結果

左：コレジャナイ。Unlitでテクスチャを貼ったもの。
右：キマったもの。FilmToneMapInverse利用。
シーンはデフォルトマップです。

||
|---|
|https://3d.nicovideo.jp/alicia/<br>![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/ffc82062-a39b-c267-3681-1776231b6ebc.png)|
|https://vroid.pixiv.net/<br>![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/782353ea-5999-10ad-d68f-00db7c767416.png)|

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

||
|---|
|https://vroid.pixiv.net/<br>![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/5945bc5f-6947-41b8-8dc7-2703d58d0415.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<BR>またはShowFlag.Tonemapper=0|◎|×|○|◎||
|Lit+PostProcess|◎|×|△|×|ForwardShadingで利用できない|
|カラーグレーディングやLUTで調整|△|◎|△|◎|手間がかかる|
|FilmToneMapInverseを利用|△|◎|◎|◎|正しい処理か不明|

◎:問題なし　○：厳密には違うが許容範囲　　△：許容できるか微妙　　×：全然ダメ
説明は省きますが、**MToonの機能を阻害しない**ことが前提です。
[以前にまとめた記事](https://qiita.com/ruyo/items/ec082d81dea3033e1500)があるので、ご興味ある方はどうぞ。

順を追って検証していきます。

#検証

##検証1  入力値をそのまま出す
###vector3で（入力＝出力）を目指す
Unlitでこんなマテリアルを組んでみます。

||
|----|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/31778cbd-8eeb-ca2a-4a7e-a87a69357fc5.png)|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/95b57ac3-d14a-5436-7b14-fed1940bbf49.png)|

出力されてほしいのは、
255 * 0.25 = 63.75
255 * 0.50 = 127.5
255 * 0.75 = 191.25
ですが、それより大きな値(158,191,209)が出ました。
これはTonemapの影響です。ひとまず無効化しましょう。
このようなPostProcessMaterialを作成しTonemapを置き換えます。

||
|----|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/34afaa3a-ef40-e6ee-63dc-2f19a5e8dcba.png)|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/1a0747d9-3106-cb93-5408-1b2aa5e8daf9.png)
|

(64,127,191)で、ほぼ一致しました。
おそらくですが、「Replacing the Tonemapper」では、Tonemapに加えてその後段のDisplayGammaまで置き換わるようです。


余談ですが、今回の色の確認にはWindows標準のペイントブラシのスポイト機能を利用します。
UE4のスポイトは、LinearColorに変換した値が格納されます。
試しにエディタで上記の色を取得すると、

||
|----|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/e271af0a-726d-b98b-87be-0fc556bca8c1.png)
|

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

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

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

||
|----|
|マテリアル<BR>![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/3c3d78f7-640a-ddfe-0233-2c0dcef8e5fc.png)|
|利用したテクスチャ<BR>![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/617d9465-1d32-f217-2d85-7fbce501c297.png)|
|レンダリング結果<BR>![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/04d2e63b-d4c6-1c90-5a4e-e424b27aa88a.png)|

値が小さく（絵的には暗く）なりました。
これはテクスチャの値がsRGBからLinearColorへ変換されているからです。
これを無効化するため、テクスチャのsRGBチェックを外します。同時にサンプラはLinearColorにします。

||
|----|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/7613e965-b8bb-0cd3-3952-ddf3da8342e1.png)|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/b9869a03-95b5-5553-1109-3b8b31353428.png)|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/ed59bd22-3ade-308f-73ab-f13dc52cdca9.png)|

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

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


##検証2 TonemapなしDisplayGamma有り を再現する
コンソールコマンドより ShowFlag.Tonemapper=0 を入力すると、Tonemapが無効化されます。
手始めにPostProcessでこの見た目を再現してみます。

中身は簡単で、ざっくりと、
OutColor = pow(InColor, 1/2.2);
とすれば完成です。

||
|----|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/b782db52-4a4a-f448-ebbf-c6fd696f0ca4.png)|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/46b499e7-8731-8ef8-e16a-fa4e59d0d7da.png)|

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

##Tonemapなしで vector3の（入力＝出力）を目指す
DisplayGammaを打ち消すために、pow(x, 2.2)を追加します。

||
|----|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/dce5f504-a549-21f0-a3b9-31c3c01611b7.png)|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/7e5e3202-ae16-ffa6-edc7-591ef2aafc7c.png)|

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


###Tonemapなしで テクスチャ値の（入力＝出力）を目指す
先程のPostProcessで テクスチャ(sRGBチェック入り)をUnlitで出してみます。

||
|----|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/81727062-9fc8-985b-0454-e4804536f9c2.png)|
|レンダリング結果とテクスチャを交互に表示したもの<BR>![01.gif](https://qiita-image-store.s3.amazonaws.com/0/36233/1f677c5b-e0d5-aaf6-b2e4-d373b2601d32.gif)|

だいたい合っていますが、厳密な数値では差(256段階で1～2程度)が出ています。
これはおそらく、sRGBToLinear(col)とpow(col,2.2)の差です。この2つは近似に利用されるくらい似たようなカーブを描きます。

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

||
|----|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/674b686c-6800-e226-883f-b825e495eee9.png)|

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

これでHDRを活かしつつ、値をそのまま出すことができました。**大抵はこの手法による対処で問題ないと思います**。
※ただし、PostProcessは効きません。
※PostProcessを使う場合は、差異を許容してr.TonemapperFilm=0で上記設定を利用するか、r.TonemapperFilm=0自体を打ち消すマテリアルを作成する必要があります。


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


##検証3 FilmicTonemapper上で（入力＝出力）を目指す

本題です。
テクスチャの値をFilmicTonemapperで出力してみましょう。
初期状態に戻すため コンソールコマンドで r.TonemapperFilm=1、ShowFlag.Tonemapper=1 として、FilmicTonemapperを有効化します。

||
|----|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/001fc34d-eac1-1cd8-9921-a3533e44ca10.png)|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/d08f6c82-5294-294b-af1b-c46b76c91dcd.png)|
|レンダリング結果とテクスチャを並べたもの<BR>![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/c451fdfe-e2d0-bbbc-f783-49b0a00c8542.png)|

矢印で示した帯はテクスチャ画像です。
だいぶ差があります。特に目立つのは、左下の白や左上の肌、青系全般も気になります。
UE4の素のUnlitで**白色や肌色がクッキリ出ない**のは この影響でしょう。

###対策1：FilmicTonemapperのパラメータで頑張る
結論だけ言ってしまえば、この方法では調整は厳しそうです。
そもそも入力値1をそのまま出力1とするためには、slopeの値を1以上にするか、他のパラメータ（DisplayGammaやExposure、マテリアル自体）をいじる必要がありそうです。
なんとなく近似できる組み合わせはありそうなのですが…

下図は初期パラメータのグラフで、入力1に対して、出力は約0.75 となっています。
つまり**真っ白を想定して1を入力しても、実際はx0.75された灰色が出力**されます。

||
|---|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/b07f4f4a-d239-7220-c266-ee65c668eb1b.png)|

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


###対策2：FilmToneMapInverse を利用する

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

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

||
|----|
|家弓メソッドです<BR>![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/985c99b5-b5e6-fbd4-7136-2dc69546f123.png)|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/49954a12-a4e2-51f2-637c-1cf6ec55d7e0.png)|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/121123dd-9394-764a-5bc1-55c8e34b3335.png)|

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

##検証4 LitのBaseColorを利用して（入力＝出力）を目指す

ここまでの検証は「Unlitのマテリアル内でTonemapを打ち消す手法」に注力してきました。
今回はアプローチを変えて、値をLitのBaseColorに格納し、それをPostProcessで拾う手法を試してみます。

||
|----|
|マテリアル<BR>![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/50814c17-1b03-fdef-2def-878a6590cc4e.png)|
|ポストプロセスマテリアル<BR>![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/4141e3bd-5f3f-22ba-645d-9cd488b09bdd.png)|
|レンダリング結果<BR>![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/99099919-2e25-252b-0304-771e626352c2.png)|

値が小さく（絵的には暗く）なりました。
検証1でやった通り、画像のsRGBチェックを外せば一致します。
毎回sRGBチェックを外すのも手間なので、今回はLinear2srgbを利用してマテリアルで対処してみます。
中身はGammaCorrectionCommon.ush の LinearToSrgbBranchingChannelにあります。

||
|----|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/4fcd14c0-b020-d205-b0d0-e280dc2bc5c2.png)|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/cc510ddd-7a58-9f2c-e01c-817fa5ed28d7.png)|

テクスチャと色があいました。
当然ながら、シーン全体がBaseColorで描画されてしまっています。空はUnlitなのでBaseColorを参照すると真っ黒になっています。
対処する場合は、CustomDepth等を利用したマスク処理が必要でしょう。
また、TemporalAAを利用している場合はジラ付きが発生します。
FXAAを利用しましょう。（MSAAが利用できない理由は後述します）

マスクを利用すると、、

||
|----|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/3453e4a2-9b4c-f8ed-5b9a-320715a84bfa.png)|

対象のモデルのみテクスチャ値が出力され、
他はFilmicTonemapperが効いている絵が出ました。

ただしこれはGBufferを利用しているためForwardShadingでは利用できません（＝MSAAも利用できない）
また対象のモデルにはPostProcessが効きません。

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

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

||
|---|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/32563b25-02d7-4264-21b7-e745d02fd008.png)|
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/36233/0a50a2ed-e16e-c85f-8caa-0b8b95205b39.png)|

同じ入力値でもUE4の方が大きな値に（絵的には明るく）なっています。
「カラーを入力してグレイスケールのテクスチャと乗算する」ような場合、同じ数値をそのまま使うと結果がずれます。

見た目を素のUnityに寄せるならば、UE4側の入力にsRGBToLinear(col)が必要です。
（環境や設定によって結果は異なることがあります）


#まとめ

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

ただ解説途中でも触れたように、対処方法によっては既存のPostProcessが使えなかったり、他モデルと並べて表示すると破綻する場合があります。
ゲームであれば、エフェクトや半透明との相性、描画負荷の問題もあるでしょう。
デバイスによってはForwardShadingで破綻ないか確認する必要もあります（モバイルやスタンドアロンのVRデバイス）

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


###宣伝：冒頭のスクショはVRM4Uを利用しました
冒頭のスクショはVRM4U（VRMローダプラグイン）を利用したものです。
もし興味あれば下記リンクからお試しください。近々VRoid Hubと連携可能になる… 予定です。
https://github.com/ruyo/VRM4U/

以上です。