From Mesh Batch to Mesh Draw Commands

icon
FMeshPassProcessor::BuildMeshDrawCommands

In this part, we will talk about the marked steps in this image:

image

And the processing part is:

image

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 of FMeshDrawCommands.

This is why you may see FMeshDrawCommands from different FMeshBatchs, 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.

image

But you can treat it as a machine, eat all FMeshBatch s, and generate FMeshDrawCommands.

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.

image

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 by FMeshPassProcessor. 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:

image
  • 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.

  1. It first creates two shader classes: TDepthOnlyVS and FDepthOnlyPS. You can check the contents for more details.
  2. Then it binds the C++ definition with the shader files using the IMPLEMENT_MATERIAL_SHADER_TYPE macro.
  3. If you want to see how

    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:

    • 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
    Don't reinvent the wheel
  4. Then, in the GetDepthPassShaders function, it queries the material from the mesh batch and asks, "Do you have a shader that is compiled with TDepthOnlyVS and FDepthOnlyPS?" It then gets the actual shaders.
  5. Finally, it passes the shaders to the BuildMeshDrawCommands function.

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.