- Mesh Processor
- Register and instancing
- Process
- Horizontal Thinking
- Vertex and Index Buffers
- Vertex and Pixel Shaders
- Example: Depth Pass Shaders
- Summary
In this part, we will talk about the marked steps in this image:
And the processing part is:
Let's start by reviewing FMeshBatch
. A mesh batch represents an individual section of a static mesh that contains only one material.
On the other hand, FMeshDrawCommand
is from the render pass view. It can be seen as:
- We have these render passes.
- We need to draw these
FMeshBatch
in these render passes. - So, for every pass, check each
FMeshBatch
, figure out what needs to be drawn, and collect the results into a list ofFMeshDrawCommand
s.
This is why you may see FMeshDrawCommand
s from different FMeshBatch
s, but from the same pass, logically put together.
Mesh Processor
The core part we will look at is the mesh processor. You can check the official document here.
But you can treat it as a machine, eat all FMeshBatch
s, and generate FMeshDrawCommand
s.
Register and instancing
The first thing is, where do they come?
To register the creation of a new mesh processor class, you can use the REGISTER_MESHPASSPROCESSOR_AND_PSOCOLLECTOR
macro. This macro functions like a macro-based reflection.
An example:
REGISTER_MESHPASSPROCESSOR_AND_PSOCOLLECTOR(DepthPass, CreateDepthPassProcessor, EShadingPath::Deferred, EMeshPass::DepthPass, EMeshPassFlags::CachedMeshCommands | EMeshPassFlags::MainView);
Of course, you need a function to do the instancing:
FMeshPassProcessor* CreateDepthPassProcessor(ERHIFeatureLevel::Type FeatureLevel, const FScene* Scene, const FSceneView* InViewIfDynamicMeshCommand, FMeshPassDrawListContext* InDrawListContext)
{
//...
return new FDepthPassMeshProcessor(EMeshPass::DepthPass, Scene, FeatureLevel, InViewIfDynamicMeshCommand, DepthPassState, true, EarlyZPassMode, bEarlyZPassMovable, false, InDrawListContext);
}
This macro adds your instancing function to the FPassProcessorManager
, which holds a look-up table that can return the corresponding FMeshProcessor
based on your render pass.
Process
The mesh processor first checks if it really needs to draw this mesh batch.
For example if you set the bCastShadow
to false, although the mesh batch is generated, the shadow pass has no need to render this mesh batch, so it can skip this one.
Then, it needs to build the FMeshDrawCommand
. To make things easier, let’s focus on FDepthPassMeshProcessor
as an example.
The function call chain is like this:
- The public interface is
AddMeshBatch
. BuildMeshDrawCommands
should be treated as a helper function provided byFMeshPassProcessor
. It can be treated as the end of our function call chain.
So the process is like:
And from BuildMeshDrawCommands
to the final FMeshDrawCommand
is :
Horizontal Thinking
Based on my personal experience, it is very difficult to understand this part by directly following the hierarchy of function calls. Therefore, I suggest dividing it horizontally into different sections and understanding it from the rightmost side (i.e., the MeshCommand side).
The main information required to render a Mesh Command includes:
- Mesh information, including Vertex and Index Buffer.
- Shader information, including Vertex and Pixel Shader.
- Render pipeline state information, including whether to enable depth culling, etc.
- Rendering-related Parameter information, such as variable values in the Constant Buffer.
Vertex and Index Buffers
We need Vertex Buffer and Index Buffer! Where should we get them from?
Of course, we can get them from the Mesh Batch itself. As mentioned in this diagram, the Mesh Batch holds references to the Vertex Factory and Batch Element, which is where we can obtain the Vertex and Index information.
Vertex and Pixel Shaders
And the Shader part requires a more careful analysis:
- Mesh Batch is not specific to a particular Pass, it only holds a reference to
MaterialRenderProxy
, so it can obtain the Shader compiled for a specific Pass from the Material. - Some Passes do not require the Shader provided by the Material (such as the Depth Pass, which only needs its own Shader), while some Passes completely rely on the precompiled Shader from the Material (such as the GBuffer Pass, note that the output color may come entirely from the artist's Material Graph).
This makes the TryGetShader
function need to handle this part of the logic. And ultimately select the correct PassShaders.
Example: Depth Pass Shaders
There are many questions about how to draw a mesh in Unreal Engine with custom vertex/pixel shaders. However, this question lacks details because different people have different goals:
- Some want to render all meshes in a custom pass, which requires pass-based vertex/pixel shaders. This is mostly for those who want to render an outline for meshes but are not satisfied with the default custom depth/stencil system.
- Others want to render a specific mesh only, but with their own shaders, ignoring any features provided by Unreal.
These are different problems, but in this document, we will only discuss the first one.
Let's see what FDepthPassMeshProcessor
does to inject its own shader, but supports all kinds of material and meshes.
- It first creates two shader classes:
TDepthOnlyVS
andFDepthOnlyPS
. You can check the contents for more details. - Then it binds the C++ definition with the shader files using the
IMPLEMENT_MATERIAL_SHADER_TYPE
macro. - The vertex factory system helps us to abstract the vertex data fetching into just some simple function calls.
- The material system helps us to evaluate the world position offset node graph with just only one function call
GetMaterialWorldPositionOffset
- Then, in the
GetDepthPassShaders
function, it queries the material from the mesh batch and asks, "Do you have a shader that is compiled withTDepthOnlyVS
andFDepthOnlyPS
?" It then gets the actual shaders. - Finally, it passes the shaders to the
BuildMeshDrawCommands
function.
It is really interest to see the vertex shader of the depth pass is actually really simple. If I clean up a little, it looks like this:
#include "Common.ush"
#include "/Engine/Generated/Material.ush"
#include "/Engine/Generated/VertexFactory.ush"
struct FDepthOnlyVSToPS
{
float4 Position : SV_POSITION;
};
#define FDepthOnlyVSOutput FDepthOnlyVSToPS
#define VertexFactoryGetInterpolants VertexFactoryGetInterpolantsVSToPS
void Main( FVertexFactoryInput Input, out FDepthOnlyVSOutput Output)
{
ResolvedView = ResolveViewFromVF(Input);
FVertexFactoryIntermediates VFIntermediates = GetVertexFactoryIntermediates(Input);
float4 WorldPos = VertexFactoryGetWorldPosition(Input, VFIntermediates);
float4 WorldPositionExcludingWPO = WorldPos;
float3x3 TangentToLocal = VertexFactoryGetTangentToLocal(Input, VFIntermediates);
FMaterialVertexParameters VertexParameters = GetMaterialVertexParameters(Input, VFIntermediates, WorldPos.xyz, TangentToLocal);
WorldPos.xyz += GetMaterialWorldPositionOffset(VertexParameters);
float4 RasterizedWorldPosition = VertexFactoryGetRasterizedWorldPosition(Input, VFIntermediates, WorldPos);
Output.Position = INVARIANT(mul(RasterizedWorldPosition, ResolvedView.TranslatedWorldToClip));
}
This is because:
Don't reinvent the wheel
Summary
I haven't analyzed all the details of this step, but I hope this will give you enough impression:
- With the help of Mesh Processor, Mesh Batch is transformed into multiple MeshDrawCommands corresponding to each Pass. Note how Mesh Batch itself and Pass-specific parts play a role in this process.
This will become the cornerstone of our discussion on Cache and Culling mechanisms.