claude-code-arcane 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +131 -41
- package/agents/game/ue-blueprint-specialist.md +125 -0
- package/agents/game/ue-gas-specialist.md +133 -0
- package/agents/game/ue-umg-specialist.md +131 -0
- package/dist/cli.js +642 -478
- package/docs/SKILLS-CATALOG.md +42 -3
- package/docs/USER-GUIDE.md +28 -54
- package/hooks/detect-unreal.sh +90 -0
- package/package.json +2 -1
- package/profiles/agile.yaml +1 -1
- package/profiles/ai.yaml +1 -1
- package/profiles/android-native.yaml +1 -1
- package/profiles/audio.yaml +1 -1
- package/profiles/backend-dotnet.yaml +1 -1
- package/profiles/backend-go.yaml +1 -1
- package/profiles/backend-nestjs.yaml +1 -1
- package/profiles/backend-nextjs.yaml +1 -1
- package/profiles/backend-ts.yaml +1 -1
- package/profiles/business.yaml +1 -1
- package/profiles/clevel.yaml +1 -1
- package/profiles/clickup.yaml +1 -1
- package/profiles/core.yaml +1 -1
- package/profiles/database.yaml +1 -1
- package/profiles/design.yaml +1 -1
- package/profiles/docs.yaml +1 -1
- package/profiles/ecommerce.yaml +1 -1
- package/profiles/finance.yaml +1 -1
- package/profiles/flutter.yaml +1 -1
- package/profiles/freelance.yaml +1 -1
- package/profiles/frontend.yaml +1 -1
- package/profiles/infra.yaml +1 -1
- package/profiles/integrations.yaml +1 -1
- package/profiles/ios-native.yaml +1 -1
- package/profiles/jira.yaml +1 -1
- package/profiles/job-hunt.yaml +1 -1
- package/profiles/marketing.yaml +1 -1
- package/profiles/mobile.yaml +1 -1
- package/profiles/regulatory.yaml +1 -1
- package/profiles/second-brain.yaml +1 -1
- package/profiles/security.yaml +1 -1
- package/profiles/self-improving.yaml +1 -1
- package/profiles/statusline.yaml +1 -1
- package/profiles/testing.yaml +1 -1
- package/profiles/unity-design.yaml +1 -1
- package/profiles/unity-dev.yaml +1 -1
- package/profiles/unreal-dev.yaml +97 -0
- package/profiles/visual-novel.yaml +1 -1
- package/rules/gamedev/unreal-code.md +125 -0
- package/skills/arcane-list/SKILL.md +12 -7
- package/skills/install-mcp/SKILL.md +135 -46
- package/skills/install-mcp/references/unreal-operations.md +55 -0
- package/skills/ue-actor-component-architecture/SKILL.md +509 -0
- package/skills/ue-actor-component-architecture/references/actor-lifecycle.md +301 -0
- package/skills/ue-actor-component-architecture/references/component-types.md +592 -0
- package/skills/ue-ai-navigation/SKILL.md +526 -0
- package/skills/ue-ai-navigation/references/behavior-tree-patterns.md +392 -0
- package/skills/ue-ai-navigation/references/behavior-tree-workflow.md +107 -0
- package/skills/ue-ai-navigation/references/custom-bttask.md +104 -0
- package/skills/ue-ai-navigation/references/eqs-reference.md +423 -0
- package/skills/ue-animation-system/SKILL.md +521 -0
- package/skills/ue-animation-system/references/anim-notify-reference.md +486 -0
- package/skills/ue-animation-system/references/locomotion-setup.md +550 -0
- package/skills/ue-async-threading/SKILL.md +433 -0
- package/skills/ue-async-threading/references/thread-safety-guide.md +213 -0
- package/skills/ue-async-threading/references/threading-patterns.md +345 -0
- package/skills/ue-audio-system/SKILL.md +527 -0
- package/skills/ue-audio-system/references/audio-setup-patterns.md +700 -0
- package/skills/ue-blueprints/SKILL.md +134 -0
- package/skills/ue-blueprints/references/communication.md +82 -0
- package/skills/ue-character-movement/SKILL.md +475 -0
- package/skills/ue-character-movement/references/cmc-extension-patterns.md +335 -0
- package/skills/ue-character-movement/references/movement-pipeline.md +189 -0
- package/skills/ue-cpp-foundations/SKILL.md +533 -0
- package/skills/ue-cpp-foundations/references/components-and-gc.md +107 -0
- package/skills/ue-cpp-foundations/references/container-patterns.md +544 -0
- package/skills/ue-cpp-foundations/references/delegate-patterns.md +497 -0
- package/skills/ue-cpp-foundations/references/gameplay-classes-cpp.md +136 -0
- package/skills/ue-cpp-foundations/references/property-specifiers.md +306 -0
- package/skills/ue-data-assets-tables/SKILL.md +527 -0
- package/skills/ue-data-assets-tables/references/asset-loading-patterns.md +306 -0
- package/skills/ue-data-assets-tables/references/data-driven-design.md +378 -0
- package/skills/ue-editor-tools/SKILL.md +523 -0
- package/skills/ue-editor-tools/references/detail-customization-patterns.md +395 -0
- package/skills/ue-editor-tools/references/editor-module-setup.md +480 -0
- package/skills/ue-game-features/SKILL.md +477 -0
- package/skills/ue-game-features/references/experience-system.md +222 -0
- package/skills/ue-game-features/references/game-feature-patterns.md +308 -0
- package/skills/ue-gameplay-abilities/SKILL.md +518 -0
- package/skills/ue-gameplay-abilities/references/ability-task-reference.md +391 -0
- package/skills/ue-gameplay-abilities/references/gameplay-effect-reference.md +397 -0
- package/skills/ue-gameplay-abilities/references/gas-setup-patterns.md +369 -0
- package/skills/ue-gameplay-framework/SKILL.md +466 -0
- package/skills/ue-gameplay-framework/references/framework-class-map.md +243 -0
- package/skills/ue-input-system/SKILL.md +394 -0
- package/skills/ue-input-system/references/cpp-setup.md +131 -0
- package/skills/ue-input-system/references/enhanced-input-workflow.md +117 -0
- package/skills/ue-input-system/references/input-action-reference.md +731 -0
- package/skills/ue-mass-entity/SKILL.md +476 -0
- package/skills/ue-mass-entity/references/mass-entity-patterns.md +319 -0
- package/skills/ue-mass-entity/references/mass-fragment-reference.md +119 -0
- package/skills/ue-materials-rendering/SKILL.md +526 -0
- package/skills/ue-materials-rendering/references/material-parameter-reference.md +216 -0
- package/skills/ue-materials-rendering/references/post-process-settings.md +383 -0
- package/skills/ue-module-build-system/SKILL.md +487 -0
- package/skills/ue-module-build-system/references/build-cs-reference.md +418 -0
- package/skills/ue-module-build-system/references/common-build-errors.md +381 -0
- package/skills/ue-naming-conventions/SKILL.md +142 -0
- package/skills/ue-naming-conventions/references/asset-name-modifiers.md +233 -0
- package/skills/ue-naming-conventions/references/content-structure.md +83 -0
- package/skills/ue-networking-replication/SKILL.md +452 -0
- package/skills/ue-networking-replication/references/replication-patterns.md +526 -0
- package/skills/ue-networking-replication/references/rpc-decision-guide.md +393 -0
- package/skills/ue-niagara-effects/SKILL.md +531 -0
- package/skills/ue-niagara-effects/references/niagara-data-interfaces.md +491 -0
- package/skills/ue-niagara-effects/references/niagara-parameter-types.md +153 -0
- package/skills/ue-niagara-effects/references/niagara-workflow.md +115 -0
- package/skills/ue-packaging/SKILL.md +116 -0
- package/skills/ue-physics-collision/SKILL.md +423 -0
- package/skills/ue-physics-collision/references/collision-channel-setup.md +218 -0
- package/skills/ue-physics-collision/references/trace-patterns.md +499 -0
- package/skills/ue-procedural-generation/SKILL.md +505 -0
- package/skills/ue-procedural-generation/references/pcg-node-reference.md +381 -0
- package/skills/ue-procedural-generation/references/procedural-mesh-patterns.md +940 -0
- package/skills/ue-project-context/SKILL.md +305 -0
- package/skills/ue-sequencer-cinematics/SKILL.md +506 -0
- package/skills/ue-sequencer-cinematics/references/sequencer-patterns.md +629 -0
- package/skills/ue-serialization-savegames/SKILL.md +524 -0
- package/skills/ue-serialization-savegames/references/save-system-architecture.md +705 -0
- package/skills/ue-state-trees/SKILL.md +489 -0
- package/skills/ue-state-trees/references/state-tree-mass-integration.md +184 -0
- package/skills/ue-state-trees/references/state-tree-patterns.md +321 -0
- package/skills/ue-testing-debugging/SKILL.md +520 -0
- package/skills/ue-testing-debugging/references/automation-test-patterns.md +387 -0
- package/skills/ue-testing-debugging/references/profiling-commands.md +357 -0
- package/skills/ue-ui-umg-slate/SKILL.md +431 -0
- package/skills/ue-ui-umg-slate/references/common-ui-setup.md +484 -0
- package/skills/ue-ui-umg-slate/references/widget-types.md +487 -0
- package/skills/ue-world-level-streaming/SKILL.md +518 -0
- package/skills/ue-world-level-streaming/references/streaming-patterns.md +414 -0
- package/templates/gamedev/unreal-project-context.md +107 -0
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
# Procedural Mesh Patterns
|
|
2
|
+
|
|
3
|
+
Common algorithmic patterns for runtime mesh and content generation in Unreal Engine using `UProceduralMeshComponent`, `UInstancedStaticMeshComponent`, and supporting math utilities.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Component Setup Boilerplate
|
|
8
|
+
|
|
9
|
+
```cpp
|
|
10
|
+
// Header — MyProceduralActor.h
|
|
11
|
+
#pragma once
|
|
12
|
+
#include "GameFramework/Actor.h"
|
|
13
|
+
#include "ProceduralMeshComponent.h"
|
|
14
|
+
#include "Components/InstancedStaticMeshComponent.h"
|
|
15
|
+
#include "Components/SplineComponent.h"
|
|
16
|
+
#include "MyProceduralActor.generated.h"
|
|
17
|
+
|
|
18
|
+
UCLASS()
|
|
19
|
+
class AMyProceduralActor : public AActor
|
|
20
|
+
{
|
|
21
|
+
GENERATED_BODY()
|
|
22
|
+
public:
|
|
23
|
+
AMyProceduralActor();
|
|
24
|
+
|
|
25
|
+
UPROPERTY(VisibleAnywhere)
|
|
26
|
+
UProceduralMeshComponent* ProceduralMesh;
|
|
27
|
+
|
|
28
|
+
UPROPERTY(VisibleAnywhere)
|
|
29
|
+
UHierarchicalInstancedStaticMeshComponent* HISM;
|
|
30
|
+
|
|
31
|
+
UPROPERTY(VisibleAnywhere)
|
|
32
|
+
USplineComponent* Spline;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Source — MyProceduralActor.cpp
|
|
36
|
+
#include "MyProceduralActor.h"
|
|
37
|
+
|
|
38
|
+
AMyProceduralActor::AMyProceduralActor()
|
|
39
|
+
{
|
|
40
|
+
PrimaryActorTick.bCanEverTick = false;
|
|
41
|
+
|
|
42
|
+
ProceduralMesh = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("ProceduralMesh"));
|
|
43
|
+
SetRootComponent(ProceduralMesh);
|
|
44
|
+
ProceduralMesh->bUseComplexAsSimpleCollision = false; // Use dedicated collision shapes
|
|
45
|
+
|
|
46
|
+
HISM = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("HISM"));
|
|
47
|
+
HISM->SetupAttachment(RootComponent);
|
|
48
|
+
HISM->SetNumCustomDataFloats(2); // Reserve per-instance float channels
|
|
49
|
+
|
|
50
|
+
Spline = CreateDefaultSubobject<USplineComponent>(TEXT("Spline"));
|
|
51
|
+
Spline->SetupAttachment(RootComponent);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 1. Flat Quad Grid (Terrain Base)
|
|
58
|
+
|
|
59
|
+
Generates a simple flat or height-mapped mesh from a 2D grid of vertices.
|
|
60
|
+
|
|
61
|
+
```cpp
|
|
62
|
+
// GridSize = number of cells per side. Vertex count = (GridSize+1)^2.
|
|
63
|
+
// Preallocate for performance.
|
|
64
|
+
void ATerrainActor::GenerateFlatGrid(int32 GridSize, float CellSize,
|
|
65
|
+
bool bCreateCollision)
|
|
66
|
+
{
|
|
67
|
+
const int32 VertexStride = GridSize + 1;
|
|
68
|
+
const int32 VertCount = VertexStride * VertexStride;
|
|
69
|
+
const int32 TriCount = GridSize * GridSize * 6;
|
|
70
|
+
|
|
71
|
+
TArray<FVector> Vertices; Vertices.Reserve(VertCount);
|
|
72
|
+
TArray<int32> Triangles; Triangles.Reserve(TriCount);
|
|
73
|
+
TArray<FVector> Normals; Normals.Reserve(VertCount);
|
|
74
|
+
TArray<FVector2D> UVs; UVs.Reserve(VertCount);
|
|
75
|
+
TArray<FColor> Colors;
|
|
76
|
+
TArray<FProcMeshTangent> Tangents;
|
|
77
|
+
|
|
78
|
+
for (int32 Row = 0; Row <= GridSize; Row++)
|
|
79
|
+
{
|
|
80
|
+
for (int32 Col = 0; Col <= GridSize; Col++)
|
|
81
|
+
{
|
|
82
|
+
float X = Col * CellSize;
|
|
83
|
+
float Y = Row * CellSize;
|
|
84
|
+
float Z = 0.f; // Replace with height sample for terrain
|
|
85
|
+
|
|
86
|
+
Vertices.Add(FVector(X, Y, Z));
|
|
87
|
+
Normals.Add(FVector::UpVector);
|
|
88
|
+
UVs.Add(FVector2D((float)Col / GridSize, (float)Row / GridSize));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (int32 Row = 0; Row < GridSize; Row++)
|
|
93
|
+
{
|
|
94
|
+
for (int32 Col = 0; Col < GridSize; Col++)
|
|
95
|
+
{
|
|
96
|
+
int32 BL = Row * VertexStride + Col;
|
|
97
|
+
int32 BR = BL + 1;
|
|
98
|
+
int32 TL = BL + VertexStride;
|
|
99
|
+
int32 TR = TL + 1;
|
|
100
|
+
|
|
101
|
+
// Counter-clockwise = front face in UE
|
|
102
|
+
Triangles.Add(BL); Triangles.Add(TL); Triangles.Add(TR);
|
|
103
|
+
Triangles.Add(BL); Triangles.Add(TR); Triangles.Add(BR);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
ProceduralMesh->CreateMeshSection(0, Vertices, Triangles, Normals,
|
|
108
|
+
UVs, Colors, Tangents, bCreateCollision);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Height-Mapped Terrain with Normal Recalculation
|
|
113
|
+
|
|
114
|
+
```cpp
|
|
115
|
+
float SampleHeight(float X, float Y, float Scale, int32 Seed)
|
|
116
|
+
{
|
|
117
|
+
// Seeded offset to vary noise field per-seed
|
|
118
|
+
float OffsetX = (float)(Seed % 1000) * 0.01f;
|
|
119
|
+
float OffsetY = (float)(Seed / 1000) * 0.01f;
|
|
120
|
+
return SampleOctaveNoise(X / Scale + OffsetX, Y / Scale + OffsetY, 5, 0.5f, 2.0f, 1.0f);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
void RecalculateNormals(const TArray<FVector>& Vertices, const TArray<int32>& Triangles,
|
|
124
|
+
TArray<FVector>& OutNormals)
|
|
125
|
+
{
|
|
126
|
+
OutNormals.Init(FVector::ZeroVector, Vertices.Num());
|
|
127
|
+
|
|
128
|
+
for (int32 i = 0; i + 2 < Triangles.Num(); i += 3)
|
|
129
|
+
{
|
|
130
|
+
const FVector& A = Vertices[Triangles[i]];
|
|
131
|
+
const FVector& B = Vertices[Triangles[i + 1]];
|
|
132
|
+
const FVector& C = Vertices[Triangles[i + 2]];
|
|
133
|
+
FVector Normal = FVector::CrossProduct(B - A, C - A).GetSafeNormal();
|
|
134
|
+
|
|
135
|
+
OutNormals[Triangles[i]] += Normal;
|
|
136
|
+
OutNormals[Triangles[i + 1]] += Normal;
|
|
137
|
+
OutNormals[Triangles[i + 2]] += Normal;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (FVector& N : OutNormals)
|
|
141
|
+
{
|
|
142
|
+
N = N.GetSafeNormal();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 2. Marching Cubes (Voxel Isosurface)
|
|
150
|
+
|
|
151
|
+
Extracts a triangulated isosurface from a 3D scalar field. Used for caves, asteroids, destructible terrain.
|
|
152
|
+
|
|
153
|
+
### Scalar Field Setup
|
|
154
|
+
|
|
155
|
+
```cpp
|
|
156
|
+
// Density grid: negative = solid, positive = air, zero = surface
|
|
157
|
+
struct FDensityGrid
|
|
158
|
+
{
|
|
159
|
+
TArray<float> Values;
|
|
160
|
+
FIntVector Size; // Width x Height x Depth
|
|
161
|
+
float VoxelSize;
|
|
162
|
+
|
|
163
|
+
float Sample(int32 X, int32 Y, int32 Z) const
|
|
164
|
+
{
|
|
165
|
+
if (X < 0 || Y < 0 || Z < 0 ||
|
|
166
|
+
X >= Size.X || Y >= Size.Y || Z >= Size.Z)
|
|
167
|
+
return 1.f; // Outside = air
|
|
168
|
+
return Values[Z * Size.Y * Size.X + Y * Size.X + X];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
FVector WorldPos(int32 X, int32 Y, int32 Z) const
|
|
172
|
+
{
|
|
173
|
+
return FVector(X, Y, Z) * VoxelSize;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Edge Interpolation
|
|
179
|
+
|
|
180
|
+
```cpp
|
|
181
|
+
FVector InterpolateEdge(FVector P0, float V0, FVector P1, float V1)
|
|
182
|
+
{
|
|
183
|
+
// Linear interpolation to find zero crossing
|
|
184
|
+
float t = FMath::Clamp(-V0 / (V1 - V0 + SMALL_NUMBER), 0.f, 1.f);
|
|
185
|
+
return FMath::Lerp(P0, P1, t);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Cube Processing
|
|
190
|
+
|
|
191
|
+
```cpp
|
|
192
|
+
// EdgeTable and TriTable are standard 256-entry lookup tables from the original
|
|
193
|
+
// Lorensen & Cline (1987) paper. They map the 8-corner sign configuration
|
|
194
|
+
// to which edges the surface crosses and how to form triangles.
|
|
195
|
+
// These tables are typically 4KB total and stored as compile-time const arrays.
|
|
196
|
+
extern const int EdgeTable[256];
|
|
197
|
+
extern const int TriTable[256][16];
|
|
198
|
+
|
|
199
|
+
void ProcessCube(const FDensityGrid& Grid, int32 X, int32 Y, int32 Z,
|
|
200
|
+
TArray<FVector>& OutVerts, TArray<int32>& OutTris)
|
|
201
|
+
{
|
|
202
|
+
// Sample 8 cube corners
|
|
203
|
+
float CubeValues[8];
|
|
204
|
+
CubeValues[0] = Grid.Sample(X, Y, Z);
|
|
205
|
+
CubeValues[1] = Grid.Sample(X + 1, Y, Z);
|
|
206
|
+
CubeValues[2] = Grid.Sample(X + 1, Y + 1, Z);
|
|
207
|
+
CubeValues[3] = Grid.Sample(X, Y + 1, Z);
|
|
208
|
+
CubeValues[4] = Grid.Sample(X, Y, Z + 1);
|
|
209
|
+
CubeValues[5] = Grid.Sample(X + 1, Y, Z + 1);
|
|
210
|
+
CubeValues[6] = Grid.Sample(X + 1, Y + 1, Z + 1);
|
|
211
|
+
CubeValues[7] = Grid.Sample(X, Y + 1, Z + 1);
|
|
212
|
+
|
|
213
|
+
FVector Corners[8];
|
|
214
|
+
Corners[0] = Grid.WorldPos(X, Y, Z);
|
|
215
|
+
Corners[1] = Grid.WorldPos(X + 1, Y, Z);
|
|
216
|
+
Corners[2] = Grid.WorldPos(X + 1, Y + 1, Z);
|
|
217
|
+
Corners[3] = Grid.WorldPos(X, Y + 1, Z);
|
|
218
|
+
Corners[4] = Grid.WorldPos(X, Y, Z + 1);
|
|
219
|
+
Corners[5] = Grid.WorldPos(X + 1, Y, Z + 1);
|
|
220
|
+
Corners[6] = Grid.WorldPos(X + 1, Y + 1, Z + 1);
|
|
221
|
+
Corners[7] = Grid.WorldPos(X, Y + 1, Z + 1);
|
|
222
|
+
|
|
223
|
+
// Build 8-bit index from which corners are below iso-level (0)
|
|
224
|
+
int32 CubeIndex = 0;
|
|
225
|
+
for (int32 i = 0; i < 8; i++)
|
|
226
|
+
if (CubeValues[i] < 0.f) CubeIndex |= (1 << i);
|
|
227
|
+
|
|
228
|
+
if (EdgeTable[CubeIndex] == 0) return; // Fully inside or outside
|
|
229
|
+
|
|
230
|
+
// Compute intersection vertices on active edges
|
|
231
|
+
FVector EdgeVerts[12];
|
|
232
|
+
if (EdgeTable[CubeIndex] & 1) EdgeVerts[0] = InterpolateEdge(Corners[0], CubeValues[0], Corners[1], CubeValues[1]);
|
|
233
|
+
if (EdgeTable[CubeIndex] & 2) EdgeVerts[1] = InterpolateEdge(Corners[1], CubeValues[1], Corners[2], CubeValues[2]);
|
|
234
|
+
if (EdgeTable[CubeIndex] & 4) EdgeVerts[2] = InterpolateEdge(Corners[2], CubeValues[2], Corners[3], CubeValues[3]);
|
|
235
|
+
if (EdgeTable[CubeIndex] & 8) EdgeVerts[3] = InterpolateEdge(Corners[3], CubeValues[3], Corners[0], CubeValues[0]);
|
|
236
|
+
if (EdgeTable[CubeIndex] & 16) EdgeVerts[4] = InterpolateEdge(Corners[4], CubeValues[4], Corners[5], CubeValues[5]);
|
|
237
|
+
if (EdgeTable[CubeIndex] & 32) EdgeVerts[5] = InterpolateEdge(Corners[5], CubeValues[5], Corners[6], CubeValues[6]);
|
|
238
|
+
if (EdgeTable[CubeIndex] & 64) EdgeVerts[6] = InterpolateEdge(Corners[6], CubeValues[6], Corners[7], CubeValues[7]);
|
|
239
|
+
if (EdgeTable[CubeIndex] & 128) EdgeVerts[7] = InterpolateEdge(Corners[7], CubeValues[7], Corners[4], CubeValues[4]);
|
|
240
|
+
if (EdgeTable[CubeIndex] & 256) EdgeVerts[8] = InterpolateEdge(Corners[0], CubeValues[0], Corners[4], CubeValues[4]);
|
|
241
|
+
if (EdgeTable[CubeIndex] & 512) EdgeVerts[9] = InterpolateEdge(Corners[1], CubeValues[1], Corners[5], CubeValues[5]);
|
|
242
|
+
if (EdgeTable[CubeIndex] & 1024) EdgeVerts[10] = InterpolateEdge(Corners[2], CubeValues[2], Corners[6], CubeValues[6]);
|
|
243
|
+
if (EdgeTable[CubeIndex] & 2048) EdgeVerts[11] = InterpolateEdge(Corners[3], CubeValues[3], Corners[7], CubeValues[7]);
|
|
244
|
+
|
|
245
|
+
// Add triangles from TriTable
|
|
246
|
+
for (int32 i = 0; TriTable[CubeIndex][i] != -1; i += 3)
|
|
247
|
+
{
|
|
248
|
+
int32 BaseIdx = OutVerts.Num();
|
|
249
|
+
OutVerts.Add(EdgeVerts[TriTable[CubeIndex][i]]);
|
|
250
|
+
OutVerts.Add(EdgeVerts[TriTable[CubeIndex][i + 1]]);
|
|
251
|
+
OutVerts.Add(EdgeVerts[TriTable[CubeIndex][i + 2]]);
|
|
252
|
+
OutTris.Add(BaseIdx); OutTris.Add(BaseIdx + 1); OutTris.Add(BaseIdx + 2);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Full Grid March
|
|
258
|
+
|
|
259
|
+
```cpp
|
|
260
|
+
void MarchCubes(const FDensityGrid& Grid, UProceduralMeshComponent* Mesh)
|
|
261
|
+
{
|
|
262
|
+
TArray<FVector> Vertices, Normals;
|
|
263
|
+
TArray<int32> Triangles;
|
|
264
|
+
TArray<FVector2D> UVs;
|
|
265
|
+
TArray<FColor> Colors;
|
|
266
|
+
TArray<FProcMeshTangent> Tangents;
|
|
267
|
+
|
|
268
|
+
for (int32 Z = 0; Z < Grid.Size.Z - 1; Z++)
|
|
269
|
+
for (int32 Y = 0; Y < Grid.Size.Y - 1; Y++)
|
|
270
|
+
for (int32 X = 0; X < Grid.Size.X - 1; X++)
|
|
271
|
+
{
|
|
272
|
+
ProcessCube(Grid, X, Y, Z, Vertices, Triangles);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Compute normals from triangles
|
|
276
|
+
RecalculateNormals(Vertices, Triangles, Normals);
|
|
277
|
+
|
|
278
|
+
// Pad UVs (can project based on position for triplanar)
|
|
279
|
+
UVs.SetNumZeroed(Vertices.Num());
|
|
280
|
+
for (int32 i = 0; i < Vertices.Num(); i++)
|
|
281
|
+
UVs[i] = FVector2D(Vertices[i].X, Vertices[i].Y) / Grid.VoxelSize;
|
|
282
|
+
|
|
283
|
+
Mesh->CreateMeshSection(0, Vertices, Triangles, Normals,
|
|
284
|
+
UVs, Colors, Tangents, /*bCreateCollision=*/true);
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## 3. Dungeon Room-and-Corridor Generation
|
|
291
|
+
|
|
292
|
+
BSP-based dungeon layout that partitions a rect into rooms and connects them.
|
|
293
|
+
|
|
294
|
+
### Data Structures
|
|
295
|
+
|
|
296
|
+
```cpp
|
|
297
|
+
struct FRoom
|
|
298
|
+
{
|
|
299
|
+
FIntRect Bounds; // X=left, Y=top, Width, Height in grid cells
|
|
300
|
+
FIntPoint Center() const
|
|
301
|
+
{
|
|
302
|
+
return FIntPoint(Bounds.Min.X + Bounds.Width() / 2,
|
|
303
|
+
Bounds.Min.Y + Bounds.Height() / 2);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
struct FDungeonLevel
|
|
308
|
+
{
|
|
309
|
+
TArray<FRoom> Rooms;
|
|
310
|
+
TArray<TPair<FIntPoint, FIntPoint>> Corridors; // pairs of cell coords
|
|
311
|
+
TArray<TArray<uint8>> Tiles; // 0=wall, 1=floor, 2=corridor
|
|
312
|
+
};
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### BSP Split
|
|
316
|
+
|
|
317
|
+
```cpp
|
|
318
|
+
void SplitRect(const FIntRect& Rect, FRandomStream& Rand,
|
|
319
|
+
int32 MinSize, TArray<FIntRect>& OutLeaves)
|
|
320
|
+
{
|
|
321
|
+
int32 W = Rect.Width(), H = Rect.Height();
|
|
322
|
+
|
|
323
|
+
if (W < MinSize * 2 && H < MinSize * 2)
|
|
324
|
+
{
|
|
325
|
+
OutLeaves.Add(Rect);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
bool bSplitH = (W > H) ? true : (H > W) ? false : Rand.RandBool();
|
|
330
|
+
|
|
331
|
+
if (bSplitH && W >= MinSize * 2)
|
|
332
|
+
{
|
|
333
|
+
int32 Split = Rand.RandRange(MinSize, W - MinSize);
|
|
334
|
+
SplitRect(FIntRect(Rect.Min, FIntPoint(Rect.Min.X + Split, Rect.Max.Y)), Rand, MinSize, OutLeaves);
|
|
335
|
+
SplitRect(FIntRect(FIntPoint(Rect.Min.X + Split, Rect.Min.Y), Rect.Max), Rand, MinSize, OutLeaves);
|
|
336
|
+
}
|
|
337
|
+
else if (!bSplitH && H >= MinSize * 2)
|
|
338
|
+
{
|
|
339
|
+
int32 Split = Rand.RandRange(MinSize, H - MinSize);
|
|
340
|
+
SplitRect(FIntRect(Rect.Min, FIntPoint(Rect.Max.X, Rect.Min.Y + Split)), Rand, MinSize, OutLeaves);
|
|
341
|
+
SplitRect(FIntRect(FIntPoint(Rect.Min.X, Rect.Min.Y + Split), Rect.Max), Rand, MinSize, OutLeaves);
|
|
342
|
+
}
|
|
343
|
+
else
|
|
344
|
+
{
|
|
345
|
+
OutLeaves.Add(Rect);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Room Placement and Corridor Carving
|
|
351
|
+
|
|
352
|
+
```cpp
|
|
353
|
+
FDungeonLevel GenerateDungeon(int32 MapW, int32 MapH, int32 Seed,
|
|
354
|
+
int32 MinRoomSize = 5, int32 Padding = 1)
|
|
355
|
+
{
|
|
356
|
+
FRandomStream Rand(Seed);
|
|
357
|
+
FDungeonLevel Level;
|
|
358
|
+
|
|
359
|
+
// Initialize tile map
|
|
360
|
+
Level.Tiles.SetNum(MapH);
|
|
361
|
+
for (auto& Row : Level.Tiles)
|
|
362
|
+
Row.Init(0, MapW); // all walls
|
|
363
|
+
|
|
364
|
+
// BSP partition
|
|
365
|
+
TArray<FIntRect> Leaves;
|
|
366
|
+
SplitRect(FIntRect(0, 0, MapW, MapH), Rand, MinRoomSize + Padding * 2, Leaves);
|
|
367
|
+
|
|
368
|
+
// Place rooms in leaves
|
|
369
|
+
for (const FIntRect& Leaf : Leaves)
|
|
370
|
+
{
|
|
371
|
+
int32 MaxW = Leaf.Width() - Padding * 2;
|
|
372
|
+
int32 MaxH = Leaf.Height() - Padding * 2;
|
|
373
|
+
if (MaxW < MinRoomSize || MaxH < MinRoomSize) continue;
|
|
374
|
+
|
|
375
|
+
int32 RW = Rand.RandRange(MinRoomSize, MaxW);
|
|
376
|
+
int32 RH = Rand.RandRange(MinRoomSize, MaxH);
|
|
377
|
+
int32 RX = Leaf.Min.X + Padding + Rand.RandRange(0, MaxW - RW);
|
|
378
|
+
int32 RY = Leaf.Min.Y + Padding + Rand.RandRange(0, MaxH - RH);
|
|
379
|
+
|
|
380
|
+
FRoom Room;
|
|
381
|
+
Room.Bounds = FIntRect(RX, RY, RX + RW, RY + RH);
|
|
382
|
+
Level.Rooms.Add(Room);
|
|
383
|
+
|
|
384
|
+
// Carve floor tiles
|
|
385
|
+
for (int32 Y = RY; Y < RY + RH; Y++)
|
|
386
|
+
for (int32 X = RX; X < RX + RW; X++)
|
|
387
|
+
Level.Tiles[Y][X] = 1;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Connect rooms with L-shaped corridors
|
|
391
|
+
for (int32 i = 1; i < Level.Rooms.Num(); i++)
|
|
392
|
+
{
|
|
393
|
+
FIntPoint A = Level.Rooms[i - 1].Center();
|
|
394
|
+
FIntPoint B = Level.Rooms[i].Center();
|
|
395
|
+
|
|
396
|
+
// Horizontal then vertical
|
|
397
|
+
int32 XDir = (B.X > A.X) ? 1 : -1;
|
|
398
|
+
for (int32 X = A.X; X != B.X; X += XDir)
|
|
399
|
+
{
|
|
400
|
+
Level.Tiles[A.Y][X] = 2;
|
|
401
|
+
Level.Corridors.Add({FIntPoint(X, A.Y), FIntPoint(X + XDir, A.Y)});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
int32 YDir = (B.Y > A.Y) ? 1 : -1;
|
|
405
|
+
for (int32 Y = A.Y; Y != B.Y; Y += YDir)
|
|
406
|
+
{
|
|
407
|
+
Level.Tiles[Y][B.X] = 2;
|
|
408
|
+
Level.Corridors.Add({FIntPoint(B.X, Y), FIntPoint(B.X, Y + YDir)});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
Level.Tiles[B.Y][B.X] = 2;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return Level;
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Tile-to-Mesh Conversion
|
|
419
|
+
|
|
420
|
+
```cpp
|
|
421
|
+
void ADungeonActor::BuildMeshFromTiles(const FDungeonLevel& Level, float TileSize)
|
|
422
|
+
{
|
|
423
|
+
TArray<FVector> Vertices;
|
|
424
|
+
TArray<int32> Triangles;
|
|
425
|
+
TArray<FVector> Normals;
|
|
426
|
+
TArray<FVector2D> UVs;
|
|
427
|
+
TArray<FColor> Colors;
|
|
428
|
+
TArray<FProcMeshTangent> Tangents;
|
|
429
|
+
|
|
430
|
+
int32 H = Level.Tiles.Num();
|
|
431
|
+
int32 W = H > 0 ? Level.Tiles[0].Num() : 0;
|
|
432
|
+
|
|
433
|
+
for (int32 Row = 0; Row < H; Row++)
|
|
434
|
+
for (int32 Col = 0; Col < W; Col++)
|
|
435
|
+
{
|
|
436
|
+
if (Level.Tiles[Row][Col] == 0) continue; // Skip walls (or add wall mesh)
|
|
437
|
+
|
|
438
|
+
int32 Base = Vertices.Num();
|
|
439
|
+
float X0 = Col * TileSize;
|
|
440
|
+
float X1 = (Col + 1) * TileSize;
|
|
441
|
+
float Y0 = Row * TileSize;
|
|
442
|
+
float Y1 = (Row + 1) * TileSize;
|
|
443
|
+
|
|
444
|
+
Vertices.Add(FVector(X0, Y0, 0)); // 0 BL
|
|
445
|
+
Vertices.Add(FVector(X1, Y0, 0)); // 1 BR
|
|
446
|
+
Vertices.Add(FVector(X1, Y1, 0)); // 2 TR
|
|
447
|
+
Vertices.Add(FVector(X0, Y1, 0)); // 3 TL
|
|
448
|
+
|
|
449
|
+
Normals.Add(FVector::UpVector);
|
|
450
|
+
Normals.Add(FVector::UpVector);
|
|
451
|
+
Normals.Add(FVector::UpVector);
|
|
452
|
+
Normals.Add(FVector::UpVector);
|
|
453
|
+
|
|
454
|
+
UVs.Add(FVector2D(0, 0)); UVs.Add(FVector2D(1, 0));
|
|
455
|
+
UVs.Add(FVector2D(1, 1)); UVs.Add(FVector2D(0, 1));
|
|
456
|
+
|
|
457
|
+
Triangles.Add(Base); Triangles.Add(Base + 2); Triangles.Add(Base + 1);
|
|
458
|
+
Triangles.Add(Base); Triangles.Add(Base + 3); Triangles.Add(Base + 2);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
ProceduralMesh->CreateMeshSection(0, Vertices, Triangles, Normals,
|
|
462
|
+
UVs, Colors, Tangents, /*bCreateCollision=*/true);
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## 4. L-System Vegetation
|
|
469
|
+
|
|
470
|
+
L-systems expand a string through production rules and interpret characters as 3D drawing commands (turtle graphics) to produce branching structures.
|
|
471
|
+
|
|
472
|
+
### Axiom and Rules
|
|
473
|
+
|
|
474
|
+
```cpp
|
|
475
|
+
struct FLSystemRules
|
|
476
|
+
{
|
|
477
|
+
FString Axiom = "F";
|
|
478
|
+
TMap<TCHAR, FString> Rules = {
|
|
479
|
+
{'F', TEXT("F[+F]F[-F]F")} // Standard plant
|
|
480
|
+
};
|
|
481
|
+
int32 Iterations = 4;
|
|
482
|
+
float AngleDeg = 25.f;
|
|
483
|
+
float SegmentLen = 50.f;
|
|
484
|
+
float LenDecay = 0.7f; // Each recursion level shortens segments
|
|
485
|
+
float WidthStart = 8.f;
|
|
486
|
+
float WidthDecay = 0.6f;
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
FString ExpandLSystem(const FLSystemRules& Rules)
|
|
490
|
+
{
|
|
491
|
+
FString Current = Rules.Axiom;
|
|
492
|
+
for (int32 Iter = 0; Iter < Rules.Iterations; Iter++)
|
|
493
|
+
{
|
|
494
|
+
FString Next;
|
|
495
|
+
Next.Reserve(Current.Len() * 3);
|
|
496
|
+
for (TCHAR C : Current)
|
|
497
|
+
{
|
|
498
|
+
const FString* Replacement = Rules.Rules.Find(C);
|
|
499
|
+
if (Replacement) Next.Append(*Replacement);
|
|
500
|
+
else Next.AppendChar(C);
|
|
501
|
+
}
|
|
502
|
+
Current = MoveTemp(Next);
|
|
503
|
+
}
|
|
504
|
+
return Current;
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Turtle Interpreter to HISM
|
|
509
|
+
|
|
510
|
+
```cpp
|
|
511
|
+
struct FTurtleState
|
|
512
|
+
{
|
|
513
|
+
FVector Position = FVector::ZeroVector;
|
|
514
|
+
FRotator Rotation = FRotator::ZeroRotator;
|
|
515
|
+
float Length = 50.f;
|
|
516
|
+
float Width = 8.f;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
void InterpretLSystem(const FString& LString, const FLSystemRules& Rules,
|
|
520
|
+
UHierarchicalInstancedStaticMeshComponent* BranchHISM,
|
|
521
|
+
UHierarchicalInstancedStaticMeshComponent* LeafHISM)
|
|
522
|
+
{
|
|
523
|
+
TArray<FTurtleState> Stack;
|
|
524
|
+
FTurtleState State;
|
|
525
|
+
State.Length = Rules.SegmentLen;
|
|
526
|
+
State.Width = Rules.WidthStart;
|
|
527
|
+
|
|
528
|
+
for (TCHAR C : LString)
|
|
529
|
+
{
|
|
530
|
+
switch (C)
|
|
531
|
+
{
|
|
532
|
+
case 'F':
|
|
533
|
+
{
|
|
534
|
+
FVector Forward = State.Rotation.Vector() * State.Length;
|
|
535
|
+
FVector End = State.Position + Forward;
|
|
536
|
+
|
|
537
|
+
// Place branch segment
|
|
538
|
+
FTransform T;
|
|
539
|
+
T.SetLocation((State.Position + End) * 0.5f);
|
|
540
|
+
T.SetRotation(State.Rotation.Quaternion());
|
|
541
|
+
T.SetScale3D(FVector(State.Width * 0.01f, State.Width * 0.01f,
|
|
542
|
+
State.Length * 0.01f));
|
|
543
|
+
BranchHISM->AddInstance(T, /*bWorldSpace=*/false);
|
|
544
|
+
|
|
545
|
+
State.Position = End;
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
case '+': State.Rotation.Yaw += Rules.AngleDeg; break;
|
|
549
|
+
case '-': State.Rotation.Yaw -= Rules.AngleDeg; break;
|
|
550
|
+
case '&': State.Rotation.Pitch += Rules.AngleDeg; break;
|
|
551
|
+
case '^': State.Rotation.Pitch -= Rules.AngleDeg; break;
|
|
552
|
+
case '/': State.Rotation.Roll += Rules.AngleDeg; break;
|
|
553
|
+
case '\\':State.Rotation.Roll -= Rules.AngleDeg; break;
|
|
554
|
+
case '[':
|
|
555
|
+
Stack.Push(State);
|
|
556
|
+
State.Length *= Rules.LenDecay;
|
|
557
|
+
State.Width *= Rules.WidthDecay;
|
|
558
|
+
break;
|
|
559
|
+
case ']':
|
|
560
|
+
// Place leaf at branch tip before popping
|
|
561
|
+
if (LeafHISM)
|
|
562
|
+
{
|
|
563
|
+
FTransform LT;
|
|
564
|
+
LT.SetLocation(State.Position);
|
|
565
|
+
LT.SetRotation(State.Rotation.Quaternion());
|
|
566
|
+
LeafHISM->AddInstance(LT, /*bWorldSpace=*/false);
|
|
567
|
+
}
|
|
568
|
+
State = Stack.Pop();
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
## 5. Wave Function Collapse (Grid Layout)
|
|
578
|
+
|
|
579
|
+
WFC fills a grid by choosing tiles that satisfy adjacency constraints. Suitable for dungeon rooms, city blocks, terrain biome transitions.
|
|
580
|
+
|
|
581
|
+
### Tile and Constraint Definition
|
|
582
|
+
|
|
583
|
+
```cpp
|
|
584
|
+
// Each tile has a set of valid neighbor tile IDs per direction
|
|
585
|
+
struct FWFCTile
|
|
586
|
+
{
|
|
587
|
+
int32 ID;
|
|
588
|
+
float Weight; // Relative spawn probability
|
|
589
|
+
TArray<int32> AllowedRight; // IDs allowed to the +X neighbor
|
|
590
|
+
TArray<int32> AllowedLeft; // IDs allowed to the -X neighbor
|
|
591
|
+
TArray<int32> AllowedUp; // IDs allowed to the +Y neighbor
|
|
592
|
+
TArray<int32> AllowedDown; // IDs allowed to the -Y neighbor
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// Cell state during WFC
|
|
596
|
+
struct FWFCCell
|
|
597
|
+
{
|
|
598
|
+
TArray<int32> PossibleTiles; // Remaining valid tile IDs
|
|
599
|
+
bool bCollapsed = false;
|
|
600
|
+
int32 CollapsedTile = -1;
|
|
601
|
+
|
|
602
|
+
bool IsContradiction() const { return PossibleTiles.Num() == 0 && !bCollapsed; }
|
|
603
|
+
float Entropy() const { return (float)PossibleTiles.Num(); } // Simplified (no weights)
|
|
604
|
+
};
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### WFC Iteration
|
|
608
|
+
|
|
609
|
+
```cpp
|
|
610
|
+
bool WFCStep(TArray<TArray<FWFCCell>>& Grid,
|
|
611
|
+
const TArray<FWFCTile>& Tiles,
|
|
612
|
+
FRandomStream& Rand,
|
|
613
|
+
int32 Width, int32 Height)
|
|
614
|
+
{
|
|
615
|
+
// 1. Find uncollapsed cell with lowest entropy
|
|
616
|
+
float MinEntropy = FLT_MAX;
|
|
617
|
+
FIntPoint CollapsePos(-1, -1);
|
|
618
|
+
|
|
619
|
+
for (int32 Y = 0; Y < Height; Y++)
|
|
620
|
+
for (int32 X = 0; X < Width; X++)
|
|
621
|
+
{
|
|
622
|
+
FWFCCell& Cell = Grid[Y][X];
|
|
623
|
+
if (Cell.bCollapsed) continue;
|
|
624
|
+
if (Cell.IsContradiction()) return false; // Contradiction — need backtrack
|
|
625
|
+
if (Cell.Entropy() < MinEntropy)
|
|
626
|
+
{
|
|
627
|
+
MinEntropy = Cell.Entropy();
|
|
628
|
+
CollapsePos = FIntPoint(X, Y);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (CollapsePos.X < 0) return true; // All cells collapsed
|
|
633
|
+
|
|
634
|
+
// 2. Collapse: choose a tile weighted by tile weight
|
|
635
|
+
FWFCCell& Cell = Grid[CollapsePos.Y][CollapsePos.X];
|
|
636
|
+
float TotalWeight = 0.f;
|
|
637
|
+
for (int32 TileID : Cell.PossibleTiles)
|
|
638
|
+
TotalWeight += Tiles[TileID].Weight;
|
|
639
|
+
|
|
640
|
+
float Pick = Rand.FRandRange(0.f, TotalWeight);
|
|
641
|
+
float Accum = 0.f;
|
|
642
|
+
int32 ChosenTile = Cell.PossibleTiles[0];
|
|
643
|
+
for (int32 TileID : Cell.PossibleTiles)
|
|
644
|
+
{
|
|
645
|
+
Accum += Tiles[TileID].Weight;
|
|
646
|
+
if (Accum >= Pick) { ChosenTile = TileID; break; }
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
Cell.bCollapsed = true;
|
|
650
|
+
Cell.CollapsedTile = ChosenTile;
|
|
651
|
+
Cell.PossibleTiles = { ChosenTile };
|
|
652
|
+
|
|
653
|
+
// 3. Propagate constraints to neighbors (BFS)
|
|
654
|
+
TQueue<FIntPoint> PropagateQueue;
|
|
655
|
+
PropagateQueue.Enqueue(CollapsePos);
|
|
656
|
+
|
|
657
|
+
while (!PropagateQueue.IsEmpty())
|
|
658
|
+
{
|
|
659
|
+
FIntPoint P;
|
|
660
|
+
PropagateQueue.Dequeue(P);
|
|
661
|
+
|
|
662
|
+
auto Propagate = [&](FIntPoint Neighbor,
|
|
663
|
+
TFunctionRef<const TArray<int32>*(const FWFCTile&)> GetAllowed)
|
|
664
|
+
{
|
|
665
|
+
if (Neighbor.X < 0 || Neighbor.X >= Width ||
|
|
666
|
+
Neighbor.Y < 0 || Neighbor.Y >= Height) return;
|
|
667
|
+
|
|
668
|
+
FWFCCell& NCell = Grid[Neighbor.Y][Neighbor.X];
|
|
669
|
+
if (NCell.bCollapsed) return;
|
|
670
|
+
|
|
671
|
+
// Collect all tiles allowed by current cell's possible set
|
|
672
|
+
TSet<int32> AllowedSet;
|
|
673
|
+
for (int32 TileID : Grid[P.Y][P.X].PossibleTiles)
|
|
674
|
+
{
|
|
675
|
+
const TArray<int32>* Allowed = GetAllowed(Tiles[TileID]);
|
|
676
|
+
if (Allowed) AllowedSet.Append(*Allowed);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Remove incompatible tiles from neighbor
|
|
680
|
+
int32 PrevCount = NCell.PossibleTiles.Num();
|
|
681
|
+
NCell.PossibleTiles.RemoveAll([&](int32 ID) { return !AllowedSet.Contains(ID); });
|
|
682
|
+
|
|
683
|
+
if (NCell.PossibleTiles.Num() != PrevCount)
|
|
684
|
+
PropagateQueue.Enqueue(Neighbor);
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
Propagate({P.X + 1, P.Y}, [](const FWFCTile& T) { return &T.AllowedRight; });
|
|
688
|
+
Propagate({P.X - 1, P.Y}, [](const FWFCTile& T) { return &T.AllowedLeft; });
|
|
689
|
+
Propagate({P.X, P.Y + 1}, [](const FWFCTile& T) { return &T.AllowedUp; });
|
|
690
|
+
Propagate({P.X, P.Y - 1}, [](const FWFCTile& T) { return &T.AllowedDown; });
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### Grid Initialization and Run
|
|
698
|
+
|
|
699
|
+
```cpp
|
|
700
|
+
void RunWFC(int32 Width, int32 Height, const TArray<FWFCTile>& Tiles,
|
|
701
|
+
FRandomStream& Rand, TArray<TArray<FWFCCell>>& OutGrid)
|
|
702
|
+
{
|
|
703
|
+
TArray<int32> AllTileIDs;
|
|
704
|
+
for (const FWFCTile& T : Tiles) AllTileIDs.Add(T.ID);
|
|
705
|
+
|
|
706
|
+
OutGrid.SetNum(Height);
|
|
707
|
+
for (auto& Row : OutGrid)
|
|
708
|
+
{
|
|
709
|
+
Row.SetNum(Width);
|
|
710
|
+
for (FWFCCell& Cell : Row)
|
|
711
|
+
Cell.PossibleTiles = AllTileIDs;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
bool bSuccess = false;
|
|
715
|
+
for (int32 MaxSteps = Width * Height; MaxSteps > 0; MaxSteps--)
|
|
716
|
+
{
|
|
717
|
+
bSuccess = WFCStep(OutGrid, Tiles, Rand, Width, Height);
|
|
718
|
+
if (!bSuccess) break; // Contradiction: re-run with different seed
|
|
719
|
+
|
|
720
|
+
// Check all cells collapsed
|
|
721
|
+
bool bDone = true;
|
|
722
|
+
for (auto& Row : OutGrid)
|
|
723
|
+
for (auto& Cell : Row)
|
|
724
|
+
if (!Cell.bCollapsed) { bDone = false; break; }
|
|
725
|
+
if (bDone) { bSuccess = true; break; }
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
732
|
+
## 6. Async Mesh Generation Pattern
|
|
733
|
+
|
|
734
|
+
For large meshes, compute vertex data on a background thread then apply on the game thread.
|
|
735
|
+
|
|
736
|
+
```cpp
|
|
737
|
+
void AProceduralTerrain::GenerateAsync(int32 GridSize, float CellSize)
|
|
738
|
+
{
|
|
739
|
+
// Capture data needed on background thread
|
|
740
|
+
int32 Seed = SeedValue;
|
|
741
|
+
|
|
742
|
+
// Lambda runs on a background worker thread
|
|
743
|
+
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, GridSize, CellSize, Seed]()
|
|
744
|
+
{
|
|
745
|
+
TArray<FVector> Vertices;
|
|
746
|
+
TArray<int32> Triangles;
|
|
747
|
+
TArray<FVector> Normals;
|
|
748
|
+
TArray<FVector2D> UVs;
|
|
749
|
+
|
|
750
|
+
// -- heavy computation here --
|
|
751
|
+
// GenerateTerrainData(GridSize, CellSize, Seed, Vertices, Triangles, Normals, UVs);
|
|
752
|
+
|
|
753
|
+
// Schedule the mesh section creation back on the game thread
|
|
754
|
+
AsyncTask(ENamedThreads::GameThread, [this,
|
|
755
|
+
V = MoveTemp(Vertices),
|
|
756
|
+
T = MoveTemp(Triangles),
|
|
757
|
+
N = MoveTemp(Normals),
|
|
758
|
+
UV = MoveTemp(UVs)]() mutable
|
|
759
|
+
{
|
|
760
|
+
if (!IsValid(this) || !IsValid(ProceduralMesh)) return;
|
|
761
|
+
|
|
762
|
+
TArray<FColor> Colors;
|
|
763
|
+
TArray<FProcMeshTangent> Tangents;
|
|
764
|
+
|
|
765
|
+
ProceduralMesh->CreateMeshSection(0, V, T, N,
|
|
766
|
+
UV, Colors, Tangents,
|
|
767
|
+
/*bCreateCollision=*/true);
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
Note: `UProceduralMeshComponent::CreateMeshSection` must be called on the **game thread**. Only the data computation can be parallelized.
|
|
774
|
+
|
|
775
|
+
---
|
|
776
|
+
|
|
777
|
+
## 7. Spline-Driven Road Mesh
|
|
778
|
+
|
|
779
|
+
Generates a road mesh by extruding a cross-section profile along a `USplineComponent`.
|
|
780
|
+
|
|
781
|
+
```cpp
|
|
782
|
+
void ARoadMeshActor::BuildRoad(float RoadWidth, float SegmentLength)
|
|
783
|
+
{
|
|
784
|
+
TArray<FVector> Vertices;
|
|
785
|
+
TArray<int32> Triangles;
|
|
786
|
+
TArray<FVector> Normals;
|
|
787
|
+
TArray<FVector2D> UVs;
|
|
788
|
+
TArray<FColor> Colors;
|
|
789
|
+
TArray<FProcMeshTangent> Tangents;
|
|
790
|
+
|
|
791
|
+
float TotalLen = Spline->GetSplineLength();
|
|
792
|
+
float UVProgress = 0.f;
|
|
793
|
+
int32 SegCount = FMath::CeilToInt(TotalLen / SegmentLength);
|
|
794
|
+
float ActualSeg = TotalLen / SegCount;
|
|
795
|
+
|
|
796
|
+
// Extrude cross-section quads along spline
|
|
797
|
+
for (int32 Seg = 0; Seg <= SegCount; Seg++)
|
|
798
|
+
{
|
|
799
|
+
float Dist = Seg * ActualSeg;
|
|
800
|
+
FTransform T = Spline->GetTransformAtDistanceAlongSpline(
|
|
801
|
+
Dist, ESplineCoordinateSpace::World, /*bUseScale=*/false);
|
|
802
|
+
|
|
803
|
+
FVector Center = T.GetLocation();
|
|
804
|
+
FVector Right = T.GetRotation().GetRightVector();
|
|
805
|
+
FVector Up = T.GetRotation().GetUpVector();
|
|
806
|
+
|
|
807
|
+
FVector Left_V = Center - Right * RoadWidth * 0.5f;
|
|
808
|
+
FVector Right_V = Center + Right * RoadWidth * 0.5f;
|
|
809
|
+
|
|
810
|
+
Vertices.Add(Left_V);
|
|
811
|
+
Vertices.Add(Right_V);
|
|
812
|
+
Normals.Add(Up);
|
|
813
|
+
Normals.Add(Up);
|
|
814
|
+
UVs.Add(FVector2D(0.f, UVProgress));
|
|
815
|
+
UVs.Add(FVector2D(1.f, UVProgress));
|
|
816
|
+
|
|
817
|
+
if (Seg > 0)
|
|
818
|
+
{
|
|
819
|
+
int32 B = (Seg - 1) * 2;
|
|
820
|
+
int32 T2 = Seg * 2;
|
|
821
|
+
// Left triangle
|
|
822
|
+
Triangles.Add(B); Triangles.Add(T2); Triangles.Add(B + 1);
|
|
823
|
+
// Right triangle
|
|
824
|
+
Triangles.Add(B + 1); Triangles.Add(T2); Triangles.Add(T2 + 1);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
UVProgress += ActualSeg / RoadWidth; // Scale UV to aspect ratio
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
ProceduralMesh->CreateMeshSection(0, Vertices, Triangles, Normals,
|
|
831
|
+
UVs, Colors, Tangents, /*bCreateCollision=*/true);
|
|
832
|
+
ProceduralMesh->SetMaterial(0, RoadMaterial);
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
---
|
|
837
|
+
|
|
838
|
+
## Performance Reference
|
|
839
|
+
|
|
840
|
+
| Scenario | Recommended Approach | Notes |
|
|
841
|
+
|---|---|---|
|
|
842
|
+
| < 100 dynamic instances, moving | ISM + `UpdateInstanceTransform` | Simple, low overhead |
|
|
843
|
+
| 100–10,000 static instances | HISM | Culling hierarchy, LOD transitions |
|
|
844
|
+
| > 10,000 static instances | HISM + `InstanceEndCullDistance` | Aggressive distance culling |
|
|
845
|
+
| Terrain (runtime, < 256x256) | `UProceduralMeshComponent` | One `CreateMeshSection` call |
|
|
846
|
+
| Terrain (large, static) | Landscape or PCG + ISM | PCG + HISM spawner avoids draw calls |
|
|
847
|
+
| Cave/voxel (< 64x64x64) | Marching Cubes + ProcMesh | Async generation on background thread |
|
|
848
|
+
| Vegetation (open world) | PCG Surface Sampler + HISM Spawner | Budget with HiGen grid |
|
|
849
|
+
| Dungeon layout | WFC or BSP + tile mesh | Bake to static at load, or keep ProcMesh |
|
|
850
|
+
| Spline road/river | SplineMeshComponent per segment | Or extruded ProcMesh for custom profile |
|
|
851
|
+
|
|
852
|
+
---
|
|
853
|
+
|
|
854
|
+
## 9. Poisson Disc Sampling
|
|
855
|
+
|
|
856
|
+
Generates uniformly distributed points with minimum separation distance (Bridson 2007). Use for natural-looking placement (trees, rocks, enemies) without clumping.
|
|
857
|
+
|
|
858
|
+
```cpp
|
|
859
|
+
// Poisson disc sampling — Bridson's fast algorithm
|
|
860
|
+
TArray<FVector2D> PoissonDiscSample(FVector2D Min, FVector2D Max,
|
|
861
|
+
float MinDist, int32 MaxAttempts, FRandomStream& Rand)
|
|
862
|
+
{
|
|
863
|
+
float CellSize = MinDist / FMath::Sqrt(2.f);
|
|
864
|
+
FVector2D Size = Max - Min;
|
|
865
|
+
int32 GW = FMath::CeilToInt(Size.X / CellSize);
|
|
866
|
+
int32 GH = FMath::CeilToInt(Size.Y / CellSize);
|
|
867
|
+
TArray<int32> Grid;
|
|
868
|
+
Grid.Init(-1, GW * GH);
|
|
869
|
+
TArray<FVector2D> Points, Active;
|
|
870
|
+
|
|
871
|
+
// Seed with first random point
|
|
872
|
+
FVector2D First(Rand.FRandRange(Min.X, Max.X), Rand.FRandRange(Min.Y, Max.Y));
|
|
873
|
+
Points.Add(First);
|
|
874
|
+
Active.Add(First);
|
|
875
|
+
Grid[FMath::FloorToInt((First.Y - Min.Y) / CellSize) * GW +
|
|
876
|
+
FMath::FloorToInt((First.X - Min.X) / CellSize)] = 0;
|
|
877
|
+
|
|
878
|
+
while (Active.Num() > 0)
|
|
879
|
+
{
|
|
880
|
+
int32 Idx = Rand.RandRange(0, Active.Num() - 1);
|
|
881
|
+
FVector2D Base = Active[Idx];
|
|
882
|
+
bool bFound = false;
|
|
883
|
+
|
|
884
|
+
for (int32 k = 0; k < MaxAttempts; k++)
|
|
885
|
+
{
|
|
886
|
+
float Angle = Rand.FRandRange(0.f, 2.f * PI);
|
|
887
|
+
float R = Rand.FRandRange(MinDist, 2.f * MinDist);
|
|
888
|
+
FVector2D Candidate = Base + FVector2D(FMath::Cos(Angle), FMath::Sin(Angle)) * R;
|
|
889
|
+
|
|
890
|
+
if (Candidate.X < Min.X || Candidate.X > Max.X ||
|
|
891
|
+
Candidate.Y < Min.Y || Candidate.Y > Max.Y)
|
|
892
|
+
continue;
|
|
893
|
+
|
|
894
|
+
int32 GX = FMath::FloorToInt((Candidate.X - Min.X) / CellSize);
|
|
895
|
+
int32 GY = FMath::FloorToInt((Candidate.Y - Min.Y) / CellSize);
|
|
896
|
+
bool bTooClose = false;
|
|
897
|
+
|
|
898
|
+
for (int32 DY = -2; DY <= 2 && !bTooClose; DY++)
|
|
899
|
+
{
|
|
900
|
+
for (int32 DX = -2; DX <= 2 && !bTooClose; DX++)
|
|
901
|
+
{
|
|
902
|
+
int32 NX = GX + DX, NY = GY + DY;
|
|
903
|
+
if (NX < 0 || NX >= GW || NY < 0 || NY >= GH) continue;
|
|
904
|
+
int32 PIdx = Grid[NY * GW + NX];
|
|
905
|
+
if (PIdx >= 0 && FVector2D::Distance(Points[PIdx], Candidate) < MinDist)
|
|
906
|
+
bTooClose = true;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (!bTooClose)
|
|
911
|
+
{
|
|
912
|
+
Grid[GY * GW + GX] = Points.Num();
|
|
913
|
+
Points.Add(Candidate);
|
|
914
|
+
Active.Add(Candidate);
|
|
915
|
+
bFound = true;
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (!bFound) Active.RemoveAtSwap(Idx);
|
|
920
|
+
}
|
|
921
|
+
return Points;
|
|
922
|
+
}
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
**Usage with ISM placement:**
|
|
926
|
+
|
|
927
|
+
```cpp
|
|
928
|
+
FRandomStream Stream(MySeed);
|
|
929
|
+
TArray<FVector2D> Placements = PoissonDiscSample(
|
|
930
|
+
FVector2D(0, 0), FVector2D(10000, 10000), 200.f, 30, Stream);
|
|
931
|
+
|
|
932
|
+
for (const FVector2D& Pos : Placements)
|
|
933
|
+
{
|
|
934
|
+
FTransform T(FRotator(0, Stream.FRandRange(0, 360), 0),
|
|
935
|
+
FVector(Pos.X, Pos.Y, GetGroundHeight(Pos)));
|
|
936
|
+
TreeISM->AddInstance(T);
|
|
937
|
+
}
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
Key properties: O(n) time complexity, guaranteed minimum separation, deterministic with `FRandomStream` seed. Increase `MaxAttempts` (default 30) for denser packing; decrease for faster generation.
|