【UE5】窓から見える紅い月 – Unreal Engine 5.4

Unreal Engine

水ノ茉の宣伝

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

始まり

恋愛アドベンチャーゲームこと、エロゲって結構なボリュームですよね。

作品名テキスト量文章量
セレクトオブリージュ約90万約3万
オトメ世界の歩き方約70万約2万
彼女たちの流儀約70万約2.5万

キャラクター名と喘ぎを含めたテキスト量なので物語の根幹はもう少しコンパクトだろうけど。
日常会話と行為時の分類モデルは用意していないので算出は諦め。
データは個人利用且つそこら辺の権利無視な生成AIの学習データに突っ込むことはしていないのでご安心を。

遊び終える度に、世のシナリオライターは凄いなぁと思うのです。

さてと、紅き月でも作りますか。

死せる月は想像よりも禍々しいので遠慮しておきます。
というかアレはエフェクトの類っぽいし。

月を描画するためのビルボードっぽいWPO計算

普通にビルボードコンポーネントを使っても良かったのですが、今回はWPOな気分です。

こんな感じのノードを組んでWorld Position Offsetピンに接続しておきます。
Object Positionって前まではマテリアル関数だったと思うのですが、いつの間に格上げされていたんですね。

ベクトルの正規化は違和感あれば入れてください。
入力値と変換内容的にたぶん要らないと思いますが。

カスタムノードの中身はこんな感じです。

return (UV.x * XAxis * CanvasSize) + (UV.y * YAxis * CanvasSize) - WorldPosition + ObjectPosition;

World Position OffsetとCustomノードの注意点

UEのシェーダー開発に慣れ始めた方はこんなことを思うかもしれません。
Scale以外の入力変数はViewやParametersから取れるのに、ノードを使う意味よ……

少なくとも筆者はこの過程を経ました。

使えることなら使いたいのですが、とある理由から使用を避けた方が無難です。
それはVelocity計算の為に、前フレーム(Prev)のWPOを要求されることです。

以下は先ほど組んだノードをHLSLコード展開したものの抜粋です。
※可読性の為に手を加えているため実際の出力とは多少の違いがあります。

MaterialFloat3 Local3 = mul(MaterialFloat3(1.0,0.0,0.0), (MaterialFloat3x3)(ResolvedView.CameraViewToTranslatedWorld));
MaterialFloat3 Local5 = mul(MaterialFloat3(0.0,-1.0,0.0), (MaterialFloat3x3)(ResolvedView.CameraViewToTranslatedWorld));
FWSVector3 Local7 = GetWorldPosition(Parameters);
FWSVector3 Local9 = TransformLocalPositionToWorld(Parameters, MaterialFloat3(0.0,0.0,0.0));
MaterialFloat3 Local11 = mul(MaterialFloat3(1.0,0.0,0.0), (MaterialFloat3x3)(ResolvedView.PrevCameraViewToTranslatedWorld));
MaterialFloat3 Local13 = mul(MaterialFloat3(0.0,-1.0,0.0), (MaterialFloat3x3)(ResolvedView.PrevCameraViewToTranslatedWorld));
FWSVector3 Local15 = GetPrevWorldPosition(Parameters);
FWSVector3 Local17 = TransformLocalPositionToPrevWorld(Parameters, MaterialFloat3(0.0,0.0,0.0));

ぱっと見だと上下で同じコードが書かれているように見えますが、よく見ると、下の方は変数や関数にPrevが付与されています。これは読んで字の如く前フレームの行列や変数を指しています。

実は現在のWPOと過去のWPOを考慮することでより正確なVelocity(速度)を計算しています。
UEさんは内部的にこういうことをこっそりやっています。

これらの現在と過去の変数や関数の展開はノードに依存した設計です。
つまりはカスタムコード内でHLSLコードでベタ書きされた子たちは非対応です。

書かれた内容が意図的に現在を取っているのか、過去を取っているのか、分からないのですから対応しようがありません。

そんな理由から、WPOに接続している一部ノードは意図的にHLSLコードをベタ書きせずに、大人しく面倒なノードから引っ張っているのでした。

……思い返すと、オーバーレイマテリアルのアーカイブはベタ書きでしたね。
普段はエンジンシェーダーに書いているのですっかり忘れていました。

気が向いたらPrevに対応したノード作成の記事でも書きましょうか。

カメラに向いてくれた

先ほどのマテリアルをアタッチしてCanvasSizeを調整するとこんな感じでビルボードっぽく振る舞ってくれます。

ここまで作っておいてなんですが、実務では採用しない方が良いです。

頂点シェーダーでそれっぽく見せているだけなので、スケールが不変ではない且つBoundsによるカリングが見た目と不一致を起こします。ある程度であればBounds Scaleで調整可能なのですが、それをするなら素直にPrimitiveComponentからビルボードを考慮した行列やBoundsを作成した方がパフォーマンスが良いです。

今回はエロゲの延長戦、趣味の範疇、気持ちが乗った方で進めただけという感じです。

スクリーンな円形法線を作る

月は球形に近いので、それに倣って球形というか円形の法線を作ります。

カスタムノードの中身と設定はこんな感じ。

float2 Pos = (UV - 0.5) * 2.0;
float2 Pos2 = Pow2(Pos);

float R2 = Pow2(Radius);

float SphereInner = sqrt( R2 - Pos2.x - Pos2.y);
float SphereOuter = sqrt(-R2 + Pos2.x + Pos2.y);

float3 SphereNormal = normalize(float3(Pos, SphereInner)) + float3(0.0, 0.0, Seed);

OutSphereInner = SphereInner;
OutSphereOuter = SphereOuter;

return SphereNormal;

FBM(Fractal Brownian Motion)で模様を作る

ノイズを重ね掛けするとそれっぽい模様が出来ます。
一般的にFBMと呼ばれるもので、UEだとNoiseノードとして用意されていますね。

先ほどのノードの出力をNoiseノードのWorldPositionに接続するとこんな感じの見た目が作れます。
Functionは基本的には好きなものを選んでもらって構わないですが、Valueだけは入力値と相性が悪く、グリッドが露出するので避けてください。

Simplex – Texture Based
Gradient – Texture Based

先ほどのSeedパラメータから模様の調整が出来ます。

模様の調整

模様が滑らか過ぎてクレーター感が薄いので量子化とSmoothStepで少しだけのっぺりさせます。

カスタムノードの中身はこんな感じ。

階調ごとに[0.0..1.0]してsmoothstepで寄せているだけですね。

float Quantized = Noise * Step;

float QuantizedFloor = floor(Quantized);
float Alpha = Quantized - QuantizedFloor;

float Smooth = smoothstep(0.5 - Feather, 0.5 + Feather, Alpha);

float Result = (QuantizedFloor + Smooth) * (1.0 / Step);

return Result;

階調(Step)とフェード幅(Feather)を適当に調整するとクレーター感が増します。

階調ごとに色を調整する

いつもなら適当に2色を線形補間させるだけなのですが、それだと原作の色塗りを再現できなかったので、カーブを用いて細かく調整していきます。

ColorCurveの作成

CurveAtlasの作成

画像撮り忘れたけど、先ほど作成したカラーカーブをセットしておいてください。

アトラスは幾つかの追加設定を行います。
アトラスを開いて赤枠の電卓みたいなアイコンをクリックします。

  1. Square Resolutionのチェックを外す。
  2. Texture Heightを1に変更。
  3. FilterをNearestに変更。
  4. Mip Load OptionsをOnly First Mipに変更

なんとなく分かると思いますが、カーブを1つしか使わないので高さを1ピクセルに変更。
フェッチする際にぼかしが入ると鬱陶しいのでポイントサンプラことNearestを採用。
同様にMipを考慮されると距離で色が変わってしまう恐れがあるのでMip0を強制。

といった感じです。

この辺りはアーティストさんが把握されていないことが多いので、描画エンジニア側でメモリ最適化の際に突いてあげると優しいかもしれませんね。あと高さに関しては2のべき乗を遵守してください。言わんでも守るだろうけど。

CurveAtlasRowParameter

CurveAtlasRowParameterノードを配置して、そこに先ほど作成したCurveとAtlasをセットします。

CurveTimeがX座標・U座標に該当します。
Default ValueがV座標・Y座標に該当するので、今回は1つしか存在しないのでゼロ値を入力です。

Atlasは所謂LUT(LookUpTexture)と同義なので、フェッチコストは掛かりますが、出力したい色相に拘りがある場合は、とても相性が良いです。綺麗に魅せたい場合にトレードオフは常識なのです。

円形のエッジのぼかし

色付けの影響で円形の外側まで塗られてしまうので、簡単にマスクしちゃいます。

stepで2値的なマスクなのでエッジがトゲトゲしてますね。
smoothstepで簡易的なアンチエイリアスを掛けて解消しましょう。

単純なんですが効果高いんですよね。

最近はTSRでエッジは簡単に綺麗になるのですが、あの子、セル調だとゴースティングが喧しいから好きじゃないんですよね。

月の内外のぼかし

色の微調整と同様に原作準拠のためのぼかし処理です。
あと纏めるの飽きてきた。

仕上げ

最後に全部の色を乗せて、内側と外側ぼかしの透明度の最大値をOpacityに突っ込んで完了です。

パラメータを調整してそれっぽく。

エンディング画面の再現

それっぽい窓は見つかりましたが、1本足りないので、Blenderさんで生やしてきます。

生やしてきました。

天球を作って、窓枠を配置して……
あ、外壁忘れた。

StaticMeshで隙間という名の外壁を埋めて……
UnlitだからReflectionPassの影響受けないからバレないバレない。

ブルームを乗せて、マテリアルパラメータを再調整して……

最終的には窓枠の幅を再調整して完成です。

ふふん。満足です。

もっと理想を言えば疑似的なガラスマテリアルを作って、窓枠との接地面を白っぽく見せたいのですが、キリが無いのでこの辺りで終わらせておきましょう。

おわり!!!

紅い瞳も素敵な白銀姉妹なのです。

甘々な鳥羽莉さんも可愛いですが、涼月が一番好みです。
130cmさんの公式サイトは認証切れしているので直リンクは諦め。

次回はヤエカさんに似たモデルを見つけたので、それを元に改変作業をするのですが、キャラのルックデヴはちょっと時間が掛かるのですよね。あとオトメきは純粋なセルルックというより、若干ディフューズとSSSも入っているので、筆者の得意分野とはちょっとだけベクトルが違うというのもネック。