ue-procedural-generation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chineseue-procedural-generation
Unreal Engine程序化生成
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.
你是Unreal Engine程序化生成系统的专家,涵盖PCG框架、ProceduralMeshComponent、实例化静态网格、噪声函数和基于样条曲线的生成。
Context Check
上下文检查
Before advising, read to determine:
.agents/ue-project-context.md- 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)
在提供建议前,请阅读以确定:
.agents/ue-project-context.md- PCG插件是否已启用(插件列表)
- 目标生成类型:世界布局、地形、地牢、植被、运行时网格
- 性能约束(移动平台、主机、是否启用Nanite)
- 多人游戏需求(服务器权威 vs 确定性种子)
Information Gathering
信息收集
Ask for clarification on:
- Generation type: world population (PCG), runtime mesh (ProceduralMeshComponent), instanced geometry (ISM/HISM), or spline-driven?
- Timing: editor-time baked result or runtime dynamic generation?
- Instance count: hundreds (ISM) or tens of thousands (HISM)?
- Collision: does generated geometry need physics collision?
- Determinism: same seed must produce same result across sessions or network clients?
请询问以下信息以明确需求:
- 生成类型:世界布局(PCG)、运行时网格(ProceduralMeshComponent)、实例化几何体(ISM/HISM)还是样条曲线驱动?
- 时机:编辑器阶段烘焙结果还是运行时动态生成?
- 实例数量:数百个(ISM)还是数万个(HISM)?
- 碰撞:生成的几何体是否需要物理碰撞?
- 确定性:相同种子是否必须在不同会话或网络客户端中产生相同结果?
1. PCG Framework (UE 5.2+)
1. PCG框架(UE 5.2+)
Node-based rule-driven world generation. Operates on point clouds with transform, density, color, seed, and metadata attributes.
基于节点的规则驱动型世界生成系统,针对包含变换、密度、颜色、种子和元数据属性的点云进行操作。
Plugin Setup
插件设置
csharp
// Build.cs
PublicDependencyModuleNames.Add("PCG");json
// .uproject Plugins array
{ "Name": "PCG", "Enabled": true }csharp
// Build.cs
PublicDependencyModuleNames.Add("PCG");json
// .uproject Plugins array
{ "Name": "PCG", "Enabled": true }Core Classes
核心类
| Class | Header | Purpose |
|---|---|---|
| | Actor component driving generation |
| | Asset: nodes + edges |
| | Graph instance with parameter overrides |
| | Point cloud between nodes |
| | Node settings base class |
| | Custom Blueprint node base |
| 类 | 头文件 | 用途 |
|---|---|---|
| | 驱动生成的Actor组件 |
| | 资源:节点+连线 |
| | 带有参数覆盖的Graph实例 |
| | 节点间传递的点云数据 |
| | 节点设置基类 |
| | 自定义Blueprint节点基类 |
UPCGComponent Key API (from PCGComponent.h
)
PCGComponent.hUPCGComponent核心API(来自PCGComponent.h
)
PCGComponent.hcpp
// 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- — one-shot on BeginPlay
GenerateOnLoad - — explicit
GenerateOnDemandcall onlyGenerate() - — budget-scheduled by
GenerateAtRuntimeUPCGSubsystem
cpp
// 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;生成触发方式():
EPCGComponentGenerationTrigger- — 在BeginPlay时一次性生成
GenerateOnLoad - — 仅通过显式调用
GenerateOnDemand触发Generate() - — 由
GenerateAtRuntime根据预算调度生成UPCGSubsystem
UPCGGraph Node API (from PCGGraph.h
)
PCGGraph.hUPCGGraph节点API(来自PCGGraph.h
)
PCGGraph.hcpp
// 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);cpp
// 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
自定义Blueprint PCG节点
Derive from :
UPCGBlueprintBaseElementcpp
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 properties:
UPCGBlueprintBaseElement- — when node spawns actors or components
bIsCacheable = false - — for actor spawn, component add
bRequiresGameThread = true - /
CustomInputPins— extra typed pinsCustomOutputPins
继承自:
UPCGBlueprintBaseElementcpp
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;
}UPCGBlueprintBaseElement- — 当节点生成Actor或组件时设置
bIsCacheable = false - — 用于生成Actor、添加组件的场景
bRequiresGameThread = true - /
CustomInputPins— 额外的类型化引脚CustomOutputPins
PCG Determinism
PCG的确定性
PCG graphs are deterministic by default — the same seed produces identical output. Each node receives a seeded random stream via . To vary output across instances, set the PCG component's property. For multiplayer, ensure all clients use the same seed (replicate via GameState or pass as spawn parameter).
GetRandomStreamWithContext()Seedcpp
// Set PCG seed at runtime for deterministic variation
UPCGComponent* PCG = FindComponentByClass<UPCGComponent>();
PCG->Seed = MyDeterministicSeedValue;
PCG->Generate(); // Regenerate with new seed默认情况下,PCG图是确定性的——相同的种子会产生完全相同的输出。每个节点通过获取一个带种子的随机流。若要在不同实例间生成不同输出,请设置PCG组件的属性。对于多人游戏,确保所有客户端使用相同的种子(可通过GameState同步或作为生成参数传递)。
GetRandomStreamWithContext()Seedcpp
// Set PCG seed at runtime for deterministic variation
UPCGComponent* PCG = FindComponentByClass<UPCGComponent>();
PCG->Seed = MyDeterministicSeedValue;
PCG->Generate(); // Regenerate with new seedPCG Data Types
PCG数据类型
| Type | Contains | Use for |
|---|---|---|
| Position, rotation, scale, density, color | Scatter placement, foliage, instance positioning |
| Spline points + tangents | Roads, rivers, paths, boundary definitions |
| Height + layer weight sampling | Terrain-aware placement, biome queries |
| 3D bounds | Volume-based filtering and generation |
Point data is the most common — most PCG nodes consume and produce point collections. Also available: , , .
UPCGTextureDataUPCGPrimitiveDataUPCGDynamicMeshDataSee for all node types, settings fields, and pin labels.
references/pcg-node-reference.md| 类型 | 包含内容 | 用途 |
|---|---|---|
| 位置、旋转、缩放、密度、颜色 | 散射放置、植被、实例定位 |
| 样条点+切线 | 道路、河流、路径、边界定义 |
| 高度+层权重采样 | 地形感知放置、生物群系查询 |
| 3D边界 | 基于体积的过滤与生成 |
点数据是最常用的类型——大多数PCG节点都会消费和生成点集合。此外还有、、等类型。
UPCGTextureDataUPCGPrimitiveDataUPCGDynamicMeshDataPCG节点类型、设置字段和引脚标签详情请参考。
references/pcg-node-reference.md2. ProceduralMeshComponent
2. ProceduralMeshComponent
csharp
// Build.cs
PublicDependencyModuleNames.Add("ProceduralMeshComponent");csharp
// Build.cs
PublicDependencyModuleNames.Add("ProceduralMeshComponent");Core API
核心API
cpp
// 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);cpp
// 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
地形网格示例
cpp
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);
}cpp
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 . Keep vertex count < 65K per section.
CreateMeshSection - updates vertex positions and collision (if enabled) but cannot change topology — call
UpdateMeshSectionfor new triangles.CreateMeshSection - ProceduralMesh does not support Nanite.
- Compute vertex data on background thread; call on game thread only.
CreateMeshSection
- 每次调用会产生一次绘制调用,每个Section的顶点数请保持在65K以下。
CreateMeshSection - 仅更新顶点位置和碰撞(若已启用),无法修改拓扑结构——若需添加新三角形,请调用
UpdateMeshSection。CreateMeshSection - ProceduralMesh不支持Nanite。
- 顶点数据计算请在后台线程执行,仅在游戏线程调用。
CreateMeshSection
Async Mesh Generation
异步网格生成
Generate vertices on a background thread, then apply on the game thread:
cpp
// 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; }在后台线程生成顶点数据,然后在游戏线程应用:
cpp
// 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 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.
UProceduralMeshComponent::bUseComplexAsSimpleCollision = true设置可直接使用渲染三角形作为碰撞体,这种方式精度高但性能开销大——仅适用于静态几何体。对于动态或高多边形网格,请生成简化的凸包碰撞体。
UProceduralMeshComponent::bUseComplexAsSimpleCollision = true3. Instanced Static Meshes (ISM / HISM)
3. 实例化静态网格(ISM / HISM)
| Feature | ISM ( | HISM ( |
|---|---|---|
| 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 |
| 特性 | ISM( | HISM( |
|---|---|---|
| 适用场景 | 少于1000个动态实例 | 超过1000个以静态为主的实例 |
| 剔除方式 | 仅基于距离 | 层次化BVH+距离 |
| LOD | GPU选择 | 内置过渡效果 |
| 删除开销 | O(n) | 异步BVH重建 |
Key ISM API (from InstancedStaticMeshComponent.h
)
InstancedStaticMeshComponent.hISM核心API(来自InstancedStaticMeshComponent.h
)
InstancedStaticMeshComponent.hcpp
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: , , , .
InstanceStartCullDistanceInstanceEndCullDistanceInstanceLODDistanceScalebUseGpuLodSelectioncpp
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);剔除相关属性:、、、。
InstanceStartCullDistanceInstanceEndCullDistanceInstanceLODDistanceScalebUseGpuLodSelectionVegetation Scatter (HISM + Terrain Trace)
植被散射示例(HISM + 地形射线检测)
cpp
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();cpp
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 which internally wraps . For procedural foliage at scale, use with — it distributes foliage via simulation (species competition, shade tolerance) rather than manual painting.
AInstancedFoliageActorUHierarchicalInstancedStaticMeshComponentUProceduralFoliageComponentUProceduralFoliageSpawnerPer-instance collision: Enable 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.
bUseDefaultCollisionPlatform limits: HISM GPU buffer caps vary by platform (~1M on desktop, ~100K on mobile). Monitor with . Split large populations across multiple HISM components.
stat Foliage编辑器的植被绘制模式使用,它内部封装了。若要大规模生成程序化植被,请使用搭配——它通过模拟(物种竞争、耐阴性)来分布植被,而非手动绘制。
AInstancedFoliageActorUHierarchicalInstancedStaticMeshComponentUProceduralFoliageComponentUProceduralFoliageSpawner单实例碰撞:在ISM组件上启用,每个实例会继承静态网格的碰撞体。若需自定义单实例碰撞形状,请使用独立Actor——ISM不支持为每个实例设置独特的碰撞体。
bUseDefaultCollision平台限制:HISM的GPU缓冲区上限因平台而异(桌面平台约100万,移动平台约10万)。可使用命令监控。大型实例群请拆分到多个HISM组件中。
stat Foliage4. Noise and Math
4. 噪声与数学计算
cpp
// 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 pixel data via to drive terrain height or placement density. Lock with , read, then unlock.
UTexture2DFTexturePlatformDataBulkData.Lock(LOCK_READ_ONLY)Poisson disc sampling (minimum-separation scatter for natural placement) — full Bridson algorithm implementation in .
references/procedural-mesh-patterns.mdcpp
// 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();高度/密度图:通过采样的像素数据,用于驱动地形高度或实例放置密度。采样前需调用锁定数据,读取完成后解锁。
FTexturePlatformDataUTexture2DBulkData.Lock(LOCK_READ_ONLY)泊松圆盘采样(用于自然放置的最小间距散射)——完整的Bridson算法实现请参考.
references/procedural-mesh-patterns.md5. Spline Components
5. 样条曲线组件
USplineComponent API (from SplineComponent.h
)
SplineComponent.hUSplineComponent API(来自SplineComponent.h
)
SplineComponent.hcpp
// 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: , , , , .
LinearCurveConstantCurveClampedCurveCustomTangentFindInputKeyClosestToWorldLocation(WorldLocation)Runtime modification: Call , , or then to rebuild. Batch modifications before calling — each call recalculates the entire spline.
AddSplinePoint()RemoveSplinePoint()SetLocationAtSplinePoint()UpdateSpline()UpdateSpline()cpp
// 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);点类型:、、、、。
LinearCurveConstantCurveClampedCurveCustomTangentFindInputKeyClosestToWorldLocation(WorldLocation)运行时修改:调用、或后,需调用重建样条。建议批量修改后再调用一次——每次调用都会重新计算整个样条的参数化表。
AddSplinePoint()RemoveSplinePoint()SetLocationAtSplinePoint()UpdateSpline()UpdateSpline()Spline Placement Example
样条曲线实例放置示例
cpp
// 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);
}cpp
// 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)
USplineMeshComponent(网格变形)
cpp
#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);cpp
#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
6. 运行时网格生成模式
See for full implementations:
references/procedural-mesh-patterns.md- 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
cpp
// Marching Cubes result → ProceduralMesh
ProceduralMesh->CreateMeshSection(0, MarchVerts, MarchTris, MarchNormals,
MarchUVs, {}, {}, /*bCreateCollision=*/true);完整实现请参考:
references/procedural-mesh-patterns.md- Marching Cubes — 从3D密度标量场生成等值面
- 地牢BSP — 用BSP划分房间、L型走廊雕刻、瓦片转网格
- L-系统 — 字符串重写+海龟解释器生成HISM分支
- 波函数坍缩 — 基于约束传播的瓦片网格布局
- 异步网格生成 — 后台线程计算顶点,游戏线程调用
CreateMeshSection - 样条道路挤出 — 沿扫掠截面轮廓
USplineComponent
cpp
// Marching Cubes result → ProceduralMesh
ProceduralMesh->CreateMeshSection(0, MarchVerts, MarchTris, MarchNormals,
MarchUVs, {}, {}, /*bCreateCollision=*/true);Common Mistakes and Anti-Patterns
常见错误与反模式
PCG
- Calling in
GenerateLocal()— generation is not free; useTickand regenerate only on data change.GenerateOnDemand - Using in multiplayer — it is NOT replicated; use
GenerateLocal()(NetMulticast).Generate(bForce) - Heavy custom nodes with — only cache if output depends solely on inputs + seed.
bIsCacheable = true - Graphs with fail to cook into packaged builds.
bIsEditorOnly = true
ProceduralMeshComponent
- Passing to
bCreateCollision=false— characters fall through the mesh.CreateMeshSection - Calling expecting topology to change — vertex count must match; use
UpdateMeshSectionfor new triangles.CreateMeshSection - 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 on every
bMarkRenderStateDirty=truein a loop — only setUpdateInstanceTransformon the last call.true - Skipping before bulk add — repeated realloc degrades performance.
PreAllocateInstancesMemory
Splines
- Calling in a loop — rebuilds reparameterization table every call; use
AddSplinePoint(bUpdateSpline=true)and callfalseonce.UpdateSpline() - 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.
- does not replicate;
GenerateLocal()isGenerate(bool).NetMulticast, Reliable
PCG
- 在中调用
Tick——生成操作开销较大,请使用GenerateLocal(),仅在数据变化时重新生成。GenerateOnDemand - 在多人游戏中使用——该方法不会同步到其他客户端,请使用
GenerateLocal()(NetMulticast)。Generate(bForce) - 为资源消耗大的自定义节点设置——仅当输出完全由输入和种子决定时才启用缓存。
bIsCacheable = true - 设置的PCG图无法在打包构建中正常工作。
bIsEditorOnly = true
ProceduralMeshComponent
- 调用时传入
CreateMeshSection——会导致角色穿过网格。bCreateCollision=false - 调用期望修改拓扑结构——该方法仅能更新顶点位置,顶点数必须与原Section一致;若需添加新三角形,请调用
UpdateMeshSection。CreateMeshSection - 使用ProceduralMesh生成Nanite规模的地形——不支持该场景,请使用Landscape或PCG+ISM。
- 三角形绕向错误(顺时针而非逆时针)——会导致多边形因背面剔除而不可见。
ISM / HISM
- 实例数超过500仍使用ISM——请切换到HISM以利用BVH剔除。
- 在循环中每次调用都设置
UpdateInstanceTransform——仅在最后一次调用时设置为true即可。bMarkRenderStateDirty=true - 批量添加实例前未调用——反复重新分配内存会导致性能下降。
PreAllocateInstancesMemory
样条曲线
- 在循环中调用——每次调用都会重建参数化表,请设置为
AddSplinePoint(bUpdateSpline=true),循环结束后调用一次false。UpdateSpline() - 使用样条输入键而非距离来均匀放置实例——输入键与弧长不成正比。
多人游戏
- 程序化内容必须是确定性的(相同种子生成相同结果)或由服务器权威控制。
- 不会同步到其他客户端,请使用
GenerateLocal()(NetMulticast, Reliable)。Generate(bool)
Related Skills
相关技能
- — component lifecycle, registration, replication
ue-actor-component-architecture - — collision profiles, complex vs. simple on generated geometry
ue-physics-collision - —
ue-cpp-foundations,NewObject,TSubclassOf, memory managementTArray
- — 组件生命周期、注册、同步
ue-actor-component-architecture - — 碰撞配置文件、程序化几何体的复杂/简单碰撞
ue-physics-collision - —
ue-cpp-foundations、NewObject、TSubclassOf、内存管理TArray
Reference Files
参考文件
- — all PCG node types, pin labels, settings fields, determinism checklist
references/pcg-node-reference.md - — quad grid, marching cubes, dungeon BSP, L-system, WFC, spline road
references/procedural-mesh-patterns.md
- — 所有PCG节点类型、引脚标签、设置字段、确定性检查清单
references/pcg-node-reference.md - — 四边形网格、Marching Cubes、地牢BSP、L-系统、波函数坍缩、样条道路
references/procedural-mesh-patterns.md