【UE5で気分転換!セルルック×ライティングでキャラを魅せたい】第4回:ライティング 陰影編 前編 – Unreal Engine 5.6

Unreal Engine

水ノ茉の宣伝

準備中...
ゲームを作る予定なの
水ノ茉こおり

シリーズ説明

本シリーズで最終的に検証したい内容は、DMXで制御されたバカみたいな量のライト、それらすべてをキャラクターに当てても破綻しないレンダリングパイプラインの構築です。

実装の手段は選ばないため、普段展開している【UE5】◯◯◯ – Unreal Engine ◯.◯と比べると多少敷居が高く、また、実装の再現性を提供しないこともあるため、いつも以上に趣味嗜好が全開となります。

始まり

セル・トゥーン調のライティングを少し進めます。

今回は陰影部分です。

俯瞰整理

始動から毎日のように実装作業を初速の熱量に任せて進めていましたが、少し休憩です。

エロゲの共通ルートを少し遊んで冷却なのです。

残りのやりたいことを整理します。

  1. 多光源で白飛びしない
  2. 多光源の陰影を階調制御しながら反映
    • 独自パス群を駆使して動作可能な領域まで落とし込むのが目的
  3. 髪影
  4. 衣類影
    • 簡単
  5. 前髪と重なる瞳や眉の透かし
  6. 半透明な生地の衣装
    • 多層レイヤーの透かし処理をするための検証が目的
  7. キャラのまわりを発光
    • ユイちゃんのアレです。ネタバレになるので詳細伏せます。
    • 水ノ茉の作品であの演出をパロりたいのでついでに作りたい

こんなものですね。

ふむ。

キャラと背景のシャドウデプス書き込みを別々にしてセルフシャドウ抑制を盛り込む予定でしたが、マルチパスを現実的なフレームレートで駆動させるためには足枷になりそうですね。

実装可否は 1 ~ 6 の後に決めるとしますか。陰影の正規化と量子化、ReceiveShadowフラグがあれば分離しなくても、なんとかなりそうな気はしますし。

さてと、残りも楽しみながら実装しちゃいます。

最近はプライベートの時間確保を最優先な立ち回りをしているので、1日1時間程度ならエロゲをしてもいいかもですね。近況に合わせたライフスタイルの変更を考え中なのです。

ライトパスで書き込む準備

Unlitと同様の書き込みからディファードライティングな書き込みに変更します。

第1回で導入したFilmToneMapInverseの注意点としてこの戻り値はトーンマップを打ち消すための数値になっています。要は0..1から逸脱しているため、普通に格納しようとすると8ビットの上限値に到達してしまい、白飛びと大差のない結果になります。

FilmToneMapInverseを通したカラーをGバッファに格納してライティングをするか、ライティングをした後にFilmToneMapInverseパスを挿入するか、どちらの運用にするかは未定です。とりあえずは前者の方向で進めます。格納精度はR11G11B10 (32bit)です。RGが2048階調、Bが1024階調です。

Unlitと同じ結果を得るためにはFP16精度が必要ですが、R11G11B10でも色彩変化に敏感な方でなければ差分の検知が困難なレベルまでは近づけられます。仮に少し違うかもと認知できてもその原因はビット差分ではなく、撮影フレームでJitter行列が異なる方だったりするかもしれません。要は凝らして分からなければ問題なしということです。セル系の描画処理は計算上の正しさよりも、見た目がゴールに近いことが優先されるケースがほとんどですからね。

陰、シェード

法線とライトベクトルから陰、シェードを作成します。

影とかシャドウとか陰影とか言うと、割とお怒り案件です。

多少の言葉揺れは許容されるべきと思いますが、陰と影では見た目が同じでも実装方法が大きく異なるため、明確にすべきです。齟齬とか喧嘩の要因になることでしょう。喧嘩に発展しない場合は相手が穏やかな性格か、諦めきっているかのどちらかでしょう。筆者は後者です。

FP16で保持している法線を元にNoLします。陰影は正規化と量子化をするので最小限な計算で問題ないでしょう。あとで閾値だけGバッファに格納して特定の場所に陰が出やすいような小細工は入れておきます。

…明色、陰影色、輪郭色でuint3も使っちゃうのか。

フィルタパスを追加した方がレイアウトや負荷の観点から良い気がしますが、この後に予定している半透明表現のためのマルチパスの色計算が未確定なので結論が出せません。

シャドウ、影

影が落ちないなと思ったら、シャドウパスのラッピングを忘れていました。

ラッピングした後もスポットとポイントの影が出ないと思ったらVSMをオミットする際に必要な条件文まで消していました。エンジンはミスった場所次第で致命傷です。

陰で触れたように正規化と量子化が待っているので適当で問題ないです。

髪影

前髪の影を落とします。

シャドウで落とすことも出来るのですが、品質がシャドウマップの解像度依存だったり、カスケードシャドウな計算で落とすとセル・トゥーンルックという観点では違和感のある落ち方をしたりするので、オフセット的な感じで疑似的に影を表現します。

CastShadowReceiveShadowに倣ってフラグを用意します。

アウトラインと同じ理由で専用のWPOピンを用意します。

ぱっと見だとピンが無効化されているように見えますが、シェーダーコンパイルされる場合には、マテリアルインスタンスに設定されたフラグを元にピンの有効性を最終決定しているので、ちゃんと展開されています。

static bool IsPropertyActive_Internal(EMaterialProperty InProperty,
	EMaterialDomain Domain,
	...省略...
//// CoffeeLive @kobayashi-arata 2025/06/08 ////
	bool bToonOutline,
	bool bCastToonOffsetShadow,
//// CoffeeLive @kobayashi-arata 2025/06/08 ////
	bool bFrontMaterialIsConnected)
{
	...省略...
//// CoffeeLive @kobayashi-arata 2025/06/01 ////
	case MP_OutlineColor:
	case MP_OutlineOpacity:
	case MP_ShadowColor1st:
	case MP_ShadowColor2nd:
	case MP_ReceiveOffsetShadow:
		Active = ShadingModels.HasAnyShadingModel({ MSM_ToonLit });
		break;
	case MP_OutlineWorldPositionOffset:
		Active = ShadingModels.HasAnyShadingModel({ MSM_ToonLit }) && bToonOutline;
		break;
	case MP_OffsetShadowWorldPositionOffset:
		Active = ShadingModels.HasAnyShadingModel({ MSM_ToonLit }) && bCastToonOffsetShadow;
		break;
//// CoffeeLive @kobayashi-arata 2025/06/01 ////
	...省略...

	return IsPropertyActive_Internal(InProperty,
		MaterialDomain,
		...省略...
	//// CoffeeLive @kobayashi-arata 2025/06/08 ////
		DerivedMaterial->IsToonOutline(),
		DerivedMaterial->IsCastToonOffsetShadow(),
	//// CoffeeLive @kobayashi-arata 2025/06/08 ////
		bFrontMaterialConnected);

アウトラインと同じような感じでフラグを元にドローコールを発行してテンポラルバッファに深度を書き込みます。

あとはトゥーンの深度とオフセットされた深度を元に前後判定をして、その結果をGバッファに書き込んで、ライティング時に髪影を考慮してあげれば簡単に表現できます。

デプスだけの判定だと、前髪とおでこの隙間次第でクリップされたり、前髪自体に影が乗ったり、制御が難しいですね。

Receive処理はピンだけ用意して必要に応じて入れようかな程度でしたが、あった方が無難な感じするので、入れておきます。これで閾値をギリギリにしても自身に影が落ちることを抑制できました。

あとは処理負荷対策としてParallelを無効化とステンシルテストをバチボコに組み込んでいます。機能的にそこまで大量のドローコールを発行することもないので愚直にParallel展開するとかえってスパイクの原因になります。後者に関しては、影を落とすピクセルはカメラを近づけない限り画面全体のほんの一部分です。普通に考えてステンシルテストして絞った方がいいです。

深度オフセットという単語を聞いたらピンとくる方もいるでしょう。えぇ、なにを隠そうこの手法は、CEDEC2021でブルプロが紹介していた手法を個人的に解釈して成熟させたものです。マジで表現力と処理負荷のバランスが良く且つ汎用性が高いため、あらゆるところで使い回しています。

あの作品、技術面は優秀な印象でしたが、運営方法がゴミカスな気がします。

オンゲはFPKが出来なければ面白くないという過激派思想を前提で話しますが、あの界隈は殺して殺されて煽って煽りあって晒して晒されあって、その復讐の連鎖の原因の頂点にPK行為が存在します。

手っ取り早く相手より強くなるために、課金という手段にいつかは手を染めるのです。そしてその後に得られるキルログを見たが最後、早々連鎖からは抜け出せないことでしょう。何故なら相手も課金してくるのですから。

そしてPKで長生きしようとするとじゃぶじゃぶアイテムを消費します。自分で作るのも限界があるのでゲーム内取引をすることでしょう。それらのアイテムの生産元は大体は非PK民です。要はゲーム内外問わずに経済が回り続ける潤滑油となるのがPKなのです。

それ以外に魅力的なゲーム要素があるならその限りじゃないと思いますが、衣装?は?舐めてんのか?何故それでゲーム内経済が回ると思った?オンゲしたことないやろ。という思いです。ゲームオンに出向して勉強すりゃもう少し良い結果が得られたでしょうに。技術面で勉強になることが多い作品だったので、それ以外の外的要因で短命にさせられたことが大変惜しいです。

なんか別資本で転生したらしいですがね。これでうまくいったら運営ゴミカスQ.E.D.ってことかな。

衣類影

髪影を応用して衣類の影も落とします。

リボンのところ。

紐のところ。

すべてワンポイントで必須ではないのですが、有ると無いとでは、視覚的に得られる情報が結構違うのです。それだけ3次元の映像は陰影が重要ということなのです。だから商用ゲーム開発はアーティストさんに工数を割いてテクスチャという人海戦術をすることが多いのです。上記含めて技術力でアプローチすることは量産という観点では軍配が上がるのですが、品質においては人海戦術には中々勝てないのです。

陰影の正規化と階調

なかのひとが同じなので当然技術を有しています。

そして本シリーズで実現したいことのひとつに、アレを現実的なフレームレートで稼働させることがあります。独自のトゥーンパス群も、5.6という最新バージョンを選んだのも、すべてはこのためです。

ライトパスを複製します。

集積用のPrePassとライティング用のMainPassの2つにバリエーションを別けます。

FDeferredToonLightPS::FPermutationDomain PermutationVector;
PermutationVector.Set<FDeferredToonLightPS::FLightPrePass>(bPrePass);

集積結果を元にペイントツールと似たような階調制御を出来るように実装していきます。

諸々はコンピュートシェーダーに計算してもらって。

陰影の閾値をGバッファに格納して。

最小値と最大値のReadbackは面倒だから一旦はSRVで突っ込んで。

それを元に色を塗れば基礎は完成です。

ぱっと見だとこの手法いい感じなんですけど、正規化に必要な最小値と最大値がカメラと対象物の位置関係、モーションによって毎フレーム変動することが欠点なんですよね。それによってフレームごとに正規化の結果が変わるので、パカパカしちゃうのです。対策方法は幾つか思い付いていますが、疲れたので次回に持ち越しです。陰影実装は一気に進めるべきじゃありませんね。身体への負荷が思ったよりえぐい。頭疲れた。

おわり!!!

想定より疲れたので次回からは隔週かもです。

セルルック関連

雑談

オフセットシャドウまでは割とエロゲと両立出来ていたんですが、多光源設計に突入した瞬間に難易度が爆上がりして破綻しました。

疲れすぎて文章を考えることにリソースを割けなくなりました。

毎回のことですが、気分と欲望に身を任せて行動しているので、リソース配分がアホになることが多いのです。

今更だけど自由な描画機能は代償が重いのです。

久々のPythonちゃん

未だにあんまり関わりたくないSNSの投稿を半自動化したいのです。

精神汚染の懸念から触れる時間を極限まで少なくするために、投稿したら爆速ログアウトをしています。

あとTwitter Stress Reductionでポストする以外をすべて非表示にしています。

ログアウトしているので必然的に毎回ログイン作業が発生するのですが、地味に面倒なんですよね。

時間の無駄で且つ未知の存在であるSNSと関わることはやっぱり控えたい。

ということで古巣のPythonちゃんとイチャ付きました。

開発アカウントの発行とAPI発行はこちらを参考にしました。
発行理由はGPT4oに考えてもらいました。便利です。

TwitterでAPIから画像付きツイートをする方法 – Tips

URIの部分だけこちらを参考にしました。

Twitter API v2(X API Free)の使い方・移行(2025年)【GAS】 - Qiita
Twitter API v2(X API Free)の覚書です。少し前に仕様が変わったためそちらも踏まえて書いておきます。 Twitter API v2(X API Free)の使い方・移行(2024年) まずはX Developer Pl...
カスタム関数のクイックスタート  |  Apps Script  |  Google for Developers

実装方法はここをコピペです。

TwitterでAPIから画像付きツイートをする方法 – Tips

動作確認も無事完了です。

当たり前ですがgitで管理する場合は、APIとSecretはenvを参照するなどしてください。漏洩した際に最悪なことになります。

import tweepy

CONSUMER_KEY = "Consumer Keys > Regenerate から生成したKey"
CONSUMER_SECRET = "Consumer Keys > Regenerate から生成したSecret"
ACCESS_TOKEN = "Authentication Tokens > Access Token and Secret > Regenerate から生成したKey"
ACCESS_TOKEN_SECRET = "Authentication Tokens > Access Token and Secret > Regenerate から生成したSecret"

if __name__ == "__main__":
    image_path = r"C:\\Users\\xxxx\\OneDrive\\旧データ\\画像\\pblog65\\33.png"
    tweet_txt = "がぞうのてすと\n\nhttps://kafues511.jp/2025/01/11/2634/"

    try:
        # Create Twitter Instance
        auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
        auth.set_access_token(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
        api = tweepy.API(auth)

        client = tweepy.Client(
            consumer_key = CONSUMER_KEY,
            consumer_secret = CONSUMER_SECRET,
            access_token = ACCESS_TOKEN,
            access_token_secret = ACCESS_TOKEN_SECRET
        )

        # Upload Image
        media = api.media_upload(image_path)

        # Tweet
        client.create_tweet(text=tweet_txt, media_ids=[media.media_id])
    except Exception as e:
        print(e)

Pythonちゃんの魅力的なところはいろんな方々が便利なライブラリを公開してくださっているので、実装が爆速で終わることです。そして得られる結果が効率化。すごく好循環です。

アカウントとキー発行が5分、環境構築と実装と動作確認が10分、計15分ぐらいで済みました。

これでSNSと関わる必要がなくなりました。さらばです。

水ノ茉として活動する際の広報担当は別途立てるので、苦手なことをわざわざやらなくても良いという方針転換です。

あとシンプルにSNSがリファラーとしての役割を果たしていないんですよね。

流入量が少なすぎる。マジで無意味。

インデックスに引っかかるように単語は詰め込んでいるんですが、なんか少ないんですよね。

GoogleのSEO対策とは違うんでしょうか。

Google先生と違って使ったことがない媒体なので、どのような流入ルートがベターなのか分からないのですよね。

興味がないので調べる気も対策する気もありませんが。

出費過多

ユノスさんことゆずソフトさんの新作を特典目当て&お布施で数点予約した後日に思い出しました。

『あ、CEDECあるやん』と。

はい、想定内の忘却していた出費が計上され、今月は無事赤字になりました。

普段が黒字なので補填すればいいだけなのですが、散財もほどほどにしないとですね。

エロゲの弊害

久々にエロゲをするとやっぱり楽しいのですが、物語を読んだり、CG絵を見たりすると、参考にして世界構築したくなったり、見た目の再現をしたい欲が湧いてきます。

直近だと虹彩表現で再現したい箇所があったので、それをそのうち書くのです。

といった感じで、やりたいことリストが更に膨れました。

………詰んでね?