Skip to content
PRODUCT GITHUB  ↗

Mesh Optimization

URealtimeMeshDataOptimizer (in the RealtimeMeshExt module — see the section overview for the Build.cs dependency) runs the industry-standard meshoptimizer passes over a FRealtimeMeshStreamSet in place. You typically run these once, on freshly generated or freshly loaded geometry, before handing the stream set to CreateBufferSet — the payoff is faster GPU rendering (fewer cache misses, less overdraw) and, for the indexing pass, a smaller vertex buffer.

All four passes operate directly on the stream set and mutate it in place. There are C++ overloads (taking RealtimeMesh::FRealtimeMeshStreamSet&) and Blueprint overloads (taking URealtimeMeshStreamSet*).


Generates — or regenerates — the triangle index buffer to remove redundant (duplicate) vertices. If the stream set already has a Triangles stream it’s reindexed; if not, one is built by treating each successive group of three vertices as a triangle. Run this first: it’s what turns an unindexed/duplicated vertex soup into a compact indexed mesh, and the later passes assume an index buffer exists.

URealtimeMeshDataOptimizer::OptimizeMeshIndexing(StreamSet);

Reorders the index buffer to maximize reuse of the GPU’s post-transform vertex cache — the single biggest win for vertex-shading cost. The quality parameter trades result quality against processing time:

URealtimeMeshDataOptimizer::OptimizeVertexCache(StreamSet,
ERealtimeMeshOptimizationQuality::RenderingEfficiency); // vs. ::GenerationSpeed

ERealtimeMeshOptimizationQuality has two values — RenderingEfficiency (best result, the Blueprint default) and GenerationSpeed (faster to compute, good when you’re optimizing at runtime and care about the hitch).

Reorders triangles to reduce pixel overdraw from the camera’s likely viewing angles. The Threshold caps how much vertex-cache efficiency it’s allowed to sacrifice to do so — 1.01 (the default) means “give up at most 1% of ACMR”, higher values allow more aggressive overdraw reduction:

URealtimeMeshDataOptimizer::OptimizeOverdraw(StreamSet, /*Threshold=*/1.01f);

Reorders the vertex buffer so that vertices are fetched roughly in the order the (already cache-optimized) index buffer references them, improving memory locality. Run this last, after the index order is settled:

URealtimeMeshDataOptimizer::OptimizeVertexFetch(StreamSet);

The passes are order-dependent — each assumes the previous one has run. Follow the order meshoptimizer recommends:

  1. OptimizeMeshIndexing — build/compact the index buffer.
  2. OptimizeVertexCache — reorder indices for the post-transform cache.
  3. OptimizeOverdraw — reorder triangles to cut overdraw (preserving most of the cache gain).
  4. OptimizeVertexFetch — reorder vertices to match the final index order.
#include "RealtimeMeshDataOptimizer.h"
#include "RealtimeMeshSimple.h"
using namespace RealtimeMesh;
void AMyActor::BuildOptimizedMesh(FRealtimeMeshStreamSet&& StreamSet)
{
// 1. Index the geometry (removes duplicate vertices, ensures a triangle stream exists).
URealtimeMeshDataOptimizer::OptimizeMeshIndexing(StreamSet);
// 2. Post-transform vertex cache — the biggest vertex-cost win.
URealtimeMeshDataOptimizer::OptimizeVertexCache(StreamSet,
ERealtimeMeshOptimizationQuality::RenderingEfficiency);
// 3. Overdraw, without giving up more than ~1% of the cache gain.
URealtimeMeshDataOptimizer::OptimizeOverdraw(StreamSet, 1.01f);
// 4. Vertex fetch locality — run last, after the index order is final.
URealtimeMeshDataOptimizer::OptimizeVertexFetch(StreamSet);
// Now hand the optimized stream set to the mesh.
URealtimeMeshSimple* Mesh = GetRealtimeMeshComponent()->InitializeRealtimeMesh<URealtimeMeshSimple>();
const FRealtimeMeshBufferSetKey GroupKey = FRealtimeMeshBufferSetKey::Create(0, FName("Optimized"));
Mesh->CreateBufferSet(GroupKey, MoveTemp(StreamSet));
}

Because the optimizer only touches CPU stream data, you can run all four passes on a worker thread and only call CreateBufferSet on the game thread.


  • Generated geometry where you emit vertices without deduplicating — OptimizeMeshIndexing alone can shrink the vertex buffer substantially.
  • Loaded geometry (e.g. from the OBJ loader) before it goes live.
  • Any static, high-poly mesh you render a lot — the cache/overdraw/fetch passes pay off every frame it’s on screen.

For meshes you rebuild every frame, weigh the one-time optimization cost against the per-frame rendering saving, and consider ERealtimeMeshOptimizationQuality::GenerationSpeed for the vertex cache pass.