LoginSignup
6
7

More than 1 year has passed since last update.

【UE5】HLSLでScreen Space Reflectionsを自作する

Last updated at Posted at 2023-06-07

概要

マテリアルのCustomノードを使ってSSRを実装しました。
ssr.gif
UE5標準機能のSSRよりクオリティは落ちますが、融通が利くので便利かもしれません。

エンジンバージョンはUE5.1を使用しました。

実装

ポストプロセスで実装していきます。
スクリーンショット 2023-06-08 033054.png

グラフはシンプルにCustomノードをEmissiveに繋いでいるだけです。
SceneTextureノードはどこかで使われていないとコンパイルが通らないので刺さっています。

コード

大まかな部分は以下の記事を参考にさせていただきました。
Unity で Screen Space Reflection の実装をしてみた
https://tips.hecomi.com/entry/2016/04/04/022550

int maxRayNum = 20;
float maxLength = 500;
float maxThickness = 500 / maxRayNum;

float3 R = ReflectionAboutCustomWorldNormal(Parameters, Parameters.WorldNormal, false);
float3 step = maxLength / maxRayNum * R;

float3 pos = LWCToFloat(Parameters.AbsoluteWorldPosition);
float2 uv = ViewportUVToBufferUV(GetViewportUV(Parameters));
float3 Color = SceneTextureLookup(uv, 14, false).xyz;

for (int i = 1; i <= maxRayNum; i++) 
{
    float3 ray = step * (i + RandFast((uv + fmod(View.GameTime,1.0)) * View.ViewSizeAndInvSize.xy));
    float3 rayPos = pos + ray;
    float4 viewRayPos = mul(float4(rayPos, 1), LWCHackToFloat(PrimaryView.WorldToClip));
    float2 rayUv = viewRayPos.xy / viewRayPos.w * float2(0.5, -0.5) + 0.5;
    float2 bufferUv = ViewportUVToBufferUV(rayUv);
    float rayDepth = viewRayPos.w;
    float gbufferDepth = CalcSceneDepth(bufferUv);

    if (max(abs(rayUv.x - 0.5), abs(rayUv.y - 0.5)) > 0.5) break;
    
    if (rayDepth - gbufferDepth > 0 && rayDepth - gbufferDepth < maxThickness)
    {
        float edgeFactor = 1.0 - pow(2.0 * length(rayUv - 0.5), 2);
        float a = pow(min(1.0, (maxLength / 2) / length(ray)), 2.0) * saturate(edgeFactor);
        a *= pow(length(rayUv - 0.5) / 0.5, 0.5);
        Color = Color * (1 - a) + SceneTextureLookup(bufferUv, 14, false).xyz * a;
        break;
    }
}

return Color;

このままCustomノードにコピペすれば動くはずです。
かいつまんで説明していきます。

変数の準備

初期状態の変数たち。

int maxRayNum = 20;
float maxLength = 500;
float maxThickness = 500 / maxRayNum;

float3 R = ReflectionAboutCustomWorldNormal(Parameters, Parameters.WorldNormal, false);
float3 step = maxLength / maxRayNum * R;

float3 pos = LWCToFloat(Parameters.AbsoluteWorldPosition);
float2 uv = ViewportUVToBufferUV(GetViewportUV(Parameters));
float3 Color = SceneTextureLookup(uv, 14, false).xyz;

Parameters.AbsoluteWorldPositionはWorld Positionノードと同じ値です。
そのままだと特殊な型になっていて使えないので、LWCToFloatでfloatに変換します。
SceneTextureLookup(uv, 14, false)でSceneTextureノードのPostProcessInput0と同じ値を取得しています。

レイを飛ばす

World PositionからReflection Vectorに向かってレイを飛ばします。

float3 ray = step * (i + RandFast((uv + fmod(View.GameTime,1.0)) * View.ViewSizeAndInvSize.xy));
float3 rayPos = pos + ray;

このときノイズをかけてStep距離にランダム感を与えます。
View.GameTimeで時間的にもノイズをかけるとより良い結果になります。
Random.ushにあったRandFast()を使いました。

レイの座標をスクリーン座標に変換

ここが一番難儀しました。
アンリアルはマテリアルからView Projection Matrixにアクセスできないと思っていたのですが、シェーダーコードを探してみるとそれっぽい処理が行われている箇所がいくつかあります。

LumenMeshSDFCulling.usf
OutPosition = mul(float4(WorldPosition, 1), LWCHackToFloat(PrimaryView.WorldToClip));

どうやらこのPrimaryView.WorldToClipというやつで変換できそうなので、流用してみました。

float4 viewRayPos = mul(float4(rayPos, 1), LWCHackToFloat(PrimaryView.WorldToClip));
float2 rayUv = viewRayPos.xy / viewRayPos.w * float2(0.5, -0.5) + 0.5;
float2 bufferUv = ViewportUVToBufferUV(rayUv);

これでレイの飛んだ位置がスクリーンではどの位置になるのかが分かりました。
可視化するとこんな感じになります。
スクリーンショット 2023-06-08 013726.png

レイのヒット判定

GBufferの深度とレイの深度を比較します。

if (rayDepth - gbufferDepth > 0)
{
    Color = Color + SceneTextureLookup(bufferUv, 14, false).xyz;
    break;
}

レイの深度がGBuffer深度を上回った(レイがめり込んだ)らヒットしたと判定、ヒット位置のスクリーン座標でシーンカラーをサンプルして加算していきます。

ループ処理

一連の流れをmaxRayNum分forループし、Stepを刻みます。

for (int i = 1; i <= maxRayNum; i++) 
{
    float3 ray = step * (i + RandFast((uv + fmod(View.GameTime,1.0)) * View.ViewSizeAndInvSize.xy));
    float3 rayPos = pos + ray;
    float4 viewRayPos = mul(float4(rayPos, 1), LWCHackToFloat(PrimaryView.WorldToClip));
    float2 rayUv = viewRayPos.xy / viewRayPos.w * float2(0.5, -0.5) + 0.5;
    float2 bufferUv = ViewportUVToBufferUV(rayUv);
    float rayDepth = viewRayPos.w;
    float gbufferDepth = CalcSceneDepth(bufferUv);
    
    if (rayDepth - gbufferDepth > 0)
    {
        Color = Color + SceneTextureLookup(bufferUv, 14, false).xyz;
        break;
    }
}

この時点でSSRのベースはできています。
スクリーンショット 2023-06-08 015132.png
かなりノイジーですがアンチエイリアスがいい感じに処理してくれるので問題ないです。

あとはレイが画面外に出ているかを判定、画面外に近づくにつれフェードアウトする処理を追加すれば綺麗になります。

for (int i = 1; i <= maxRayNum; i++) 
{
    float3 ray = step * (i + RandFast((uv + fmod(View.GameTime,1.0)) * View.ViewSizeAndInvSize.xy));
    float3 rayPos = pos + ray;
    float4 viewRayPos = mul(float4(rayPos, 1), LWCHackToFloat(PrimaryView.WorldToClip));
    float2 rayUv = viewRayPos.xy / viewRayPos.w * float2(0.5, -0.5) + 0.5;
    float2 bufferUv = ViewportUVToBufferUV(rayUv);
    float rayDepth = viewRayPos.w;
    float gbufferDepth = CalcSceneDepth(bufferUv);

    if (max(abs(rayUv.x - 0.5), abs(rayUv.y - 0.5)) > 0.5) break;
    
    if (rayDepth - gbufferDepth > 0 && rayDepth - gbufferDepth < maxThickness)
    {
        float edgeFactor = 1.0 - pow(2.0 * length(rayUv - 0.5), 2);
        float a = pow(min(1.0, (maxLength / 2) / length(ray)), 2.0) * saturate(edgeFactor);
        Color = Color * (1 - a) + SceneTextureLookup(bufferUv, 14, false).xyz * a;
        break;
    }
}

半透明マテリアルでの実装

コードを少し修正すれば半透明マテリアルでも流用できます。
本来は半透明マテリアルでSSRを使用する場合、比較的重いSurface Translucency VolumeかSurface Forward Shadingへ切り替える必要があります。
しかしCustomノードでSSRを実装してしまえば、一番軽いVolumetric Non Directionalで反射だけSSRを使うといった芸当が可能です。

コード

int maxRayNum = 20;
float maxLength = 500;
float maxThickness = 500 / maxRayNum;

float2 uv = ViewportUVToBufferUV(GetViewportUV(Parameters));
float3 R = ReflectionAboutCustomWorldNormal(Parameters, Parameters.WorldNormal, false);
float3 step = maxLength / maxRayNum * R;

float3 pos = LWCToFloat(Parameters.AbsoluteWorldPosition);
float3 SpecularIBL = MaterialExpressionSkyLightEnvMapSample(R, 0);

for (int i = 1; i <= maxRayNum; i++) 
{
    float3 ray = step * (i + RandFast((uv + fmod(View.GameTime,1.0)) * View.ViewSizeAndInvSize.xy));
    float3 rayPos = pos + ray;
    float4 viewRayPos = mul(float4(rayPos, 1), LWCHackToFloat(PrimaryView.WorldToClip));
    float2 rayUv = viewRayPos.xy / viewRayPos.w * float2(0.5, -0.5) + 0.5;
    float2 bufferUv = ViewportUVToBufferUV(rayUv);
    float rayDepth = viewRayPos.w;
    float gbufferDepth = CalcSceneDepth(bufferUv);

    if (max(abs(rayUv.x - 0.5), abs(rayUv.y - 0.5)) > 0.5) break;
    
    if (rayDepth - gbufferDepth > 0 && rayDepth - gbufferDepth < maxThickness)
    {
        float edgeFactor = 1.0 - pow(2.0 * length(rayUv - 0.5), 2);
        float a = pow(min(1.0, (maxLength / 2) / length(ray)), 2.0) * saturate(edgeFactor);
        SpecularIBL = SpecularIBL * (1 - a) + DecodeSceneColorForMaterialNode(bufferUv) * a;
        break;
    }
}

return SpecularIBL;

ポスプロ実装との差分

シーンテクスチャのサンプル方式が変わるだけです。
SceneTextureLookupはポスプロ専用なので、DecodeSceneColorForMaterialNode()を使用しています。
Dummyとして刺すノードもScene Colorノードに変更します。
image.png
また、初期状態ではSkyLightをサンプルしています。

float3 SpecularIBL = MaterialExpressionSkyLightEnvMapSample(R, 0);

これによりレイが届かないスクリーン外の領域を空の色でカバーできます。
そのまま使おうとしてもコンパイルが通らないので、SkyAtmosphereCommon.ushをインクルードします。
スクリーンショット 2023-06-07 192648.png

応用

レイが飛ぶ方向をReflection Vectorではなく、Refraction Vectorにすることで精度の高い屈折表現も可能です。
屈折角はマテリアル関数のRefractで簡単に計算できます。
変数のRをパラメータとして外に出して使ってみました。
スクリーンショット 2023-06-08 033751.png
refract.gif

注意事項

バージョンが変わるとシェーダーコードの関数名が変わったりするので動かなくなる可能性があります。
PrimaryView.WorldToClipは割と最近この名前に変わったようです。

参考

Unity で Screen Space Reflection の実装をしてみた
https://tips.hecomi.com/entry/2016/04/04/022550

6
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7