Updating Mesh Data
Once a section group exists, there are several ways to change its geometry, spanning a spectrum from “throw away the buffers and re-upload everything” to “poke a few bytes of an existing GPU buffer in place.” Picking the right one is the single biggest lever on update performance. This page walks that spectrum, all grounded in URealtimeMeshSimple example actors.
The rule of thumb:
- Changing topology (vertex/triangle counts) → full re-push with
UpdateBufferSet. - Changing values only (positions, colors, normals) on a Dynamic group → in-place
EditMeshInPlace/EditMeshInPlaceRanged.
Full re-push: UpdateBufferSet
Section titled “Full re-push: UpdateBufferSet”The simplest update rebuilds the StreamSet and hands it back. This works for any group, handles topology changes, and is the right default when the mesh isn’t changing every frame:
FRealtimeMeshStreamSet StreamSet;// ...rebuild the whole grid into StreamSet with a builder...
const FRealtimeMeshBufferSetKey GroupKey = FRealtimeMeshBufferSetKey::Create(0, FName("DynamicMesh"));
// Create the group on the first call, then just update it afterward.if (RealtimeMesh->GetBufferSets(FRealtimeMeshLODKey(0)).Contains(GroupKey)){ RealtimeMesh->UpdateBufferSet(GroupKey, StreamSet);}else{ RealtimeMesh->CreateBufferSet(GroupKey, StreamSet);}UpdateBufferSet has const-ref and rvalue (MoveTemp(StreamSet)) overloads. For a Static group this republishes by recreating the proxy; that’s fine for occasional changes but is why frame-by-frame animation wants either a Dynamic group or the in-place path below.
This is exactly what ARealtimeMeshExample_Simple_DynamicUpdate (Source/RealtimeMeshExamples/Private/Simple/RealtimeMeshExample_Simple_DynamicUpdate.cpp) does — it rebuilds and re-pushes a rippling grid every frame.
In-place fast paths: EditMeshInPlace
Section titled “In-place fast paths: EditMeshInPlace”When the topology is fixed and you only change vertex values, you can edit the group’s CPU streams in place and upload just the changed streams — no reallocation and no new proxy version. This requires a Dynamic draw-type group (see Section Groups & Sections):
// At creation: Dynamic draw type -> BUF_Dynamic buffers -> in-place path eligible.RealtimeMesh->CreateBufferSet( GroupKey, StreamSet, FRealtimeMeshBufferSetConfig(ERealtimeMeshSectionDrawType::Dynamic));EditMeshInPlace takes a callback that mutates the FRealtimeMeshStreamSet and returns the set of stream keys it changed:
RealtimeMesh->EditMeshInPlace(GroupKey, [Time, Width](FRealtimeMeshStreamSet& Streams) -> TSet<FRealtimeMeshStreamKey> { FRealtimeMeshStream* Positions = Streams.Find(FRealtimeMeshStreams::Position); if (!Positions) { return {}; }
TRealtimeMeshStreamBuilder<FVector3f, void> Position(*Positions); for (int32 Index = 0; Index < Position.Num(); ++Index) { FVector3f V = Position.GetValue(Index); V.Z = /* ...new height... */; Position.Set(Index, V); }
// Tell RMC which streams changed so only those upload. return { FRealtimeMeshStreams::Position }; });Eligible vertex streams on a Dynamic group are uploaded via a buffer lock with no realloc and no new proxy version; anything not eligible falls back to a full update.
EditMeshInPlaceRanged — upload only a sub-range
Section titled “EditMeshInPlaceRanged — upload only a sub-range”If you touch only part of a stream, EditMeshInPlaceRanged lets you return, per changed stream, the half-open element range [lower, upper) you actually modified — and only that GPU sub-region uploads:
RealtimeMesh->EditMeshInPlaceRanged(GroupKey, [/* captures */](FRealtimeMeshStreamSet& Streams) -> TMap<FRealtimeMeshStreamKey, FInt32Range> { TMap<FRealtimeMeshStreamKey, FInt32Range> Updated;
FRealtimeMeshStream* Positions = Streams.Find(FRealtimeMeshStreams::Position); if (!Positions) { return Updated; }
TRealtimeMeshStreamBuilder<FVector3f, void> Position(*Positions); for (int32 Index = VertexStart; Index < VertexEnd; ++Index) { FVector3f V = Position.GetValue(Index); V.Z = /* ...new height... */; Position.Set(Index, V); }
Updated.Add(FRealtimeMeshStreams::Position, FInt32Range(VertexStart, VertexEnd)); return Updated; });Only the returned range is written to the GPU, so untouched regions stay exactly as they were. Streams/ranges that aren’t eligible fall back to a full update.
Both patterns come straight from ARealtimeMeshExample_Simple_FastUpdate (Source/RealtimeMeshExamples/Private/Simple/RealtimeMeshExample_Simple_FastUpdate.cpp), which animates a Dynamic grid — the full-grid mode uses EditMeshInPlace, the middle-band mode uses EditMeshInPlaceRanged so the top and bottom bands visibly stay flat.
Reading data back: ProcessMesh
Section titled “Reading data back: ProcessMesh”To read a group’s current CPU data without copying it out, use ProcessMesh. It invokes your callback with the FRealtimeMeshStreamSet while the accessor lock is held, so the reference never escapes the lock:
RealtimeMesh->ProcessMesh(GroupKey, [](const FRealtimeMeshStreamSet& Streams){ // Inspect Streams here. Do NOT retain the reference past this callback.});Don’t store the reference beyond the callback, and don’t call a write/edit API on the same mesh from inside it. (URealtimeMeshSimple also offers ProcessBufferSet, which hands you the buffer set under the same lock discipline.)
Building off-thread, committing on the game thread
Section titled “Building off-thread, committing on the game thread”Heavy procedural builds shouldn’t stall the game thread. A FRealtimeMeshStreamSet is a pure CPU container with no UObject access, so it’s safe to build on a worker thread and then hop back to commit it. RMC ships async helpers for exactly this (Core/RealtimeMeshFuture.h, in namespace RealtimeMesh):
using namespace RealtimeMesh;
TWeakObjectPtr<URealtimeMeshSimple> WeakMesh(RealtimeMesh);
// 1) Build the heavy StreamSet off the game thread.TFuture<FRealtimeMeshStreamSet> BuildFuture = DoOnAsyncThread([](){ return BuildGridStreamSet(); // pure CPU, worker-thread safe});
// 2) Hop back to the game thread to create/update the section group.ContinueOnGameThread(MoveTemp(BuildFuture), [WeakMesh](TFuture<FRealtimeMeshStreamSet>&& Result) { const FRealtimeMeshStreamSet& StreamSet = Result.Get(); if (URealtimeMeshSimple* Mesh = WeakMesh.Get()) { Mesh->CreateBufferSet( FRealtimeMeshBufferSetKey::Create(0, FName("AsyncMesh")), StreamSet); } });This is the recommended pattern for expensive geometry, and it’s what ARealtimeMeshExample_Simple_AsyncBuild (Source/RealtimeMeshExamples/Private/Simple/RealtimeMeshExample_Simple_AsyncBuild.cpp) demonstrates — it runs the build in BeginPlay (not OnConstruction) so it isn’t re-triggered while editing. Use TWeakObjectPtr captures so the callback is safe if the mesh or actor is destroyed before the build finishes. See Async & Threading for more on these helpers.
The update-status return
Section titled “The update-status return”Every geometry-changing call — CreateBufferSet, UpdateBufferSet, EditMeshInPlace(Ranged), SetSectionVisibility, and so on — returns a TFuture<ERealtimeMeshProxyUpdateStatus> that completes once the render proxy has caught up. The status is one of:
Updated— the proxy was updated.NoUpdate— nothing needed updating.NoProxy— there was no render proxy to update (e.g. the mesh isn’t currently rendered).
You can ignore the future entirely (the update still happens), or await it to chain follow-up work:
RealtimeMesh->UpdateBufferSet(GroupKey, MoveTemp(StreamSet)) .Next([](ERealtimeMeshProxyUpdateStatus Status) { /* ...after the proxy updates... */ });From Blueprint, the same operations take an OnComplete completion delegate (FRealtimeMeshManagedCompletionCallback) that fires with the ERealtimeMeshProxyUpdateStatus when the update lands, instead of returning a future.