rdInst Tutorial 3.6 – Using Niagara Meshes
Last Updated: 4th September 2026
Tutorial created with rdInst version 1.54
rdInst version 1.53 introduced support for Niagara Meshes in all engines from 4.27 up.
Version 1.54 Exposed these to BP as well. They are much faster when everything is handled by the rdECS system or C++ routines, but there are times when you may want to control some dynamic meshes quickly and easily in BP. The only real limitation is the blueprint nodes in your loops.
Niagara meshes are very fast, but do have a few limitations – they cannot be Nanite (it just disables Nanite on any meshes supplied to it so can still use them).
This tutorial first creates a BP that spawns some Niagara meshes and moves them around, then from Step 6 on there are C++ examples for both Game Thread routines and Background Thread routines.
The Background Thread routines use the high-speed instance queues which are thread friendly and have highly optimized rendering routines on the other side – this is the way to get the fastest possible spawn and movement routines. You should be able run simple move routines on 100,000s in a background thread without slowing the system down.
The BP routines are surprisingly fast, I was able to get 10FPS in PIE with 10,000 cubes rotating on my middle-of-the-line hardware (BP loop rather than slow rendering).
Step 1. Create a new level and a BP based on Actor
The first step is to create a Basic Level, delete the floor, and create a new BP based on Actor – call it “BP_SpawnNiagaraMeshes”. Drag one into the level (at around 0,0,0 – but it doesn’t matter where it is).
Step 2. Create the Variables
Add some variables as below (don’t worry about the first one, “Mats” as we can drag off a pin and promote it later)

Step 3. Create the Functions
Now Add 2 functions, SpawnNiagaraMeshes and RotateNiagaraMeshes – neither need any parameters or results. You can click “Call in Editor” on the Spawn function if you want to test it without playing the level. If you hadn’t created the “Mats” array property, drag out from the “rdGetSMXsid” functions “Mats” pin and select “Promote to Variable”.


Step 4. Setup the calls to the Functions
Now just add the Spawning in the BeginPlay, note we wait until the next frame to allow our rdInstSettings window a guaranteed init. Also add the Rotation to the Tick event.

Step 5. Done for the BP side – Play the Level
That’s it – if you made the spawn function callable in editor you can click it to test, otherwise play the level and watch the cubes rotate.

Step 6. Create a C++ class based on AActor
Create a new C++ class in your project. Depending on the IDE you use the options will be different – in Rider, it’s “New UE Class” for instance. Call it “ArdSpawnNiagaraMeshes”.

Step 7. Add the properties and function definitions to the header
UCLASS()
class RDTOOLS_580_API ArdSpawnNiagaraMeshes : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ArdSpawnNiagaraMeshes();
UFUNCTION(BlueprintCallable,Category="NiagaraMeshes")
void SpawnNiagaraMeshes(UStaticMesh* mesh,int32 num);
void RotateNiagaraMeshes(float DeltaTime);
void RotateNiagaraMeshesBGT(float DeltaTime);
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="NiagaraMeshes")
UStaticMesh* staticMesh=nullptr;
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="NiagaraMeshes")
int32 numToSpawn=10000;
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="NiagaraMeshes")
float speed=1.0;
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="NiagaraMeshes")
bool bUseBackgroundThread=false;
private:
ArdInstBaseActor* base=nullptr;
FName sid;
TArray<FVector> locations;
TArray<FTransform> transforms;
};
Step 8. Implement the GameThread and Background Thread routines in the C++ file
#include "ArdSpawnNiagaraMeshes.h"
// Sets default values
ArdSpawnNiagaraMeshes::ArdSpawnNiagaraMeshes()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
}
// Called when the game starts or when spawned
void ArdSpawnNiagaraMeshes::BeginPlay()
{
Super::BeginPlay();
UrdInstSubsystem* rdInstSubsystem=GEngine?GEngine->GetEngineSubsystem<UrdInstSubsystem>():nullptr;
if(rdInstSubsystem)
{
base=rdInstSubsystem->rdGetBase();
SpawnNiagaraMeshes(staticMesh,numToSpawn);
}
}
// Spawn "num" amount of Niagara Meshes
void ArdSpawnNiagaraMeshes::SpawnNiagaraMeshes(UStaticMesh* mesh,int32 num)
{
if (!base) return;
locations.Empty();
transforms.Empty();
base->rdRemoveAllNiagaraMesh();
sid=base->rdGetSMsid(mesh,ErdSpawnType::NiagaraMesh);
FTransform t(FRotator(0,0,0),FVector(0,0,0),FVector(1,1,1));
locations.SetNum(num);
transforms.SetNum(num);
ParallelFor(num,[this,&t](int32 i)
{
float angle=FMath::FRandRange(0.0f,359.9f);
float radius=FMath::FRandRange(100.0f,5000.0f);
float height=FMath::FRandRange(0.0f,5000.0f);
locations[i]=FVector(radius,angle,height);
t.SetTranslation(FVector(radius,0.0f,height).RotateAngleAxis(angle,FVector(0.0f,0.0f,1.0f)));
transforms[i]=t;
});
int32 numAdded=base->rdAddNiagaraMeshBatch(sid,transforms);
}
// Called every frame
void ArdSpawnNiagaraMeshes::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (bUseBackgroundThread)
{
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask,[this,DeltaTime]() {
RotateNiagaraMeshesBGT(DeltaTime);
});
}
else
{
RotateNiagaraMeshes(DeltaTime);
}
}
void ArdSpawnNiagaraMeshes::RotateNiagaraMeshes(float DeltaTime)
{
if (!base) return;
int32 num=locations.Num();
ParallelFor(num,[this,DeltaTime](int32 i)
{
FTransform& t=transforms[i];
FVector& vec=locations[i];
vec.Y+=DeltaTime*speed;
t.SetTranslation( FVector(vec.X, 0.0f, vec.Z).RotateAngleAxis(vec.Y, FVector(0.0f, 0.0f, 1.0f)));
});
base->rdUpdateNiagaraMeshTransforms(sid,0,transforms);
}
void ArdSpawnNiagaraMeshes::RotateNiagaraMeshesBGT(float DeltaTime)
{
if (!base) return;
int32 num=locations.Num();
TArray<TTuple<int32,FTransform>> tmoveArray;
tmoveArray.SetNum(num);
ParallelFor(num,[this,DeltaTime,&tmoveArray](int32 i)
{
FTransform& t=transforms[i];
FVector& vec=locations[i];
vec.Y+=DeltaTime*speed;
FVector oldVec=t.GetTranslation();
FVector newVec=FVector(vec.X, 0.0f, vec.Z).RotateAngleAxis(vec.Y, FVector(0.0f, 0.0f, 1.0f));
if (base->rdHasTimeSlice(oldVec,newVec))
{
tmoveArray[i]=TTuple<int32,FTransform>(i,t);
t.SetTranslation(newVec);
}
});
if (tmoveArray.Num()>0)
{
rdLock writeLock(base->scopeLock);
TArray<TTuple<int32,FTransform>>& moveArray=base->tmoveMap.FindOrAdd(sid);
moveArray.Append(MoveTemp(tmoveArray));
}
}
Step 10. Done
