[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

2 Likes

Slightly late answer, but you can do this with AddCopyTexturePass
Basically you need to create another render pass after you add your shader pass.

FRHICopyTextureInfo CopyInfo;
	CopyInfo.Size = FIntVector(TextureSize.X, TextureSize.Y, 0);
	AddCopyTexturePass(*GraphBuilder, FromTexture, ToTexture, CopyInfo);

where the textures are FRDGTexture
You can create an RDG texture like so:

GraphBuilder.RegisterExternalTexture(CreateRenderTarget(MyTexture, TEXT("MyTextureRT")));

hope this helps

1 Like

Hey, how can I copy a UTextureRenderTarget2DArray with a hlsl version of Texture2DArray?

FRHICopyTextureInfo CopyInfo;
CopyInfo.Size = FIntVector(TextureSize.X, TextureSize.Y, 0);
AddCopyTexturePass(*GraphBuilder, FromTexture, ToTexture, CopyInfo);

Only copies the first Slice of the Texture, all other slices stay empty …

Help is appreciated since I can not find a single info about it in the net

EDIT: Got it working with Texture2DArrays… here the steps I’ve done

  1. Declare the Render target 2D Array in the compute shader like so:
SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2DArray<float4>, RW_MyTexture)

And use the same type in the .usf file

  1. Create the UAV
	static FORCEINLINE FRDGTextureUAVRef CreateWriteTextureArray_Custom_Out(FRDGBuilder& GraphBuilder, FRDGTextureRef& OutputTextureRef, const int32& SizeX, const int32& SizeY, const int32& Slice, const TCHAR* TextureName, const EPixelFormat& Format)
	{
		const FRDGTextureDesc OutTextureDescription = FRDGTextureDesc::Create2DArray(
			FIntPoint(SizeX, SizeY),
			Format,
			FClearValueBinding::Transparent,
			TexCreate_ShaderResource | TexCreate_UAV | TexCreate_Dynamic | TexCreate_RenderTargetable | TexCreate_External | TexCreate_Shared,
			Slice, GET1_NUMBER, GET1_NUMBER);
		
		OutputTextureRef = GraphBuilder.CreateTexture(OutTextureDescription, TextureName);
		
		return GraphBuilder.CreateUAV(FRDGTextureUAVDesc(OutputTextureRef));
	}

And allocate the Texture with it

  1. Call the copy Function
const FRDGTextureRef RenderTargetOutputTexture = GraphBuilder.RegisterExternalTexture(CreateRenderTarget(AnimationOutputTexture->GetRenderTargetResource()->GetTexture2DArrayRHI(), FCrowdPlugin_BoneTransform_CS_Lf::BoneTransformsTextureCopyDebugName));

FRHICopyTextureInfo CopyInfo = FRHICopyTextureInfo();
CopyInfo.Size = RenderTargetOutputTexture->Desc.GetSize();
CopyInfo.NumSlices = AnimationOutputTexture->Slices;

AddCopyTexturePass(GraphBuilder, AnimationOutputTextureRef, RenderTargetOutputTexture, CopyInfo);	

In the shader part, 1 slice is handled like 1 texture, so the calculation goes like this if you have an Index:

				uint Slice = GPUBoneIndexBase / (OutputTextureSizeX * OutputTextureSizeY);
				uint SliceBase = GPUBoneIndexBase % (OutputTextureSizeX * OutputTextureSizeY);
				uint Mod = SliceBase % OutputTextureSizeX;
				uint Div = SliceBase / OutputTextureSizeY;
RW_BoneTransform_OutputTexture[uint3(Mod, Div, Slice)] = BoneMatrix[0];

Hope it helps :slight_smile: