表題の通りです。エフェクトに限った事ではありませんが。
例えばゲーム中、爆弾を投げてそれがコロコロと転がり、一定時間後に大きめの爆発が発生するようなエフェクトを再生したとします。
↑壁際で爆発した例
もしこれが壁際で再生した場合、何も対処しなければ上記画像のように爆発が壁を越えて見えてしまいます。
一人用ゲームならば特に問題は無いでしょう。むしろ判定すらも貫通して意図的にユーザーに有利をもたらすものもあります。
ですが、これが対戦ゲームの場合話が変わってきます。突然壁の向こう側から爆発が発生して攻撃を受けた!というのはあまり納得出来るものでは無いはずです。
その場合再度レイチェックをしてヒットしない、という事にするとは思いますが、それでもエフェクトだけ突き抜けてくるので大変気持ち悪い事になります。
(エルデンリングなんかはその辺割り切っていて貫通してきますが)
というわけで今回はこのエフェクト貫通問題に対してレイマーチングを利用した方法で解決を目指してみます。
ちなみにUEのバージョンは4.27です。UE5だと勝手が違うかもしれないので各自調べてみてください
シェーダーを書く
今回はUE4を使用してシェーダーを書く訳ですが、UE4のシェーダーノードには何故かループ文が存在しません。理由は知らん
という訳でCustomノードを使用して書いていきます。以下がそのコードです。
float3 toDir = CenterPositionW - PixelPositionW;
float3 rayDirection = normalize(toDir);
float3 currentPosition = PixelPositionW;
float totalDistanceTravel = 0.0;
float maxDistanceToTravel = length(toDir);
for (int i = 0; i < 10; ++i) {
float1 distanceToSurface = GetDistanceToNearestSurfaceGlobal(currentPosition).r;
currentPosition += rayDirection * distanceToSurface; // 中心点へ向かう
totalDistanceTravel += distanceToSurface;
// 到達した(中心を越えた)
if (totalDistanceTravel > maxDistanceToTravel + ThreshouldVal) {
return 1-i*0.1+0.1;
}
}
return 0; // 進む量が少なすぎてループ終了までに辿り着けなかった(遮蔽されてるかどうかは不明)
そして適当なMaterialを作成して適当なモデルに適用します(今回は平面モデル)
これを実行すると以下の様な結果になります。
左右にオレンジのオブジェクトがあり、中心に大きめの平面オブジェクトが置かれています。
そして、それに対して遮蔽されている部分が黒くなっているのが確認出来ると思います。
この黒くなっている部分をα値として利用しようという訳です。
ちょっとピンクっぽくなっている部分はレイマーチングの回数を表しています。色が濃い程回数が多い部分です。
解説
ではコードの解説をしていきます。とは言ってもやっている事はとてもシンプルです。
float1 distanceToSurface = GetDistanceToNearestSurfaceGlobal(currentPosition).r;
WorldPositionを入力してGlobal Distance Fieldから値を取得しています。currentPositionはPixel位置からActorの中心点に向かって少しずつ進んでいきます。
レイマーチングと言えば距離関数ですが、今回はその部分をエンジン側が用意したVolume Textureを利用するという形で置き換えている訳ですね。
currentPosition += rayDirection * distanceToSurface; // 中心点へ向かう
totalDistanceTravel += distanceToSurface;
いわゆるマーチングを行う為に座標をずらし、加えて終了条件用に移動量の合計を記録しておきます。
// 到達した(中心を越えた)
if (totalDistanceTravel > maxDistanceToTravel + ThreshouldVal) {
return float3(1, 1-i*0.1+0.1, 1);
}
Distance Fieldから取得した値が0に近い値であれば終了すると言うのが一般的なレイマーチングですが、今回は中心点から見て遮蔽されているかどうかを見るので、予め計算しておいた最大距離と比較するという条件で終了させます。
Actorの中心点を超えるまで移動出来たという事は、間に遮蔽が無かったという事です。ですがこの判定には幾つかの問題点があります(後述)
return 0;// 進む量が少なすぎてループ終了までに辿り着けなかった(遮蔽されてるかどうかは不明)
最大回数分マーチングを行っても中心点に辿り着けなかった = 遮蔽部分として処理します(遮蔽されているは限らない)
以上です。シンプルだね。
実際に試してみる
という訳ちゃんとしたエフェクトに適用してみましょう。
壁の近くに炎のParticleが置かれており、その炎が左側の壁を貫通してしまっているのを確認出来ますね。
この炎ParticleのMaterialに先程のシェーダーを繋いでやります(returnの値はfloat1に変更してあります。)
すると
近くに寄って壁の向こう側から見ても完全に見えません。やったぜ。
Material Function の中身はこんな感じ。Customノードの中に先程のコードが全て入っていますが、何故か DistanceToNearestSurface をノードで繋がないとCustomノード内で使用出来ません。謎
なので繋いではいますが、使用はしていません。
問題点
さて、とても利便性の高い便利なシェーダーに思えますが、問題点も当然あります。
問題点1 : 不均等にスケールがすると精度が悪くなる
公式ドキュメントに以下の注意書きがあります。
参照元
https://docs.unrealengine.com/4.27/ja/BuildingWorlds/LightingAndShadows/MeshDistanceFields/
では試してみます
適当なBOXを置き、思いっきりスケールを掛けて薄くしました。確かに炎が貫通してしまっています。
こちらの壁は元々この形で作り、0.5倍ずつにスケールを掛けています。こちらは上手くいっているようです。
というように、ドキュメント通り不均等なスケール値は正しい値が取得出来ないようです。スケールを掛ける時は気をつけましょう。
ただし、これはDistance Fieldを作成する側の話です。利用する側は極端にスケールを掛けない限りは影響はあまり無いはずです。
詳しい理由については調べていないので知りません。
問題点2 : 中心点が壁に近すぎる場合
マーチングの距離が小さすぎた場合、遮蔽されていなくても辿り着けない事があるという事です。以下の画像はその例です。
Actorの中心点が壁にめり込むギリギリまで近づけました。すると、赤矢印の部分が遮蔽されていないにも関わらず黒くなってしまっています。
Distance Fieldによるレイマーチングはその地点で一番近くにあるSurfaceの距離を取得してその分進めるという処理の都合上、Surfaceに近すぎると進む距離が短くなりすぎてしまう為、遮蔽されていないにも関わらず中心点にたどり着けなくなるという現象が起こります。
片側であれば壁から距離を離す等の運用でカバー出来ると思いますが、以下の画像のような遮蔽に囲まれている状況では結構まずいです。
対策として、WorldSetting に Global DistanceField View Distance というのがあるので、この値を下げる事で精度を上げる事が出来る。
一応それっぽくなりました。
もしくは、Soft ParticleのようなDepthを利用して別のαで上書きするとかも考えられる気がします(未検証)
ですが、Distance Fieldはこの機能の為では無くShadowMap等に主に使用されるものなので、その辺りの兼ね合いという話にもなってきます。
その他注意点
・処理負荷はそこそこ重い。実際ゲームで使うには上手いことif文を使わないようにする必要があると思われる。
・可能であれば地面はDistance Fieldの作成を切った方が良い。
各メッシュ設定にAffect Distance Field LightingというのがあるのでこれをOFFにする
おわり