こんにちは。株式会社スパーククリエイティブの開発部 クライアントエンジニア リーダーの森田です。
第3回では少し趣向を変えて今までのUnlitマテリアルでの表現ではなく、ポストプロセス エフェクトを用いてセルシェーディングのような表現を目指します。
ポストプロセスとは
ポストプロセスを簡単に説明すると、最終的なレンダリングを行う直前にその画面の上にレイヤーをかぶせるようなものです。
UEでポストプロセス エフェクトを表現するには、カメラに定義するか「PostProcessVolume」が必要となります。詳しくは公式サイトをご覧ください。UEのポストプロセス エフェクトには標準でDOFやBloomなどの機能が用意されています。
今回は「ポストプロセス マテリアル」を利用し水彩画のような表現を行なっていきたいと思います。
ポストプロセス マテリアルについて
ポストプロセス マテリアル では、他のマテリアルと異なる「SceneTexture:○○」といったノードを用いることで、レンダリング前に計算されたレイヤーごとの画面のカラー情報を取得することができます。こちらも詳しくは公式サイトをご覧ください。
以下の手順でセルルックを目指していきます。
1:見た目をセル調にする
2:近景遠景にフォグ表現を加える
3:画面全体を紙っぽい表現にする
4:アウトラインを引く
4-1:専用のアクターマテリアル
4-2:別ポストプロセス マテリアルでアウトライン準備
4-3:ポストプロセス マテリアルで色付け
STEP 01:見た目をセル調にする
まず見た目をセル調にしていきます。かなり色々な計算をするため、カスタムノードを利用します。
InputにはBaseColor、LightValue、AmbientOcclusion、ShadowColorを用意します。こちらの計算によって、疑似的にUnlitで行なった光と法線の内積を再現しています。
/* Light */
float LightPower = min(LightValue, AmbientOcclusion);
BaseColor.rgb = lerp(ShadowColor.rgb, BaseColor.rgb, LightPower);
/* AO */
BaseColor.rgb = lerp(BaseColor.rgb, BaseColor.rgb * AmbientOcclusion, AmbientOcclusionDarkness);
/* Final */
return BaseColor;

ポストプロセス マテリアルの適用前後は以下のようになります。


このように、Unlitで表現したセル調のような、陰影がはっきりとした表現になりました。
STEP 02:近景遠景にフォグ表現を加える
先ほどのカスタムノードにFog表現の処理を追加します。
Inputには新たに FogColor、DepthRateを追加します。
/* Light */
float LightPower = min(LightValue, AmbientOcclusion);
BaseColor.rgb = lerp(ShadowColor.rgb, BaseColor.rgb, LightPower);
/* AO */
BaseColor.rgb = lerp(BaseColor.rgb, BaseColor.rgb * AmbientOcclusion, AmbientOcclusionDarkness);
/* Fog */
BaseColor.rgb = lerp(BaseColor.rgb, FogColor, DepthRate);
/* Final */
return BaseColor;
DepthRateは「SceneTexture:シーン深度」を用います。ただ、シーン深度はあくまで深度値を取得するだけのため、このままではどのくらいの値が近景・遠景に該当するかがわかりません。
そこでどこまでを近景とするか、どこからを遠景とするかをこちらで決め、それらの値の範囲内であればBaseColorを使い、範囲外であればFogColorを使うようにします。


キャラクターのみだと近景遠景がわかりづらいので、背景を別途用意しました。


STEP 03:画面全体を紙っぽい表現にする
今回は画面に影響を与える紙テクスチャと、影響度を決めるフレームテクスチャを使います。
今まで計算してきたカラー値と紙テクスチャの線形補間をすることで、画面の端に紙テクスチャを貼り付けることができます。



STEP 04:アウトラインを引く
Unlitでは、
・メッシュを複製する
・メッシュを裏面描画する
・法線を利用してワールド位置オフセットを変更する
といった方法でアウトラインを引く方法を前回説明しました。
ただ、今回のポストプロセス マテリアルでは、メッシュの描画処理は終えてしまっているので、Unlitの場合の手段を使うことができません。そのため、別の方法を用いてアウトラインを引きます。
4-1:専用のアクターマテリアルを用意
4-2:別ポストプロセス マテリアルでアウトライン準備
4-3:ポストプロセス マテリアルでアウトラインに色付け
4-1:専用のアクターマテリアルを用意
ポストプロセス マテリアルで取得できるSceneTextureを使ってアウトラインを引くため、専用のアクターマテリアルでは、その準備を行います。
今回利用するSceneTextureは以下の4種類です。
・法線
・深度
・スペキュラ
・メタリック
そのうちアクターマテリアルでは法線、スペキュラ、メタリックの値を渡しています。
法線は法線マップを渡します。スペキュラ、Unlitで行った光源のベクトルと法線の内積を渡します(今回ここについては割愛しています。詳細を知りたい方は連載第1回をご覧ください)。
メタリックは本来金属表現に用いられますが、今回のポストプロセス マテリアルではメタリック表現としてSceneTextureを使用していません。そのため、今回はマテリアルID用として値を渡します。
このマテリアルIDを用いることで、別々のマテリアルが重なった際に描かれてしまうアウトラインを描かないようにすることができます。
4-2:別ポストプロセス マテリアルでアウトライン準備
画面上のどこをアウトラインとみなすかを計算するために、上下左右に隣接するサンプルを用意します。

ポストプロセス マテリアルで取得できる以下のSceneTextureから、中心と上下左右に隣接したサンプルを用いてアウトラインを引く準備をします。
法線
中心と上下左右に隣接したサンプルの内積が一定値以上のとき、アウトライン対象にします。
float edgeL = dot(NormalL, NormalC);
float edgeR = dot(NormalR, NormalC);
float edgeT = dot(NormalT, NormalC);
float edgeB = dot(NormalB, NormalC);
float edge = min(min(edgeL, edgeR), min(edgeT, edgeB));
return step(edge, NormalThreshold) * NormalEdgePower;
深度
中心と上下左右に隣接したサンプルの差分が一定値以上のとき、アウトライン対象にします。
float edgeL = DepthL.r - DepthC.r;
float edgeR = DepthC.r - DepthR.r;
float edgeB = DepthB.r - DepthC.r;
float edgeT = DepthC.r - DepthT.r;
float Limit = step(Depth, DepthLimit);
return step(DepthThreshold, max(abs(edgeL - edgeR), abs(edgeT - edgeB))) * Limit * DepthEdgePower;
スペキュラ
中心と上下左右に隣接したサンプルの値に同一ではないところをアウトライン対象にします。
float edgeL = SpecL != SpecC;
float edgeR = SpecR != SpecC;
float edgeT = SpecT != SpecC;
float edgeB = SpecB != SpecC;
float edge = max(min(edgeL, edgeR), max(edgeT, edgeB));
return edge * SpecEdgePower;
ID
スペキュラと同様、中心と上下左右に隣接したサンプルの値に同一ではないところをアウトライン対象にします。
float edgeL = IdL != IdC;
float edgeR = IdR != IdC;
float edgeT = IdT != IdC;
float edgeB = IdB != IdC;
float edge = max(min(edgeL, edgeR), max(edgeT, edgeB));
return edge * IdEdgePower;
Max 表現式を用い、それぞれの計算結果の一番高い方の値を出力します。オパシティ値に書き込むことで、見た目に影響を与えず別マテリアルに情報を渡しています。

これで下準備は完了です。
仮に、オパシティ値ではなくエミッシブカラーに出力した場合はこのような見た目になります。

4-3:ポストプロセス マテリアルでアウトラインに色付け
別ポストプロセス マテリアルの準備が整ったら、いよいよセルシェーディングで利用したポストプロセス マテリアルを改良していきます。
まず、別ポストプロセス マテリアルの値を取得できるようにします。
別ポストプロセス マテリアルのBlendablePriorityを0に、セルシェーディング用のポストプロセス マテリアルのBlendablePriorityを1に設定します。
この設定をすることで、別ポストプロセスマテリアルを先に描くことができ、後からのセルシェーディング用のポストプロセスマテリアルでは既に書いている別ポストプロセスマテリアルの値を取得することができます。

今までのポストプロセス マテリアルのカスタムノードに、アウトラインの計算を追加します。
/* Light */
float LightPower = min(LightValue, AmbientOcclusion);
BaseColor.rgb = lerp(ShadowColor.rgb, BaseColor.rgb, LightPower);
/* AO */
BaseColor.rgb = lerp(BaseColor.rgb, BaseColor.rgb * AmbientOcclusion, AmbientOcclusionDarkness);
/* Fog */
BaseColor.rgb = lerp(BaseColor.rgb, FogColor, DepthRate);
/* Final */
return BaseColor;
▲変更前
/* Light */
float LightPower = min(LightValue, AmbientOcclusion);
BaseColor.rgb = lerp(ShadowColor.rgb, BaseColor.rgb, LightPower);
/* AO */
BaseColor.rgb = lerp(BaseColor.rgb, BaseColor.rgb * AmbientOcclusion, AmbientOcclusionDarkness);
/* Fog */
BaseColor.rgb = lerp(BaseColor.rgb, FogColor, DepthRate);
/* Edge */
float Brightness = Luminance(BaseColor.rgb);
float EdgePower = lerp(0.1, 0.5, Brightness);
float3 FinalEdgeColor = max(BaseColor.rgb * 0.5, saturate(BaseColor.rgb - EdgePower)) * EdgeColor.rgb;
BaseColor.rgb = lerp(BaseColor.rgb, FinalEdgeColor, Edge);
/* Final */
return BaseColor;
▲アウトライン追加
今回アウトライン用のカラーは、基本色から少し暗めの色になるように計算しています。EdgeColor.rgbは、全体的にもっと暗くできるよう用意しています。
float Brightness = Luminance(BaseColor.rgb);
float EdgePower = lerp(0.1, 0.5, Brightness);
float3 FinalEdgeColor = max(BaseColor.rgb * 0.5, saturate(BaseColor.rgb - EdgePower))* EdgeColor.rgb;
そして、アウトライン用のカラーと今まで用意してきたカラーを別ポストプロセス マテリアルで用意したオパシティ値で線形補間することで、アウトラインを表示することができます。
その結果、見た目は以下のようになりました。

最後に
今回はポストプロセスマテリアルを使ったセルシェーディングを目指しました。
ポストプロセス マテリアルは画面全体に影響を与えるため、使い方には工夫が必要ですが、使いこなすことができれば様々な表現をすることができます。
次回はいよいよエンジン改造を用いたセルシェーディングの表現の紹介をしていきます。お楽しみに!
森田友樹/Yuki Morita
様々なゲーム会社を経て2019年、SPARK CREATIVEに入社。プロジェクトのリードエンジニアとしてエンジニアリング業務はもちろんチームを牽引する業務も多くこなす傍ら、社内のTechブログの運営や社内勉強会を実施している。また自身の成長のために日々研鑽を積むことを忘れておらず、今回の連載にも名乗りを上げた。
●SPARKグループ公式サイト
spark-group.jp
●SPARK CREATIVE Techブログ
tech.spark-creative.co.jp
●SPARK CREATIVE 採用ページ(新卒採用/キャリア採用)
spark-group.jp/recruit/
TEXT_森田友樹 / Yuuki Morita(SPARK CREATIVE)
EDIT_小村仁美 / Hitomi Komura(CGWORLD)