Exam : Try to draw a cube by yourself

In this chapter, we'll try something different from the last one. Unfortunately, you'll need a source build engine for this. If you don't know how to get one, please check these instructions.

Our goal is to draw our cube (and all other mesh batches) directly onto the final render target in a red color!

Compile it and have a good sleep!

Now, think about what we need to do:

  • Although our target is draw a red cube to the final buffer, but what we actually want to do is, for all the related mesh batches, we draw them by ourselves.
  • So from the rendererā€™s view, what we need is a new pass.
  • And to setup and render the new pass, we need a mesh processor.

Add a new mesh pass enum

Add a new item in EMeshPass , inside MeshPassProcessor.h

Code

/** Mesh pass types supported. */
namespace EMeshPass
{
	enum Type : uint8
	{
		DepthPass,
		//...
		MeshDecal,
#if WITH_EDITOR
		HitProxy,
		//...
#endif
		RedCubePass, //<-- Here
		Num,
		NumBits = 6,
	};
}

If you directly compile, you will get an error:

Engine\Source\Runtime\Renderer\Public\MeshPassProcessor.h(120): error C2338: Need to update switch(MeshPass) after changing EMeshPass

That is because you need also update GetMeshPassName function, just like the error message says.

And increase the static_assert number.

inline const TCHAR* GetMeshPassName(EMeshPass::Type MeshPass)
{
	switch (MeshPass)
	{
	case EMeshPass::DepthPass: return TEXT("DepthPass");
	//...
#if WITH_EDITOR
	case EMeshPass::HitProxy: return TEXT("HitProxy");
	//...
#endif
	case EMeshPass::RedCubePass: return TEXT("RedCubePass"); //<-- Here
	}

// Change the number from 29 to 30. If you use new version, just calculate by yourself.
#if WITH_EDITOR
	static_assert(EMeshPass::Num == 30 + 4, "Need to update switch(MeshPass) after changing EMeshPass"); // GUID to prevent incorrect auto-resolves, please change when changing the expression: {A6E82589-44B3-4DAD-AC57-8AF6BD50DF43}
#else
	static_assert(EMeshPass::Num == 30, "Need to update switch(MeshPass) after changing EMeshPass"); // GUID to prevent incorrect auto-resolves, please change when changing the expression: {A6E82589-44B3-4DAD-AC57-8AF6BD50DF43}
#endif

	checkf(0, TEXT("Missing case for EMeshPass %u"), (uint32)MeshPass);
	return nullptr;
}

We will get a new error:

image

Letā€™s follow what it say, increase the MaxPSOCollectorCount in FPSOColectorCreateManager, inside PSOPrecache.h

class ENGINE_API FPSOCollectorCreateManager
{
public:

	constexpr static uint32 MaxPSOCollectorCount = 34; //from 33 to 34

Add a new mesh processor

Like the examples before, we still just want to create a minimal mesh processor.

  • We will reuse the TDepthOnlyVS as our vertex shader
  • But we will create a really simple pixel shader, just output one color for all pixels.

Add files

Letā€™s put two files in Engine\Source\Runtime\Renderer\Private\ : RedCubeRendering.h and RedCubeRendering.cpp

Add a shader file

Add a file called RedCubePixelShader.usf in \Engine\Shaders\Private

The content is just a pixel shader, which returns a solid color

#include "Common.ush"
#include "/Engine/Generated/Material.ush"
#include "/Engine/Generated/VertexFactory.ush"

void Main(
#if !MATERIALBLENDING_SOLID || OUTPUT_PIXEL_DEPTH_OFFSET
	in INPUT_POSITION_QUALIFIERS float4 SvPosition : SV_Position,
#endif

#if !MATERIALBLENDING_SOLID || OUTPUT_PIXEL_DEPTH_OFFSET
	FVertexFactoryInterpolantsVSToPS FactoryInterpolants
	, float4 PixelPosition : TEXCOORD6 //-- Not used currently
#if USE_WORLD_POSITION_EXCLUDING_SHADER_OFFSETS
	, float3 PixelPositionExcludingWPO : TEXCOORD7
#endif
	OPTIONAL_IsFrontFace
	OPTIONAL_OutDepthConservative,
#endif
	out float4 OutColor : SV_Target0
#if MATERIALBLENDING_MASKED_USING_COVERAGE
	, out uint OutCoverage : SV_Coverage
#endif
	)
{
	OutColor = float4(1.0f, 0.0f, 0.0f, 1.0f);
}

Add a mesh processor class

Create the class defination in the header:

class FRedCubeMeshProcessor : public FSceneRenderingAllocatorObject<FRedCubeMeshProcessor>, public FMeshPassProcessor

Add a constructor:

FRedCubeMeshProcessor(
		EMeshPass::Type InMeshPassType,
		const FScene* Scene,
		ERHIFeatureLevel::Type FeatureLevel,
		const FSceneView* InViewIfDynamicMeshCommand,
		const FMeshPassProcessorRenderState& InPassDrawRenderState,
		FMeshPassDrawListContext* InDrawListContext);

We override this function in the header:

virtual void AddMeshBatch(const FMeshBatch& RESTRICT MeshBatch, uint64 BatchElementMask, const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy, int32 StaticMeshId = -1) override final;

Next, we need to define the shader on the C++ side, which we'll call FRedCubePS. As always, I'll try to keep things as simple as possible:

class FRedCubePS : public FMeshMaterialShader
{
	DECLARE_SHADER_TYPE(FRedCubePS, MeshMaterial);
public:
	static bool ShouldCompilePermutation(const FMeshMaterialShaderPermutationParameters& Parameters);

	FRedCubePS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) :
		FMeshMaterialShader(Initializer){}

	static void ModifyCompilationEnvironment(const FMaterialShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
	{
		FMeshMaterialShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
	}

	FRedCubePS() {}

	void GetShaderBindings(
		const FScene* Scene,
		ERHIFeatureLevel::Type FeatureLevel,
		const FPrimitiveSceneProxy* PrimitiveSceneProxy,
		const FMaterialRenderProxy& MaterialRenderProxy,
		const FMaterial& Material,
		const FMeshPassProcessorRenderState& DrawRenderState,
		const FMeshMaterialShaderElementData& ShaderElementData,
		FMeshDrawSingleShaderBindings& ShaderBindings) const
	{
		FMeshMaterialShader::GetShaderBindings(Scene, FeatureLevel, PrimitiveSceneProxy, MaterialRenderProxy, Material, DrawRenderState, ShaderElementData, ShaderBindings);
	}
};

Implementation

Registration

Register shader

IMPLEMENT_MATERIAL_SHADER_TYPE(, FRedCubePS, TEXT("/Engine/Private/RedCubePixelShader.usf"), TEXT("Main"), SF_Pixel);

Register shader pipeline type

Here we reused the TDepthOnlyVS from the depth prepass.

IMPLEMENT_SHADERPIPELINE_TYPE_VSPS(RedCubePipeline, TDepthOnlyVS<false>, FRedCubePS, true);

Register the mesh processor

We talked about the mesh processor registration in .

REGISTER_MESHPASSPROCESSOR_AND_PSOCOLLECTOR(RedCubePass, CreateRedCubePassProcessor, EShadingPath::Deferred, EMeshPass::RedCubePass, EMeshPassFlags::CachedMeshCommands | EMeshPassFlags::MainView);

Register the pass parameters

Here we define the pass parameters we need to pass to our shader. This is actually some general shaders if you want to make your mesh shader works:

BEGIN_SHADER_PARAMETER_STRUCT(FRedCubePassParameters, )
	SHADER_PARAMETER_STRUCT_INCLUDE(FViewShaderParameters, View)
	SHADER_PARAMETER_STRUCT_INCLUDE(FInstanceCullingDrawParams, InstanceCullingDrawParams)
	RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()

Implement MeshProcessor

Init mesh processor

This code is nearly identical to the constructor for FMeshPassProcessor, with the exception that we save the PassDrawRenderState.

FRedCubeMeshProcessor::FRedCubeMeshProcessor(EMeshPass::Type InMeshPassType, const FScene* Scene, ERHIFeatureLevel::Type FeatureLevel, const FSceneView* InViewIfDynamicMeshCommand, const FMeshPassProcessorRenderState& InPassDrawRenderState, FMeshPassDrawListContext* InDrawListContext)
	: FMeshPassProcessor(InMeshPassType, Scene, FeatureLevel, InViewIfDynamicMeshCommand, InDrawListContext)
	, PassDrawRenderState(InPassDrawRenderState)
{}

ā­ AddMeshBatch function

This is one of the core functions we need to implement. Our goal is to call the BuildMeshDrawCommands function. As shown in this image, since we already have these as input parameters, we can directly pass them to BuildMeshDrawCommands:

Mesh Batch
BatchElementMask
PrimitiveSceneProxy

But now it's our job to:

  • MaterialRenderProxy: We can get this from MeshBatch.MaterialRenderProxy.
  • Material: We can get this from MaterialRenderProxy. We just obtained this from the last step.
  • DrawRenderState: We saved this here. Just use that.
  • ā­ PassShaders: This is the area where we need to focus:
  • For simplification, use ERasterizerFillMode::FM_Solid for MeshFillMode and ERasterizerCullMode::CM_None for MeshCullMode.
  • We reuse CalculateDepthPassMeshStaticSortKey for SortKey. If you donā€™t know what this step is doing, check this:
  • Use EMeshPassFeatures::Default for MeshPassFeatures.
  • To initialize ShaderElementData, use InitializeMeshMaterialData.

For reference, you can check the code of this function here:

ā€£
Code

Inject rendering

Now itā€™s the time for changing the rendering code of unreal engine.

You need to modify the following pieces:

Add render pass functions

This is the function call stack we want:

So letā€™s add RenderRedCubePass function to the FDeferredShadingSceneRenderer

void RenderRedCubePass(FRDGBuilder& GraphBuilder, FRDGTextureRef RenderTargetTexture, FInstanceCullingManager& InstanceCullingManager);
	;

Of course we want FDeferredShadingSceneRenderer to call this function, so letā€™s change void FDeferredShadingSceneRenderer::Render(FRDGBuilder& GraphBuilder). Since we want to modify the GBuffer texture, letā€™s put the pass code here:

RenderBasePass(GraphBuilder, SceneTextures, DBufferTextures, BasePassDepthStencilAccess, ForwardScreenSpaceShadowMaskTexture, InstanceCullingManager, bNaniteEnabled, NaniteRasterResults);
		GraphBuilder.AddDispatchHint();

//Red Cube
RenderRedCubePass(GraphBuilder, SceneTextures.GBufferC, InstanceCullingManager);

Implment the pass render function

This part is related to what we talked in From Mesh Draw Commands to RHI Commands

We need to write some RDG(render dependency graph) code here. We will discuss about RDG later. But you can ignore the strange lambda style temporally and just treat the codes inside the lambda is executed directly ( this is not true!)

So what we did is:

  • Check if we need to render the view.
  • Set the render state
  • Get and pass the PassParameters
  • Call BuildRenderingCommands to build the final rendering commands. This function is related to the Culling part.
  • DispatchDraw : our draw request sends to the RHICommandList.
ā€£
Code

Inject relevance

If you directly open the editor, without this step, you will see our mesh command will directly be skipped. It is because we donā€™t add our pass relevance to the SceneVisibility.cpp. This part is also related to .

Just add this line below the DepthPass

image

Please check this flow chart. The code has been added at the circle numbered 2.

Open the editor

If everything is successful, you will see a huge number of material shader compiling when you open the editor for the first time.

Then open the cube map you already made and saved before, you will see your red cube now:

image

Render doc capture result

image

Our pixel shader is used:

image
image

Full Code

Summary

This is a global look of what we did

image

Warning

  • This is not a production-ready example for "Adding a pass to Unreal Engine".
  • You may experience crashes in many cases, such as when creating a new material in the content browser.
  • As previously stated, this example is meant to be minimal, allowing you to go through all the things we discussed.