Download

[RDG] Transfer Result of Compute Shader to Material (RenderTarget?)

Hello everyone,

I spend my Sunday trying to understand how to get custom Compute Shaders to work in UE4.
I found some tutorials that I followed and while it worked for the first few tries, I ran into issues with resources not resetting properly.
After asking for help on Unreal Slackers, I was told that one should use RDG nowadays.

Now,… there isn’t much documentation about this, despite some Crash Course and a Blog Post that uses the same Course to explain things a little bit more.

What I have by now:

  • A simple TestShader.utf, which tries to map a pseudo random number to each pixel (so basically generate noise), which has one parameter for the output RWTexture2D<float> OutputTexture;
  • An Actor that, on Tick (?) utilizes ENQUEUE_RENDER_COMMAND to create a FRDGBuilder (because I don’t know where else I would get the RHICmdList from).

Now in that Render Command, I create a FRDGTextureDesc, a FRDGTexture* and a FRDGTextureUAV*. I then grab my TestShader, call GraphBuilder.AddPass, in which I call FComputeShaderUtils::Dispatch. In the end, I call GraphBuilder.Execute(). So far so good, all of this compiles and doesn’t crash when called.

I will add the actual code further down so you can have a look.

My main questions currently are:

  1. How do I update the Params that I pass into the Shader? I was able to do that with the non-RDG version. Do I just call this on Tick and pass in the updated variables via the ENQUEUE_RENDER_COMMAND?
  2. How do I actually use the result of this Compute Shader? In the non-RDG version, the example copied the Shader Output into a RenderTarget that is used in a Material on a StaticMeshComponent. How do I do this with RDG? I tried using AddCopyTexturePass, but this expects two FRDGTexture* inputs and I only have one. The other is a simple UTextureRenderTarget2D pointer. I know that they are sort of communicating with the RHI resource, but I have no idea how to convert between them.

Here is the actual code:

TestShader.utf



#include "/Engine/Public/Platform.ush"

RWTexture2D<float> OutputTexture;

uint hash(uint state)
{
    state ^= 2747636419u;
    state *= 2654435769u;
    state ^= state >> 16;
    state *= 2654435769u;
    state ^= state >> 16;
    state *= 2654435769u;
    return state;
}

float scaleToRange01(uint number)
{
    return (float)number / 4294967295.0;
}

[numthreads(THREADGROUPSIZE_X, THREADGROUPSIZE_Y, THREADGROUPSIZE_Z)]
void MainComputeShader(uint3 id : SV_DispatchThreadID)
{
    const float2 pixel = id.xy;

    const float randomNumber = scaleToRange01(hash(pixel));

    OutputTexture[id.xy] = randomNumber;
}


TestShader.h



#pragma once

#include "CoreMinimal.h"
#include "GlobalShader.h"
#include "RenderGraphUtils.h"
#include "ShaderParameterStruct.h"

#define NUM_THREADS_PER_GROUP_DIMENSION 32

class SHADERDECLARATIONS_API FTestShader : public FGlobalShader
{
    DECLARE_GLOBAL_SHADER(FTestShader);

    SHADER_USE_PARAMETER_STRUCT(FTestShader, FGlobalShader);

    static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
    {
        return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5);
    }

    static inline void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
    {
        FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);

        OutEnvironment.SetDefine(TEXT("THREADGROUPSIZE_X"), NUM_THREADS_PER_GROUP_DIMENSION);
        OutEnvironment.SetDefine(TEXT("THREADGROUPSIZE_Y"), NUM_THREADS_PER_GROUP_DIMENSION);
        OutEnvironment.SetDefine(TEXT("THREADGROUPSIZE_Z"), 1);
    }

    BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
        SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D<float>, OutputTexture)
    END_SHADER_PARAMETER_STRUCT()
};


TestShader.cpp


#include "TestShader.h"

IMPLEMENT_GLOBAL_SHADER(FTestShader, "/CustomShaders/TestShader.usf", "MainComputeShader", SF_Compute);

And the AShaderTestActor::Tick function in which I call the ENQUEUE_RENDER_COMMAND (not sure this should be on tick).


void AShaderTestActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    ENQUEUE_RENDER_COMMAND(TestShaderCommand)(
        [this] (FRHICommandListImmediate& RHICmdList)
        {
            FRDGBuilder GraphBuilder(RHICmdList);

            FRDGTextureDesc OutputTextureDesc = FRDGTextureDesc::Create2DDesc(
                // Render Target Size for now hardcoded
                FIntPoint(800, 800),
                PF_FloatRGBA,
                FClearValueBinding::Black,
                TexCreate_None,
                TexCreate_ShaderResource | TexCreate_RenderTargetable | TexCreate_UAV,
                false
            );

            FRDGTextureRef OutputTexture = GraphBuilder.CreateTexture(OutputTextureDesc, TEXT("OutputTexture"));
            FRDGTextureUAVRef OutputTextureUAV = GraphBuilder.CreateUAV(OutputTexture);

            FTestShader::FParameters* PassParams = GraphBuilder.AllocParameters<FTestShader::FParameters>();
            PassParams->OutputTexture = OutputTextureUAV;

            TShaderMapRef<FTestShader> TestShader(GetGlobalShaderMap(GMaxRHIFeatureLevel));

            GraphBuilder.AddPass(
                RDG_EVENT_NAME("TestShader 800x800"),
                PassParams,
                ERDGPassFlags::Compute,
                [PassParams, TestShader](FRHICommandList& RHICmdList)
                {
                    FComputeShaderUtils::Dispatch(RHICmdList, TestShader, *PassParams, FIntVector(800, 800, 1));
                }
            );

            GraphBuilder.Execute();
        });
}

I’ve tried searching for Text and Video Tutorials, I tried looking for usage in the Engine Source Code. I’m a bit at a loss.

If anyone could explain to me how I get my created noise visibly into a material and how I can update the params per frame, that would be great.
Will probably also help others in the future.

Kind regards,
Cedric