Skip to content
PRODUCT GITHUB  ↗

Loading Meshes at Runtime

RMC can load geometry from disk at runtime. The built-in loader handles Wavefront OBJ files via URealtimeMeshObjLoader (in the RealtimeMeshExt module — see the section overview for the Build.cs dependency). It parses the OBJ into a FRealtimeMeshStreamSet and returns the material definitions from the accompanying .mtl file so you can build the actual UE materials yourself.

For any other format, the general pattern at the bottom of this page applies: parse the file, fill a stream set, and create a section group.

The canonical demonstration is the example actor ARealtimeMeshExample_Simple_ObjLoad (Source/RealtimeMeshExamples/.../Simple/).


URealtimeMeshObjLoader parses the OBJ, de-duplicates vertices (by position/normal/texcoord tuple), and fills a stream set with:

  • Position and Triangles (always).
  • Tangents — a normal + tangent basis, seeded from the OBJ normals and then regenerated (see options below).
  • TexCoords — UV channel 0, if the file has any.
  • Color — vertex colors, if the file supplies them.
  • PolyGroups — one poly group per OBJ material index. After load the loader organizes the triangles by poly group, so each material becomes its own section when you create the section group. Faces with no material are assigned to group 0.

Only triangle faces are supported — the loader fails with a message if it encounters an n-gon, so triangulate your OBJ on export.

Materials are returned as data, not as UMaterialInterface objects. Each FRealtimeMeshOBJMaterial carries the parsed MTL fields (diffuse/specular/emission colors, roughness, metallic, IOR, and the texture references as FRealtimeMeshOBJTextureInfo with file paths and sampler options). It is up to you to turn those into engine materials and assign them to the mesh’s material slots.


FRealtimeMeshOBJLoadOptions controls the parse:

FRealtimeMeshOBJLoadOptions Options;
Options.MaterialSearchPath = TEXT(""); // where to look for the .mtl (defaults to "./")
Options.bReverseWinding = false; // flip triangle winding
Options.bGenerateTangents = true; // (re)generate the tangent basis after loading
Options.bGenerateSmoothTangents = true; // smooth (vs. per-face) tangents

If either bGenerateTangents or bGenerateSmoothTangents is set, the loader runs RMC’s tangent generator over the loaded data, so you get a proper tangent basis even when the OBJ only had normals (or none).


The synchronous C++ entry point fills a stream set directly. Parsing a large OBJ is not cheap, so prefer the async path on the game thread — but the synchronous call is fine for worker threads or small files.

#include "RealtimeMeshObjLoader.h"
#include "RealtimeMeshSimple.h"
using namespace RealtimeMesh;
void AMyActor::LoadObj(const FString& FilePath)
{
FRealtimeMeshStreamSet StreamSet;
TArray<FRealtimeMeshOBJMaterial> Materials;
FRealtimeMeshOBJLoadOptions Options; // defaults are fine for most files
const FRealtimeMeshOBJLoadResult Result =
URealtimeMeshObjLoader::LoadStreamSetFromOBJFile(StreamSet, Materials, FilePath, Options);
if (!Result.bSuccess)
{
UE_LOG(LogTemp, Warning, TEXT("OBJ load failed: %s"), *Result.Message);
return;
}
URealtimeMeshSimple* Mesh = GetRealtimeMeshComponent()->InitializeRealtimeMesh<URealtimeMeshSimple>();
// One material slot per OBJ material. Build your UMaterialInterface from the
// FRealtimeMeshOBJMaterial fields (diffuse color, texture paths, etc.) as needed.
for (int32 Index = 0; Index < Materials.Num(); ++Index)
{
Mesh->SetupMaterialSlot(Index, FName(*Materials[Index].Name));
}
// The stream set carries poly groups (one per material), so creating the group
// auto-creates a section per material index.
const FRealtimeMeshBufferSetKey GroupKey =
FRealtimeMeshBufferSetKey::Create(0, FName("LoadedOBJ"));
Mesh->CreateBufferSet(GroupKey, MoveTemp(StreamSet));
}

LoadStreamSetFromOBJFile returns false in Result.bSuccess on any parse error, with a human-readable Result.Message.


For anything you load on the game thread, use the async entry point. It runs the entire parse/dedup/tangent-generation on the thread pool and hands back a TFuture<FRealtimeMeshOBJLoadData>. The FRealtimeMeshOBJLoadData is a single movable payload containing the Streams, the Materials, and the Result.

The future is fulfilled on the completing worker thread — marshal back to the game thread yourself before you touch any UObject (including the mesh component):

using namespace RealtimeMesh;
URealtimeMeshObjLoader::LoadStreamSetFromOBJFileAsync(FilePath, FRealtimeMeshOBJLoadOptions())
.Next([WeakThis = TWeakObjectPtr<AMyActor>(this)](FRealtimeMeshOBJLoadData Data)
{
// Still on the worker thread here — hop to the game thread for the UObject work.
AsyncTask(ENamedThreads::GameThread, [WeakThis, Data = MoveTemp(Data)]() mutable
{
AMyActor* Self = WeakThis.Get();
if (!Self || !Data.Result.bSuccess)
{
return;
}
URealtimeMeshSimple* Mesh =
Self->GetRealtimeMeshComponent()->InitializeRealtimeMesh<URealtimeMeshSimple>();
for (int32 Index = 0; Index < Data.Materials.Num(); ++Index)
{
Mesh->SetupMaterialSlot(Index, FName(*Data.Materials[Index].Name));
}
const FRealtimeMeshBufferSetKey GroupKey =
FRealtimeMeshBufferSetKey::Create(0, FName("LoadedOBJ"));
Mesh->CreateBufferSet(GroupKey, MoveTemp(Data.Streams));
});
});

LoadStreamSetFromOBJFileAsync also has a Blueprint-friendly overload. You pass a URealtimeMeshStreamSet to fill and a completion delegate; the parse runs off the game thread and the delegate fires on the game thread once the stream set has been populated:

// C++ shape of the Blueprint node — in Blueprint this is a latent-style call with an event pin.
URealtimeMeshObjLoader::LoadStreamSetFromOBJFileAsync(
StreamSetObject, // URealtimeMeshStreamSet* to fill (must stay valid until completion)
FilePath,
Options,
OnComplete); // fires with FRealtimeMeshOBJLoadResult + TArray<FRealtimeMeshOBJMaterial>

The delegate signature is FRealtimeMeshOBJLoadCompleteCallback(FRealtimeMeshOBJLoadResult Result, const TArray<FRealtimeMeshOBJMaterial>& Materials). Inside the callback the passed URealtimeMeshStreamSet is already filled, so create your section group from it there. Keep the stream set object alive (e.g. as a UPROPERTY) until the callback runs.


There is no built-in loader for other formats, but the pipeline is format-agnostic — the loader above is just one way to fill a stream set. To support any other format:

  1. Parse the file into plain arrays (positions, indices, whatever attributes it carries).
  2. Fill a stream set. Either add streams directly with StreamSet.AddStream<...>(FRealtimeMeshStreams::Position) and friends, or drive a TRealtimeMeshBuilderLocal (the same builder used throughout the examples) which handles the stream layout for you.
  3. Create the section group — CreateBufferSet(GroupKey, MoveTemp(StreamSet)).

If your data is heavy, do steps 1 and 2 on a worker thread and only step 3 on the game thread, just like the async OBJ path above.


  • Mesh Optimization — run the optimizer over the loaded stream set before creating the section group.
  • Key Concepts — stream sets, builders, and section groups.