rdInst Tutorial 3.7 – Using Componentless Primitive Meshes
Last Updated: 4th September 2026
Tutorial created with rdInst version 1.54
rdInst version 1.53 introduced support for Componentless Primitive Meshes, version 1.54 exposed them to BP.
They can be spawned with rdECS containers, spawned from BP and C++, and used as Proxies (both Promoted object and/or Distant object).
This tutorial spawns some Compless Meshes in BP, C++, and moves them around – then creates an ISM which swaps to a Compless Mesh when close.
Step 1. Create a new Level and 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_SpawnCompless”. 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, SpawnCompless and RotateCompless – 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 “ArdSpawnCompless”.

Step 7. Add the properties and function definitions to the header
UCLASS()
class RDTOOLS_580_API ArdSpawnCompless : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ArdSpawnCompless();
UFUNCTION(BlueprintCallable,Category="ComplessMeshes")
void SpawnCompless(UStaticMesh* mesh,int32 num);
void RotateCompless(float DeltaTime);
void RotateComplessBGT(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="ComplessMeshes")
UStaticMesh* staticMesh=nullptr;
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="ComplessMeshes")
int32 numToSpawn=100000;
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="ComplessMeshes")
float speed=1.0;
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="ComplessMeshes")
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 "ArdSpawnCompless.h"
// Sets default values
ArdSpawnCompless::ArdSpawnCompless()
{
// 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 ArdSpawnCompless::BeginPlay()
{
Super::BeginPlay();
UrdInstSubsystem* rdInstSubsystem=GEngine?GEngine->GetEngineSubsystem<UrdInstSubsystem>():nullptr;
if(rdInstSubsystem)
{
base=rdInstSubsystem->rdGetBase();
SpawnCompless(staticMesh,numToSpawn);
}
}
void ArdSpawnCompless::SpawnCompless(UStaticMesh* mesh,int32 num)
{
if (!base) return;
locations.Empty();
transforms.Empty();
base->rdRemoveAllCompless();
sid=base->rdGetSMsid(mesh,ErdSpawnType::ComponentlessMesh);
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 numSpawned=base->rdSpawnComplessBatch(sid,transforms);
}
// Called every frame
void ArdSpawnCompless::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (bUseBackgroundThread)
{
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask,[this,DeltaTime]() {
RotateComplessBGT(DeltaTime);
});
}
else
{
RotateCompless(DeltaTime);
}
}
void ArdSpawnCompless::RotateCompless(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->rdUpdateComplessTransformsX(sid,0,transforms);
}
void ArdSpawnCompless::RotateComplessBGT(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 9. Done
