【UE5】トゥーン表現の顔陰を任意に表現するFaceShadowを作ってみた(簡易版)

始まり

法線とライトベクトルの内積から作る陰(以降「普通・通常の陰」と表記。)とアニメやイラストで見かけるような調整された都合の良い陰(例:縦陰)どちらが好きですか。

普通・通常の陰
調整された陰・縦陰

筆者はトゥーン表現を扱うことが多いので後者の都合の良い特殊な陰の方が見慣れています。なので割と好きな方ですかね。

このような任意の陰を描く方法のひとつにFaceShadowがあります。

今回はそんなFaceShadowの簡易版をかる~く紹介していきます。

FaceShadowとは

説明は動画に丸投げスタイル。


Unityは動画のようなサンプルや解説がちょこちょこ見つかるのですが、Unreal Engineの情報があんまり見当たらないんですよねぇ。

事実としてトゥーン表現するにはUnityの方が圧倒的に雰囲気良さそうなんですよね。
Unreal Engineはリアル調を前提とした計算とかパス構成なので非現実的なトゥーン表現とはシンプルに相性が悪い。

FaceShadowノードを組んでいく

FaceShadowで縦陰を描けるように組んでいきます。

注意点として簡易版のため動画のような曲線を描くことは基本的にはできません。
※マテリアルをアタッチするモデル次第では勝手に曲線が描かれることもあります。
 細かい説明は面倒なのでざっくりですが、球形UV算出で頂点座標を扱っていることに起因します。

最近は Named Reroute Declaration が推し

ノードベースのマテリアルエディタでありながら変数のように振る舞ってくれる最強のノードです。

法線 / World Normal

Normalピンを接続している場合にはVertexNormalWSPixelNormalWSに置換してくださいね。

ライトベクトル / Light Vector

ライトベクトルはCPU側で正規化しているので直に使えます。

RenderDocで見るほどのものでもないですが、DeferredLightUniforms構造体とView構造体に同じベクトルが格納されていることが確認できます。

ライトパスにセットされているCBuffer(DeferredLightUniforms)を表示
ベースパスにセットされているCBuffer(View)を表示

通常の陰 / NoL

説明不要だろう。

カメラベクトル

行列計算はちょっと怖いので正規化を挟んでいます。

顔の前方ベクトル

Unityだと奥行はZAxisですけど、UEはYAxisなんですよねぇ。座標系の違い本当に面倒で嫌。

モデルの行列を元にベクトルを算出していますが、理想は面倒くさがらずにFaceBoneのTransformを使うことでしょう。

顔の右ベクトル

前方ベクトルの右版です。

球形UV / Sphere UV


return float2(0.5 + atan2(Position.x, Position.y) / (2.0 * PI), 0.0);

atan2でXY座標の角度(ラジアン)が求められるため、それを元に球形UVのU座標を算出しています。

atan2で返ってくる最大値(3.14)をPI * 2.0 (6.28)で除算すると0.5、これに+0.5すると最大値が1.0。
atan2で返ってくる最小値(-3.14)をPI * 2.0 (6.28)で除算すると-0.5、これに+0.5すると最大値が0.0。
ぐるっと一周する 0.0 ~ 1.0 のU座標ができましたと。

V座標は使用しないため0.0で固定しています。

厳密な球にしたい場合は正規化した座標をatan2にぶち込むべきですが、簡易版という言い訳を盾にざっくりな計算です。

atan2のちゃんとした説明を載せようかなと公式サイトのリファレンスを見たら機械翻訳が控えめに言ってゴミカスだった。なんやねん「πする-π」っておっぱいかと思ったわ。「象限を決定」も言葉が難しすぎるんじゃい。

x パラメーターと y パラメーターの符号を使用して、πする -π の範囲内の戻り値の象限を決定します。

atan2 (Corecrt\_math.h) - Win32 apps | Microsoft Learn

顔陰の閾値 / Face Shadow Texture


// CustomNodeをあんまり信用していないので分岐命令を明示的に指定
BRANCH if (FaceShadowMask > 0.5)
{
	float2 UV = SphereUV - float2(0.5, 0.5);
	float2 InvUV = float2(1.0 - SphereUV.x, SphereUV.y) - float2(0.5, 0.5);
	float2 FaceShadowTexture = float2(length(UV) * sin(atan2(UV.x, UV.y)), length(InvUV) * sin(atan2(InvUV.x, InvUV.y)));
	return float2(0.5, 0.5) + FaceShadowTexture;
}
else
{
	return float2(0.0, 0.0);
}

顔陰の形状を決める重要な閾値です。
今回は簡易版でぶつ切りな縦陰が描ければいいので、右から左・左から右に流しているだけです。

顔の右向きとライトの向きに応じて使用するチャンネルを切り替えています。
1チャンネルに付き180度、2チャンネル合わせて360度対応している感じです。

赤陰がFaceShadowTexture(R), 緑陰がFaceShadowTexture(G)で作った陰

顔陰


BRANCH if (any(FaceShadowTexture != 0.0))
{
	float FoL = dot(FaceForwardVector, LightVector);
	float RoL = dot(FaceRightVector, LightVector);

	float ShadowThreshold = RoL >= 0.0 ? FaceShadowTexture.g : FaceShadowTexture.r;
	float ShadowDir = FoL * 0.5 + 0.5;

	float FaceShadow = ShadowDir - ShadowThreshold;

	return FaceShadow;
}
else
{
	return 1.0;
}

今更感もありますがFaceShadowの計算では高さ成分を除いたベクトルを使用します。
高さを考慮するとこの手法の利点が薄れてしまうので、その場合は素直に法線転写をしましょう。

顔陰と通常陰のブレンド

すこぉぉぉしだけ通常陰をブレンドすることで定規で引いたような縦陰に暖かみを持たせられます。

FaceShadowBlend: 1.0
FaceShadowBlend: 0.99

完成!!!

ドシンプルな縦陰はこんな感じで簡単に且つ割と高品質で実装できます。

これはUnlitで組んでいるので実質フォワード挙動です。ちなみにディファードの場合はベクトルのエンコードとデコードによる精度劣化が結構凄まじいのでstepで2値化しないと品質がゴミになります。標準設定のRGBA8の場合ね、フル品質のFP16の場合はそんなこともないだろうけど、GBufferの全チャンネルをその精度で動かしている作品なんてあるのかな。。。ないよね。。。ある?

FaceShadowBlend: 1.0
FaceShadowBlend: 0.99

おわり!!!

予定ではちゃんとFaceShadowの解説記事を書く予定でしたのよ。

気が変わったというか削がれたのは、このFaceShadowと同等の表現ってアニメ作品に多いから、ガイドライン設けられているような作品ないかなーと、過去に視聴した数少ないものをポチポチしていたの、そうしたら悉く著作権に抵触しそうで萎えちゃったの。萎えた結果、やる気を失ってしまったので、一旦簡易版を公開して、後日天使の輪で英気を養ってから再開しよかなと思ったの。

天使の輪はADV(AVG)のガイドラインに則って掲載できますからね。気分が上がります。その上がった気分の残留思念で次々回あたりには曲線対応したFaceShadowを上げる予定です。予定は未定は常識ですが。ふふふのふ~ん。

日が落ちてからの方が筆が乗るのですが、文章に感情がダダ漏れ、筆滑現象が発生します。
あとで見返すとなんだこいつ(お前じゃい)となるんですよねぇ。
ーーーおやすみ。