UE4でMToonを再現したい(2/2)


前半はこちら → https://qiita.com/ruyo/items/ec082d81dea3033e1500


前半のあらすじ

マテリアル と 自前Shadowmapでトゥーンレンダリングする方針に決めました。


いざ実装


  • 以下の順に解説していきます


    • Shadowmapを適用する

    • 輪郭線を書く

    • その他の機能について補足



利用しているモデルはこちらです → http://3d.nicovideo.jp/works/td32797


Shadowmapの適用

Shadowmapによる影を落とすには、ライトから見た深度値と、それに利用したProjectionMatrix(の逆行列)がわかればOKです。

これらを取得し、マテリアルに渡します。

主色を赤、影を緑にしたもの。右の赤いテクスチャは深度値

test7.gif


Shadowmapを作成する

SceneCaptureComponent2D を利用しました。

Orthographic(平行投影※)でCaptureSourceを「SceneDepth in R」に、TexrureTargetのフォーマットは「RTF R32f」にします。

これにより、深度が線形にUnrealUnit(デフォルトだと1=1cm相当)でバッファに書き込まれます。お手軽。

※蛇足ですがUE4の平行投影描画には昔から不具合があり、修正が完了していないようです。

今回の深度値描画は問題なさそうですが、ご利用の際は十分動作検証することをオススメします。

自前シャドウマップの設定

image.png
image.png

深度値を1/100して表示したもの
手前は黒く、奥に行くにつれ赤くなっている

image.png


ProjectionMatrixの逆行列を得る

SceneCaptureComponent2DのTransformの逆行列を取得し、orthoWidth(前段で256と指定したもの)ぶんスケールさせました。

orthoWidthで割る処理はシェーダで行っています。

逆行列はCPP側で、以下のようなコードで得られます。


逆行列

SceneCaptureComponent2D *capture;

...
FMatrix inv = capture->GetComponentTransform().ToMatrixWithScale().Inverse();
...

※透視投影を利用する場合は、SceneCaptureRendering.cpp の BuildProjectionMatrix() を参考に実装すれば良さそうです。


マテリアルに渡す

逆行列をfloat[16]としてCPP側から受け取り、マテリアルパラメータとして渡しました。

UE4標準のノードでは柔軟な行列計算ができないので 少し手間がかかります。

見苦しいノードになってしまいましたが、以下のようなBlueprintとマテリアルを組みました。


Blueprint側

前述の逆行列をCPPから受け取り、マテリアルへ渡します。float4を4個ぶん(m0,m1,m2,m3)です。

素直にLinearColor[4]を返すノードを作れば良かった気がします…

image.png


マテリアル側

カスタムノードにて、パラメータをfloat4x4に詰め込んで計算しました。

これでワールド座標から対応するShadowmapを参照できるようになりました。

マテリアルファンクションで、結果を0-1で返すようにしています。

image.png


  • 補足


    • 256で割ってるのは orthoWidthぶんです

    • -5してるのは、シャドウアクネを抑えるためです



この結果の1を赤、0を緑で塗ると、冒頭に貼ったような絵が出ます。

MToonの再現という意味では、それぞれの領域に主色と陰色を適用すればOKです。


輪郭線

背面法で書きました。UE4で組む上ではヒストリアさんのページが詳しいです。

参考にさせていただきました。http://historia.co.jp/archives/5587/

線の太さ制御に関して、少し変更しています。

線の太さを変更する様子。カメラが近付いても太さは一定

test10.gif


マテリアル

以下のようなノードになっています。

輪郭線を実際にProjectionした時に太さが何ピクセルに相当するのか算出し、その逆数を掛けています。

image.png


  • 具体的なノードで説明すると、


    • Projectionした結果 → TransformPositionノードでViewSpaceに変換してる部分

    • 太さのピクセル数 → VectorLengthの値

    • 逆数を掛ける → VectorLengthの値で割っている部分



  • ScreenResolusionを利用しているのは、ウインドウサイズやアスペクト比の影響を受けなくするためです


    • (これで対応できてるか若干自信ありません…)



MToonの再現という意味では、「太さ一定モード」と「ワールド座標基準モード」を選択できるようにする必要があります。


その他のパラメータや機能

残りはマテリアルパラメータを追加して、値を反映させていきます。冗長になるので割愛します。(解説に力尽きました…)

ライトとノーマルからシェーディングを行って、前述のShadowmapと合成すれば 9割方完成です。ShadingShiftも適用できるでしょう。

主光源とGIについては、カスタムノードに以下を記述すれば取得できます。

usfファイルを、GetSkySHDiffuseSimpleや、GetSkySHDiffuseで検索すると参考になります。

最終出力にSkyLightの色(ResolvedView.SkyLightColor.rgb)を掛けるのを忘れずに。


主光源.usf

return ResolvedView.DirectionalLightColor;



GIの影響.usf

#if SIMPLE_FORWARD_SHADING

float4 NormalVector = float4(Normal, 1);

float3 Intermediate0;
Intermediate0.x = dot(View.SkyIrradianceEnvironmentMap[0], NormalVector);
Intermediate0.y = dot(View.SkyIrradianceEnvironmentMap[1], NormalVector);
Intermediate0.z = dot(View.SkyIrradianceEnvironmentMap[2], NormalVector);

// max to not get negative colors
return max(0, Intermediate0) * ResolvedView.SkyLightColor.rgb;
#else
float4 NormalVector = float4(Normal, 1);

float3 Intermediate0, Intermediate1, Intermediate2;
Intermediate0.x = dot(View.SkyIrradianceEnvironmentMap[0], NormalVector);
Intermediate0.y = dot(View.SkyIrradianceEnvironmentMap[1], NormalVector);
Intermediate0.z = dot(View.SkyIrradianceEnvironmentMap[2], NormalVector);

float4 vB = NormalVector.xyzz * NormalVector.yzzx;
Intermediate1.x = dot(View.SkyIrradianceEnvironmentMap[3], vB);
Intermediate1.y = dot(View.SkyIrradianceEnvironmentMap[4], vB);
Intermediate1.z = dot(View.SkyIrradianceEnvironmentMap[5], vB);

float vC = NormalVector.x * NormalVector.x - NormalVector.y * NormalVector.y;
Intermediate2 = View.SkyIrradianceEnvironmentMap[6].xyz * vC;

// max to not get negative colors
return max(0, Intermediate0 + Intermediate1 + Intermediate2) * ResolvedView.SkyLightColor.rgb;
#endif


このあたりの処理については、ユニティちゃんシェーダー2.0 の解説がとても参考になりました。

https://www.slideshare.net/Unite2017Tokyo/unite-2017-tokyounity-75800622

(UE4の解説記事からリンクしてしまってすみません…)

ここまでの内容で、それぞれの機能をUE4で再現するにはどうしたらよいか、見当がつく… と思います。


実装した感想

UE4でMToonを再現できた…? と思います。8割くらい。

「仕様を詰めた先人は偉大だな」と。

今回はエンジニア視点でなるべく仕様を反映できるよう組みました。

結果それらしい絵が出てきて、ちょっと感動しました。(アートの方から見たら まだまだ調整不足に感じると思われますが)

細かいパラメータ対応の解説は割愛しましたが、そこに至るまでに多くの苦労があったろうと思います。


最後に

実装&解説にあたり、多くの方の情報を参考にさせていただきました。ありがとうございます。

このマテリアルはVRM4U(VRMインポータ)に追加予定です。

ご興味ありましたらお試しください。

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


おまけ

サンプル背景においてみた

image.png
image.png
image.png