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/).
What the loader produces
Section titled “What the loader produces”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.
Loading options
Section titled “Loading options”FRealtimeMeshOBJLoadOptions controls the parse:
FRealtimeMeshOBJLoadOptions Options;Options.MaterialSearchPath = TEXT(""); // where to look for the .mtl (defaults to "./")Options.bReverseWinding = false; // flip triangle windingOptions.bGenerateTangents = true; // (re)generate the tangent basis after loadingOptions.bGenerateSmoothTangents = true; // smooth (vs. per-face) tangentsIf 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).
Synchronous load (C++)
Section titled “Synchronous load (C++)”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.
Async load (C++)
Section titled “Async load (C++)”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)); }); });Async load (Blueprint)
Section titled “Async load (Blueprint)”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.
Loading other formats
Section titled “Loading other formats”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:
- Parse the file into plain arrays (positions, indices, whatever attributes it carries).
- Fill a stream set. Either add streams directly with
StreamSet.AddStream<...>(FRealtimeMeshStreams::Position)and friends, or drive aTRealtimeMeshBuilderLocal(the same builder used throughout the examples) which handles the stream layout for you. - 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.
Related
Section titled “Related”- Mesh Optimization — run the optimizer over the loaded stream set before creating the section group.
- Key Concepts — stream sets, builders, and section groups.