
Ue Procedural Generation
- 613 installs
- 301 repo stars
- Updated March 1, 2026
- quodsoler/unreal-engine-skills
ue-procedural-generation is a Claude Code skill that guides Unreal Engine 5.2+ procedural pipelines using UPCGComponent graphs, HISM scatter, ProceduralMeshComponent, and spline-driven generation for game developers.
About
ue-procedural-generation is a Claude Code skill (version 1.0.0) from quodsoler/unreal-engine-skills for Unreal Engine procedural content pipelines. The skill documents the PCG framework on UE 5.2+, ProceduralMeshComponent runtime meshes, ISM and HISM instancing, noise functions, and spline-based generation for terrain, dungeons, vegetation, and world layout. It includes C++ API patterns for UPCGComponent, custom Blueprint PCG nodes, multiplayer-safe Generate() replication, and performance guidance such as switching to HISM above roughly 500 instances. Two bundled references—pcg-node-reference.md and procedural-mesh-patterns.md—cover PCG node types plus six mesh patterns including marching cubes, dungeon BSP, L-systems, and wave function collapse. Developers reach for it when wiring procedural systems in C++ or Blueprint rather than hand-placing static level art.
- Documents UPCGComponent Generate/GenerateLocal/Cleanup APIs for UE 5.2+ PCG framework
- Covers ISM vs HISM thresholds: ISM under ~500 instances, HISM for tens of thousands
- Includes marching cubes, dungeon BSP, L-system, and Wave Function Collapse mesh patterns
- 2 reference files: pcg-node-reference.md and procedural-mesh-patterns.md
- Flags multiplayer trap: GenerateLocal is not replicated—use NetMulticast Generate()
Ue Procedural Generation by the numbers
- 613 all-time installs (skills.sh)
- +48 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #36 of 247 Game Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/quodsoler/unreal-engine-skills --skill ue-procedural-generationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 613 |
|---|---|
| repo stars | ★ 301 |
| Last updated | March 1, 2026 |
| Repository | quodsoler/unreal-engine-skills ↗ |
How do you implement PCG graphs in Unreal Engine?
Apply ue-procedural-generation to wire UPCGComponent graphs, HISM vegetation scatter, and ProceduralMeshComponent runtime meshes in Unreal Engine 5.2+ C++ or Blueprint projects.
Who is it for?
Unreal Engine developers implementing PCG, runtime meshes, or large-scale instanced procedural worlds in UE 5.2+ C++ or Blueprint projects.
Skip if: Developers needing general UE physics, save games, or non-procedural level streaming should load sibling ue-physics-collision or ue-world-level-streaming skills instead.
When should I use this skill?
User works on UE PCG framework, ProceduralMesh, HISM, spline generation, terrain, dungeon, noise, or runtime mesh generation in Unreal Engine.
What you get
PCG graph setup, HISM scatter code, ProceduralMeshComponent sections, spline placement loops, and reference-backed node and mesh pattern implementations.
- PCG graph configuration
- HISM scatter implementation
- ProceduralMeshComponent runtime mesh code
By the numbers
- Skill version 1.0.0
- Targets Unreal Engine 5.2+ PCG framework
- Bundles 2 reference files and 6 procedural mesh generation patterns
Files
ue-procedural-generation
You are an expert in Unreal Engine's procedural generation systems, including the PCG framework, ProceduralMeshComponent, instanced static meshes, noise functions, and spline-based generation.
Context Check
Before advising, read .agents/ue-project-context.md to determine:
- Whether the PCG plugin is enabled (plugins list)
- Target generation type: world layout, terrain, dungeon, vegetation, runtime mesh
- Performance constraints (mobile, console, Nanite enabled)
- Multiplayer requirements (server authority vs. deterministic seeding)
Information Gathering
Ask for clarification on: 1. Generation type: world population (PCG), runtime mesh (ProceduralMeshComponent), instanced geometry (ISM/HISM), or spline-driven? 2. Timing: editor-time baked result or runtime dynamic generation? 3. Instance count: hundreds (ISM) or tens of thousands (HISM)? 4. Collision: does generated geometry need physics collision? 5. Determinism: same seed must produce same result across sessions or network clients?
---
1. PCG Framework (UE 5.2+)
Node-based rule-driven world generation. Operates on point clouds with transform, density, color, seed, and metadata attributes.
Plugin Setup
// Build.cs
PublicDependencyModuleNames.Add("PCG");// .uproject Plugins array
{ "Name": "PCG", "Enabled": true }Core Classes
| Class | Header | Purpose |
|---|---|---|
UPCGComponent | PCGComponent.h | Actor component driving generation |
UPCGGraph | PCGGraph.h | Asset: nodes + edges |
UPCGGraphInstance | PCGGraph.h | Graph instance with parameter overrides |
UPCGPointData | Data/PCGPointData.h | Point cloud between nodes |
UPCGSettings | PCGSettings.h | Node settings base class |
UPCGBlueprintBaseElement | Elements/Blueprint/PCGBlueprintBaseElement.h | Custom Blueprint node base |
UPCGComponent Key API (from PCGComponent.h)
// Assign graph (NetMulticast)
void SetGraph(UPCGGraphInterface* InGraph);
// Trigger generation (NetMulticast, Reliable) — use for multiplayer
void Generate(bool bForce);
// Local non-replicated generation
void GenerateLocal(bool bForce);
// Cleanup
void Cleanup(bool bRemoveComponents);
void CleanupLocal(bool bRemoveComponents);
// Notify to re-evaluate after Blueprint property change
void NotifyPropertiesChangedFromBlueprint();
// Read generated output
const FPCGDataCollection& GetGeneratedGraphOutput() const;Generation triggers (EPCGComponentGenerationTrigger):
GenerateOnLoad— one-shot on BeginPlayGenerateOnDemand— explicitGenerate()call onlyGenerateAtRuntime— budget-scheduled byUPCGSubsystem
UPCGGraph Node API (from PCGGraph.h)
// Add node by settings class
UPCGNode* AddNodeOfType(TSubclassOf<UPCGSettings> InSettingsClass, UPCGSettings*& DefaultNodeSettings);
// Connect two nodes
UPCGNode* AddEdge(UPCGNode* From, const FName& FromPinLabel, UPCGNode* To, const FName& ToPinLabel);
// Graph parameters (typed template)
template<typename T>
TValueOrError<T, EPropertyBagResult> GetGraphParameter(const FName PropertyName) const;
template<typename T>
EPropertyBagResult SetGraphParameter(const FName PropertyName, const T& Value);Custom Blueprint PCG Node
Derive from UPCGBlueprintBaseElement:
UCLASS(BlueprintType, Blueprintable)
class UMyPCGNode : public UPCGBlueprintBaseElement
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PCG|Execution")
void Execute(const FPCGDataCollection& Input, FPCGDataCollection& Output);
};
// In Execute:
FRandomStream Stream = GetRandomStreamWithContext(GetContextHandle()); // deterministic seed
for (const FPCGTaggedData& In : Input.GetInputsByPin(PCGPinConstants::DefaultInputLabel))
{
const UPCGPointData* InPts = Cast<UPCGPointData>(In.Data);
if (!InPts) continue;
UPCGPointData* OutPts = NewObject<UPCGPointData>();
for (const FPCGPoint& Pt : InPts->GetPoints())
{
FPCGPoint NewPt = Pt;
NewPt.Density = Stream.FRandRange(0.5f, 1.0f);
OutPts->GetMutablePoints().Add(NewPt);
}
Output.TaggedData.Emplace_GetRef().Data = OutPts;
}Key UPCGBlueprintBaseElement properties:
bIsCacheable = false— when node spawns actors or componentsbRequiresGameThread = true— for actor spawn, component addCustomInputPins/CustomOutputPins— extra typed pins
PCG Determinism
PCG graphs are deterministic by default — the same seed produces identical output. Each node receives a seeded random stream via GetRandomStreamWithContext(). To vary output across instances, set the PCG component's Seed property. For multiplayer, ensure all clients use the same seed (replicate via GameState or pass as spawn parameter).
// Set PCG seed at runtime for deterministic variation
UPCGComponent* PCG = FindComponentByClass<UPCGComponent>();
PCG->Seed = MyDeterministicSeedValue;
PCG->Generate(); // Regenerate with new seedPCG Data Types
| Type | Contains | Use for |
|---|---|---|
FPCGPoint / Point Data | Position, rotation, scale, density, color | Scatter placement, foliage, instance positioning |
UPCGSplineData | Spline points + tangents | Roads, rivers, paths, boundary definitions |
UPCGLandscapeData | Height + layer weight sampling | Terrain-aware placement, biome queries |
UPCGVolumeData | 3D bounds | Volume-based filtering and generation |
Point data is the most common — most PCG nodes consume and produce point collections. Also available: UPCGTextureData, UPCGPrimitiveData, UPCGDynamicMeshData.
See references/pcg-node-reference.md for all node types, settings fields, and pin labels.
---
2. ProceduralMeshComponent
// Build.cs
PublicDependencyModuleNames.Add("ProceduralMeshComponent");Core API
// Create section: vertices, triangles (CCW = front), normals, UVs, colors, tangents
void CreateMeshSection(int32 SectionIndex,
const TArray<FVector>& Vertices, const TArray<int32>& Triangles,
const TArray<FVector>& Normals, const TArray<FVector2D>& UV0,
const TArray<FColor>& VertexColors, const TArray<FProcMeshTangent>& Tangents,
bool bCreateCollision);
// Updates vertex positions (incl. collision if enabled). Cannot change topology.
void UpdateMeshSection(int32 SectionIndex,
const TArray<FVector>& Vertices, const TArray<FVector>& Normals,
const TArray<FVector2D>& UV0, const TArray<FColor>& VertexColors,
const TArray<FProcMeshTangent>& Tangents);
void ClearMeshSection(int32 SectionIndex);
void ClearAllMeshSections();
void SetMeshSectionVisible(int32 SectionIndex, bool bNewVisibility);
void SetMaterial(int32 ElementIndex, UMaterialInterface* Material);Terrain Grid Example
void ATerrainActor::Build(int32 Grid, float Cell)
{
TArray<FVector> Verts; TArray<int32> Tris; TArray<FVector> Norms;
TArray<FVector2D> UVs; TArray<FColor> Colors; TArray<FProcMeshTangent> Tangs;
for (int32 Y = 0; Y <= Grid; Y++)
for (int32 X = 0; X <= Grid; X++)
{
float Z = SampleOctaveNoise(X * Cell, Y * Cell, 4, 0.5f, 2.f, 80.f);
Verts.Add(FVector(X * Cell, Y * Cell, Z));
Norms.Add(FVector::UpVector);
UVs.Add(FVector2D((float)X / Grid, (float)Y / Grid));
}
for (int32 Y = 0; Y < Grid; Y++)
for (int32 X = 0; X < Grid; X++)
{
int32 BL = Y*(Grid+1)+X, BR=BL+1, TL=BL+(Grid+1), TR=TL+1;
Tris.Add(BL); Tris.Add(TL); Tris.Add(TR);
Tris.Add(BL); Tris.Add(TR); Tris.Add(BR);
}
ProceduralMesh->CreateMeshSection(0, Verts, Tris, Norms,
UVs, Colors, Tangs, /*bCreateCollision=*/true);
}Performance Notes
- One draw call per
CreateMeshSection. Keep vertex count < 65K per section. UpdateMeshSectionupdates vertex positions and collision (if enabled) but cannot change topology — callCreateMeshSectionfor new triangles.- ProceduralMesh does not support Nanite.
- Compute vertex data on background thread; call
CreateMeshSectionon game thread only.
Async Mesh Generation
Generate vertices on a background thread, then apply on the game thread:
// Background task — compute vertices
class FMeshGenTask : public FNonAbandonableTask
{
public:
TArray<FVector> Vertices;
TArray<int32> Triangles;
void DoWork() { /* Marching cubes, noise sampling, etc. */ }
FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FMeshGenTask, STATGROUP_ThreadPoolAsyncTasks); }
};
// Launch and poll
// Use FAsyncTask (not FAutoDeleteAsyncTask) when polling IsDone() is needed.
// FAutoDeleteAsyncTask deletes itself on completion — calling IsDone() afterward is a use-after-free.
auto* Task = new FAsyncTask<FMeshGenTask>();
Task->StartBackgroundTask();
// Poll safely: if (Task->IsDone()) { /* use Task->GetTask().Vertices */ delete Task; }Collision on Procedural Meshes
Set UProceduralMeshComponent::bUseComplexAsSimpleCollision = true to use the rendered triangles directly for collision. This is accurate but expensive — only use for static geometry. For dynamic or high-poly meshes, generate simplified convex hulls instead.
---
3. Instanced Static Meshes (ISM / HISM)
| Feature | ISM (InstancedStaticMeshComponent.h) | HISM (HierarchicalInstancedStaticMeshComponent.h) |
|---|---|---|
| Best for | < 1,000 dynamic instances | > 1,000 mostly static |
| Culling | Distance only | Hierarchical BVH + distance |
| LOD | GPU selection | Built-in transitions |
| Remove cost | O(n) | async BVH rebuild |
Key ISM API (from InstancedStaticMeshComponent.h)
virtual int32 AddInstance(const FTransform& T, bool bWorldSpace = false);
virtual TArray<int32> AddInstances(const TArray<FTransform>& Ts,
bool bShouldReturnIndices, bool bWorldSpace = false, bool bUpdateNavigation = true);
virtual bool UpdateInstanceTransform(int32 Idx, const FTransform& NewT,
bool bWorldSpace = false, bool bMarkRenderStateDirty = false, bool bTeleport = false);
virtual bool BatchUpdateInstancesTransforms(int32 StartIdx, const TArray<FTransform>& NewTs,
bool bWorldSpace = false, bool bMarkRenderStateDirty = false, bool bTeleport = false);
bool GetInstanceTransform(int32 Idx, FTransform& OutT, bool bWorldSpace = false) const;
virtual bool RemoveInstance(int32 InstanceIndex); // O(n) for ISM; triggers async BVH rebuild for HISM
virtual void PreAllocateInstancesMemory(int32 AddedCount);
int32 GetNumInstances() const;
// Per-instance custom float data (read in materials via PerInstanceCustomData)
virtual void SetNumCustomDataFloats(int32 N);
virtual bool SetCustomDataValue(int32 Idx, int32 DataIdx, float Value,
bool bMarkRenderStateDirty = false);
virtual bool SetCustomData(int32 Idx, TArrayView<const float> Floats,
bool bMarkRenderStateDirty = false);Culling properties: InstanceStartCullDistance, InstanceEndCullDistance, InstanceLODDistanceScale, bUseGpuLodSelection.
Vegetation Scatter (HISM + Terrain Trace)
HISM->SetStaticMesh(TreeMesh);
HISM->SetNumCustomDataFloats(1);
HISM->PreAllocateInstancesMemory(Count);
FRandomStream Rand(Seed);
TArray<FTransform> Transforms; Transforms.Reserve(Count);
for (int32 i = 0; i < Count; i++)
{
FVector Loc(Rand.FRandRange(Min.X, Max.X), Rand.FRandRange(Min.Y, Max.Y), 0);
FHitResult Hit;
if (GetWorld()->LineTraceSingleByChannel(Hit,
Loc + FVector(0,0,5000), Loc - FVector(0,0,5000), ECC_WorldStatic))
Loc.Z = Hit.Location.Z;
Transforms.Add(FTransform(
FRotator(0, Rand.FRandRange(0,360), 0), Loc,
FVector(Rand.FRandRange(0.8f, 1.3f))));
}
TArray<int32> Indices = HISM->AddInstances(Transforms, true, true);
for (int32 i = 0; i < Indices.Num(); i++)
HISM->SetCustomDataValue(Indices[i], 0, Rand.FRand(), false);
HISM->MarkRenderStateDirty();Foliage System
The editor's Foliage paint mode uses AInstancedFoliageActor which internally wraps UHierarchicalInstancedStaticMeshComponent. For procedural foliage at scale, use UProceduralFoliageComponent with UProceduralFoliageSpawner — it distributes foliage via simulation (species competition, shade tolerance) rather than manual painting.
Per-instance collision: Enable bUseDefaultCollision on the ISM component. Each instance inherits the static mesh's collision. For custom per-instance collision shapes, use separate actors — ISM does not support unique collision per instance.
Platform limits: HISM GPU buffer caps vary by platform (~1M on desktop, ~100K on mobile). Monitor with stat Foliage. Split large populations across multiple HISM components.
---
4. Noise and Math
// Built-in Perlin (all output in [-1, 1])
float N1 = FMath::PerlinNoise1D(X * Freq);
float N2 = FMath::PerlinNoise2D(FVector2D(X, Y) * Freq);
float N3 = FMath::PerlinNoise3D(FVector(X, Y, Z) * Freq);
// Octave noise
float OctaveNoise(float X, float Y, int32 Oct, float Persist, float Lacu, float Scale)
{
float V=0, A=1, F=1.f/Scale, Max=0;
for (int32 i=0; i<Oct; i++) {
V += FMath::PerlinNoise2D(FVector2D(X,Y)*F) * A;
Max += A; A *= Persist; F *= Lacu;
}
return V / Max;
}
// Seeded deterministic random
FRandomStream Stream(Seed);
float R = Stream.FRandRange(Min, Max);
int32 I = Stream.RandRange(MinI, MaxI);
FVector Dir = Stream.VRand();Height/density maps: Sample UTexture2D pixel data via FTexturePlatformData to drive terrain height or placement density. Lock with BulkData.Lock(LOCK_READ_ONLY), read, then unlock.
Poisson disc sampling (minimum-separation scatter for natural placement) — full Bridson algorithm implementation in references/procedural-mesh-patterns.md.
---
5. Spline Components
USplineComponent API (from SplineComponent.h)
// Build spline (always batch with bUpdateSpline=false, call UpdateSpline() once after)
void AddSplinePoint(const FVector& Pos, ESplineCoordinateSpace::Type Space, bool bUpdate=true);
void SetSplinePoints(const TArray<FVector>& Pts, ESplineCoordinateSpace::Type Space, bool bUpdate=true);
void ClearSplinePoints(bool bUpdate=true);
virtual void UpdateSpline(); // Rebuild reparameterization table
// Query by arc-length distance
FVector GetLocationAtDistanceAlongSpline(float Dist, ESplineCoordinateSpace::Type Space) const;
FVector GetDirectionAtDistanceAlongSpline(float Dist, ESplineCoordinateSpace::Type Space) const;
FVector GetRightVectorAtDistanceAlongSpline(float Dist, ESplineCoordinateSpace::Type Space) const;
FRotator GetRotationAtDistanceAlongSpline(float Dist, ESplineCoordinateSpace::Type Space) const;
FTransform GetTransformAtDistanceAlongSpline(float Dist, ESplineCoordinateSpace::Type Space, bool bUseScale=false) const;
float GetSplineLength() const;
// Point editing
int32 GetNumberOfSplinePoints() const;
void SetSplinePointType(int32 Idx, ESplinePointType::Type Type, bool bUpdate=true);
void SetClosedLoop(bool bClosed, bool bUpdate=true);
void SetTangentsAtSplinePoint(int32 Idx, const FVector& Arrive, const FVector& Leave,
ESplineCoordinateSpace::Type Space, bool bUpdate=true);Point types: Linear, Curve, Constant, CurveClamped, CurveCustomTangent.
FindInputKeyClosestToWorldLocation(WorldLocation) — returns the spline key nearest to a world position (useful for snapping actors to splines).
Runtime modification: Call AddSplinePoint(), RemoveSplinePoint(), or SetLocationAtSplinePoint() then UpdateSpline() to rebuild. Batch modifications before calling UpdateSpline() — each call recalculates the entire spline.
Spline Placement Example
// Place instances evenly along spline
float Len = Spline->GetSplineLength();
for (float D = 0.f; D <= Len; D += Spacing)
{
FTransform T = Spline->GetTransformAtDistanceAlongSpline(D, ESplineCoordinateSpace::World);
HISM->AddInstance(T, /*bWorldSpace=*/true);
}USplineMeshComponent (Mesh Deformation)
#include "Components/SplineMeshComponent.h"
USplineMeshComponent* SM = NewObject<USplineMeshComponent>(this);
SM->SetStaticMesh(PipeMesh);
SM->RegisterComponent();
FVector SP, ST, EP, ET;
Spline->GetLocationAndTangentAtSplinePoint(Seg, SP, ST, ESplineCoordinateSpace::Local);
Spline->GetLocationAndTangentAtSplinePoint(Seg+1, EP, ET, ESplineCoordinateSpace::Local);
SM->SetStartAndEnd(SP, ST, EP, ET, /*bUpdateMesh=*/true);
SM->SetForwardAxis(ESplineMeshAxis::X);---
6. Runtime Mesh Generation Patterns
See references/procedural-mesh-patterns.md for full implementations:
- Marching Cubes — isosurface from 3D density scalar field
- Dungeon BSP — BSP partition into rooms, L-corridor carving, tile-to-mesh
- L-System — string rewriting + turtle interpreter to HISM branches
- Wave Function Collapse — constraint-propagation tile grid layout
- Async mesh generation — background thread vertex computation, game thread
CreateMeshSection - Spline road extrusion — cross-section profile swept along
USplineComponent
// Marching Cubes result → ProceduralMesh
ProceduralMesh->CreateMeshSection(0, MarchVerts, MarchTris, MarchNormals,
MarchUVs, {}, {}, /*bCreateCollision=*/true);---
Common Mistakes and Anti-Patterns
PCG
- Calling
GenerateLocal()inTick— generation is not free; useGenerateOnDemandand regenerate only on data change. - Using
GenerateLocal()in multiplayer — it is NOT replicated; useGenerate(bForce)(NetMulticast). - Heavy custom nodes with
bIsCacheable = true— only cache if output depends solely on inputs + seed. - Graphs with
bIsEditorOnly = truefail to cook into packaged builds.
ProceduralMeshComponent
- Passing
bCreateCollision=falsetoCreateMeshSection— characters fall through the mesh. - Calling
UpdateMeshSectionexpecting topology to change — vertex count must match; useCreateMeshSectionfor new triangles. - Using ProceduralMesh for Nanite-scale terrain — not supported; use Landscape or PCG + ISM.
- Wrong triangle winding (CW instead of CCW) — polygons are invisible due to back-face culling.
ISM / HISM
- Using ISM above ~500 instances — switch to HISM for BVH culling.
- Setting
bMarkRenderStateDirty=trueon everyUpdateInstanceTransformin a loop — only settrueon the last call. - Skipping
PreAllocateInstancesMemorybefore bulk add — repeated realloc degrades performance.
Splines
- Calling
AddSplinePoint(bUpdateSpline=true)in a loop — rebuilds reparameterization table every call; usefalseand callUpdateSpline()once. - Using spline input key (not distance) for even spacing — key is NOT proportional to arc length.
Multiplayer
- Procedural content must be deterministic (same seed) or server-authoritative.
GenerateLocal()does not replicate;Generate(bool)isNetMulticast, Reliable.
---
Related Skills
ue-actor-component-architecture— component lifecycle, registration, replicationue-physics-collision— collision profiles, complex vs. simple on generated geometryue-cpp-foundations—NewObject,TSubclassOf,TArray, memory management
Reference Files
references/pcg-node-reference.md— all PCG node types, pin labels, settings fields, determinism checklistreferences/procedural-mesh-patterns.md— quad grid, marching cubes, dungeon BSP, L-system, WFC, spline road
PCG Node Reference
PCG nodes are categorized by their EPCGSettingsType enum value. Each node is a UPCGSettings subclass paired with a FPCGElement (or IPCGElement) that performs the actual work. Nodes connect through typed pins carrying UPCGData-derived objects.
---
Data Flow Model
[Actor/Landscape/Spline Input] --> [Sampler] --> [Filter/Density] --> [Spawner] --> [Output]
|
[UPCGPointData]
TArray<FPCGPoint>
Each point has:
FTransform Transform
float Density (0..1)
FVector BoundsMin
FVector BoundsMax
FVector4 Color
int32 Seed
int64 MetadataEntry
float SteepnessData flows between nodes as FPCGTaggedData entries in an FPCGDataCollection. Each entry carries:
Data— pointer toUPCGDatasubclassPin—FNamematching the target pin labelTags—TSet<FString>for filtering
---
Standard Pin Labels
| Label constant | String value | Usage |
|---|---|---|
PCGPinConstants::DefaultInputLabel | "In" | Default input pin |
PCGPinConstants::DefaultOutputLabel | "Out" | Default output pin |
PCGPinConstants::DefaultParamsLabel | "Overrides" | Overridable parameter input (was "Params" before 5.6) |
---
Node Categories and Settings Types
InputOutput (EPCGSettingsType::InputOutput)
Get Actor Data (UPCGDataFromActorSettings)
- Collects spatial data from actors tagged with a PCG tag.
- Produces:
UPCGSpatialData(volume, surface, spline depending on actor components). - Key fields:
ActorSelector(tag, class, or explicit reference),bParseActor.
Get Landscape Data (UPCGLandscapeData)
- Wraps landscape heightfield as a surface for sampling.
- Produces:
UPCGLandscapeData.
Get Spline Data (UPCGSplineData)
- Wraps
USplineComponentas PCG spline data. - Can be used as a surface boundary or point source.
- Produces:
UPCGSplineData,UPCGSplineInteriorSurfaceData.
---
Sampler (EPCGSettingsType::Sampler)
Surface Sampler (UPCGSurfaceSamplerSettings)
- Scatters points on a surface (landscape, mesh, spline-bounded area).
- Key fields:
PointsPerSquaredMeter— density of scatterPointExtents— bounding box half-size per pointLooseness— boundary tolerance (0 = strict inside)bApplyDensityToPoints— use surface density to reject pointsSeed— deterministic seed for scatter- Produces:
UPCGPointData
Spline Sampler (UPCGSplineSamplerSettings)
- Generates points along a spline or inside a spline boundary.
Mode:Edge(along spline),Interior(inside closed spline).Dimension:OnSpline(1D),OnHorizontalSurface(2D),OnVolume(3D).- Key fields:
NumSegments,SubdivisionCount,Fillmode. - Produces:
UPCGPointData
Volume Sampler (UPCGVolumeSamplerSettings)
- Samples points in 3D space within a volume.
- Key fields:
VoxelSize. - Produces:
UPCGPointData
Point Grid (UPCGCreatePointsGridSettings)
- Creates a regular grid of points.
- Key fields:
CellSize,NumCells,Center. - Produces:
UPCGPointData
Point Sphere (UPCGCreatePointsSphereSettings)
- Creates points on or inside a sphere.
- Key fields:
NumPoints,Radius,bFillSphere. - Produces:
UPCGPointData
---
Filter (EPCGSettingsType::Filter)
Density Filter (UPCGDensityFilterSettings)
- Removes points below a density threshold with optional random culling.
- Key fields:
LowerBound,UpperBound,bInvertFilter,Seed.
Attribute Filter (UPCGAttributeFilterSettings)
- Filters points by metadata attribute comparison.
- Key fields:
TargetAttribute,Operator(==,!=,<,>,<=,>=),ConstantValueorOtherAttributeSource.
Bounds Check (UPCGCullPointsOutsideActorBoundsSettings)
- Removes points outside the owning actor's bounding box.
- No required configuration beyond the node itself.
Point Filter (generic UPCGFilterByAttributeSettings)
- Keeps or removes points based on arbitrary attribute predicate.
---
Density (EPCGSettingsType::Density)
Density Noise (UPCGAttributeNoiseSettings applied to density)
- Modulates point density using Perlin noise or other noise modes.
- Key fields:
NoiseMode(Perlin,Value, etc.),Frequency,Seed,InvertSourceDensity.
Density Remap (UPCGAttributeRemapSettings)
- Remaps a numeric attribute from one range to another.
- Key fields:
SourceAttribute,InRange,OutRange,ClampOutput.
Blur (UPCGBlurSettings)
- Blurs point attributes by averaging neighbor values.
- Key fields:
Iterations,KernelSize.
---
Spawner (EPCGSettingsType::Spawner)
Static Mesh Spawner (UPCGStaticMeshSpawnerSettings)
- Takes
UPCGPointDataand spawnsUHierarchicalInstancedStaticMeshComponentinstances. - Key fields:
MeshEntries— weighted list ofFSoftObjectPathmesh assetsbOverrideDescriptors— override ISM component properties per meshInstancePackingMode—StaticMesh,Actor, orISM- Uses
FPCGProceduralISMComponentDescriptor(USTRUCT) for per-mesh ISM settings.
Actor Spawner (UPCGSpawnActorSettings)
- Spawns
AActorsubclasses at point positions. - Key fields:
TemplateActor,SpawnMode(bOverlapExisting, etc.),PostSpawnFunction.
Create Spline (UPCGCreateSplineSettings)
- Creates a
USplineComponentfrom input point positions. - Key fields:
bCreateFromInputPositions,SplineType.
---
Metadata (EPCGSettingsType::Metadata)
Create Attribute (UPCGCreateAttributeSettings)
- Adds a named metadata attribute to all points.
- Key fields:
OutputAttributeName,Type(Float, Int, Vector, etc.),DefaultValue.
Copy Attributes (UPCGCopyAttributesSettings)
- Copies one or more attributes from source data to output.
- Key fields:
SourceAttributeNames,DestinationAttributeNames.
Attribute Noise (UPCGAttributeNoiseSettings)
- Applies noise to any float/vector attribute.
- Key fields:
TargetAttribute,NoiseMode,Frequency,Amplitude,Seed.
Attribute Remap (UPCGAttributeRemapSettings)
- Remaps attribute values between ranges with optional curves.
Attribute Cast (UPCGAttributeCastSettings)
- Casts attribute type (e.g., float to int, FVector to FVector2D).
Break Transform (UPCGMetadataBreakTransformSettings)
- Decomposes
FTransformattribute into Translation, Rotation, Scale attributes.
Make Transform (UPCGMetadataMakeTransformSettings)
- Composes Translation, Rotation, Scale attributes into
FTransform.
Math Operations (UPCGMetadataMathsOpElementSettings)
- Float/vector math: Add, Subtract, Multiply, Divide, Min, Max, Abs, Clamp, etc.
- Key fields:
Operation,InputA,InputB(attributes or constants).
---
ControlFlow (EPCGSettingsType::ControlFlow)
Branch (UPCGBranchSettings)
- Routes data to one of two output pins based on a bool attribute or parameter.
- Pins:
In,OutTrue,OutFalse.
Switch (UPCGSwitchSettings)
- Routes data to N output pins based on an int or enum attribute.
Boolean Select (UPCGBooleanSelectSettings)
- Selects between two data inputs based on a bool parameter.
Wait (UPCGWaitSettings)
- Waits for upstream tasks to complete before forwarding data.
- Used to enforce ordering in asynchronous graphs.
Quality Branch (UPCGQualityBranchSettings)
- Branches based on current scalability/quality level.
---
Subgraph (EPCGSettingsType::Subgraph)
Subgraph (UPCGSubgraphSettings)
- Executes a nested
UPCGGraphas a subgraph node. - Key fields:
SubgraphGraph(asset reference), parameter overrides viaFPCGOverrideInstancedPropertyBag. - Subgraph pins match the subgraph's own input/output node pins.
---
PointOps (EPCGSettingsType::PointOps)
Copy Points (UPCGCopyPointsSettings)
- Copies points from one dataset to positions defined by another (source into target).
- Key fields:
CopyMode(FixedRotation, InheritRotation, etc.).
Combine Points (UPCGCombinePointsSettings)
- Merges two point datasets into one output.
Collapse (UPCGCollapseSettings)
- Merges nearby points into single representatives.
- Key fields:
CollapseRadius.
Attract (UPCGAttractSettings)
- Displaces points toward or away from attractor points.
- Key fields:
Weight,Radius,FalloffType.
Bounds Modifier (UPCGBoundsModifierSettings)
- Adjusts
BoundsMin/BoundsMaxper point. - Key fields:
BoundsMode(Set, Add, Scale),Bounds.
Apply Scale to Bounds (UPCGApplyScaleToBoundsSettings)
- Applies point scale to its bounds for accurate spatial queries.
---
Grammar (EPCGSettingsType::Generic — Grammar namespace)
Spline to Segment (UPCGSplineToSegmentSettings)
- Converts a spline into discrete linear segments (points at control points).
Subdivide Spline (UPCGSubdivideSplineSettings)
- Inserts additional control points along a spline at regular intervals.
Subdivide Segment (UPCGSubdivideSegmentSettings)
- Subdivides linear segments into smaller sub-segments.
Select Grammar (UPCGSelectGrammarSettings)
- Applies L-system–style grammar rules to select/transform points.
Duplicate Cross Sections (UPCGDuplicateCrossSectionsSettings)
- Duplicates points at cross-section intervals along a spline.
---
Blueprint Custom Nodes (EPCGSettingsType::Blueprint)
Derive from UPCGBlueprintBaseElement. Key configuration:
UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Settings|Input & Output")
TArray<FPCGPinProperties> CustomInputPins;
UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Settings|Input & Output")
TArray<FPCGPinProperties> CustomOutputPins;
UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = Settings)
bool bIsCacheable = false; // false if node creates actors/components
UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = Settings)
bool bRequiresGameThread = true; // true for actor spawn, component addThe Execute function signature:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PCG|Execution")
void Execute(const FPCGDataCollection& Input, FPCGDataCollection& Output);---
Graph Parameter System
Graph-level parameters (FInstancedPropertyBag UserParameters on UPCGGraph) allow exposing typed parameters to instances and Blueprints.
// Read a parameter by name (typed template)
TValueOrError<float, EPropertyBagResult> Result =
Graph->GetGraphParameter<float>(TEXT("SpawnRadius"));
if (Result.HasValue())
{
float Radius = Result.GetValue();
}
// Write a parameter
Graph->SetGraphParameter<float>(TEXT("SpawnRadius"), 250.f);
// For instances, overrides are tracked per-property:
GraphInstance->UpdatePropertyOverride(Property, /*bMarkAsOverridden=*/true);
GraphInstance->ResetPropertyToDefault(Property);
bool bOverridden = GraphInstance->IsPropertyOverridden(Property);Parameter change events (EPCGGraphParameterEvent): GraphChanged, GraphPostLoad, Added, RemovedUnused, RemovedUsed, PropertyMoved, PropertyRenamed, PropertyTypeModified, ValueModifiedLocally, ValueModifiedByParent, MultiplePropertiesAdded, UndoRedo, CategoryChanged.
---
PCGComponent Generation Modes
From EPCGComponentGenerationTrigger:
| Value | Behavior |
|---|---|
GenerateOnLoad | Generates once when component registers (BeginPlay or editor load) |
GenerateOnDemand | Only generates when Generate() or GenerateLocal() called explicitly |
GenerateAtRuntime | Managed by UPCGSubsystem runtime scheduler; budget-limited per frame |
Input source (EPCGComponentInput):
Actor— uses the owning actor's bounds and componentsLandscape— uses the landscape as the primary spatial inputOther— customUPCGDataprovided programmatically
Dirty flags (EPCGComponentDirtyFlag): Actor, Landscape, Input, Data, All. Call NotifyPropertiesChangedFromBlueprint() to mark dirty and trigger conditional regeneration.
---
Hierarchical Generation (HiGen)
Enable on UPCGGraph:
bool bUseHierarchicalGeneration = true;
EPCGHiGenGrid HiGenGridSize = EPCGHiGenGrid::Grid256; // default grid cell size
uint32 HiGenExponential = 0; // shifts grid sizes up by this exponent
bool bUse2DGrid = true; // 2D grid (XY plane) vs 3D volumetricGrid sizes available: Grid16, Grid32, Grid64, Grid128, Grid256, Grid512, Grid1024, Grid2048, GridUnbounded.
Nodes run at the minimum of all incoming data grid sizes. Use Get Grid Size nodes to force execution at a specific resolution.
---
ISM Descriptor (PCG Spawner)
FPCGProceduralISMComponentDescriptor (USTRUCT from Components/PCGProceduralISMComponentDescriptor.h) controls per-mesh ISM properties when spawned by the Static Mesh Spawner node:
Key settings mirrored from UInstancedStaticMeshComponent:
StaticMesh— mesh assetOverrideMaterials— material slotsInstanceStartCullDistance/InstanceEndCullDistanceInstanceLODDistanceScalebUseGpuLodSelectionNumCustomDataFloats— per-instance float channels- Collision preset, body instance settings
---
Compute (GPU) Nodes
PCG supports GPU-accelerated nodes via UPCGComputeKernel (UE 5.4+). These nodes implement EPCGSettingsType::GPU and execute HLSL kernels on the GPU point buffer. Useful for mass transforms, noise sampling, or attribute operations at millions of points.
Key classes: UPCGComputeKernel, UPCGComputeSource, FPCGDataBinding, FPCGDataDescription.
GPU nodes are identified by returning EPCGSettingsType::GPU from GetType() (override in your settings class) and must reference a UPCGComputeKernel-derived kernel asset with matching data binding descriptors.
---
Determinism Checklist
For reproducible procedural results: 1. Set a fixed Seed on the UPCGComponent (or use actor position as seed input). 2. Use GetSeedWithContext in custom Blueprint nodes rather than FMath::Rand. 3. Ensure custom nodes set bIsCacheable = true when outputs depend only on inputs + seed. 4. For multiplayer: use Generate(bForce) (NetMulticast) not GenerateLocal. 5. Sort input point arrays before processing — order from GetActorPCGData is not guaranteed to be stable across platforms.
Procedural Mesh Patterns
Common algorithmic patterns for runtime mesh and content generation in Unreal Engine using UProceduralMeshComponent, UInstancedStaticMeshComponent, and supporting math utilities.
---
Component Setup Boilerplate
// Header — MyProceduralActor.h
#pragma once
#include "GameFramework/Actor.h"
#include "ProceduralMeshComponent.h"
#include "Components/InstancedStaticMeshComponent.h"
#include "Components/SplineComponent.h"
#include "MyProceduralActor.generated.h"
UCLASS()
class AMyProceduralActor : public AActor
{
GENERATED_BODY()
public:
AMyProceduralActor();
UPROPERTY(VisibleAnywhere)
UProceduralMeshComponent* ProceduralMesh;
UPROPERTY(VisibleAnywhere)
UHierarchicalInstancedStaticMeshComponent* HISM;
UPROPERTY(VisibleAnywhere)
USplineComponent* Spline;
};
// Source — MyProceduralActor.cpp
#include "MyProceduralActor.h"
AMyProceduralActor::AMyProceduralActor()
{
PrimaryActorTick.bCanEverTick = false;
ProceduralMesh = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("ProceduralMesh"));
SetRootComponent(ProceduralMesh);
ProceduralMesh->bUseComplexAsSimpleCollision = false; // Use dedicated collision shapes
HISM = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("HISM"));
HISM->SetupAttachment(RootComponent);
HISM->SetNumCustomDataFloats(2); // Reserve per-instance float channels
Spline = CreateDefaultSubobject<USplineComponent>(TEXT("Spline"));
Spline->SetupAttachment(RootComponent);
}---
1. Flat Quad Grid (Terrain Base)
Generates a simple flat or height-mapped mesh from a 2D grid of vertices.
// GridSize = number of cells per side. Vertex count = (GridSize+1)^2.
// Preallocate for performance.
void ATerrainActor::GenerateFlatGrid(int32 GridSize, float CellSize,
bool bCreateCollision)
{
const int32 VertexStride = GridSize + 1;
const int32 VertCount = VertexStride * VertexStride;
const int32 TriCount = GridSize * GridSize * 6;
TArray<FVector> Vertices; Vertices.Reserve(VertCount);
TArray<int32> Triangles; Triangles.Reserve(TriCount);
TArray<FVector> Normals; Normals.Reserve(VertCount);
TArray<FVector2D> UVs; UVs.Reserve(VertCount);
TArray<FColor> Colors;
TArray<FProcMeshTangent> Tangents;
for (int32 Row = 0; Row <= GridSize; Row++)
{
for (int32 Col = 0; Col <= GridSize; Col++)
{
float X = Col * CellSize;
float Y = Row * CellSize;
float Z = 0.f; // Replace with height sample for terrain
Vertices.Add(FVector(X, Y, Z));
Normals.Add(FVector::UpVector);
UVs.Add(FVector2D((float)Col / GridSize, (float)Row / GridSize));
}
}
for (int32 Row = 0; Row < GridSize; Row++)
{
for (int32 Col = 0; Col < GridSize; Col++)
{
int32 BL = Row * VertexStride + Col;
int32 BR = BL + 1;
int32 TL = BL + VertexStride;
int32 TR = TL + 1;
// Counter-clockwise = front face in UE
Triangles.Add(BL); Triangles.Add(TL); Triangles.Add(TR);
Triangles.Add(BL); Triangles.Add(TR); Triangles.Add(BR);
}
}
ProceduralMesh->CreateMeshSection(0, Vertices, Triangles, Normals,
UVs, Colors, Tangents, bCreateCollision);
}Height-Mapped Terrain with Normal Recalculation
float SampleHeight(float X, float Y, float Scale, int32 Seed)
{
// Seeded offset to vary noise field per-seed
float OffsetX = (float)(Seed % 1000) * 0.01f;
float OffsetY = (float)(Seed / 1000) * 0.01f;
return SampleOctaveNoise(X / Scale + OffsetX, Y / Scale + OffsetY, 5, 0.5f, 2.0f, 1.0f);
}
void RecalculateNormals(const TArray<FVector>& Vertices, const TArray<int32>& Triangles,
TArray<FVector>& OutNormals)
{
OutNormals.Init(FVector::ZeroVector, Vertices.Num());
for (int32 i = 0; i + 2 < Triangles.Num(); i += 3)
{
const FVector& A = Vertices[Triangles[i]];
const FVector& B = Vertices[Triangles[i + 1]];
const FVector& C = Vertices[Triangles[i + 2]];
FVector Normal = FVector::CrossProduct(B - A, C - A).GetSafeNormal();
OutNormals[Triangles[i]] += Normal;
OutNormals[Triangles[i + 1]] += Normal;
OutNormals[Triangles[i + 2]] += Normal;
}
for (FVector& N : OutNormals)
{
N = N.GetSafeNormal();
}
}---
2. Marching Cubes (Voxel Isosurface)
Extracts a triangulated isosurface from a 3D scalar field. Used for caves, asteroids, destructible terrain.
Scalar Field Setup
// Density grid: negative = solid, positive = air, zero = surface
struct FDensityGrid
{
TArray<float> Values;
FIntVector Size; // Width x Height x Depth
float VoxelSize;
float Sample(int32 X, int32 Y, int32 Z) const
{
if (X < 0 || Y < 0 || Z < 0 ||
X >= Size.X || Y >= Size.Y || Z >= Size.Z)
return 1.f; // Outside = air
return Values[Z * Size.Y * Size.X + Y * Size.X + X];
}
FVector WorldPos(int32 X, int32 Y, int32 Z) const
{
return FVector(X, Y, Z) * VoxelSize;
}
};Edge Interpolation
FVector InterpolateEdge(FVector P0, float V0, FVector P1, float V1)
{
// Linear interpolation to find zero crossing
float t = FMath::Clamp(-V0 / (V1 - V0 + SMALL_NUMBER), 0.f, 1.f);
return FMath::Lerp(P0, P1, t);
}Cube Processing
// EdgeTable and TriTable are standard 256-entry lookup tables from the original
// Lorensen & Cline (1987) paper. They map the 8-corner sign configuration
// to which edges the surface crosses and how to form triangles.
// These tables are typically 4KB total and stored as compile-time const arrays.
extern const int EdgeTable[256];
extern const int TriTable[256][16];
void ProcessCube(const FDensityGrid& Grid, int32 X, int32 Y, int32 Z,
TArray<FVector>& OutVerts, TArray<int32>& OutTris)
{
// Sample 8 cube corners
float CubeValues[8];
CubeValues[0] = Grid.Sample(X, Y, Z);
CubeValues[1] = Grid.Sample(X + 1, Y, Z);
CubeValues[2] = Grid.Sample(X + 1, Y + 1, Z);
CubeValues[3] = Grid.Sample(X, Y + 1, Z);
CubeValues[4] = Grid.Sample(X, Y, Z + 1);
CubeValues[5] = Grid.Sample(X + 1, Y, Z + 1);
CubeValues[6] = Grid.Sample(X + 1, Y + 1, Z + 1);
CubeValues[7] = Grid.Sample(X, Y + 1, Z + 1);
FVector Corners[8];
Corners[0] = Grid.WorldPos(X, Y, Z);
Corners[1] = Grid.WorldPos(X + 1, Y, Z);
Corners[2] = Grid.WorldPos(X + 1, Y + 1, Z);
Corners[3] = Grid.WorldPos(X, Y + 1, Z);
Corners[4] = Grid.WorldPos(X, Y, Z + 1);
Corners[5] = Grid.WorldPos(X + 1, Y, Z + 1);
Corners[6] = Grid.WorldPos(X + 1, Y + 1, Z + 1);
Corners[7] = Grid.WorldPos(X, Y + 1, Z + 1);
// Build 8-bit index from which corners are below iso-level (0)
int32 CubeIndex = 0;
for (int32 i = 0; i < 8; i++)
if (CubeValues[i] < 0.f) CubeIndex |= (1 << i);
if (EdgeTable[CubeIndex] == 0) return; // Fully inside or outside
// Compute intersection vertices on active edges
FVector EdgeVerts[12];
if (EdgeTable[CubeIndex] & 1) EdgeVerts[0] = InterpolateEdge(Corners[0], CubeValues[0], Corners[1], CubeValues[1]);
if (EdgeTable[CubeIndex] & 2) EdgeVerts[1] = InterpolateEdge(Corners[1], CubeValues[1], Corners[2], CubeValues[2]);
if (EdgeTable[CubeIndex] & 4) EdgeVerts[2] = InterpolateEdge(Corners[2], CubeValues[2], Corners[3], CubeValues[3]);
if (EdgeTable[CubeIndex] & 8) EdgeVerts[3] = InterpolateEdge(Corners[3], CubeValues[3], Corners[0], CubeValues[0]);
if (EdgeTable[CubeIndex] & 16) EdgeVerts[4] = InterpolateEdge(Corners[4], CubeValues[4], Corners[5], CubeValues[5]);
if (EdgeTable[CubeIndex] & 32) EdgeVerts[5] = InterpolateEdge(Corners[5], CubeValues[5], Corners[6], CubeValues[6]);
if (EdgeTable[CubeIndex] & 64) EdgeVerts[6] = InterpolateEdge(Corners[6], CubeValues[6], Corners[7], CubeValues[7]);
if (EdgeTable[CubeIndex] & 128) EdgeVerts[7] = InterpolateEdge(Corners[7], CubeValues[7], Corners[4], CubeValues[4]);
if (EdgeTable[CubeIndex] & 256) EdgeVerts[8] = InterpolateEdge(Corners[0], CubeValues[0], Corners[4], CubeValues[4]);
if (EdgeTable[CubeIndex] & 512) EdgeVerts[9] = InterpolateEdge(Corners[1], CubeValues[1], Corners[5], CubeValues[5]);
if (EdgeTable[CubeIndex] & 1024) EdgeVerts[10] = InterpolateEdge(Corners[2], CubeValues[2], Corners[6], CubeValues[6]);
if (EdgeTable[CubeIndex] & 2048) EdgeVerts[11] = InterpolateEdge(Corners[3], CubeValues[3], Corners[7], CubeValues[7]);
// Add triangles from TriTable
for (int32 i = 0; TriTable[CubeIndex][i] != -1; i += 3)
{
int32 BaseIdx = OutVerts.Num();
OutVerts.Add(EdgeVerts[TriTable[CubeIndex][i]]);
OutVerts.Add(EdgeVerts[TriTable[CubeIndex][i + 1]]);
OutVerts.Add(EdgeVerts[TriTable[CubeIndex][i + 2]]);
OutTris.Add(BaseIdx); OutTris.Add(BaseIdx + 1); OutTris.Add(BaseIdx + 2);
}
}Full Grid March
void MarchCubes(const FDensityGrid& Grid, UProceduralMeshComponent* Mesh)
{
TArray<FVector> Vertices, Normals;
TArray<int32> Triangles;
TArray<FVector2D> UVs;
TArray<FColor> Colors;
TArray<FProcMeshTangent> Tangents;
for (int32 Z = 0; Z < Grid.Size.Z - 1; Z++)
for (int32 Y = 0; Y < Grid.Size.Y - 1; Y++)
for (int32 X = 0; X < Grid.Size.X - 1; X++)
{
ProcessCube(Grid, X, Y, Z, Vertices, Triangles);
}
// Compute normals from triangles
RecalculateNormals(Vertices, Triangles, Normals);
// Pad UVs (can project based on position for triplanar)
UVs.SetNumZeroed(Vertices.Num());
for (int32 i = 0; i < Vertices.Num(); i++)
UVs[i] = FVector2D(Vertices[i].X, Vertices[i].Y) / Grid.VoxelSize;
Mesh->CreateMeshSection(0, Vertices, Triangles, Normals,
UVs, Colors, Tangents, /*bCreateCollision=*/true);
}---
3. Dungeon Room-and-Corridor Generation
BSP-based dungeon layout that partitions a rect into rooms and connects them.
Data Structures
struct FRoom
{
FIntRect Bounds; // X=left, Y=top, Width, Height in grid cells
FIntPoint Center() const
{
return FIntPoint(Bounds.Min.X + Bounds.Width() / 2,
Bounds.Min.Y + Bounds.Height() / 2);
}
};
struct FDungeonLevel
{
TArray<FRoom> Rooms;
TArray<TPair<FIntPoint, FIntPoint>> Corridors; // pairs of cell coords
TArray<TArray<uint8>> Tiles; // 0=wall, 1=floor, 2=corridor
};BSP Split
void SplitRect(const FIntRect& Rect, FRandomStream& Rand,
int32 MinSize, TArray<FIntRect>& OutLeaves)
{
int32 W = Rect.Width(), H = Rect.Height();
if (W < MinSize * 2 && H < MinSize * 2)
{
OutLeaves.Add(Rect);
return;
}
bool bSplitH = (W > H) ? true : (H > W) ? false : Rand.RandBool();
if (bSplitH && W >= MinSize * 2)
{
int32 Split = Rand.RandRange(MinSize, W - MinSize);
SplitRect(FIntRect(Rect.Min, FIntPoint(Rect.Min.X + Split, Rect.Max.Y)), Rand, MinSize, OutLeaves);
SplitRect(FIntRect(FIntPoint(Rect.Min.X + Split, Rect.Min.Y), Rect.Max), Rand, MinSize, OutLeaves);
}
else if (!bSplitH && H >= MinSize * 2)
{
int32 Split = Rand.RandRange(MinSize, H - MinSize);
SplitRect(FIntRect(Rect.Min, FIntPoint(Rect.Max.X, Rect.Min.Y + Split)), Rand, MinSize, OutLeaves);
SplitRect(FIntRect(FIntPoint(Rect.Min.X, Rect.Min.Y + Split), Rect.Max), Rand, MinSize, OutLeaves);
}
else
{
OutLeaves.Add(Rect);
}
}Room Placement and Corridor Carving
FDungeonLevel GenerateDungeon(int32 MapW, int32 MapH, int32 Seed,
int32 MinRoomSize = 5, int32 Padding = 1)
{
FRandomStream Rand(Seed);
FDungeonLevel Level;
// Initialize tile map
Level.Tiles.SetNum(MapH);
for (auto& Row : Level.Tiles)
Row.Init(0, MapW); // all walls
// BSP partition
TArray<FIntRect> Leaves;
SplitRect(FIntRect(0, 0, MapW, MapH), Rand, MinRoomSize + Padding * 2, Leaves);
// Place rooms in leaves
for (const FIntRect& Leaf : Leaves)
{
int32 MaxW = Leaf.Width() - Padding * 2;
int32 MaxH = Leaf.Height() - Padding * 2;
if (MaxW < MinRoomSize || MaxH < MinRoomSize) continue;
int32 RW = Rand.RandRange(MinRoomSize, MaxW);
int32 RH = Rand.RandRange(MinRoomSize, MaxH);
int32 RX = Leaf.Min.X + Padding + Rand.RandRange(0, MaxW - RW);
int32 RY = Leaf.Min.Y + Padding + Rand.RandRange(0, MaxH - RH);
FRoom Room;
Room.Bounds = FIntRect(RX, RY, RX + RW, RY + RH);
Level.Rooms.Add(Room);
// Carve floor tiles
for (int32 Y = RY; Y < RY + RH; Y++)
for (int32 X = RX; X < RX + RW; X++)
Level.Tiles[Y][X] = 1;
}
// Connect rooms with L-shaped corridors
for (int32 i = 1; i < Level.Rooms.Num(); i++)
{
FIntPoint A = Level.Rooms[i - 1].Center();
FIntPoint B = Level.Rooms[i].Center();
// Horizontal then vertical
int32 XDir = (B.X > A.X) ? 1 : -1;
for (int32 X = A.X; X != B.X; X += XDir)
{
Level.Tiles[A.Y][X] = 2;
Level.Corridors.Add({FIntPoint(X, A.Y), FIntPoint(X + XDir, A.Y)});
}
int32 YDir = (B.Y > A.Y) ? 1 : -1;
for (int32 Y = A.Y; Y != B.Y; Y += YDir)
{
Level.Tiles[Y][B.X] = 2;
Level.Corridors.Add({FIntPoint(B.X, Y), FIntPoint(B.X, Y + YDir)});
}
Level.Tiles[B.Y][B.X] = 2;
}
return Level;
}Tile-to-Mesh Conversion
void ADungeonActor::BuildMeshFromTiles(const FDungeonLevel& Level, float TileSize)
{
TArray<FVector> Vertices;
TArray<int32> Triangles;
TArray<FVector> Normals;
TArray<FVector2D> UVs;
TArray<FColor> Colors;
TArray<FProcMeshTangent> Tangents;
int32 H = Level.Tiles.Num();
int32 W = H > 0 ? Level.Tiles[0].Num() : 0;
for (int32 Row = 0; Row < H; Row++)
for (int32 Col = 0; Col < W; Col++)
{
if (Level.Tiles[Row][Col] == 0) continue; // Skip walls (or add wall mesh)
int32 Base = Vertices.Num();
float X0 = Col * TileSize;
float X1 = (Col + 1) * TileSize;
float Y0 = Row * TileSize;
float Y1 = (Row + 1) * TileSize;
Vertices.Add(FVector(X0, Y0, 0)); // 0 BL
Vertices.Add(FVector(X1, Y0, 0)); // 1 BR
Vertices.Add(FVector(X1, Y1, 0)); // 2 TR
Vertices.Add(FVector(X0, Y1, 0)); // 3 TL
Normals.Add(FVector::UpVector);
Normals.Add(FVector::UpVector);
Normals.Add(FVector::UpVector);
Normals.Add(FVector::UpVector);
UVs.Add(FVector2D(0, 0)); UVs.Add(FVector2D(1, 0));
UVs.Add(FVector2D(1, 1)); UVs.Add(FVector2D(0, 1));
Triangles.Add(Base); Triangles.Add(Base + 2); Triangles.Add(Base + 1);
Triangles.Add(Base); Triangles.Add(Base + 3); Triangles.Add(Base + 2);
}
ProceduralMesh->CreateMeshSection(0, Vertices, Triangles, Normals,
UVs, Colors, Tangents, /*bCreateCollision=*/true);
}---
4. L-System Vegetation
L-systems expand a string through production rules and interpret characters as 3D drawing commands (turtle graphics) to produce branching structures.
Axiom and Rules
struct FLSystemRules
{
FString Axiom = "F";
TMap<TCHAR, FString> Rules = {
{'F', TEXT("F[+F]F[-F]F")} // Standard plant
};
int32 Iterations = 4;
float AngleDeg = 25.f;
float SegmentLen = 50.f;
float LenDecay = 0.7f; // Each recursion level shortens segments
float WidthStart = 8.f;
float WidthDecay = 0.6f;
};
FString ExpandLSystem(const FLSystemRules& Rules)
{
FString Current = Rules.Axiom;
for (int32 Iter = 0; Iter < Rules.Iterations; Iter++)
{
FString Next;
Next.Reserve(Current.Len() * 3);
for (TCHAR C : Current)
{
const FString* Replacement = Rules.Rules.Find(C);
if (Replacement) Next.Append(*Replacement);
else Next.AppendChar(C);
}
Current = MoveTemp(Next);
}
return Current;
}Turtle Interpreter to HISM
struct FTurtleState
{
FVector Position = FVector::ZeroVector;
FRotator Rotation = FRotator::ZeroRotator;
float Length = 50.f;
float Width = 8.f;
};
void InterpretLSystem(const FString& LString, const FLSystemRules& Rules,
UHierarchicalInstancedStaticMeshComponent* BranchHISM,
UHierarchicalInstancedStaticMeshComponent* LeafHISM)
{
TArray<FTurtleState> Stack;
FTurtleState State;
State.Length = Rules.SegmentLen;
State.Width = Rules.WidthStart;
for (TCHAR C : LString)
{
switch (C)
{
case 'F':
{
FVector Forward = State.Rotation.Vector() * State.Length;
FVector End = State.Position + Forward;
// Place branch segment
FTransform T;
T.SetLocation((State.Position + End) * 0.5f);
T.SetRotation(State.Rotation.Quaternion());
T.SetScale3D(FVector(State.Width * 0.01f, State.Width * 0.01f,
State.Length * 0.01f));
BranchHISM->AddInstance(T, /*bWorldSpace=*/false);
State.Position = End;
break;
}
case '+': State.Rotation.Yaw += Rules.AngleDeg; break;
case '-': State.Rotation.Yaw -= Rules.AngleDeg; break;
case '&': State.Rotation.Pitch += Rules.AngleDeg; break;
case '^': State.Rotation.Pitch -= Rules.AngleDeg; break;
case '/': State.Rotation.Roll += Rules.AngleDeg; break;
case '\\':State.Rotation.Roll -= Rules.AngleDeg; break;
case '[':
Stack.Push(State);
State.Length *= Rules.LenDecay;
State.Width *= Rules.WidthDecay;
break;
case ']':
// Place leaf at branch tip before popping
if (LeafHISM)
{
FTransform LT;
LT.SetLocation(State.Position);
LT.SetRotation(State.Rotation.Quaternion());
LeafHISM->AddInstance(LT, /*bWorldSpace=*/false);
}
State = Stack.Pop();
break;
}
}
}---
5. Wave Function Collapse (Grid Layout)
WFC fills a grid by choosing tiles that satisfy adjacency constraints. Suitable for dungeon rooms, city blocks, terrain biome transitions.
Tile and Constraint Definition
// Each tile has a set of valid neighbor tile IDs per direction
struct FWFCTile
{
int32 ID;
float Weight; // Relative spawn probability
TArray<int32> AllowedRight; // IDs allowed to the +X neighbor
TArray<int32> AllowedLeft; // IDs allowed to the -X neighbor
TArray<int32> AllowedUp; // IDs allowed to the +Y neighbor
TArray<int32> AllowedDown; // IDs allowed to the -Y neighbor
};
// Cell state during WFC
struct FWFCCell
{
TArray<int32> PossibleTiles; // Remaining valid tile IDs
bool bCollapsed = false;
int32 CollapsedTile = -1;
bool IsContradiction() const { return PossibleTiles.Num() == 0 && !bCollapsed; }
float Entropy() const { return (float)PossibleTiles.Num(); } // Simplified (no weights)
};WFC Iteration
bool WFCStep(TArray<TArray<FWFCCell>>& Grid,
const TArray<FWFCTile>& Tiles,
FRandomStream& Rand,
int32 Width, int32 Height)
{
// 1. Find uncollapsed cell with lowest entropy
float MinEntropy = FLT_MAX;
FIntPoint CollapsePos(-1, -1);
for (int32 Y = 0; Y < Height; Y++)
for (int32 X = 0; X < Width; X++)
{
FWFCCell& Cell = Grid[Y][X];
if (Cell.bCollapsed) continue;
if (Cell.IsContradiction()) return false; // Contradiction — need backtrack
if (Cell.Entropy() < MinEntropy)
{
MinEntropy = Cell.Entropy();
CollapsePos = FIntPoint(X, Y);
}
}
if (CollapsePos.X < 0) return true; // All cells collapsed
// 2. Collapse: choose a tile weighted by tile weight
FWFCCell& Cell = Grid[CollapsePos.Y][CollapsePos.X];
float TotalWeight = 0.f;
for (int32 TileID : Cell.PossibleTiles)
TotalWeight += Tiles[TileID].Weight;
float Pick = Rand.FRandRange(0.f, TotalWeight);
float Accum = 0.f;
int32 ChosenTile = Cell.PossibleTiles[0];
for (int32 TileID : Cell.PossibleTiles)
{
Accum += Tiles[TileID].Weight;
if (Accum >= Pick) { ChosenTile = TileID; break; }
}
Cell.bCollapsed = true;
Cell.CollapsedTile = ChosenTile;
Cell.PossibleTiles = { ChosenTile };
// 3. Propagate constraints to neighbors (BFS)
TQueue<FIntPoint> PropagateQueue;
PropagateQueue.Enqueue(CollapsePos);
while (!PropagateQueue.IsEmpty())
{
FIntPoint P;
PropagateQueue.Dequeue(P);
auto Propagate = [&](FIntPoint Neighbor,
TFunctionRef<const TArray<int32>*(const FWFCTile&)> GetAllowed)
{
if (Neighbor.X < 0 || Neighbor.X >= Width ||
Neighbor.Y < 0 || Neighbor.Y >= Height) return;
FWFCCell& NCell = Grid[Neighbor.Y][Neighbor.X];
if (NCell.bCollapsed) return;
// Collect all tiles allowed by current cell's possible set
TSet<int32> AllowedSet;
for (int32 TileID : Grid[P.Y][P.X].PossibleTiles)
{
const TArray<int32>* Allowed = GetAllowed(Tiles[TileID]);
if (Allowed) AllowedSet.Append(*Allowed);
}
// Remove incompatible tiles from neighbor
int32 PrevCount = NCell.PossibleTiles.Num();
NCell.PossibleTiles.RemoveAll([&](int32 ID) { return !AllowedSet.Contains(ID); });
if (NCell.PossibleTiles.Num() != PrevCount)
PropagateQueue.Enqueue(Neighbor);
};
Propagate({P.X + 1, P.Y}, [](const FWFCTile& T) { return &T.AllowedRight; });
Propagate({P.X - 1, P.Y}, [](const FWFCTile& T) { return &T.AllowedLeft; });
Propagate({P.X, P.Y + 1}, [](const FWFCTile& T) { return &T.AllowedUp; });
Propagate({P.X, P.Y - 1}, [](const FWFCTile& T) { return &T.AllowedDown; });
}
return true;
}Grid Initialization and Run
void RunWFC(int32 Width, int32 Height, const TArray<FWFCTile>& Tiles,
FRandomStream& Rand, TArray<TArray<FWFCCell>>& OutGrid)
{
TArray<int32> AllTileIDs;
for (const FWFCTile& T : Tiles) AllTileIDs.Add(T.ID);
OutGrid.SetNum(Height);
for (auto& Row : OutGrid)
{
Row.SetNum(Width);
for (FWFCCell& Cell : Row)
Cell.PossibleTiles = AllTileIDs;
}
bool bSuccess = false;
for (int32 MaxSteps = Width * Height; MaxSteps > 0; MaxSteps--)
{
bSuccess = WFCStep(OutGrid, Tiles, Rand, Width, Height);
if (!bSuccess) break; // Contradiction: re-run with different seed
// Check all cells collapsed
bool bDone = true;
for (auto& Row : OutGrid)
for (auto& Cell : Row)
if (!Cell.bCollapsed) { bDone = false; break; }
if (bDone) { bSuccess = true; break; }
}
}---
6. Async Mesh Generation Pattern
For large meshes, compute vertex data on a background thread then apply on the game thread.
void AProceduralTerrain::GenerateAsync(int32 GridSize, float CellSize)
{
// Capture data needed on background thread
int32 Seed = SeedValue;
// Lambda runs on a background worker thread
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, GridSize, CellSize, Seed]()
{
TArray<FVector> Vertices;
TArray<int32> Triangles;
TArray<FVector> Normals;
TArray<FVector2D> UVs;
// -- heavy computation here --
// GenerateTerrainData(GridSize, CellSize, Seed, Vertices, Triangles, Normals, UVs);
// Schedule the mesh section creation back on the game thread
AsyncTask(ENamedThreads::GameThread, [this,
V = MoveTemp(Vertices),
T = MoveTemp(Triangles),
N = MoveTemp(Normals),
UV = MoveTemp(UVs)]() mutable
{
if (!IsValid(this) || !IsValid(ProceduralMesh)) return;
TArray<FColor> Colors;
TArray<FProcMeshTangent> Tangents;
ProceduralMesh->CreateMeshSection(0, V, T, N,
UV, Colors, Tangents,
/*bCreateCollision=*/true);
});
});
}Note: UProceduralMeshComponent::CreateMeshSection must be called on the game thread. Only the data computation can be parallelized.
---
7. Spline-Driven Road Mesh
Generates a road mesh by extruding a cross-section profile along a USplineComponent.
void ARoadMeshActor::BuildRoad(float RoadWidth, float SegmentLength)
{
TArray<FVector> Vertices;
TArray<int32> Triangles;
TArray<FVector> Normals;
TArray<FVector2D> UVs;
TArray<FColor> Colors;
TArray<FProcMeshTangent> Tangents;
float TotalLen = Spline->GetSplineLength();
float UVProgress = 0.f;
int32 SegCount = FMath::CeilToInt(TotalLen / SegmentLength);
float ActualSeg = TotalLen / SegCount;
// Extrude cross-section quads along spline
for (int32 Seg = 0; Seg <= SegCount; Seg++)
{
float Dist = Seg * ActualSeg;
FTransform T = Spline->GetTransformAtDistanceAlongSpline(
Dist, ESplineCoordinateSpace::World, /*bUseScale=*/false);
FVector Center = T.GetLocation();
FVector Right = T.GetRotation().GetRightVector();
FVector Up = T.GetRotation().GetUpVector();
FVector Left_V = Center - Right * RoadWidth * 0.5f;
FVector Right_V = Center + Right * RoadWidth * 0.5f;
Vertices.Add(Left_V);
Vertices.Add(Right_V);
Normals.Add(Up);
Normals.Add(Up);
UVs.Add(FVector2D(0.f, UVProgress));
UVs.Add(FVector2D(1.f, UVProgress));
if (Seg > 0)
{
int32 B = (Seg - 1) * 2;
int32 T2 = Seg * 2;
// Left triangle
Triangles.Add(B); Triangles.Add(T2); Triangles.Add(B + 1);
// Right triangle
Triangles.Add(B + 1); Triangles.Add(T2); Triangles.Add(T2 + 1);
}
UVProgress += ActualSeg / RoadWidth; // Scale UV to aspect ratio
}
ProceduralMesh->CreateMeshSection(0, Vertices, Triangles, Normals,
UVs, Colors, Tangents, /*bCreateCollision=*/true);
ProceduralMesh->SetMaterial(0, RoadMaterial);
}---
Performance Reference
| Scenario | Recommended Approach | Notes |
|---|---|---|
| < 100 dynamic instances, moving | ISM + UpdateInstanceTransform | Simple, low overhead |
| 100–10,000 static instances | HISM | Culling hierarchy, LOD transitions |
| > 10,000 static instances | HISM + InstanceEndCullDistance | Aggressive distance culling |
| Terrain (runtime, < 256x256) | UProceduralMeshComponent | One CreateMeshSection call |
| Terrain (large, static) | Landscape or PCG + ISM | PCG + HISM spawner avoids draw calls |
| Cave/voxel (< 64x64x64) | Marching Cubes + ProcMesh | Async generation on background thread |
| Vegetation (open world) | PCG Surface Sampler + HISM Spawner | Budget with HiGen grid |
| Dungeon layout | WFC or BSP + tile mesh | Bake to static at load, or keep ProcMesh |
| Spline road/river | SplineMeshComponent per segment | Or extruded ProcMesh for custom profile |
---
9. Poisson Disc Sampling
Generates uniformly distributed points with minimum separation distance (Bridson 2007). Use for natural-looking placement (trees, rocks, enemies) without clumping.
// Poisson disc sampling — Bridson's fast algorithm
TArray<FVector2D> PoissonDiscSample(FVector2D Min, FVector2D Max,
float MinDist, int32 MaxAttempts, FRandomStream& Rand)
{
float CellSize = MinDist / FMath::Sqrt(2.f);
FVector2D Size = Max - Min;
int32 GW = FMath::CeilToInt(Size.X / CellSize);
int32 GH = FMath::CeilToInt(Size.Y / CellSize);
TArray<int32> Grid;
Grid.Init(-1, GW * GH);
TArray<FVector2D> Points, Active;
// Seed with first random point
FVector2D First(Rand.FRandRange(Min.X, Max.X), Rand.FRandRange(Min.Y, Max.Y));
Points.Add(First);
Active.Add(First);
Grid[FMath::FloorToInt((First.Y - Min.Y) / CellSize) * GW +
FMath::FloorToInt((First.X - Min.X) / CellSize)] = 0;
while (Active.Num() > 0)
{
int32 Idx = Rand.RandRange(0, Active.Num() - 1);
FVector2D Base = Active[Idx];
bool bFound = false;
for (int32 k = 0; k < MaxAttempts; k++)
{
float Angle = Rand.FRandRange(0.f, 2.f * PI);
float R = Rand.FRandRange(MinDist, 2.f * MinDist);
FVector2D Candidate = Base + FVector2D(FMath::Cos(Angle), FMath::Sin(Angle)) * R;
if (Candidate.X < Min.X || Candidate.X > Max.X ||
Candidate.Y < Min.Y || Candidate.Y > Max.Y)
continue;
int32 GX = FMath::FloorToInt((Candidate.X - Min.X) / CellSize);
int32 GY = FMath::FloorToInt((Candidate.Y - Min.Y) / CellSize);
bool bTooClose = false;
for (int32 DY = -2; DY <= 2 && !bTooClose; DY++)
{
for (int32 DX = -2; DX <= 2 && !bTooClose; DX++)
{
int32 NX = GX + DX, NY = GY + DY;
if (NX < 0 || NX >= GW || NY < 0 || NY >= GH) continue;
int32 PIdx = Grid[NY * GW + NX];
if (PIdx >= 0 && FVector2D::Distance(Points[PIdx], Candidate) < MinDist)
bTooClose = true;
}
}
if (!bTooClose)
{
Grid[GY * GW + GX] = Points.Num();
Points.Add(Candidate);
Active.Add(Candidate);
bFound = true;
break;
}
}
if (!bFound) Active.RemoveAtSwap(Idx);
}
return Points;
}Usage with ISM placement:
FRandomStream Stream(MySeed);
TArray<FVector2D> Placements = PoissonDiscSample(
FVector2D(0, 0), FVector2D(10000, 10000), 200.f, 30, Stream);
for (const FVector2D& Pos : Placements)
{
FTransform T(FRotator(0, Stream.FRandRange(0, 360), 0),
FVector(Pos.X, Pos.Y, GetGroundHeight(Pos)));
TreeISM->AddInstance(T);
}Key properties: O(n) time complexity, guaranteed minimum separation, deterministic with FRandomStream seed. Increase MaxAttempts (default 30) for denser packing; decrease for faster generation.
Related skills
FAQ
Which Unreal Engine version does ue-procedural-generation target?
ue-procedural-generation targets Unreal Engine 5.2 and later, focusing on the PCG plugin framework alongside ProceduralMeshComponent, ISM or HISM instancing, and spline-driven generation in C++ or Blueprint.
What reference files ship with ue-procedural-generation?
ue-procedural-generation bundles pcg-node-reference.md for PCG node types, pin labels, and determinism checks, plus procedural-mesh-patterns.md covering marching cubes, dungeon BSP, L-system, WFC, async mesh, and spline road patterns.