SPARKCREATIVE Tech Blog

https://www.spark-creative.jp/

【UE5】ポストプロセスでシャドウマップを扱えるようにしてみた

こんにちは!!!クライアントエンジニアの小林です。

今回はポストプロセスマテリアルでシャドウマップを扱えるようにしてみました。

こんな感じ

作業環境

windows 10
visual studio 2022
visual studio code
・UnrealEngine 5.0.3

Q & A

Q1.
どうしてポストプロセスマテリアルでシャドウマップテクスチャを扱えないの?
A1.
テクスチャをセットしていないからです。

Q2.
テクスチャをセットしたら扱えるの?
A2.
はい。

ということで雑にシャドウマップテクスチャをセットしていきます。

改造ルール等

改造個所には//// PP_LightAttenuation 2022/11/12 ////と記述しています。

Engine\Source\Runtime\Renderer\Private\PostProcess\PostProcessMaterial.h

BEGIN_SHADER_PARAMETER_STRUCT(FPostProcessMaterialParameters, )
    SHADER_PARAMETER_STRUCT_REF(FViewUniformShaderParameters, View)
    SHADER_PARAMETER_STRUCT_INCLUDE(FSceneTextureShaderParameters, SceneTextures)
    SHADER_PARAMETER_STRUCT(FScreenPassTextureViewportParameters, PostProcessOutput)
    SHADER_PARAMETER_STRUCT_ARRAY(FScreenPassTextureInput, PostProcessInput, [kPostProcessMaterialInputCountMax])
    SHADER_PARAMETER_SAMPLER(SamplerState, PostProcessInput_BilinearSampler)
    SHADER_PARAMETER_RDG_TEXTURE(Texture2D, MobileCustomStencilTexture)
    SHADER_PARAMETER_SAMPLER(SamplerState, MobileCustomStencilTextureSampler)
//// PP_LightAttenuation 2022/11/12 ////
    SHADER_PARAMETER_RDG_TEXTURE(Texture2D, LightAttenuationTexture)
    SHADER_PARAMETER_SAMPLER(SamplerState, LightAttenuationTextureSampler)
//// PP_LightAttenuation 2022/11/12 ////
    SHADER_PARAMETER_RDG_TEXTURE(Texture2D, EyeAdaptationTexture)
    SHADER_PARAMETER_RDG_BUFFER_SRV(Buffer<float4>, EyeAdaptationBuffer)
    SHADER_PARAMETER(int32, MobileStencilValueRef)
    SHADER_PARAMETER(uint32, bFlipYAxis)
    SHADER_PARAMETER(uint32, bMetalMSAAHDRDecode)
    RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()

ポストプロセスマテリアルに対応したシェーダーパラメータにシャドウマップテクスチャとサンプラーを追加します。

記事内でのシャドウマップテクスチャは、シャドウデプスとシーンデプスから影を算出し、その影を書き込んだテクスチャのことを指します。

以降シャドウマップのことはLightAttenuationやLightAttenuationTextureと書いていきます。
「明るさの減衰」なので、まぁ意味は分かりますね。

    /** The output texture format to use if a new texture is created. Uses the input format if left unknown. */
    EPixelFormat OutputFormat = PF_Unknown;

    /** Custom stencil texture used for stencil operations. */
    FRDGTextureRef CustomDepthTexture = nullptr;

//// PP_LightAttenuation 2022/11/12 ////
    FRDGTextureRef LightAttenuationTexture = nullptr;
//// PP_LightAttenuation 2022/11/12 ////

    /** The uniform buffer containing all scene textures. */
    FSceneTextureShaderParameters SceneTextures;

Engine\Source\Runtime\Renderer\Private\PostProcess\PostProcessing.h

struct FPostProcessingInputs
{
    TRDGUniformBufferRef<FSceneTextureUniformParameters> SceneTextures = nullptr;
    FRDGTextureRef ViewFamilyTexture = nullptr;
    FRDGTextureRef CustomDepthTexture = nullptr;
//// PP_LightAttenuation 2022/11/12 ////
    FRDGTextureRef LightAttenuationTexture = nullptr;
//// PP_LightAttenuation 2022/11/12 ////
    FTranslucencyViewResourcesMap TranslucencyViewResourcesMap;

Engine\Source\Runtime\Renderer\Private\PostProcess\PostProcessing.cpp

const auto GetPostProcessMaterialInputs = [&](FScreenPassTexture InSceneColor)
{ 
    FPostProcessMaterialInputs PostProcessMaterialInputs;
	
    PostProcessMaterialInputs.SetInput(EPostProcessMaterialInput::SceneColor, InSceneColor);
    PostProcessMaterialInputs.SetInput(EPostProcessMaterialInput::SeparateTranslucency, FScreenPassTexture(PostDOFTranslucencyResources.GetColorForRead(GraphBuilder), PostDOFTranslucencyResources.ViewRect));
    PostProcessMaterialInputs.SetInput(EPostProcessMaterialInput::Velocity, Velocity);
    PostProcessMaterialInputs.SceneTextures = GetSceneTextureShaderParameters(Inputs.SceneTextures);
    PostProcessMaterialInputs.CustomDepthTexture = CustomDepth.Texture;
//// PP_LightAttenuation 2022/11/12 ////
    PostProcessMaterialInputs.LightAttenuationTexture = Inputs.LightAttenuationTexture;
//// PP_LightAttenuation 2022/11/12 ////

Engine\Source\Runtime\Renderer\Private\DeferredShadingRenderer.h

/** Renders the scene's lighting. */
void RenderLights(
    FRDGBuilder& GraphBuilder,
    FMinimalSceneTextures& SceneTextures,
    const FTranslucencyLightingVolumeTextures& TranslucencyLightingVolumeTextures,
    FRDGTextureRef LightingChannelsTexture,
    FSortedLightSetSceneInfo& SortedLightSet
//// PP_LightAttenuation 2022/11/12 ////
    , FRDGTextureRef& LightAttenuationTexture);
//// PP_LightAttenuation 2022/11/12 ////

RenderLights内でシャドウマップを生成しています。

Engine\Source\Runtime\Renderer\Private\LightRendering.cpp

void FDeferredShadingSceneRenderer::RenderLights(
    FRDGBuilder& GraphBuilder,
    FMinimalSceneTextures& SceneTextures,
    const FTranslucencyLightingVolumeTextures& TranslucencyLightingVolumeTextures,
    FRDGTextureRef LightingChannelsTexture,
    FSortedLightSetSceneInfo& SortedLightSet
//// PP_LightAttenuation 2022/11/12 ////
    , FRDGTextureRef& LightAttenuationTexture)
//// PP_LightAttenuation 2022/11/12 ////
    bool bInjectedTranslucentVolume = false;
    bool bUsedShadowMaskTexture = false;
//// PP_LightAttenuation 2022/11/12 ////
    const bool bLightAttenuation = bDrawShadows && !LightAttenuationTexture;
//// PP_LightAttenuation 2022/11/12 ////

今回取得するシャドウマップはひとつのディレクショナルライトです。
マージシェーダーでも作ればポイントライトなどのシャドウマップも含めることができますが、そこまでは興味が無かったです。

また、どのディレクショナルライトを取ってくるかはUnrealEngineさんのソート次第になっています。
ここら辺を制御したい方は、LightComponentuint8 UsePostProcessingShadowとか用意してあげるとRenderLights内で、その値を元に判定できるので良いのではないでしょうか。

if (bDrawShadows || bDrawLightFunction || bDrawPreviewIndicator)
{
    if (!SharedScreenShadowMaskTexture)
    {
        const FRDGTextureDesc SharedScreenShadowMaskTextureDesc(FRDGTextureDesc::Create2D(SceneTextures.Config.Extent, PF_B8G8R8A8, FClearValueBinding::White, TexCreate_RenderTargetable | TexCreate_ShaderResource | GFastVRamConfig.ScreenSpaceShadowMask));
        SharedScreenShadowMaskTexture = GraphBuilder.CreateTexture(SharedScreenShadowMaskTextureDesc, TEXT("ShadowMaskTexture"));

    //// PP_LightAttenuation 2022/11/12 ////
        if (bLightAttenuation)
        {
            LightAttenuationTexture = GraphBuilder.CreateTexture(SharedScreenShadowMaskTextureDesc, TEXT("LightAttenuationTexture"));
        }
    //// PP_LightAttenuation 2022/11/12 ////

        if (bUseHairLighting)
        {
            SharedScreenShadowMaskSubPixelTexture = GraphBuilder.CreateTexture(SharedScreenShadowMaskTextureDesc, TEXT("ShadowMaskSubPixelTexture"));
        }
    }
//// PP_LightAttenuation 2022/11/12 ////
    else if (bLightAttenuation)
    {
        const FRDGTextureDesc SharedScreenShadowMaskTextureDesc(FRDGTextureDesc::Create2D(SceneTextures.Config.Extent, PF_B8G8R8A8, FClearValueBinding::White, TexCreate_RenderTargetable | TexCreate_ShaderResource | GFastVRamConfig.ScreenSpaceShadowMask));
        LightAttenuationTexture = GraphBuilder.CreateTexture(SharedScreenShadowMaskTextureDesc, TEXT("LightAttenuationTexture"));
    }
//// PP_LightAttenuation 2022/11/12 ////
    ScreenShadowMaskTexture = SharedScreenShadowMaskTexture;
    ScreenShadowMaskSubPixelTexture = SharedScreenShadowMaskSubPixelTexture;
}

ここは本当に設計が雑で申し訳ないです。
やってることとしてはシャドウマップと同様のDescを元にコピー先であるLightAttenuationTextureテクスチャを生成しています。

    // Render the light to the scene color buffer, conditionally using the attenuation buffer or a 1x1 white texture as input 
    if (bDirectLighting)
    {
        for (int32 ViewIndex = 0, ViewCount = Views.Num(); ViewIndex < ViewCount; ++ViewIndex)
        {
            const FViewInfo& View = Views[ViewIndex];

            RDG_EVENT_SCOPE_CONDITIONAL(GraphBuilder, ViewCount > 1, "View%d", ViewIndex);
            SCOPED_GPU_MASK(GraphBuilder.RHICmdList, View.GPUMask);
            RenderLight(GraphBuilder, Scene, View, SceneTextures, &LightSceneInfo, ScreenShadowMaskTexture, LightingChannelsTexture, false /*bRenderOverlap*/, true /*bCloudShadow*/);
        }
    }

//// PP_LightAttenuation 2022/11/12 ////
    if (bLightAttenuation)
    {
        AddCopyTexturePass(GraphBuilder, ScreenShadowMaskTexture, LightAttenuationTexture, FRHICopyTextureInfo());
    }
//// PP_LightAttenuation 2022/11/12 ////

テクスチャのコピーをしています。

最適化をしたい場合はコピーはしないほうがいいです。
1パス作るよりMRTしたほうがUnrealEngineにおいてはコストダウンだと思います。
今回はシェーダーコンパイルを省きたかったのでやっていませんが、ざっと載せるとこんな感じ。

// Engine\Shaders\Private\ShadowProjectionPixelShader.usf
#if (FEATURE_LEVEL > FEATURE_LEVEL_ES3_1)
EARLYDEPTHSTENCIL
#endif
void Main(
    in float4 SVPos : SV_POSITION,
    out float4 OutColor : SV_Target0
//// PP_LightAttenuation 2022/11/12 ////
#if USE_POSTPROCESSING_SHADOW
    , out float4 OutColor1 : SV_Target1
#endif
//// PP_LightAttenuation 2022/11/12 ////
    )
{

あとは最後の方でOutColor1 = OutColor;みたいなことをすればコピーパスを生成せずにいけますね。
こんな感じでやり方が複数あると考える余地が生まれるので楽しいですね。

Engine\Source\Runtime\Renderer\Private\DeferredShadingRenderer.cpp

#if RHI_RAYTRACING
    // If Lumen did not force an earlier ray tracing scene sync, we must wait for it here.
    if (!bRayTracingSceneReady)
    {
        WaitForRayTracingScene(GraphBuilder, DynamicGeometryScratchBuffer);
        bRayTracingSceneReady = true;
    }
#endif // RHI_RAYTRACING

//// PP_LightAttenuation 2022/11/12 ////
    FRDGTextureRef LightAttenuationTexture = nullptr;
//// PP_LightAttenuation 2022/11/12 ////
#if RHI_RAYTRACING
        if (IsRayTracingEnabled())
        {
            RenderDitheredLODFadingOutMask(GraphBuilder, Views[0], SceneTextures.Depth.Target);
        }
#endif

        GraphBuilder.SetCommandListStat(GET_STATID(STAT_CLM_Lighting));
    //// PP_LightAttenuation 2022/11/12 ////
    #if 0
        RenderLights(GraphBuilder, SceneTextures, TranslucencyLightingVolumeTextures, LightingChannelsTexture, SortedLightSet);
    #else
        RenderLights(GraphBuilder, SceneTextures, TranslucencyLightingVolumeTextures, LightingChannelsTexture, SortedLightSet, LightAttenuationTexture);
    #endif
    //// PP_LightAttenuation 2022/11/12 ////
        GraphBuilder.SetCommandListStat(GET_STATID(STAT_CLM_AfterLighting));
    RDG_EVENT_SCOPE(GraphBuilder, "PostProcessing");
    RDG_GPU_STAT_SCOPE(GraphBuilder, Postprocessing);
    SCOPE_CYCLE_COUNTER(STAT_FinishRenderViewTargetTime);

    GraphBuilder.SetCommandListStat(GET_STATID(STAT_CLM_PostProcessing));

    FPostProcessingInputs PostProcessingInputs;
    PostProcessingInputs.ViewFamilyTexture = ViewFamilyTexture;
    PostProcessingInputs.CustomDepthTexture = SceneTextures.CustomDepth.Depth;
    PostProcessingInputs.SceneTextures = SceneTextures.UniformBuffer;
//// PP_LightAttenuation 2022/11/12 ////
    PostProcessingInputs.LightAttenuationTexture = LightAttenuationTexture;
//// PP_LightAttenuation 2022/11/12 ////

Engine\Source\Runtime\Renderer\Private\PostProcess\PostProcessMaterial.cpp

    PostProcessMaterialParameters->PostProcessOutput = GetScreenPassTextureViewportParameters(OutputViewport);
    PostProcessMaterialParameters->MobileCustomStencilTexture = DepthStencilTexture;
    PostProcessMaterialParameters->MobileCustomStencilTextureSampler = TStaticSamplerState<SF_Point, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();
//// PP_LightAttenuation 2022/11/12 ////
    PostProcessMaterialParameters->LightAttenuationTexture = Inputs.LightAttenuationTexture ? Inputs.LightAttenuationTexture : GSystemTextures.GetWhiteDummy(GraphBuilder);
    PostProcessMaterialParameters->LightAttenuationTextureSampler = TStaticSamplerState<SF_Point, AM_Wrap, AM_Wrap, AM_Wrap>::GetRHI();
//// PP_LightAttenuation 2022/11/12 ////

Engine\Shaders\Private\PostProcessMaterialShaders.usf

Texture2D MobileCustomStencilTexture;

//// PP_LightAttenuation 2022/11/12 ////
// 初回起動した後はここ消しておk
//// PP_LightAttenuation 2022/11/12 ////

SamplerState MobileCustomStencilTextureSampler;

int MobileStencilValueRef; // Use integer cause it has to be negative to make the less function to pass the test as always

LightAttenuationTextureLightAttenuationTextureSamplerCommon.ushで宣言されているのでわざわざ書く必要がありません。
なんですが、シェーダーに変更を加えていないのにC++上ではシェーダーパラメータが追加されているという、コンパイラが混乱する状態を起こしています。
そのためPostProcessMaterialShaders.usfの適当な行で改行とかして、シェーダーの変更をキャッチさせてあげます。
ビルドが完了して正常に起動した後は、その改行部分は削除して大丈夫です。

ちょっと面倒ですが、これをしないと永遠と「シェーダーパラメータのサイズが違うよ!再コンパイルして!!」という警告文が出力されてしまいます。

ポストプロセスマテリアルで試してみる

return GetPerPixelLightAttenuation(ViewportUVToBufferUV(UV));

おわり!!!

お疲れさまでした!!!