April 25

Rendering notes


📝Overview

Sources:


1. FPrimitiveSceneProxy

UE provide UPrimitiveComponent - a base class for all actor components "that contains or generate some sort of geometry, generally to be rendered or used as collision data". That class is created and managed by the Game Thread.

To be rendered that component create the class FPrimitiveSceneProxy - an Unreal’s render-thread representation of a primitive. It acts as a bridge between the game-thread and the render-thread, storing minimal data needed to render the mesh efficiently.

// copied code from UPrimitiveComponent class UE5.2

/** 
* Creates a proxy to represent the primitive to the scene manager 
* in the rendering thread. 
* @return The proxy object. 
*/

virtual FPrimitiveSceneProxy* CreateSceneProxy()
{    
   return NULL;
}

For every new class derived from the UPrimitiveComponent UE implements a new primitive proxy class derived from FPrimitiveSceneProxy: FSpriteSceneProxy, FSphereSceneProxy, FTextRenderSceneProxy, FParticleSystemSceneProxy, FWidget3DSceneProxy etc.

The same way for creating this objects each new class derived from the UPrimitiveComponent overrides CreateSceneProxy() function.

FPrimitiveSceneProxy is responsible for submitting FMeshBatch's to the renderer through the callbacks to GetDynamicMeshElements() and DrawStaticElements().


2. FMeshBatch

FMeshBatch describes a single draw call to the GPU. It specifies:

  • Vertex buffers, index buffers
  • Material/shader references
  • Render states (depth, stencil, blending)
  • LOD and mesh pass information

When the rendering pipeline prepares to draw meshes, it assembles them into batches (FMeshBatch) grouped by compatible render states and materials.

One mesh can generate multiple batches:

  • Multiple materials → multiple batches.
  • Different rendering passes (shadow, depth pre-pass, base pass) → separate batches.

🔸Simple Example:

Mesh: SM_Rock
- Material: M_Stone → FMeshBatch #1 (Base Pass)
- Shadow Pass → FMeshBatch #2 (Shadow Pass)

FMeshBatch decouples the FPrimitiveSceneProxy implementation (user code) from mesh passes (private renderer module). It contains everything the pass needs to figure out final shader bindings and render state, so the proxy never knows what passes it will be rendered in.


3. Mesh Pass

A Mesh Pass is a step (or "stage") of rendering, each with different goals:

______________________________________________________________________
   Mesh Pass Name   |                   Goal                         
--------------------+-------------------------------------------------
Depth Pre-Pass      |Fill depth buffer early (efficient depth testing
Base Pass           |Main color/material rendering                   
Shadow Pass         |Shadow map generation                           
Translucency Pass   |Draw transparent objects                        
Post-Processing Pass|Effects (lighting, bloom, reflections)          
----------------------------------------------------------------------

Each mesh pass collects and processes specific FMeshBatches.


4. FMeshDrawCommand

The next step is to convert FMeshBatch into a mesh pass specific FMeshDrawCommand .

FMeshDrawCommand is an interface between FMeshBatch and the RHI. It's a fully stateless draw description that stores everything that the RHI needs to know about a mesh draw:

  • Which shaders to use.
  • Their resource bindings.
  • Draw call parameters.

This enables caching and merging the draw calls just above the RHI level. FMeshDrawCommand is created from a FMeshBatch by a mesh pass specific FMeshPassProcessor.

Finally, SubmitMeshDrawCommands() is used to convert FMeshDrawCommand into a series of RHI commands set on a RHICommandList.


5. RHI

RHI is Unreal’s abstraction layer over graphics APIs (DirectX, Vulkan, Metal, etc.):

  • It standardizes rendering commands across platforms.
  • Allows Unreal to support multiple graphics APIs without rewriting rendering logic.
  • Handles buffers, shaders, textures, and GPU communication.

[Game Thread (Component)]
       ↓
[Render Thread (FPrimitiveSceneProxy -> FMeshBatch -> MeshPasses)]
       ↓
[RHI Commands]
       ↓
[Vulkan / DirectX / Metal API calls]
       ↓
[GPU]


6. Under the UE hood

⚙️ What UE5 Does by Default

When you have multiple identical StaticMeshActors:

  • Rendering pipeline creates a separate FPrimitiveSceneProxy and separate FMeshBatch for each actor/component.
  • Each separate FMeshBatch typically becomes a separate GPU draw call.
  • More actors → More draw calls → Higher CPU overhead → Lower frame rate.

UE5 optimizes many things (occlusion culling, frustum culling, Nanite for high-density geometry), but does NOT automatically combine separate actors into batches. Each actor is considered a unique primitive.

🔥 How InstancedStaticMeshComponent (ISM) Optimizes Rendering

When you explicitly group actors into ISMs:

  • A single FPrimitiveSceneProxy is created for the entire instanced group.
  • A single (or fewer) FMeshBatch per mesh/material → dramatically reduced draw calls.
  • GPU efficiently draws multiple instances with only one GPU call, changing transforms internally per-instance.