Render Dependency Graph

Render Dependency Graph es un nuevo subsistema de rendering (versiones 4.22 y superiores) basado en la idea de descomponer las tareas asociadas al render en función de sus dependencias.

Esta idea es tomada "prestada" del motor Frostbite, salvo que en Frostbite lo llaman Framegraph.

Un deferred shading pipeline de ejemplo

Gracias a este grafo de dependencias explícitas se pueden conseguir mejoras en el rendimiento, mejoras en la gestión de memoria y mejor mantenibilidad del sistema de render.

¿Cómo se consigue esta mejora de rendimiento? Definiendo la tarea de render en una función lambda. Esta función lambda será ejecutada en el momento oportuno (cuando las dependencias estén satisfechas) y liberará los recursos cuando salgamos de su scope. Mejor rendimiento y mejor mantenibilidad.

La clase principal para interactuar con Render Dependency Graph es FRDGBuilder

Nota al margen: En el código fuente de RenderGraph.h aparece documentación de gran relevancia en los comentarios.

Mandelbrot

Veamos un ejemplo sencillo, vamos a crear un compute shader para renderizar el famoso fractal de mandelbrot.

Empecemos por la parte de la declaración del shader:

Declaración del shader para Mandelbrot

Antes de nada si necesitas repasar que es un compute shader aquí implementamos uno.

  • He creado un plugin llamado RDGPlugindónde almacenar los shaders
Jerarquía del proyecto

También podrías haber usado un módulo aparte tal y como hacemos aquí.

  • Al cargar el módulo debemos mapear la carpeta dónde están los shaders para que sean visibles al motor:
void FRDGPluginModule::StartupModule()
{
	const FString PluginShaderDir = FPaths::Combine(IPluginManager::Get().FindPlugin(TEXT("RDGPlugin"))->GetBaseDir(), TEXT("Shaders"));
	AddShaderSourceDirectoryMapping(TEXT("/Plugin/RDGPlugin"), PluginShaderDir);
}
  • El fichero HLSL del compute shader para calcular mandelbrot es el siguiente:
#include "/Engine/Public/Platform.ush"

RWTexture2D<float> OutTexture;
float2 Size;

[numthreads(8, 8, 1)]
void mainCS(uint3 GroupId : SV_GroupID, uint3 DispatchThreadId : SV_DispatchThreadID, uint3 GroupThreadId : SV_GroupThreadID, uint GroupIndex : SV_GroupIndex)
{
	float2 C = 2.0 * (float2(DispatchThreadId.xy) - 0.5 * Size) / Size;
	C.y *= Size.y / Size.x; // fix aspect ratio
	C += float2(-0.5, 0.0); // offset to center
	
	float2 Z = C;

	const int max_iter = 64;
	
	int iter;
	for (iter = 0; iter < max_iter; ++iter)
	{
		float2 Zn = Z;
		Zn.x = Z.x * Z.x - Z.y * Z.y;
		Zn.y = 2.0 * Z.x * Z.y;
        
		Z = Zn + C;
        
		if (dot(Z,Z) > 2.0)
		{
			break;
		}
	}
    
	float s = 2.0 * abs(float(iter) / float(max_iter) - 0.5);
	OutTexture[DispatchThreadId.xy] = s;
}
  • Declara ahora la parte de la CPU, esto es, la clase que abstrae el shader y que usaremos como "puente" para comunicarnos:
// MandelbrotCS.h

#pragma once

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

class RDGPLUGIN_API FMandelbrotCS : public FGlobalShader
{
	DECLARE_GLOBAL_SHADER(FMandelbrotCS)

	SHADER_USE_PARAMETER_STRUCT(FMandelbrotCS, FGlobalShader)

	BEGIN_SHADER_PARAMETER_STRUCT(FParameters,)
		SHADER_PARAMETER_UAV(RWTexture<float>, OutTexture)
		SHADER_PARAMETER(FVector2D, Size)
	END_SHADER_PARAMETER_STRUCT()
	
public:
	static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
	{
		return RHISupportsComputeShaders(Parameters.Platform);
	}

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

		//OutEnvironment.SetDefine(TEXT("THREADGROUPSIZE_X"), 8);
		//OutEnvironment.SetDefine(TEXT("THREADGROUPSIZE_Y"), 8);
	}

	static constexpr int32 ThreadGroupSizeX = 8;
	static constexpr int32 ThreadGroupSizeY = 8;
};

Y el fichero cpp:

#include "MandelbrotCS.h"

IMPLEMENT_GLOBAL_SHADER(FMandelbrotCS, "/Plugin/RDGPlugin/Mandelbrot.usf", "mainCS", SF_Compute);

Si los parámetros que declaras en la estructura de parámetros en C++ no hace match perfecto con los parámetros del propio fichero usf entonces unreal se quejará muy fuerte (crash!).

Y ahora la parte interesante con Render Dependency Graph.

Usar RDG para lanzar compute shader

Crea un actor llamado MandelbrotActor en el módulo del proyecto:

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MandelbrotActor.generated.h"

class UTextureRenderTarget2D;

UCLASS()
class RDGEXAMPLE_API AMandelbrotActor : public AActor
{
	GENERATED_BODY()

public:
	AMandelbrotActor();

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Texture")
	UTextureRenderTarget2D* MandelbrotTexture;
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Mandelbrot")
	int TextureWidth;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Mandelbrot")
	int TextureHeight;

protected:
	virtual void BeginPlay() override;

	virtual void BeginDestroy() override;

public:
	virtual void Tick(float DeltaTime) override;

private:
	FUnorderedAccessViewRHIRef MandelbrotTextureUAV;
};

Fíjate que el último parámetro es precisamente el descriptor UAV que necesitamos pasarle a nuestro computer shader (en concreto en su parámetro OutTexture).

Y su implementación, empezamos con el BeginPlay:

void AMandelbrotActor::BeginPlay()
{
	Super::BeginPlay();

	MandelbrotTexture = NewObject<UTextureRenderTarget2D>();

	// this line is important:
	MandelbrotTexture->bCanCreateUAV = true;
	// because it's needed to create Unordered Access View (UAV)
	
	MandelbrotTexture->bAutoGenerateMips = false;
	MandelbrotTexture->RenderTargetFormat = ETextureRenderTargetFormat::RTF_R8;
	MandelbrotTexture->MipsSamplerFilter = TextureFilter::TF_Bilinear;
	MandelbrotTexture->ClearColor = FColor::Black;
	MandelbrotTexture->InitAutoFormat(TextureWidth, TextureHeight);
	MandelbrotTexture->UpdateResourceImmediate(true);

	ENQUEUE_RENDER_COMMAND(Mandelbrot_CreateUAV)(
		[this](FRHICommandListImmediate& RHICmdList)
		{
			auto TextureRHI = MandelbrotTexture->Resource->TextureRHI;
			MandelbrotTextureUAV = RHICreateUnorderedAccessView(TextureRHI);
		}
	);
}

La parte más interesante es la creación del UAV.

Posteriormente lo pasaremos como parámetro al shader.

Otro punto interesante es la destrucción del mismo:

void AMandelbrotActor::BeginDestroy()
{
	ENQUEUE_RENDER_COMMAND(Mandelbrot_DestroyUAV)(
		[this](FRHICommandListImmediate& RHICmdList)
		{
			MandelbrotTextureUAV.SafeRelease();
		}
	);
	Super::BeginDestroy();
}

Y ahora lanzar el compute shader vía RDG:

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

	auto FeatureLevel = GetWorld()->FeatureLevel;

	ENQUEUE_RENDER_COMMAND(Mandelbrot_Render)(
		[this, FeatureLevel](FRHICommandListImmediate& RHICmdList)
		{
			FRDGBuilder GraphBuilder(RHICmdList);
			
			// aquí la parte interesante dónde añadiremos dependencias

			GraphBuilder.Execute();
		}
	);
}

¿Recuerdas que usamos RDG para añadir dependencias? En concreto las dependencias son en forma de "pases".

En nuestro caso, queremos un "pase" de compute shader. Para ello podemos usar la biblioteca de utilidades de Compute Shader Utils:

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

	auto FeatureLevel = GetWorld()->FeatureLevel;

	ENQUEUE_RENDER_COMMAND(Mandelbrot_Render)(
		[this, FeatureLevel](FRHICommandListImmediate& RHICmdList)
		{
			FRDGBuilder GraphBuilder(RHICmdList);
			
			TShaderMapRef<FMandelbrotCS> Shader /*...*/

			FMandelbrotCS::FParameters* ShaderParams = GraphBuilder.AllocParameters<FMandelbrotCS::FParameters>();
			/* initialize shader params */
			
            const FIntVector ComputeGroupXYZCount(....);
			
			FComputeShaderUtils::AddPass(
				GraphBuilder,
				RDG_EVENT_NAME("Mandelbrot"),
				Shader,
				ShaderParams,
				ComputeGroupXYZCount
			);

			GraphBuilder.Execute();
		}
	);
}

El código final, con todo relleno, quedaría:

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

	auto FeatureLevel = GetWorld()->FeatureLevel;

	ENQUEUE_RENDER_COMMAND(Mandelbrot_Render)(
		[this, FeatureLevel](FRHICommandListImmediate& RHICmdList)
		{
			FRDGBuilder GraphBuilder(RHICmdList);
			
			FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(FeatureLevel);
			TShaderMapRef<FMandelbrotCS> Shader(GlobalShaderMap);

			FMandelbrotCS::FParameters* ShaderParams = GraphBuilder.AllocParameters<FMandelbrotCS::FParameters>();
			ShaderParams->Size = FVector2D(TextureWidth, TextureHeight);
			ShaderParams->OutTexture = MandelbrotTextureUAV;
			
			const FIntVector ComputeGroupXYZCount(
							FMath::CeilToInt(TextureWidth / static_cast<float>(FMandelbrotCS::ThreadGroupSizeX)),
							FMath::CeilToInt(TextureHeight / static_cast<float>(FMandelbrotCS::ThreadGroupSizeY)),
							1);
			
            // para GroupCount tambien se puede hacer uso de FComputeShaderUtils::GetGroupCount(...)
            
			FComputeShaderUtils::AddPass(
				GraphBuilder,
				RDG_EVENT_NAME("Mandelbrot"),
				Shader,
				ShaderParams,
				ComputeGroupXYZCount
			);

			GraphBuilder.Execute();
		}
	);
}

Fíjate que no necesitamos transicionar los recursos:

RHICmdList.Transition(FRHITransitionInfo(MandelbrotTextureUAV, ERHIAccess::SRVGraphics, ERHIAccess::UAVCompute));
No es necesario

Ni siquiera en el Begin Play para preparar el recurso para su uso en compute shader:

RHICmdList.Transition(FRHITransitionInfo(MandelbrotTextureUAV, ERHIAccess::Unknown, ERHIAccess::SRVGraphics));
¡Tampoco es necesario!

¡Render Dependency Graph lo hace por nosotros bajo demanda!

Recursos externos

En Render Dependency Graph puedes crear cualquier tipo de recurso:

FRDGTexture*      SceneColor    = GraphBuilder.CreateTexture(....);
FRDGTextureUAV*   SceneColorUAV = GraphBuilder.CreateUAV(....);
FRDGTextureSRVRef MipSRV        = GraphBuilder.CreateSRV(....);
FRDGBuffer*       Buffer        = GraphBuilder.CreateBuffer(....);
Algunos ejemplos de creación de recursos

La gracia del asunto es que estos recursos, al estar gestionados por RDG, son autogestionados (alocados, optimizados y liberados) por el propio RDG.

Sin embargo, ¿qué ocurre si quisiéramos usar un buffer inicializado con unos datos?

Por la propia naturaleza de RDG, no puedes hacerlo de forma directa ya que el buffer ¡ni siquiera está disponible cuando haces CreateBuffer!

Su "allocation" es diferida en un punto del futuro cuando todo el grafo de dependencias es completado, y por tanto optimizado y listo para ser ejecutado.

¿Cómo lo hacemos? ¡Añadiendo un nuevo pase!

const void* SourcePtr = ...;

FRDGBufferDesc BufferDesc = FRDGBufferDesc::CreateStructuredDesc(BytesPerElement, NumElements)
FRDGBufferRef Buffer = GraphBuilder.CreateBuffer(BufferDesc, TEXT("BufferTestName"));

FCopyBufferParameters* PassParameters = GraphBuilder.AllocParameters<FCopyBufferParameters>();
PassParameters->Buffer = Buffer;

GraphBuilder.AddPass(
	RDG_EVENT_NAME("StructuredBufferUpload(%s)", Buffer->Name),
	PassParameters,
	ERDGPassFlags::Copy,
	[Buffer, SourcePtr, InitialDataSize](FRHICommandListImmediate& RHICmdList)
{
	FRHIStructuredBuffer* StructuredBuffer = Buffer->GetRHIStructuredBuffer();
	void* DestPtr = RHICmdList.LockStructuredBuffer(StructuredBuffer, 0, InitialDataSize, RLM_WriteOnly);
	FMemory::Memcpy(DestPtr, SourcePtr, InitialDataSize);
	RHICmdList.UnlockStructuredBuffer(StructuredBuffer);
});
Un nuevo pase, para copiar datos

Nota al margen: Si vienes de hacer uso de la api legacy anterior a RDG, es decir, haciendo llamadas como RHICreateStructuredBuffer para crear el buffer y/o haciendo uso de TResourceArray puedes hacerlo pero deberás registrarlos con GraphBuilder.TryRegisterExternalBuffer(...) ó  GraphBuilder.TryRegisterExternalTexture(...) para poder ser usado en RDG.

La buena noticia es que, es tan común crear un nuevo buffer e inicializarlo con datos, que Epic ya se ha encargado de encapsular este código en una función en RenderGraphUtils.h

Por lo que todo el código anterior quedaría:

FRDGBufferRef DataSource = CreateStructuredBuffer(
	GraphBuilder, 
    TEXT("DataSource"), 
    sizeof(float), Size, 
    InitialData.GetData(), TotalDataSize, 
    ERDGInitialDataFlags::None
);
Crear un buffer con RDG

Multiplicar por una constante

Veamos un caso muy sencillo pero muy ilustrativo, un compute shader que toma dos buffers, uno de entrada poblado con datos iniciales y otro de salida. El resultado es multiplicar el primer buffer por una constante (también pasada como parámetro).

El shader quedaría así de simple:

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

StructuredBuffer<float>   Data;   // buffer de entrada
RWStructuredBuffer<float> Result; // buffer de salida
float K;

[numthreads(8, 1, 1)]
void mainCS(uint3 GroupId : SV_GroupID, 
            uint3 DispatchThreadId : SV_DispatchThreadID, 
            uint3 GroupThreadId : SV_GroupThreadID, 
            uint GroupIndex : SV_GroupIndex)
{
	Result[DispatchThreadId.x] = K * Data[DispatchThreadId.x];
}
MultiplyByConstant.usf

Por la parte de C++ la declaración es también muy simple:

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

class RDGPLUGIN_API FMultiplyByConstantCS : public FGlobalShader
{
	DECLARE_GLOBAL_SHADER(FMultiplyByConstantCS)

	SHADER_USE_PARAMETER_STRUCT(FMultiplyByConstantCS, FGlobalShader)

	BEGIN_SHADER_PARAMETER_STRUCT(FParameters,)
		SHADER_PARAMETER_RDG_BUFFER_SRV(Buffer<float>, Data)
		SHADER_PARAMETER_RDG_BUFFER_UAV(FRWBuffer<float>, Result)
		SHADER_PARAMETER(float, K)
	END_SHADER_PARAMETER_STRUCT()
	
public:
	static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
	{
		return RHISupportsComputeShaders(Parameters.Platform);
	}

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

		//OutEnvironment.SetDefine(TEXT("THREADGROUPSIZE_X"), ThreadGroupSizeX);
		//OutEnvironment.SetDefine(TEXT("THREADGROUPSIZE_Y"), ThreadGroupSizeY);
	}

	static constexpr int32 ThreadGroupSizeX = 8;
};
MultiplyByConstantCS.h

Y ahora la parte interesante, la gestión en RDG:

ENQUEUE_RENDER_COMMAND(MultiplyByConstant_Execute)(
	[FeatureLevel, this](FRHICommandListImmediate& RHICommandList)
	{
		FRDGBuilder GraphBuilder(RHICommandList);

		TArray<float> InitialData;
		for (int i = 0; i < Size; ++i)
		{
			InitialData.Push(static_cast<float>(i));
		}

		const uint64 TotalDataSize = sizeof(float) * Size;
            
        // ...
            
            
		GraphBuilder.Execute();
	}
);
Enqueue render command

Primero necesitamos crear los bufferes:

FRDGBufferRef DataSource = CreateStructuredBuffer(GraphBuilder, TEXT("DataSource"), sizeof(float), Size, InitialData.GetData(), TotalDataSize, ERDGInitialDataFlags::None);
FRDGBufferRef DataTarget = GraphBuilder.CreateBuffer(FRDGBufferDesc::CreateStructuredDesc(sizeof(float), Size), TEXT("DataTarget"));

FRDGBufferSRVRef DataSourceSRV = GraphBuilder.CreateSRV(DataSource, EPixelFormat::PF_R32_FLOAT);
FRDGBufferUAVRef DataTargetUAV = GraphBuilder.CreateUAV(DataTarget, EPixelFormat::PF_R32_FLOAT);
Creación de búferes

Lanzar el compute shader:

auto GlobalShaderMap = GetGlobalShaderMap(FeatureLevel);
TShaderMapRef<FMultiplyByConstantCS> ComputeShader(GlobalShaderMap);

auto ShaderParams = GraphBuilder.AllocParameters<FMultiplyByConstantCS::FParameters>();
ShaderParams->Data = DataSourceSRV;
ShaderParams->Result = DataTargetUAV;
ShaderParams->K = K;

const FIntVector GroupCount(Size / FMultiplyByConstantCS::ThreadGroupSizeX, 1, 1);

FComputeShaderUtils::AddPass(
    GraphBuilder,
    RDG_EVENT_NAME("MultiplyByK"),
    ERDGPassFlags::Compute,
    ComputeShader,
    ShaderParams,
    GroupCount);
Siguiente pase, lanzar el compute shader

Y por último, copiar de vuelta los datos del buffer, en este caso creamos una subpass propia:

FCopyParameters* CopyParams = GraphBuilder.AllocParameters<FCopyParameters>();
CopyParams->Buffer = DataTarget;

GraphBuilder.AddPass(
    RDG_EVENT_NAME("MultiplyByConstant_CopyBackBuffer"),
    CopyParams,
    ERDGPassFlags::Readback,
    [TotalDataSize, DataTarget, this](FRHICommandListImmediate& RHICmdList)
{
    FRHIStructuredBuffer* Buffer = DataTarget->GetRHIStructuredBuffer();
    void* SourcePtr = RHICmdList.LockStructuredBuffer(Buffer, 0, TotalDataSize, RLM_ReadOnly);
    FMemory::Memcpy(Result.GetData(), SourcePtr, TotalDataSize);
    RHICmdList.UnlockStructuredBuffer(Buffer);
});
Copy buffer

Notas importantes:

  1. Hacemos uso de FCopyParametersque es una estructura propia que hemos declarado (más adelante su declaración)
  2. Se pone en los flags que el pase es Readback para que RDG optimice y haga sus validaciones. Ojo que poner otro flag puede ocasionar error de validación (por ejemplo si pones el flag Copyespera que hagas uso de un render target y se quejará).
  3. Es importante pasarle los parámetros del punto 1 ya que con esto RDG conoce el ciclo de vida de cada recurso y la dependencia entre pases.

La declaración de FCopyParameters es la siguiente (se puede colocar en el cpp):

BEGIN_SHADER_PARAMETER_STRUCT(FCopyParameters, )
	RDG_BUFFER_ACCESS(Buffer, ERHIAccess::CopySrc)
END_SHADER_PARAMETER_STRUCT()
FCopyParameters

Fíjate como se indica RDG_BUFFER_ACCESS y que el acceso será CopySrc.

Por último es interesante el uso de FRenderCommandFence para sincronizar el hilo de Draw y Game.

Esto es importante que cuando escribimos de vuelta el contenido del buffer al array Result queremos "esperarnos" a que se termine de copiar el contenido antes de acceder al mismo.

FRenderCommandFence RenderFence;

TArray<float> Result;
Result.Init(0, Size);

// ....

RenderFence.BeginFence();

ENQUEUE_RENDER_COMMAND(MultiplyByConstant_Execute)(
	[FeatureLevel, this](FRHICommandListImmediate& RHICommandList)
    {
    	....
        FMemory::Memcpy(Result.GetData(), SourcePtr, TotalDataSize);
        ....
    }
);

RenderFence.Wait();
FRenderCommandFence

Un ejemplo sencillo

Aquí un ejemplo sencillo haciendo uso de todo lo visto hasta ahora. Se trata de unas esferas que son dibujadas en una textura (RenderTexture) que se mueven hacía arriba, estas esferas son especificadas a través de un Buffer.

Se trata de un ejemplo muy simple pero completo.

Aquí el shader:

#pragma once

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

class RDGPLUGIN_API FCirclesCS : public FGlobalShader
{
public:

	struct FCircle
	{
		FVector2D Center;
		float Radius;
		FVector Color;
	};
	
	DECLARE_GLOBAL_SHADER(FCirclesCS)

	SHADER_USE_PARAMETER_STRUCT(FCirclesCS, FGlobalShader)

	BEGIN_SHADER_PARAMETER_STRUCT(FParameters,)
		SHADER_PARAMETER_UAV(RWTexture<float>, OutTexture)
		SHADER_PARAMETER_RDG_BUFFER_SRV(FCircle, Circles)
		SHADER_PARAMETER(int, NumCircles)
		SHADER_PARAMETER(float, TexResolution)
	END_SHADER_PARAMETER_STRUCT()
	
	static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
	{
		return RHISupportsComputeShaders(Parameters.Platform);
	}

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

	}

	static constexpr int32 ThreadGroupSizeX = 16;
	static constexpr int32 ThreadGroupSizeY = 16;
};
CirclesCS.h

El shader:

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

struct Circle
{
	float2 Center;
	float Radius;
	float3 Color;
};

RWTexture2D<float4> OutTexture;
StructuredBuffer<Circle> Circles;
int NumCircles;
float TexResolution;

[numthreads(16, 16, 1)]
void mainCS(uint3 GroupId : SV_GroupID, uint3 DispatchThreadId : SV_DispatchThreadID, uint3 GroupThreadId : SV_GroupThreadID, uint GroupIndex : SV_GroupIndex)
{
	float3 Color = float3(0.0, 0.0, 0.0);
	for (int i = 0; i < NumCircles; ++i)
	{
		uint2 Center = uint2(uint(TexResolution * Circles[i].Center.x), uint(TexResolution * Circles[i].Center.y));
		uint Radius = TexResolution * Circles[i].Radius;
		if (distance(DispatchThreadId.xy, Center) < Radius)
		{
			Color += 0.85 * Circles[i].Color;
		}
	}
	OutTexture[DispatchThreadId.xy] = float4(Color, 1.0);
}
Circles.usf

En cuanto a la creación del render target y del UAV es el mismo procedimiento que con el ejemplo de Mandelbrot.

La parte dónde lanzar el compute shader, en cada tick:

auto FeatureLevel = GetWorld()->Scene->GetFeatureLevel();
ENQUEUE_RENDER_COMMAND(Circles_Tick)(
[FeatureLevel, this](FRHICommandListImmediate& RHICmdList)
{
    FRDGBuilder GraphBuilder(RHICmdList);

    FRDGBufferDesc BufferDesc = FRDGBufferDesc::CreateBufferDesc(sizeof(FCirclesCS::FCircle), Circles.Num()); 
    auto Buffer = CreateStructuredBuffer(GraphBuilder,
        TEXT("CirclesBuffer"),
        sizeof(FCirclesCS::FCircle), Circles.Num(),
        Circles.GetData(), sizeof(FCirclesCS::FCircle) * Circles.Num());
    auto BufferUAV = GraphBuilder.CreateSRV(FRDGBufferSRVDesc(Buffer));

    auto ShaderGlobalMap = GetGlobalShaderMap(FeatureLevel);
    TShaderMapRef<FCirclesCS> ComputeShader(ShaderGlobalMap);

    auto Params = GraphBuilder.AllocParameters<FCirclesCS::FParameters>();
    Params->NumCircles = Circles.Num();
    Params->Circles = BufferUAV;
    Params->TexResolution = TextureSize;
    Params->OutTexture = TextureRenderTargetUAV;

    FComputeShaderUtils::AddPass(GraphBuilder,
        RDG_EVENT_NAME("CircleTick"),
        ERDGPassFlags::Compute,
        ComputeShader, Params,
        FComputeShaderUtils::GetGroupCount(FIntPoint(TextureSize, TextureSize), FCirclesCS::ThreadGroupSizeX));

    GraphBuilder.Execute();
}
);
Enqueue render command
Jorge Moreno Aguilera

Jorge Moreno Aguilera