Este artigo é baseado no código-fonte do Unreal Engine 4.19 e descreve o pipeline anterior à versão 4.22, onde DrawingPolicy e DrawList foram substituídos pelo MeshDrawPipeline. Para entender a versão mais recente, consulte meu artigo sobre MeshDrawPipeline.
O pipeline antes do UE 4.22 utilizava DrawingPolicy e DrawList. A partir da versão 4.22, a arquitetura foi refatorada para MeshDraw. Este texto aborda a primeira abordagem.
Estrutura do Pipeline de Renderização no UE 4 (pré-4.22)
No Unreal Engine 4, as classes responsáveis pela renderização na thread de renderização são DrawingPolicy e DrawingPolicyFactory. Para criar um renderizador personalizado e otimizar a renderização em baixo nível, é essencial compreender esses dois componentes. Antes de explorá-los, é importante entender o framwork geral de renderiazção do Unreal. Aqui estão os pontos principais, divididos em três partes:
- Geração de Shaders
- Gerenciamento de Dados de Desenho via DrawingPolicy
- Processo de Renderização
Conceitos fundamentais:
- Os shaders são compilados antes do uso. Eles podem ser considerados recursos pré-compilados. Diferentes plataformas e configurações exigem shaders distintos, e um mesmo efeito pode ter várias versões de shader, selecionadas conforme condições específicas.
- As classes C++ de shader (no lado da CPU) e os shaders em si (executados na GPU) são entidades separadas. Confundi-los é um erro comum.
- Definição, compilação e uso de shaders são processos independentes e não lineares.
Geração de Shaders
No Unreal 4, as classes C++ de shader se dividem em três categorias principais: Material Shader, Mesh Material Shader e Global Shader. MaterialShader e MeshMaterialShader permitem múltiplas instâncias, enquanto GlobalShader possui apenas uma instância global.
O editor de materiais preenche um modelo de função chamado Material Template. Ao compilar, os nós do editor geram uma biblioteca de funções no arquivo MaterialTemplate.usf. A compilação também produz múltiplas variações de shader com base em macros, estados de renderização e configurações de qualidade (baixa, média, alta), armazenadas no ShaderMap do FMaterial. Cada macro pode dobrar a quantidade de shaders gerados.
Exemplo Prático: Global Shader
Para ilustrar, vamos criar um Global Shader manualmente. Crie três arquivos no projeto: SkyRender.usf, SkyRender.h e SkyRender.cpp.
SkyRender.usf:
#include "Common.ush"
uniform float4x4 UnityVP;
uniform float3 TestColor;
void MainVS(
in float3 InPosition : ATTRIBUTE0,
out float4 Position : SV_POSITION
)
{
Position = mul(float4(InPosition, 1.0f), UnityVP);
}
void MainPS(
out float4 OutColor : SV_Target0
)
{
#if SAMPLELEVEL == 0
OutColor = float4(1, 0, 0, 1);
#elif SAMPLELEVEL == 1
OutColor = float4(0, 1, 0, 1);
#else
OutColor = float4(0, 0, 1, 1);
#endif
}
SkyRender.h:
#pragma once
#include "CoreMinimal.h"
#include "RenderResource.h"
struct FDebugPane
{
FDebugPane();
~FDebugPane();
void FillRawData();
void Init();
TArray<FVector> VerBuffer;
TArray<uint16> InBuffer;
uint32 Stride;
bool Initialized;
uint32 VertexCount;
uint32 PrimitiveCount;
FVertexBufferRHIRef VertexBufferRHI;
FIndexBufferRHIRef IndexBufferRHI;
};
void FDebugPane::FillRawData()
{
VerBuffer = { FVector(0,0,0), FVector(100,0,0), FVector(100,100,0), FVector(0,100,0) };
InBuffer = { 0,1,2, 0,2,3 };
}
SkyRender.cpp (trechos principais):
#include "SkyRender.h"
#include "SceneRendering.h"
#include "RHICommandList.h"
#include "Shader.h"
template<uint32 SampleLevel>
class TSkyRenderVS : public FGlobalShader
{
DECLARE_SHADER_TYPE(TSkyRenderVS, Global);
FShaderParameter Unity_VP;
public:
TSkyRenderVS() {}
TSkyRenderVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
: FGlobalShader(Initializer) { Unity_VP.Bind(Initializer.ParameterMap, TEXT("UnityVP")); }
static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
OutEnvironment.SetDefine(TEXT("SAMPLELEVEL"), SampleLevel);
}
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters) { return true; }
virtual bool Serialize(FArchive& Ar) override
{
bool bOutdated = FGlobalShader::Serialize(Ar);
Ar << Unity_VP;
return bOutdated;
}
void SetMatrices(FRHICommandList& RHICmdList, const FScene* Scene, const FViewInfo* View)
{
SetShaderValue(RHICmdList, GetVertexShader(), Unity_VP, View->ViewMatrices.GetViewProjectionMatrix());
}
};
IMPLEMENT_SHADER_TYPE(template<>, TSkyRenderVS<0>, TEXT("/Engine/Private/SkyRender.usf"), TEXT("MainVS"), SF_Vertex);
IMPLEMENT_SHADER_TYPE(template<>, TSkyRenderVS<1>, TEXT("/Engine/Private/SkyRender.usf"), TEXT("MainVS"), SF_Vertex);
IMPLEMENT_SHADER_TYPE(template<>, TSkyRenderVS<2>, TEXT("/Engine/Private/SkyRender.usf"), TEXT("MainVS"), SF_Vertex);
// Classe PS similar (omitida por brevidade)
void FSceneRenderer::RenderMyMesh(FRHICommandListImmediate& RHICmdList, const TArrayView<const FViewInfo*> PassViews, int32 Quality)
{
check(IsInRenderingThread());
auto* ShaderMap = GetGlobalShaderMap(FeatureLevel);
FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);
SceneContext.BeginRenderingSceneColor(RHICmdList, ESimpleRenderTargetMode::EExistingColorAndDepth, FExclusiveDepthStencil::DepthRead_StencilWrite, true);
FGraphicsPipelineStateInitializer PSOInit;
RHICmdList.ApplyCachedRenderTargets(PSOInit);
PSOInit.RasterizerState = TStaticRasterizerState<FM_Solid, CM_None, false, false>::GetRHI();
PSOInit.BlendState = TStaticBlendState<>::GetRHI();
PSOInit.DepthStencilState = TStaticDepthStencilState<false, CF_GreaterEqual>::GetRHI();
PSOInit.PrimitiveType = PT_TriangleList;
PSOInit.BoundShaderState.VertexDeclarationRHI = GetVertexDeclarationFVector3();
const uint32 Level = 2;
TShaderMapRef<TSkyRenderVS<Level>> Vs(ShaderMap);
TShaderMapRef<TSkyRenderPS<Level>> Ps(ShaderMap);
PSOInit.BoundShaderState.VertexShaderRHI = Vs.GetVertexShader();
PSOInit.BoundShaderState.PixelShaderRHI = Ps.GetPixelShader();
SetGraphicsPipelineState(RHICmdList, PSOInit);
for (const FViewInfo* View : PassViews)
{
Ps->SetUniforms(RHICmdList, Scene, View);
Vs->SetMatrices(RHICmdList, Scene, View);
if (!DebugMesh.Initialized) DebugMesh.Init();
RHICmdList.SetStreamSource(0, DebugMesh.VertexBufferRHI, 0);
RHICmdList.DrawIndexedPrimitive(DebugMesh.IndexBufferRHI, PT_TriangleList, 0, 0, DebugMesh.VertexCount, 0, DebugMesh.PrimitiveCount, 1);
}
}
O exemplo registra três variações de shader para cada macro, demonstrando a compilação antecipada. Para usar, adicione a função RenderMyMesh à classe FSceneRenderer e chame-a no Render do DeferredShadingRenderer.
GlobalShaders são obtidos diretamente do GlobalShaderMap. MaterialShaders e MeshMaterialShaders são mais complexos, pois são específicos por malha, exigindo gerenciamento via DrawingPolicy e DrawingPolicyFactory.
DrawingPolicy e DrawingPolicyFactory
DrawingPolicy gerencia as condições de renderização (estados, macros) e é responsável pelo desenho. DrawingPolicyFactory combina dados para criar instâncias de DrawingPolicy.
Shaders são compilados e armazenados em ShaderMap. Para usá-los, é necessário recuperá-los do mapa. A complexidade surge de variações como qualidade gráfica, formato de vértice (estático vs. esqueleto), macros e modos de blend. DrawingPolicy resolve isso encapsulando a lógica de seleção e configuração.
Material Shader na prática
Macros como IMPLEMENT_MATERIAL_SHADER_TYPE vinculam a classe C++ do shader ao arquivo de shader, ao tipo e ao DrawingPolicy correspondente. Isso permite que o DrawingPolicy encontre o shader correto no ShaderMap.
DrawingPolicy e DrawingPolicyFactory abstraem a criação de instâncias de shader C++, permitindo combinações flexíveis sem escrever código repetitivo.
A compilação ocorre em FMaterialShaderMap::Compile, que cria múltiplos ShaderMap por formato de vértice. Em BeginCompile, as instâncias de shader C++ são compiladas com base no MaterialEnvironment (que contém macros e configurações).
Gerenciamento de Dados de Desenho
O uso de shaders divide-se em dois caminhos: malhas estáticas e dinâmicas.
- Malhas Estáticas: Ao adicionar uma malha à cena,
AddPrimitivechamaAddStaticMeshes, que insereFStaticMeshemDrawListviaDrawingPolicyFactory. Durante a renderização, oDrawListjá contémDrawingPolicyinicializados. - Malhas Dinâmicas: Antes de desenhar, um novo
DrawingPolicyé criado e seus shaders são inicializados a partir doMaterial.
No construtor do DrawingPolicy, os shaders são obtidos do ShaderMap usando parâmetros pré-definidos. O método Draw do policy então chama o RHICommandList para executar a renderização.
StaticDrawList armazena DrawingPolicy para objetos estáticos (aqueles cujo estado de renderização não muda). Os policies são ordenados antes do desenho e recuperados por ID, compartilhando dados de geometria (VB/IB) de um mesmo lote.
Processo Completo de Renderização
O ciclo divide-se em duas fases:
- Editor: Preparação de recursos (importação de malhas, texturas, animações) e construção de
DrawList, além de baking de LightMap e SH. - Runtime: A classe
FSceneRenderergerencia a renderização. O Unreal 4 oferece dois renderizadores: Mobile e Deferred. A cada quadro, a funçãoRenderé chamada, iniciando pela preparação doSceneContext(GBuffer) e pela extração de dados dinâmicos viaInitViews.
Em seguida, vários passes são executados (BasePass, Depth, etc.). No BasePass, o código separa malhas estáticas (usando DrawList pré-construído) de dinâmicas (criendo DrawingPolicy sob demanda). O DrawList é ordenado e os DrawingPolicy corretos são recuperados por ID.
Este fluxo geral, embora com muitos detalhes, representa a base do pipeline de renderização do Unreal 4 antes da versão 4.22.