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 aFRDGBuilder
(because I don’t know where else I would get theRHICmdList
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:
- 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
? - 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 twoFRDGTexture*
inputs and I only have one. The other is a simpleUTextureRenderTarget2D
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