- Add a new mesh pass enum
- Add a new mesh processor
- Add files
- Add a shader file
- Add a mesh processor class
- Implementation
- Registration
- Register shader
- Register shader pipeline type
- Register the mesh processor
- Register the pass parameters
- Implement MeshProcessor
- Init mesh processor
- ā AddMeshBatch function
- Inject rendering
- Add render pass functions
- Implment the pass render function
- Inject relevance
- Open the editor
- Render doc capture result
- Full Code
- Summary
- Warning
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:
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
:
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:
- As discussed in From Material to Shaders, we cannot directly use the shaders from the Add a shader file step. Instead, we need to obtain the compiled shaders from the material. These compiled shaders include the shader pieces we created in the Add a shader file step.
- Therefore, the process here should be:
- For simplification, use
ERasterizerFillMode::FM_Solid
for MeshFillMode andERasterizerCullMode::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:
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.
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
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:
Render doc capture result
Our pixel shader is used:
Full Code
Summary
This is a global look of what we did
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.