Upload of all files, starting point
This commit is contained in:
+489
@@ -0,0 +1,489 @@
|
|||||||
|
# VoxelForge — Code Map & Knowledge Index
|
||||||
|
|
||||||
|
> Purpose: a navigation index so anyone (human or AI) can locate and modify code
|
||||||
|
> without re-reading the whole plugin. Anchors are `File:line` — line numbers drift
|
||||||
|
> as code is edited, so trust the **symbol name** first and the line as a hint.
|
||||||
|
>
|
||||||
|
> Plugin root: `Source/VoxelForge/` · ~8,300 lines of C++ across 22 files.
|
||||||
|
> Comments in the code are mixed **French + English**. UE module = `VoxelForge` (Runtime).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What this plugin is
|
||||||
|
|
||||||
|
A **density-field voxel terrain** plugin for Unreal Engine, built around underground
|
||||||
|
**"strates"** (vertical geological layers, each a self-contained mini-world).
|
||||||
|
|
||||||
|
- **No block grid.** Terrain is a continuous scalar density field evaluated on the fly
|
||||||
|
from world coordinates. Convention: **negative = solid rock, positive = air**
|
||||||
|
(the Marching Cubes convention used throughout).
|
||||||
|
- **Marching Cubes** turns the density field into a smooth mesh per 32³ chunk.
|
||||||
|
- **Strates** stack downward from Z=0. Each strate is a `UVoxelStrateDefinition` data
|
||||||
|
asset that picks a generator type and a huge bag of cave-shaping params.
|
||||||
|
- **Async streaming**: chunks load/unload around the player on background tasks; meshes
|
||||||
|
are applied on the game thread under a per-frame budget.
|
||||||
|
- **Player edits** (carve/fill) are stored as a *diff layer* added on top of the
|
||||||
|
procedural density — procedural generation stays deterministic.
|
||||||
|
|
||||||
|
### Core terminology
|
||||||
|
| Term | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| Chunk | 32×32×32 voxels (`CHUNK_SIZE`). Voxel = 25 cm (`VOXEL_SIZE`). |
|
||||||
|
| Density | Scalar field. `< 0` solid, `> 0` air, `0` = surface (`IsoLevel`). |
|
||||||
|
| Strate | Vertical layer of the world, stacked downward. Has its own generator + params. |
|
||||||
|
| Generator type | Archetype per strate: `TunnelNetwork`, `FlatPlain`, `CrystalChamber`, `Maze`, `SurfaceWorld`, `VerticalShafts`, `FloatingIslands`, `Underwater`. See §8. |
|
||||||
|
| (0,0) spine | Guaranteed open landing column at world XY (0,0) in every strate; descent is player-dug through the seals. See §8. |
|
||||||
|
| Terrain op | Optional density modifier (pit, arch, terrace…) attached to a strate. |
|
||||||
|
| Passage | Carved tunnel connecting two adjacent strates (progression path). |
|
||||||
|
| Diff layer | Player carve/fill modifications stored on top of procedural density. |
|
||||||
|
| Epoch | Generation counter; stale async results are discarded on mismatch. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The big picture — data flow
|
||||||
|
|
||||||
|
```
|
||||||
|
AVoxelWorld (actor, orchestrator)
|
||||||
|
Tick → UpdateChunksAroundPosition
|
||||||
|
│ (load/unload around player, by distance + LOD)
|
||||||
|
▼
|
||||||
|
LoadChunk → UE::Tasks::Launch ──────────► background thread
|
||||||
|
│
|
||||||
|
UVoxelMarchingCubesMesher::GenerateMesh(chunk, step) ◄────────────────┘
|
||||||
|
│ samples density per cell corner
|
||||||
|
▼
|
||||||
|
UVoxelGenerator::GetDensityAt(x,y,z) ← THE density entry point
|
||||||
|
│ asks StrateManager which strate/params/generator-type applies
|
||||||
|
├─ TunnelNetwork → GetDensityWithParams() (rooms+tunnels+ops+worms+seal+passages)
|
||||||
|
│ └─ VoxelCaveMorphology::BuildChunkCache + EvaluateSDFCached (room/tunnel SDF)
|
||||||
|
├─ FlatPlain / CrystalChamber → GetSlabDensity() (floor+ceiling+columns+seal+passages)
|
||||||
|
└─ + UVoxelDiffLayer::GetDensityOffset() (player carve/fill)
|
||||||
|
│
|
||||||
|
▼ FVoxelMeshData (verts/tris/uvs/normals)
|
||||||
|
ProcessQueue (lock-free) ──► game thread: ProcessPendingChunks → ApplyMeshToChunk
|
||||||
|
(RealtimeMeshComponent)
|
||||||
|
```
|
||||||
|
|
||||||
|
`UVoxelStrateManager` is the side oracle: "what strate is at this Z, what params,
|
||||||
|
what generator type, and is there a passage/elevator SDF near here?"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. File-by-file reference
|
||||||
|
|
||||||
|
Paths relative to `Source/VoxelForge/`. `Public/` = headers, `Private/` = impl.
|
||||||
|
|
||||||
|
### 3.1 Module & build
|
||||||
|
| File | Role |
|
||||||
|
|------|------|
|
||||||
|
| `../../VoxelForge.uplugin` | Plugin manifest. One Runtime module `VoxelForge`. Beta. |
|
||||||
|
| `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. |
|
||||||
|
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
|
||||||
|
|
||||||
|
### 3.2 Foundational types — `Public/VoxelTypes.h` (no UClass, everyone includes it)
|
||||||
|
| Symbol | Line | Notes |
|
||||||
|
|--------|------|-------|
|
||||||
|
| `CHUNK_SIZE` (32), `CHUNK_SIZE_SQUARED`, `CHUNK_VOLUME` | 19-21 | Chunk dimensions. |
|
||||||
|
| `VOXEL_SIZE` (25.0f cm) | 23 | World scale. |
|
||||||
|
| `EVoxelFace` enum + `GetFaceDirection` / `GetFaceNormal` | 33-61 | 6 cube faces. |
|
||||||
|
| `WorldToChunkCoord` / `WorldToLocalCoord` / `ChunkToWorldPos` | 74-104 | Coord-space conversions (handle negatives via floor/positive-modulo). |
|
||||||
|
| `LocalToIndex` / `IndexToLocal` / `IsValidLocalCoord` | 107-131 | Flat-array 3D↔1D indexing. |
|
||||||
|
| `SmoothStep01` | 140 | 3x²-2x³ — used everywhere for blends. |
|
||||||
|
| `VOXEL_NOISE_SCALE` (1.25f) | 147 | Rescales UE PerlinNoise3D to ~[-1,1]. |
|
||||||
|
| `FVoxelMeshData` struct | 157-173 | Mesher output (Vertices/Triangles/UVs/Normals). Plain C++, not USTRUCT. |
|
||||||
|
|
||||||
|
### 3.3 Chunk identity — `Public/VoxelChunk.h`
|
||||||
|
`FVoxelChunk` (USTRUCT, line 19): just a `ChunkCoord` + `GetWorldPosition()`. In a
|
||||||
|
density-only world the chunk stores no voxels — it's a coord wrapper. Room to cache
|
||||||
|
per-chunk info later.
|
||||||
|
|
||||||
|
### 3.4 Settings — `Public/VoxelSettings.h`
|
||||||
|
`UVoxelSettings : UPrimaryDataAsset` — the single tuning asset assigned on `AVoxelWorld`.
|
||||||
|
| Group | Fields (line) |
|
||||||
|
|-------|---------------|
|
||||||
|
| Streaming | `ViewDistanceXY=16` (26), `ViewDistanceUp/Down=5` (29/32), `MaxConcurrentTasks=16` (36), `MaxMeshAppliesPerFrame=4` (40) |
|
||||||
|
| LOD | `LOD0Distance=4` (48), `LOD1Distance=8` (53) |
|
||||||
|
| Rendering | `VoxelMaterial` (61) |
|
||||||
|
| Strates | `Seed` (69), `CurrentSeason=1` (73), `StratePool` (78), `FixedStrates` map (83), `TotalStrates=10` (87) |
|
||||||
|
| Carving budget | `MaxModifications=0` (97), `MaxBrushRadius=15` (102), `MaxTotalVolume=0` (107). 0 = unlimited. |
|
||||||
|
|
||||||
|
### 3.5 World orchestrator — `Public/VoxelWorld.h` + `Private/VoxelWorld.cpp`
|
||||||
|
`AVoxelWorld : AActor` — owns everything, drives streaming. Also `FChunkResult` struct
|
||||||
|
(VoxelWorld.h:35) = async task payload (coord, chunk, meshdata, LOD, **Epoch**).
|
||||||
|
|
||||||
|
**Owned objects (UPROPERTY):** `Settings`, `Generator`, `Mesher`, `StrateManager`,
|
||||||
|
`DiffLayer` (VoxelWorld.h:55-78). **Storage:** `Chunks` map, `ChunkMeshes` map,
|
||||||
|
`ChunkLODs`, `ProcessQueue` (TQueue), `PendingChunkCoord` (TSet) (VoxelWorld.h:85-312).
|
||||||
|
**Async state:** `bShuttingDown`, `ActiveTaskCount` (atomics), `GenerationEpoch`.
|
||||||
|
|
||||||
|
| Method | .cpp line | Role |
|
||||||
|
|--------|-----------|------|
|
||||||
|
| `AVoxelWorld()` ctor | 12 | Enables Tick. |
|
||||||
|
| `RegenerateAllChunks()` | 21 | Bumps epoch, unloads all → Tick reloads. CallInEditor button. |
|
||||||
|
| `PostEditChangeProperty` | 45 | Editor live-edit hook. |
|
||||||
|
| `OnObjectModifiedInEditor` | 58 | Regenerates when a strate asset is edited (if `bLiveEditStrates`). |
|
||||||
|
| `EndPlay` | 140 | Sets `bShuttingDown`, **waits for `ActiveTaskCount`→0**, unbinds delegate. |
|
||||||
|
| `BeginPlay` | 177 | Constructs Generator/Mesher/StrateManager/DiffLayer, wires services, seeds. |
|
||||||
|
| `Tick` | 220 | `UpdateChunksAroundPosition(player)` + `ProcessPendingChunks()`. |
|
||||||
|
| `GetPlayerPosition` | 231 | Pawn position or zero. |
|
||||||
|
| `GetLODForChunk` / `LODToStep` | 242 / 268 | Distance→LOD (0/1/2) → step (1/2/4). |
|
||||||
|
| `IsChunkInRange` | 275 | View-distance test. |
|
||||||
|
| `ProcessPendingChunks` | 301 | Drains ProcessQueue under per-frame budget; **discards stale epochs**; applies meshes. |
|
||||||
|
| `UpdateChunksAroundPosition` | 362 | Builds desired set, sorts by distance, loads/unloads, handles LOD changes. |
|
||||||
|
| `LoadChunk` | 445 | Budget check → `UE::Tasks::Launch` background gen+mesh; RAII task guard. |
|
||||||
|
| `UnloadChunk` | 493 | Destroys mesh component + map entries. |
|
||||||
|
| `ApplyMeshToChunk` | 503 | Get/create RealtimeMeshComponent, upload geometry, assign material. |
|
||||||
|
| `GetStrateAtPosition` | 679 | Gameplay query → strate index. |
|
||||||
|
| `CarveAtPosition` / `FillAtPosition` | 691 / 709 | Build `FVoxelModification` → DiffLayer → RemeshDirtyChunks. |
|
||||||
|
| `ClearAllModifications` | 726 | Clears diff layer, regenerates. |
|
||||||
|
| `ChangeSeed` | 740 | **Season reset**: new seed everywhere, clear diffs, bump season, reload. |
|
||||||
|
| `GetCurrentSeed` / `GetCurrentSeason` | 784 / 789 | Accessors. |
|
||||||
|
| `RemeshDirtyChunks` | 798 | Re-queue loaded chunks for async re-mesh (no visual pop). |
|
||||||
|
|
||||||
|
### 3.6 Density generator — `Public/VoxelGenerator.h` + `Private/VoxelGenerator.cpp`
|
||||||
|
`UVoxelGenerator : UObject` — lightweight; holds `Seed`, and injected services
|
||||||
|
`StrateManager` + `DiffLayer` (both nullable). This is **where terrain shape lives.**
|
||||||
|
|
||||||
|
| Symbol | .cpp line | Role |
|
||||||
|
|--------|-----------|------|
|
||||||
|
| `FractalNoise3D` (static) | 25 | fBM (layered Perlin). |
|
||||||
|
| `RidgedNoise3D` (static) | 55 | Ridged multifractal — craggy. |
|
||||||
|
| `CellularNoise3D` (static) | 101 | Worley/cellular — grotto/scallop. |
|
||||||
|
| `ApplyBoundarySeal` (static) | 170 | Solidifies strate top/bottom shells. |
|
||||||
|
| `ApplyPassageCarving` (static) | 197 | Punches passages/elevator through the seal. |
|
||||||
|
| `InitializeSettings` | 211 | Copies seed from settings. |
|
||||||
|
| **`GetDensityAt`** | 218 | **Entry point.** Picks strate + generator type, dispatches, adds diff offset. |
|
||||||
|
| **`GetDensityWithParams`** | 277 | TunnelNetwork pipeline (~1000 lines). See §4. |
|
||||||
|
| **`GetSlabDensity`** | 1306 | FlatPlain/CrystalChamber pipeline. See §4.2. |
|
||||||
|
|
||||||
|
### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp`
|
||||||
|
Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
||||||
|
|
||||||
|
- `namespace VoxelSDF` (h:46): `Sphere`, `Ellipsoid`, `Capsule`, `RoundedBox`,
|
||||||
|
`TaperedCapsule`, `SmoothMin`, `SmoothMax` — all FORCEINLINE SDF primitives.
|
||||||
|
- `namespace VoxelHash` (h:158): `Mix`, `Cell`, `Pair`, `ToFloat01`, `ToFloatSigned`
|
||||||
|
— deterministic hashing for room/tunnel placement (no storage, infinite worlds).
|
||||||
|
- Cache structs (h:224-330): `FCachedRoom`, `FCachedTunnel`, `FCachedPit`,
|
||||||
|
`FCachedChimney`, `FCachedColumn`, `FChunkSDFCache`.
|
||||||
|
- `namespace VoxelCaveMorphology`:
|
||||||
|
| Function | .cpp line | Role |
|
||||||
|
|----------|-----------|------|
|
||||||
|
| `BuildChunkCache` | 47 | **Phase 1** (once/chunk): collect rooms, nearest-neighbor backbone, decide tunnels, pre-bake pits/chimneys/columns, hash-roll per-room terrain op. |
|
||||||
|
| `EvaluateSDFCached` | 589 | **Phase 2** (per voxel): SmoothMin over cached rooms/tunnels; returns nearest room idx for terrain-op lookup. |
|
||||||
|
| `EvaluateSDF` | 738 | Convenience wrapper (builds temp cache) for one-off queries. |
|
||||||
|
|
||||||
|
Performance note (h:209-220): caching rooms/tunnels once per chunk instead of per
|
||||||
|
voxel is the single biggest CPU win.
|
||||||
|
|
||||||
|
### 3.8 Strate system
|
||||||
|
**`Public/VoxelStrateTypes.h`** — shared structs/enums (1228 lines, the data vocabulary):
|
||||||
|
| Symbol | Line | Role |
|
||||||
|
|--------|------|------|
|
||||||
|
| `EVoxelPassageType` | 38 | Sloped/Vertical/Spiral/Cascading/Crack passage shapes. |
|
||||||
|
| `ESurfaceType` | 78 | Floor/Wall/Ceiling/Any (decoration placement). |
|
||||||
|
| `EVoxelNoiseType` | 99 | FBM/Ridged/Mixed/Cellular. |
|
||||||
|
| `ECaveGeneratorType` | 146 | TunnelNetwork / FlatPlain / CrystalChamber. |
|
||||||
|
| `EVoxelStrateTransition` | 183 | Gradient / Hard / Interleaved boundary blends. |
|
||||||
|
| **`FStrateGenerationParams`** | 213 | The giant TunnelNetwork param bag (rock, worms, rooms, tunnels, warp, roughness, all terrain-op transport fields, boundary seal). `Lerp()` static at 844 blends two sets at boundaries. |
|
||||||
|
| `FStrateTerrainOpEntry` | 965 | Soft-ptr to a terrain op + Weight + Probability. |
|
||||||
|
| **`FSlabGenerationParams`** | 1019 | Floor/ceiling heights, roughness, columns, seal — for slab generators. |
|
||||||
|
| `FStrateDecoration` / `FStrateAmbientActor` / `FStrateCreature` | 1160 / 1192 / 1212 | Content spawn entries (consumed by future systems). |
|
||||||
|
|
||||||
|
**`Public/VoxelStrateDefinition.h`** — `UVoxelStrateDefinition : UPrimaryDataAsset`
|
||||||
|
(line 36). One asset = one strate *type*. Fields: identity, `StrateHeightInChunks`(60),
|
||||||
|
`TransitionType`(79)/`TransitionBlendChunks`(89), `GeneratorType`(102),
|
||||||
|
`GenerationParams`(113), `SlabParams`(124), `TerrainOperations`(147), visuals/fog/light,
|
||||||
|
content lists, audio, `GameplayTags`(223). EditConditions show/hide param groups by generator type.
|
||||||
|
|
||||||
|
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:108).
|
||||||
|
Maps depth→strate at runtime; owns passages.
|
||||||
|
- `FVoxelPassage` (h:39): endpoints, radius, type, control points.
|
||||||
|
- `FStrateSlot` (h:84): definition + chunk-Z range + index.
|
||||||
|
| Method | .cpp line | Role |
|
||||||
|
|--------|-----------|------|
|
||||||
|
| `Initialize` | 10 | Builds the stacked layout from settings+seed (fixed slots + shuffled pool), then `GeneratePassages`. |
|
||||||
|
| `GeneratePassages` | 146 | Deterministic passages between consecutive strates (per-type control points). |
|
||||||
|
| `EvaluateModifierSDF` | 371 | SDF of all passages + elevator shaft at a point (for carving). |
|
||||||
|
| `FindSlotIndexForChunkZ` | 427 | Z → layout index. |
|
||||||
|
| `GetStrateAt` / `GetStrateIndex` | 443 / 455 | World-Z queries. |
|
||||||
|
| `GetStrateForChunk` | 466 | Chunk → definition. |
|
||||||
|
| `GetGeneratorTypeForChunk` | 476 | Chunk → generator type. |
|
||||||
|
| `GetSlabParamsForChunk` | 490 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
|
||||||
|
| `GetGenerationParams` | 515 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
|
||||||
|
| `BuildParamsFromDefinition` (static) | 771 | Base params + merge all referenced terrain op assets. The one place ops fold into params. |
|
||||||
|
|
||||||
|
**`Public/VoxelTerrainOpDefinition.h` + `.cpp`** — `UVoxelTerrainOpDefinition : UPrimaryDataAsset`
|
||||||
|
(h:67). One asset = one terrain op. `EVoxelTerrainOpType` (h:36): Terrace, LayerLines,
|
||||||
|
Ribbing, Cliff, Scallop, Overhang, Arch, Column, Pit, Chimney, Dome, Pinch. Per-type
|
||||||
|
param groups gated by EditCondition. `ApplyTo(OutParams, Weight)` (.cpp:6) copies only
|
||||||
|
the active type's fields into `FStrateGenerationParams`, scaled by Weight.
|
||||||
|
|
||||||
|
### 3.9 Player edits — `Public/VoxelDiffLayer.h` + `.cpp`
|
||||||
|
`UVoxelDiffLayer : UObject` (h:77). Stores `FVoxelModification` (h:43: Center/Radius/Strength;
|
||||||
|
**negative Strength = carve, positive = fill**) grouped by chunk in `TMap ChunkMods`.
|
||||||
|
| Method | .cpp line | Role |
|
||||||
|
|--------|-----------|------|
|
||||||
|
| `SetBudget` | 10 | From VoxelSettings carving caps. |
|
||||||
|
| `CanModify` | 20 | Budget check (no consume) — for UI. |
|
||||||
|
| `GetRemainingModifications` / `GetRemainingVolume` | 47 / 53 | -1 = unlimited. |
|
||||||
|
| `ApplyModification` | 63 | Enforces budget, stores in all overlapped chunks, returns dirty coords. |
|
||||||
|
| `GetDensityOffset` | 131 | Per-voxel combined diff (smoothstep falloff, additive). |
|
||||||
|
| `HasModifications` | 160 | Fast reject for hot path. |
|
||||||
|
| `Clear` | 170 | Wipe all (season reset). |
|
||||||
|
| `GetTotalModificationCount` / `GetModifiedChunkCount` | 182 / 192 | Stats. |
|
||||||
|
|
||||||
|
### 3.10 Mesher — `Public/VoxelMarchingCubesMesher.h` + `.cpp`
|
||||||
|
`UVoxelMarchingCubesMesher : UObject` (h:21). Holds `Generator` ptr, `IsoLevel=0`,
|
||||||
|
`GradientOffset=1`.
|
||||||
|
| Method | .cpp line | Role |
|
||||||
|
|--------|-----------|------|
|
||||||
|
| `GetDensity` | 11 | Local coord → world → `Generator->GetDensityAt`. |
|
||||||
|
| `InterpolateEdge` | 28 | Linear edge crossing between two corner densities. |
|
||||||
|
| `ComputeGradientNormal` | 48 | Central-difference gradient → smooth normal. |
|
||||||
|
| **`GenerateMesh`** | 75 | The MC loop over cells; `Step` controls LOD sampling. |
|
||||||
|
|
||||||
|
**`Public/MarchingCubesTables.h`** — `EdgeTable` + `TriTable` reference data (Paul
|
||||||
|
Bourke). Cube corner/edge layout documented at top (lines 7-37). Rarely needs editing.
|
||||||
|
|
||||||
|
### 3.11 Per-chunk content & per-strate atmosphere (2026 redesign — see §8)
|
||||||
|
| File | Role |
|
||||||
|
|------|------|
|
||||||
|
| `Public/Private/VoxelContentManager.h/.cpp` | `UVoxelContentManager` — deterministic decoration scatter (LOD0) + aesthetic water planes, per chunk. Owned by `AVoxelWorld`. §8.5. |
|
||||||
|
| `Public/Private/VoxelAtmosphereManager.h/.cpp` | `UVoxelAtmosphereManager` — per-strate fog/skylight + persistent ceiling/floor layer actors + full `AtmosphereActor` override. Owned by `AVoxelWorld`. §8.6. |
|
||||||
|
|
||||||
|
> The big 2026 redesign (8 archetypes, (0,0) spine, inter-strate gap, per-strate passages,
|
||||||
|
> disturbances, content/atmosphere, brush shapes, perf invariants) is documented in **§8** —
|
||||||
|
> read it first when touching generation/strates/passages.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. The density pipeline (most-edited hot path)
|
||||||
|
|
||||||
|
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:277
|
||||||
|
Stage order (negative=solid throughout). Each stage's anchor:
|
||||||
|
| Step | Line | What |
|
||||||
|
|------|------|------|
|
||||||
|
| 1 — Vertical scale | 307 | Stretch Z before noise (`VerticalScale`). |
|
||||||
|
| 2 — Base density | 316 | Everything starts solid at `BaseDensity`. |
|
||||||
|
| 3 — Cave warp | 321 | Domain-warp the SDF query coords (organic shapes). |
|
||||||
|
| 4 — SDF morphology | 368 | Rooms+tunnels via `BuildChunkCache`/`EvaluateSDFCached`. |
|
||||||
|
| 4b — Surface roughness | 564 | Volumetric noise near surfaces (fBM/Ridged/Mixed). |
|
||||||
|
| 4c–4h — Terrain ops | 688 | Per-room op applied near surfaces. Sub-anchors below. |
|
||||||
|
| · Terracing | 727 | Step-like ledges. |
|
||||||
|
| · Layer lines | 823 | Horizontal grooves (sin of Z). |
|
||||||
|
| · Ribbing | 853 | Parallel ridges (sin of Z). |
|
||||||
|
| · Overhangs | 882 | Low-Z-freq noise shelves. |
|
||||||
|
| · Cliff sharpening | 918 | Amplify vertical gradient. |
|
||||||
|
| · Scallop | 962 | Cellular erosion bowls. |
|
||||||
|
| · Arch/Bridge | 998 | Hash-placed capsules across voids. |
|
||||||
|
| 4d — Columns | 1053 | Pre-baked vertical cylinders. |
|
||||||
|
| 4g — Domes | 1077 | Room-relative hemispherical ceilings. |
|
||||||
|
| 4h — Pinch | 1142 | Passage bottlenecks. |
|
||||||
|
| 5 — Worm tunnels | 1241 | abs(noise1)+abs(noise2) connectivity. |
|
||||||
|
| 6 — Boundary seal | 1274 | Solid top/bottom shells (`ApplyBoundarySeal`). |
|
||||||
|
| 7 — Inter-strate passages | 1281 | Carve passages/elevator (`ApplyPassageCarving`). |
|
||||||
|
|
||||||
|
### 4.2 `GetSlabDensity` (FlatPlain / CrystalChamber) — VoxelGenerator.cpp:1306
|
||||||
|
| Step | Line | What |
|
||||||
|
|------|------|------|
|
||||||
|
| 1 — Floor surface | 1317 | Noisy floor height. |
|
||||||
|
| 2 — Ceiling surface | 1343 | Formations hang downward (`abs(noise)`). |
|
||||||
|
| 3 — Void→base density | 1379 | Solid outside [floor,ceiling]. |
|
||||||
|
| 4 — Columns | 1399 | World-space hash grid, full-height. |
|
||||||
|
| 5+6 — Seal + passages | 1461 | Same seal/passage carving as TunnelNetwork. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. "I want to change X" → go here
|
||||||
|
|
||||||
|
| Goal | Location |
|
||||||
|
|------|----------|
|
||||||
|
| Chunk size / voxel scale | `VoxelTypes.h:19-23` (rebuild everything). |
|
||||||
|
| View distance / task budget / LOD distances | `VoxelSettings.h` (no recompile of logic — data asset). |
|
||||||
|
| LOD step mapping | `AVoxelWorld::LODToStep` VoxelWorld.cpp:268; `GetLODForChunk` :242. |
|
||||||
|
| How chunks stream in/out | `UpdateChunksAroundPosition` VoxelWorld.cpp:362. |
|
||||||
|
| Async threading / stale-result handling | `LoadChunk` :445, `ProcessPendingChunks` :301, Epoch logic. |
|
||||||
|
| Add a new cave feature / terrain op | Add enum in `VoxelTerrainOpDefinition.h:36`, params there, `ApplyTo` (.cpp:6), transport fields in `FStrateGenerationParams`, consume it in a new Step inside `GetDensityWithParams`. |
|
||||||
|
| Tweak room/tunnel shapes | `VoxelCaveMorphology.cpp` `BuildChunkCache` :47 / `EvaluateSDFCached` :589. |
|
||||||
|
| Worm tunnel behavior | `GetDensityWithParams` Step 5, VoxelGenerator.cpp:1241. |
|
||||||
|
| Strate stacking / which strate where | `UVoxelStrateManager::Initialize` :10. |
|
||||||
|
| Boundary blend between strates | `GetGenerationParams` :515 + `FStrateGenerationParams::Lerp` (StrateTypes.h:844). |
|
||||||
|
| Passages between strates | `GeneratePassages` :146 + `EvaluateModifierSDF` :371 + `ApplyPassageCarving` (Generator.cpp:197). |
|
||||||
|
| Player carve/fill | `CarveAtPosition`/`FillAtPosition` VoxelWorld.cpp:691/709 → `UVoxelDiffLayer::ApplyModification` :63. |
|
||||||
|
| Mesh smoothness / normals | `UVoxelMarchingCubesMesher::ComputeGradientNormal` :48, `IsoLevel`/`GradientOffset` (h:51/55). |
|
||||||
|
| New slab/flat-world generator | `GetSlabDensity` Generator.cpp:1306 + `FSlabGenerationParams` (StrateTypes.h:1019). |
|
||||||
|
| Season reset | `AVoxelWorld::ChangeSeed` :740. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Conventions & gotchas
|
||||||
|
|
||||||
|
- **Density sign is the #1 source of confusion.** Internally (MC) **negative = solid,
|
||||||
|
positive = air**. Carve = subtract density (toward positive); Fill = add (toward negative).
|
||||||
|
`FVoxelModification::Strength` negative = carve. Comments sometimes say the inverse in
|
||||||
|
different layers — trust the MC convention at the mesher.
|
||||||
|
- **Coordinate units:** density functions take **voxel coords** (not cm). World↔voxel
|
||||||
|
conversions live in `VoxelTypes.h`. Mesher converts before calling the generator.
|
||||||
|
- **Determinism:** all randomness is hash-of-(coord, seed, strateIndex) — no RNG state.
|
||||||
|
Same seed ⇒ identical world. Player edits are the only non-deterministic overlay.
|
||||||
|
- **Async safety:** background tasks must check `bShuttingDown` and only touch the
|
||||||
|
generator/mesher (no UObject mutation). Results return via `ProcessQueue`. `EndPlay`
|
||||||
|
blocks until `ActiveTaskCount == 0`.
|
||||||
|
- **Epoch:** every regeneration bumps `GenerationEpoch`; results tagged with an old epoch
|
||||||
|
are dropped in `ProcessPendingChunks`. Always carry the epoch through new async paths.
|
||||||
|
- **Generated code** under `Intermediate/` and `Binaries/` is build output — never edit.
|
||||||
|
`*.generated.h` / `*.gen.cpp` are UHT output for the `UCLASS`/`USTRUCT` above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Files NOT to touch
|
||||||
|
`Binaries/`, `Intermediate/` — compiler/UHT output, regenerated on build.
|
||||||
|
`MarchingCubesTables.h` — canonical reference tables, only change if switching MC variant.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Archetypes, spine, disturbances, content & carving (2026 redesign)
|
||||||
|
|
||||||
|
A large A-to-Z expansion. The world is a stack of strates the player descends through;
|
||||||
|
each strate can be a fundamentally different *archetype*, connected at (0,0).
|
||||||
|
|
||||||
|
### 8.1 Archetypes (`ECaveGeneratorType`, VoxelStrateTypes.h)
|
||||||
|
Each archetype has its own param `USTRUCT` (on `UVoxelStrateDefinition`, EditCondition-gated
|
||||||
|
by `GeneratorType`) and its own density function in `VoxelGenerator.cpp`, dispatched by the
|
||||||
|
`switch` in `GetDensityAt`.
|
||||||
|
|
||||||
|
| Archetype | Params struct | Density fn | Idea |
|
||||||
|
|-----------|---------------|------------|------|
|
||||||
|
| TunnelNetwork | `FStrateGenerationParams` | `GetDensityWithParams` | rooms+tunnels (original) |
|
||||||
|
| FlatPlain / CrystalChamber | `FSlabGenerationParams` | `GetSlabDensity` | floor/ceiling void (original) |
|
||||||
|
| Maze | `FMazeGenerationParams` | `GetMazeDensity` | tight corridors on a 3D lattice (per-voxel, no cache; edge = lower node + axis hash) |
|
||||||
|
| SurfaceWorld | `FSurfaceGenerationParams` | `GetSurfaceDensity` | heightfield terrain (continents+ridged mtns+detail), beaches at water line, high sky-cap ceiling |
|
||||||
|
| VerticalShafts | `FVerticalShaftParams` | `GetVerticalShaftDensity` | full-height shafts + horizontal connectors + partial ledges |
|
||||||
|
| FloatingIslands | `FFloatingIslandParams` | `GetFloatingIslandDensity` | asymmetric islands: flat land top + underside tapering to a point, lobed (domain-warped) outline, in an open void |
|
||||||
|
| Underwater | `FStrateGenerationParams` + water | (reuses `GetDensityWithParams`) | tunnel rock + high water table |
|
||||||
|
|
||||||
|
All density fns share the convention: internal **positive=solid**, apply origin spine →
|
||||||
|
boundary seal → inter-strate passages, then `return -Density` (MC: negative=solid).
|
||||||
|
StrateManager provides params per chunk via `GetMaze/Surface/VerticalShaft/FloatingIslandParamsForChunk`
|
||||||
|
(macro `VF_ARCHETYPE_PARAMS_GETTER`) — no cross-boundary blend (Hard transitions between archetypes).
|
||||||
|
|
||||||
|
### 8.2 (0,0) spine & hybrid connections
|
||||||
|
- `ApplyOriginSpine` (VoxelGenerator.cpp, static helper) carves a guaranteed open vertical
|
||||||
|
column at XY (0,0) in every strate's **interior** (seals untouched). Radius =
|
||||||
|
`UVoxelGenerator::OriginSpineRadius` ← `VoxelSettings::OriginSpineRadius`. Called before
|
||||||
|
every `ApplyBoundarySeal`.
|
||||||
|
- Descent is **player-dug** through the thin seals at (0,0). The single auto-opened
|
||||||
|
connection is the **surface entry shaft** at (0,0) through the top of strate 0
|
||||||
|
(`GeneratePassages`, `bOpenSurfaceEntry`).
|
||||||
|
- **Hybrid extras:** auto-carved *shortcut* passages per boundary, placed away from (0,0).
|
||||||
|
Now fully **per-strate** — see §8.8 (the upper strate's `PassageConfig` drives count/style/shape).
|
||||||
|
|
||||||
|
### 8.3 Disturbance layer (the "wow" post-process)
|
||||||
|
`FStrateDisturbanceParams` (on the definition, all archetypes). `ApplyDisturbances`
|
||||||
|
(VoxelGenerator.cpp static, **MC convention**) runs in `GetDensityAt` after dispatch:
|
||||||
|
chasms (carve air), bridges (solid spans), ridges (solid blades). Stays inside seal bands.
|
||||||
|
Provided per chunk by `StrateManager::GetDisturbanceParamsForChunk`.
|
||||||
|
|
||||||
|
### 8.4 Cross-chunk determinism (the seam-prevention invariant)
|
||||||
|
`BuildChunkCache` (VoxelCaveMorphology.cpp) uses **two regions**: a wide *COLLECT* region
|
||||||
|
(`2*MaxTunnelLength + MaxInfluence`) over which connectivity is decided (NN filtered to
|
||||||
|
`<= MaxTunnelLength`, origin cap = deterministic top-N by hash), and a tight *STORE* region
|
||||||
|
(`+MaxInfluence`) kept for per-voxel eval. This makes the room/tunnel graph window-invariant.
|
||||||
|
**If you add a connectivity rule with longer edges, the COLLECT region must still cover the
|
||||||
|
max edge reach, and decisions must not depend on the stored window.**
|
||||||
|
|
||||||
|
### 8.5 Content scatter & water — `VoxelContentManager.h/.cpp` (NEW)
|
||||||
|
`UVoxelContentManager` (owned by `AVoxelWorld`, game-thread). Chunk lifecycle:
|
||||||
|
`PopulateChunk(coord, mesh, LOD)` in `ApplyMeshToChunk`, `ClearChunk` in `UnloadChunk`,
|
||||||
|
`ClearAll`/`SetSeed` in `ChangeSeed`. Deterministic decoration scatter on mesh vertices
|
||||||
|
(surface-type / water-relative / align-to-normal / random-yaw / scale / `MaxPerChunk`, from
|
||||||
|
`FStrateDecoration`) — **only at LOD 0** (perf). Aesthetic water = one scaled engine plane
|
||||||
|
(`/Engine/BasicShapes/Plane`) per water-surface chunk (any LOD), material
|
||||||
|
`UVoxelStrateDefinition::WaterMaterial`. Water Z: `bHasWater` + `WaterLevelRelative`
|
||||||
|
(Surface/tunnel params) → `StrateManager::GetWaterLevelWorldZForChunk`.
|
||||||
|
|
||||||
|
### 8.6 Atmosphere — `VoxelAtmosphereManager.h/.cpp` (NEW)
|
||||||
|
`UVoxelAtmosphereManager` (owned by `AVoxelWorld`, gated by `bManageAtmosphere`).
|
||||||
|
`UpdateForPlayer(pos)` each Tick, reacts only on strate change. Drives a managed
|
||||||
|
`UExponentialHeightFogComponent` + movable `USkyLightComponent` from the player's strate
|
||||||
|
(`FogColor/FogDensity/bVolumetricFog/AmbientLightColor/AmbientLightIntensity`), and spawns
|
||||||
|
PERSISTENT ceiling/floor "layer" actors (`Def->CeilingLayerActor`/`FloorLayerActor` + ZOffsets
|
||||||
|
+ rotations) that follow the player in XY — the sky-island sea-of-clouds / two-sided fog.
|
||||||
|
`Def->AtmosphereActor` (a full BP with your own fog/sky/postprocess) OVERRIDES the managed
|
||||||
|
fog+sky for that strate. `Reset()` on ChangeSeed/EndPlay. (Skylight ambient underground is
|
||||||
|
weak — captures a dark scene; fog is the strong visual.)
|
||||||
|
|
||||||
|
### 8.7 Inter-strate bedrock gap
|
||||||
|
`VoxelSettings::InterStrateGapChunks` (N) inserts N chunks of SOLID bedrock between consecutive
|
||||||
|
strates (`StrateManager::Initialize` leaves the gap in the layout). `IsGapChunk` detects it;
|
||||||
|
`GetDensityAt` renders gap chunks as solid + passages only (no caves/spine/seal) so the player
|
||||||
|
digs (0,0) through the gap to descend. `GetStrateUnrealZRange` gives a strate's cm Z range.
|
||||||
|
|
||||||
|
### 8.8 Inter-strate passages — PER-STRATE (`FStratePassageConfig` on the definition)
|
||||||
|
Each strate's `PassageConfig` (VoxelStrateTypes.h) controls its descent tunnels to the layer
|
||||||
|
below: `Connections`, `Style` (`EVoxelPassageStyle`: Straight/Worm/Spiral/Cascading),
|
||||||
|
`MouthRadius`/`MidRadius` (tapered width → `FVoxelPassage::ControlRadii` + `VoxelSDF::TaperedCapsule`),
|
||||||
|
`ReachMin/Max` (depth into each strate), `DistanceMin/Max` (from the (0,0) spine), `Wander`,
|
||||||
|
`Segments`, `VerticalWobble`, Spiral/Cascade params. Built in `StrateManager::GeneratePassages`
|
||||||
|
as control-point chains. **Worm = independent fBM per horizontal axis** (`PassageFBM` static)
|
||||||
|
with a flat-top envelope → organic squirm (NOT a 1D zigzag, NOT a same-freq 2-channel spiral).
|
||||||
|
`EvaluateModifierSDF` (per voxel) **bounding-sphere-culls** each passage
|
||||||
|
(`FVoxelPassage::BoundCenter/BoundRadiusSq`) — perf-critical. The (0,0) surface entry is a
|
||||||
|
simple straight tube. Global passage settings were removed from `VoxelSettings`.
|
||||||
|
|
||||||
|
### 8.9 Carving — brush shapes + editor controls
|
||||||
|
`FVoxelModification` has `EVoxelBrushShape {Sphere,Box,Capsule}` + `BoxExtent`/`CapsuleEnd`/
|
||||||
|
`Falloff` + `GetWorldBounds`. `UVoxelDiffLayer::GetDensityOffset` switches per shape; chunk
|
||||||
|
overlap uses the shape AABB. `AVoxelWorld`: `CarveBox/FillBox/CarveCapsule/FillCapsule/
|
||||||
|
ApplyModification` (BlueprintCallable) + `EditorCarveSphere/EditorFillSphere` (CallInEditor)
|
||||||
|
driven by `EditorBrush*` props.
|
||||||
|
|
||||||
|
### 8.10 Performance invariants (DON'T regress)
|
||||||
|
- **Streaming** (`UpdateChunksAroundPosition`): rebuild/cull the desired set ONLY when the
|
||||||
|
player crosses a chunk boundary (`LastUpdateCenter`); use the `DesiredSet` TSet for the cull;
|
||||||
|
idle via `bAllChunksLoaded`. Stationary player ≈ free. (Old per-frame O(loaded×desired) scan = 22ms.)
|
||||||
|
- **LOD** changes HOT-SWAP (`LoadChunk` only, never unload-first) → no holes. LOD
|
||||||
|
reconciliation lives in the PERSISTENT per-frame submit loop (same loop as new-chunk loads),
|
||||||
|
NOT as a one-shot on the boundary-cross frame — a one-shot drops every chunk past the task
|
||||||
|
budget and strands it at a stale LOD. Idle (`bAllChunksLoaded`) only when a full scan finds
|
||||||
|
no loads AND no LOD mismatches outstanding.
|
||||||
|
- **SDF cache** (`GetDensityWithParams`): search-BOX validity, not chunk-key — gradient ±1
|
||||||
|
sampling must not thrash the (expensive) rebuild.
|
||||||
|
- **Per-chunk param cache** in `GetDensityAt`: GenType + param struct + disturbance cached
|
||||||
|
thread-locally per chunk; don't move the fetch/blend back to per-voxel.
|
||||||
|
- **Passage cull** (§8.8) + **morphology two-region** (§8.4): both are per-voxel-cost critical.
|
||||||
|
- **Mesher density grid** (`GenerateMesh`): sample each grid point ONCE into a flat
|
||||||
|
`(CHUNK_SIZE/Step + 1)³` array, then the cell loop reads its 8 corners from it. Adjacent
|
||||||
|
cells share corners, so per-cell sampling calls `GetDensityAt` ~8× too often (262k→36k at
|
||||||
|
LOD0). Output is bit-identical (`GetDensityAt` is pure in world coords). Don't refactor
|
||||||
|
back to per-corner `GetDensity` calls inside the cell loop.
|
||||||
|
- **`ProcessQueue` MUST be `EQueueMode::Mpsc`** (`VoxelWorld.h`): up to `MaxConcurrentTasks`
|
||||||
|
`ChunkGen` worker threads `Enqueue` concurrently; the game thread is the sole consumer.
|
||||||
|
The default `Spsc` is single-producer — concurrent enqueues race the tail link and silently
|
||||||
|
DROP results, leaking `PendingChunkCoord` slots until the budget is exhausted and streaming
|
||||||
|
stalls for good (intermittent; worst during the completion bursts right after the player moves).
|
||||||
|
|
||||||
|
### 8.11 Live tuning & debug (`AVoxelWorld`, CallInEditor / PIE)
|
||||||
|
- `RebuildStrates` — re-reads ALL of `VoxelSettings` and rebuilds layout/gap/passages/spine +
|
||||||
|
regenerates. Use after changing those (plain `RegenerateAllChunks` keeps the old layout/passages).
|
||||||
|
- `bDebugDrawPassages` — draws every passage (cyan path, green=upper / red=lower endpoints).
|
||||||
|
- `EditorCarveSphere`/`EditorFillSphere` + `EditorBrush*` props — manual carve/fill in PIE.
|
||||||
|
|
||||||
|
### 8.12 Authoring a strate (data asset)
|
||||||
|
1. Create `UVoxelStrateDefinition`, pick `GeneratorType` → its param group appears; tune it.
|
||||||
|
2. `PassageConfig` → how THIS strate connects DOWN (count / style / tapered width / length / placement).
|
||||||
|
3. `Disturbances` for chasms/bridges/ridges; `bHasWater`+`WaterMaterial`(+`WaterLevelRelative`) for water.
|
||||||
|
4. Atmosphere: `FogColor/Density`, `AmbientLight*`, `bVolumetricFog`, or a full `AtmosphereActor` BP;
|
||||||
|
`CeilingLayerActor`/`FloorLayerActor` (+offsets/rotations) for cloud seas.
|
||||||
|
5. `Decorations`/`AmbientActors` (placement rules) for content + lights.
|
||||||
|
6. Reference from `VoxelSettings` (`StratePool`/`FixedStrates`). Global knobs there:
|
||||||
|
`OriginSpineRadius`, `bOpenSurfaceEntry`, `InterStrateGapChunks`, view distances, LOD, carving budget.
|
||||||
|
|
||||||
|
### 8.13 New files this redesign
|
||||||
|
`Public/Private/VoxelContentManager.h/.cpp` (§8.5) · `Public/Private/VoxelAtmosphereManager.h/.cpp` (§8.6).
|
||||||
|
Everything else extended existing files: `VoxelStrateTypes.h` (archetype params, disturbance,
|
||||||
|
`FStratePassageConfig`, enums), `VoxelStrateDefinition.h`, `VoxelGenerator.h/.cpp` (archetype
|
||||||
|
density fns + spine/disturbance/param-cache), `VoxelStrateManager.h/.cpp` (per-archetype getters,
|
||||||
|
passages, gap, atmosphere Z helper), `VoxelWorld.h/.cpp` (managers, streaming perf, brush API,
|
||||||
|
editor buttons), `VoxelDiffLayer.h/.cpp` (brush shapes), `VoxelSettings.h`, `VoxelCaveMorphology.cpp`
|
||||||
|
(two-region determinism). Status: compiles & runs in-editor.
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
// VoxelAtmosphereManager.cpp
|
||||||
|
|
||||||
|
#include "VoxelAtmosphereManager.h"
|
||||||
|
#include "VoxelStrateManager.h"
|
||||||
|
#include "VoxelStrateDefinition.h"
|
||||||
|
#include "Components/ExponentialHeightFogComponent.h"
|
||||||
|
#include "Components/SkyLightComponent.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
|
||||||
|
void UVoxelAtmosphereManager::Initialize(AActor* InOwner, UVoxelStrateManager* InStrateManager)
|
||||||
|
{
|
||||||
|
Owner = InOwner;
|
||||||
|
StrateManager = InStrateManager;
|
||||||
|
|
||||||
|
if (!InOwner) return;
|
||||||
|
USceneComponent* Root = InOwner->GetRootComponent();
|
||||||
|
|
||||||
|
// Managed height fog. Hidden until a strate with fog is entered.
|
||||||
|
Fog = NewObject<UExponentialHeightFogComponent>(InOwner);
|
||||||
|
Fog->SetMobility(EComponentMobility::Movable);
|
||||||
|
Fog->RegisterComponent();
|
||||||
|
if (Root) Fog->AttachToComponent(Root, FAttachmentTransformRules::KeepRelativeTransform);
|
||||||
|
Fog->SetVisibility(false);
|
||||||
|
|
||||||
|
// Managed skylight for controllable ambient (movable so we can change it live).
|
||||||
|
Sky = NewObject<USkyLightComponent>(InOwner);
|
||||||
|
Sky->SetMobility(EComponentMobility::Movable);
|
||||||
|
Sky->SourceType = ESkyLightSourceType::SLS_CapturedScene;
|
||||||
|
Sky->bLowerHemisphereIsBlack = false; // let LowerHemisphereColor act as flat ambient
|
||||||
|
Sky->RegisterComponent();
|
||||||
|
if (Root) Sky->AttachToComponent(Root, FAttachmentTransformRules::KeepRelativeTransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVoxelAtmosphereManager::UpdateForPlayer(const FVector& PlayerWorldPos)
|
||||||
|
{
|
||||||
|
if (!StrateManager) return;
|
||||||
|
AActor* O = Owner.Get();
|
||||||
|
if (!O) return;
|
||||||
|
|
||||||
|
const int32 Idx = StrateManager->GetStrateIndex(PlayerWorldPos.Z);
|
||||||
|
const UVoxelStrateDefinition* Def = StrateManager->GetStrateAt(PlayerWorldPos.Z);
|
||||||
|
|
||||||
|
// React only when the player changes strate (cheap on every other frame).
|
||||||
|
if (Idx != CurrentStrateIndex)
|
||||||
|
{
|
||||||
|
CurrentStrateIndex = Idx;
|
||||||
|
ApplyStrate(Def);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anchor the full-atmosphere BP to the player (so any localized volumes/effects
|
||||||
|
// inside it follow). Global fog/skylight ignore position, so this is harmless.
|
||||||
|
if (AtmosphereActorInstance)
|
||||||
|
{
|
||||||
|
AtmosphereActorInstance->SetActorLocation(PlayerWorldPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the layer actors centered on the player in XY, glued to the strate bounds,
|
||||||
|
// with the authored rotation applied.
|
||||||
|
if (Def)
|
||||||
|
{
|
||||||
|
float TopZ, BottomZ;
|
||||||
|
if (StrateManager->GetStrateUnrealZRange(PlayerWorldPos.Z, TopZ, BottomZ))
|
||||||
|
{
|
||||||
|
if (CeilingActor)
|
||||||
|
{
|
||||||
|
CeilingActor->SetActorLocationAndRotation(
|
||||||
|
FVector(PlayerWorldPos.X, PlayerWorldPos.Y, TopZ + Def->CeilingLayerZOffset),
|
||||||
|
Def->CeilingLayerRotation);
|
||||||
|
}
|
||||||
|
if (FloorActor)
|
||||||
|
{
|
||||||
|
FloorActor->SetActorLocationAndRotation(
|
||||||
|
FVector(PlayerWorldPos.X, PlayerWorldPos.Y, BottomZ + Def->FloorLayerZOffset),
|
||||||
|
Def->FloorLayerRotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVoxelAtmosphereManager::ApplyStrate(const UVoxelStrateDefinition* Def)
|
||||||
|
{
|
||||||
|
AActor* O = Owner.Get();
|
||||||
|
UWorld* W = O ? O->GetWorld() : nullptr;
|
||||||
|
|
||||||
|
FActorSpawnParameters Params;
|
||||||
|
Params.Owner = O;
|
||||||
|
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
|
|
||||||
|
// ── FULL ATMOSPHERE OVERRIDE ──
|
||||||
|
// If the strate provides its own atmosphere BP, it owns the entire look; spawn it
|
||||||
|
// and disable the plugin's managed fog/skylight so they don't double up.
|
||||||
|
if (AtmosphereActorInstance) { AtmosphereActorInstance->Destroy(); AtmosphereActorInstance = nullptr; }
|
||||||
|
|
||||||
|
const bool bUseOverride = (Def && Def->AtmosphereActor);
|
||||||
|
if (bUseOverride && W)
|
||||||
|
{
|
||||||
|
AtmosphereActorInstance = W->SpawnActor<AActor>(Def->AtmosphereActor, FTransform::Identity, Params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Managed fog (only when NOT overridden) ──
|
||||||
|
if (Fog)
|
||||||
|
{
|
||||||
|
if (!bUseOverride && Def && Def->FogDensity > 0.0f)
|
||||||
|
{
|
||||||
|
Fog->SetVisibility(true);
|
||||||
|
// FogDensity is authored 0..1; EHF density is tiny — scale into a sane range.
|
||||||
|
Fog->SetFogDensity(Def->FogDensity * 0.05f);
|
||||||
|
Fog->SetFogInscatteringColor(Def->FogColor);
|
||||||
|
Fog->SetVolumetricFog(Def->bVolumetricFog);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Fog->SetVisibility(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Managed ambient skylight (only when NOT overridden) ──
|
||||||
|
if (Sky)
|
||||||
|
{
|
||||||
|
if (!bUseOverride && Def)
|
||||||
|
{
|
||||||
|
Sky->SetIntensity(Def->AmbientLightIntensity);
|
||||||
|
Sky->SetLightColor(Def->AmbientLightColor);
|
||||||
|
Sky->SetLowerHemisphereColor(Def->AmbientLightColor);
|
||||||
|
Sky->RecaptureSky();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Sky->SetIntensity(0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ceiling / floor layer actors — destroy old, spawn new for this strate ──
|
||||||
|
// (Independent of the override — you can have cloud seas with either fog path.)
|
||||||
|
if (CeilingActor) { CeilingActor->Destroy(); CeilingActor = nullptr; }
|
||||||
|
if (FloorActor) { FloorActor->Destroy(); FloorActor = nullptr; }
|
||||||
|
|
||||||
|
if (Def && W)
|
||||||
|
{
|
||||||
|
if (Def->CeilingLayerActor)
|
||||||
|
{
|
||||||
|
CeilingActor = W->SpawnActor<AActor>(Def->CeilingLayerActor, FTransform::Identity, Params);
|
||||||
|
}
|
||||||
|
if (Def->FloorLayerActor)
|
||||||
|
{
|
||||||
|
FloorActor = W->SpawnActor<AActor>(Def->FloorLayerActor, FTransform::Identity, Params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVoxelAtmosphereManager::Reset()
|
||||||
|
{
|
||||||
|
if (AtmosphereActorInstance) { AtmosphereActorInstance->Destroy(); AtmosphereActorInstance = nullptr; }
|
||||||
|
if (CeilingActor) { CeilingActor->Destroy(); CeilingActor = nullptr; }
|
||||||
|
if (FloorActor) { FloorActor->Destroy(); FloorActor = nullptr; }
|
||||||
|
CurrentStrateIndex = INT32_MIN;
|
||||||
|
if (Fog) Fog->SetVisibility(false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,814 @@
|
|||||||
|
// VoxelCaveMorphology.cpp
|
||||||
|
// Hash-based room/tunnel generation and SDF evaluation.
|
||||||
|
//
|
||||||
|
// TWO-PHASE EVALUATION:
|
||||||
|
// Phase 1 — BuildChunkCache: collects rooms, builds backbone, resolves tunnels.
|
||||||
|
// Called ONCE per chunk (~1 time per 32³ = 32,768 voxels).
|
||||||
|
// Phase 2 — EvaluateSDFCached: evaluates room/tunnel SDFs with distance culling.
|
||||||
|
// Called PER VOXEL using the cached data.
|
||||||
|
//
|
||||||
|
// FEATURES:
|
||||||
|
// - Origin room: guaranteed large room at (0,0) per strate — the hub / (0,0) spine
|
||||||
|
// - Hash-based rooms: ellipsoid, rounded box, and capsule shapes
|
||||||
|
// - Tunnels: tapered capsules with curved paths (midpoint warping)
|
||||||
|
// - Horizontal bias: tunnels prefer horizontal connections
|
||||||
|
// - Endpoint Z offset: tunnels enter rooms at different heights
|
||||||
|
// - Per-room/tunnel distance culling: skip SDFs that can't affect this voxel
|
||||||
|
//
|
||||||
|
// CROSS-CHUNK DETERMINISM (the invariant that prevents seams):
|
||||||
|
// The room/tunnel GRAPH must reconstruct IDENTICALLY in every chunk whose voxels a
|
||||||
|
// tunnel touches. Room geometry is a pure hash of its cell, so that half is trivially
|
||||||
|
// identical. The fragile half is the EXISTENCE decision (the nearest-neighbor
|
||||||
|
// backbone), which historically depended on the per-chunk window of collected rooms.
|
||||||
|
// We now separate two regions (see BuildChunkCache):
|
||||||
|
// - COLLECT region (wide): every room whose NN-candidate set could influence a
|
||||||
|
// tunnel touching this chunk. Connectivity is decided over THIS set, so the
|
||||||
|
// decision is window-invariant.
|
||||||
|
// - STORE region (tight): only rooms/tunnels that can actually reach a voxel in
|
||||||
|
// this chunk are kept for the per-voxel loop, so hot-path cost stays low.
|
||||||
|
|
||||||
|
#include "VoxelCaveMorphology.h"
|
||||||
|
#include "VoxelTypes.h" // Pour VOXEL_NOISE_SCALE, SmoothStep01
|
||||||
|
#include "VoxelStrateTypes.h"
|
||||||
|
#include "VoxelTerrainOpDefinition.h"
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// INTERNAL: Full room data used during cache building only.
|
||||||
|
// The CellX/CellY fields are needed for tunnel pair hashing but NOT for
|
||||||
|
// per-voxel SDF evaluation, so they don't go into the cached FCachedRoom.
|
||||||
|
//=============================================================================
|
||||||
|
struct FBuildRoom
|
||||||
|
{
|
||||||
|
FVector Center; // World position of the room center
|
||||||
|
float RadiusXY; // Horizontal radius
|
||||||
|
float RadiusZ; // Vertical radius
|
||||||
|
int32 CellX, CellY; // Grid cell (needed for pair hash during tunnel decisions)
|
||||||
|
uint32 Hash; // Cell hash (for shape selection + tunnel property derivation)
|
||||||
|
bool bIsOrigin; // True for the origin room at (0,0)
|
||||||
|
bool bStore; // True if this room can affect a voxel in THIS chunk
|
||||||
|
// (collected for connectivity decisions either way).
|
||||||
|
};
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// PHASE 1: BUILD CHUNK CACHE
|
||||||
|
//=============================================================================
|
||||||
|
// Collects all rooms in the COLLECT region, computes a window-invariant
|
||||||
|
// nearest-neighbor backbone for connectivity, decides tunnel connections, and
|
||||||
|
// pre-computes all tunnel geometry (radii, Z offsets, midpoint warping, bounding
|
||||||
|
// spheres). Only rooms/tunnels relevant to the chunk (STORE region) are kept.
|
||||||
|
//
|
||||||
|
// The result is stored in OutCache and reused for every voxel in the chunk.
|
||||||
|
|
||||||
|
void VoxelCaveMorphology::BuildChunkCache(
|
||||||
|
FChunkSDFCache& OutCache,
|
||||||
|
float SearchMinX, float SearchMinY,
|
||||||
|
float SearchMaxX, float SearchMaxY,
|
||||||
|
const FStrateGenerationParams& Params,
|
||||||
|
uint32 Seed, int32 StrateIndex,
|
||||||
|
const TArray<FStrateTerrainOpEntry>* TerrainOps)
|
||||||
|
{
|
||||||
|
// Clear previous data (arrays keep their allocation for reuse)
|
||||||
|
OutCache.Rooms.Reset();
|
||||||
|
OutCache.Tunnels.Reset();
|
||||||
|
OutCache.Pits.Reset();
|
||||||
|
OutCache.Chimneys.Reset();
|
||||||
|
OutCache.Columns.Reset();
|
||||||
|
|
||||||
|
// Combine world seed with strate index so each strate gets unique caves
|
||||||
|
const uint32 StrateSeed = VoxelHash::Mix(Seed ^ (uint32)(StrateIndex * 7919 + 104729));
|
||||||
|
|
||||||
|
const float CellSize = Params.RoomSpacing;
|
||||||
|
if (CellSize <= 0.0f) return;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// INFLUENCE RADII
|
||||||
|
//=========================================================================
|
||||||
|
// MaxInfluence = how far a room body / tunnel TUBE reaches PERPENDICULAR to its
|
||||||
|
// anchor — NOT its length. A room or tunnel whose anchor lies within MaxInfluence
|
||||||
|
// of a box can touch a voxel inside that box.
|
||||||
|
const float MaxInfluence = FMath::Max(
|
||||||
|
Params.MaxRoomRadius,
|
||||||
|
Params.TunnelWarpStrength + Params.TunnelMaxRadius
|
||||||
|
) + Params.SDFBlendRadius;
|
||||||
|
|
||||||
|
const float MaxTunnelLen = FMath::Max(Params.MaxTunnelLength, 0.0f);
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// STORE region — what we keep for the per-voxel loop.
|
||||||
|
//=========================================================================
|
||||||
|
// A room/tunnel whose influence overlaps the search box is RELEVANT to this
|
||||||
|
// chunk. STORE box = search box + MaxInfluence. (Per-voxel culling refines this.)
|
||||||
|
const float StoreMinX = SearchMinX - MaxInfluence;
|
||||||
|
const float StoreMinY = SearchMinY - MaxInfluence;
|
||||||
|
const float StoreMaxX = SearchMaxX + MaxInfluence;
|
||||||
|
const float StoreMaxY = SearchMaxY + MaxInfluence;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// COLLECT region — what we hash into existence for the connectivity decision.
|
||||||
|
//=========================================================================
|
||||||
|
// To DECIDE the graph identically in neighboring chunks, we must see, for every
|
||||||
|
// room that could emit a tunnel touching this chunk, that room's ENTIRE
|
||||||
|
// nearest-neighbor candidate set (all rooms within MaxTunnelLength of it):
|
||||||
|
// - A tunnel touching the chunk has BOTH endpoints within
|
||||||
|
// (MaxTunnelLength + MaxInfluence) of the search box (capsule len <= MaxTunnelLength).
|
||||||
|
// - Each endpoint's NN candidates lie within MaxTunnelLength of that endpoint.
|
||||||
|
// => collect within (2 * MaxTunnelLength + MaxInfluence) of the search box.
|
||||||
|
// Combined with NN candidates being filtered to <= MaxTunnelLength below, this
|
||||||
|
// makes the backbone decision for any STORED tunnel window-invariant.
|
||||||
|
const float CollectMargin = 2.0f * MaxTunnelLen + MaxInfluence;
|
||||||
|
const float CollectMinX = SearchMinX - CollectMargin;
|
||||||
|
const float CollectMinY = SearchMinY - CollectMargin;
|
||||||
|
const float CollectMaxX = SearchMaxX + CollectMargin;
|
||||||
|
const float CollectMaxY = SearchMaxY + CollectMargin;
|
||||||
|
|
||||||
|
// Convert COLLECT bounds to cell range
|
||||||
|
const int32 CellMinX = FMath::FloorToInt(CollectMinX / CellSize);
|
||||||
|
const int32 CellMaxX = FMath::FloorToInt(CollectMaxX / CellSize);
|
||||||
|
const int32 CellMinY = FMath::FloorToInt(CollectMinY / CellSize);
|
||||||
|
const int32 CellMaxY = FMath::FloorToInt(CollectMaxY / CellSize);
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// Vertical range for room CENTER placement.
|
||||||
|
//=========================================================================
|
||||||
|
// Buffer = seal thickness + max room half-height.
|
||||||
|
// This guarantees the tallest possible room (MaxRoomRadius * RoomHeightRatio)
|
||||||
|
// fits entirely within the seal boundary — no room gets its ceiling or floor
|
||||||
|
// cut flat by the seal. Smaller rooms have proportionally more margin.
|
||||||
|
const float RoomZBuffer = Params.MaxRoomRadius * Params.RoomHeightRatio;
|
||||||
|
const float StrateMinZ = Params.StrateBottomWorldZ + Params.BoundarySealThickness + RoomZBuffer;
|
||||||
|
const float StrateMaxZ = Params.StrateTopWorldZ - Params.BoundarySealThickness - RoomZBuffer;
|
||||||
|
const float StrateRangeZ = StrateMaxZ - StrateMinZ;
|
||||||
|
const float StrateCenterZ = (StrateMinZ + StrateMaxZ) * 0.5f;
|
||||||
|
|
||||||
|
const float BlendK = Params.SDFBlendRadius;
|
||||||
|
|
||||||
|
// Conservative "this room can reach the STORE box" test. A room's cull sphere
|
||||||
|
// radius is <= MaxInfluence, and STORE box already includes a MaxInfluence
|
||||||
|
// margin, so testing the center against the STORE box is a correct superset.
|
||||||
|
auto CenterInStoreBox = [&](const FVector& C) -> bool
|
||||||
|
{
|
||||||
|
return C.X >= StoreMinX && C.X <= StoreMaxX
|
||||||
|
&& C.Y >= StoreMinY && C.Y <= StoreMaxY;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sphere-vs-search-box test in XY (treated as infinite in Z; per-voxel culling
|
||||||
|
// resolves Z). Used to decide whether a tunnel is worth storing for this chunk.
|
||||||
|
auto SphereTouchesSearchXY = [&](const FVector& C, float RSq) -> bool
|
||||||
|
{
|
||||||
|
const float dx = FMath::Max3((float)(SearchMinX - C.X), 0.0f, (float)(C.X - SearchMaxX));
|
||||||
|
const float dy = FMath::Max3((float)(SearchMinY - C.Y), 0.0f, (float)(C.Y - SearchMaxY));
|
||||||
|
return (dx * dx + dy * dy) <= RSq;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Temporary array with cell coordinates for tunnel connection decisions
|
||||||
|
TArray<FBuildRoom, TInlineAllocator<64>> BuildRooms;
|
||||||
|
|
||||||
|
// --- ORIGIN ROOM ---
|
||||||
|
// Guaranteed large room at (0, 0) in each strate — the (0,0) descent spine hub.
|
||||||
|
// Collected whenever (0,0) is inside the COLLECT region so it participates in the
|
||||||
|
// connectivity decision; only stored if it can reach this chunk.
|
||||||
|
int32 OriginIdx = -1;
|
||||||
|
if (Params.OriginRoomRadius > 0.0f)
|
||||||
|
{
|
||||||
|
if (0.0f >= CollectMinX && 0.0f <= CollectMaxX &&
|
||||||
|
0.0f >= CollectMinY && 0.0f <= CollectMaxY)
|
||||||
|
{
|
||||||
|
FBuildRoom OriginRoom;
|
||||||
|
OriginRoom.CellX = INT32_MAX; // Sentinel — never matches a real grid cell
|
||||||
|
OriginRoom.CellY = INT32_MAX;
|
||||||
|
OriginRoom.Hash = VoxelHash::Cell(0, 0, StrateSeed ^ 0x0A161Cu);
|
||||||
|
OriginRoom.Center = FVector(0.0f, 0.0f, StrateCenterZ);
|
||||||
|
OriginRoom.RadiusXY = Params.OriginRoomRadius;
|
||||||
|
OriginRoom.RadiusZ = Params.OriginRoomRadius * Params.RoomHeightRatio;
|
||||||
|
OriginRoom.bIsOrigin = true;
|
||||||
|
OriginRoom.bStore = CenterInStoreBox(OriginRoom.Center);
|
||||||
|
OriginIdx = BuildRooms.Add(OriginRoom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- HASH-BASED ROOMS ---
|
||||||
|
for (int32 CY = CellMinY; CY <= CellMaxY; CY++)
|
||||||
|
{
|
||||||
|
for (int32 CX = CellMinX; CX <= CellMaxX; CX++)
|
||||||
|
{
|
||||||
|
// Hash this cell to decide if it has a room
|
||||||
|
const uint32 CellHash = VoxelHash::Cell(CX, CY, StrateSeed);
|
||||||
|
const float RoomChance = VoxelHash::ToFloat01(CellHash);
|
||||||
|
|
||||||
|
// Skip empty cells (no room here)
|
||||||
|
if (RoomChance >= Params.RoomDensity) continue;
|
||||||
|
|
||||||
|
// Room position: jittered within the cell
|
||||||
|
const float JitterX = VoxelHash::ToFloat01(VoxelHash::Mix(CellHash ^ 0x12345678u));
|
||||||
|
const float JitterY = VoxelHash::ToFloat01(VoxelHash::Mix(CellHash ^ 0x9ABCDEF0u));
|
||||||
|
const float JitterZ = VoxelHash::ToFloat01(VoxelHash::Mix(CellHash ^ 0x55AA55AAu));
|
||||||
|
|
||||||
|
FBuildRoom Room;
|
||||||
|
Room.CellX = CX;
|
||||||
|
Room.CellY = CY;
|
||||||
|
Room.Hash = CellHash;
|
||||||
|
Room.Center.X = (CX + 0.15f + JitterX * 0.7f) * CellSize;
|
||||||
|
Room.Center.Y = (CY + 0.15f + JitterY * 0.7f) * CellSize;
|
||||||
|
Room.Center.Z = StrateMinZ + JitterZ * FMath::Max(StrateRangeZ, 1.0f);
|
||||||
|
|
||||||
|
// Room size: lerp between min and max
|
||||||
|
const float SizeFactor = VoxelHash::ToFloat01(VoxelHash::Mix(CellHash ^ 0xFEDCBA98u));
|
||||||
|
Room.RadiusXY = FMath::Lerp(Params.MinRoomRadius, Params.MaxRoomRadius, SizeFactor);
|
||||||
|
Room.RadiusZ = Room.RadiusXY * Params.RoomHeightRatio;
|
||||||
|
Room.bIsOrigin = false;
|
||||||
|
Room.bStore = CenterInStoreBox(Room.Center);
|
||||||
|
|
||||||
|
BuildRooms.Add(Room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 NumRooms = BuildRooms.Num();
|
||||||
|
if (NumRooms == 0) return;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// Window-invariant nearest-neighbor backbone
|
||||||
|
//=========================================================================
|
||||||
|
// Each room's nearest neighbor (among rooms within MaxTunnelLength) is a
|
||||||
|
// GUARANTEED tunnel connection. Filtering candidates to <= MaxTunnelLength is
|
||||||
|
// what makes the result identical across chunks: rooms beyond MaxTunnelLength
|
||||||
|
// can never be a tunnel anyway, so excluding them removes the only source of
|
||||||
|
// window dependence. This builds a connected tree backbone; TunnelDensity adds
|
||||||
|
// loops on top.
|
||||||
|
TArray<int32, TInlineAllocator<64>> NearestNeighbor;
|
||||||
|
NearestNeighbor.SetNumUninitialized(NumRooms);
|
||||||
|
|
||||||
|
const float MaxTunnelLenSq = MaxTunnelLen * MaxTunnelLen;
|
||||||
|
|
||||||
|
for (int32 I = 0; I < NumRooms; I++)
|
||||||
|
{
|
||||||
|
float BestDistSq = FLT_MAX;
|
||||||
|
int32 BestJ = -1;
|
||||||
|
for (int32 J = 0; J < NumRooms; J++)
|
||||||
|
{
|
||||||
|
if (I == J) continue;
|
||||||
|
const float DSq = FVector::DistSquared(BuildRooms[I].Center, BuildRooms[J].Center);
|
||||||
|
if (DSq > MaxTunnelLenSq) continue; // out of reach — never a tunnel
|
||||||
|
if (DSq < BestDistSq)
|
||||||
|
{
|
||||||
|
BestDistSq = DSq;
|
||||||
|
BestJ = J;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NearestNeighbor[I] = BestJ;
|
||||||
|
}
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// ORIGIN CONNECTION CAP (deterministic, order-independent)
|
||||||
|
//=========================================================================
|
||||||
|
// OriginRoomMaxConnections caps how many rooms backbone-force into the origin.
|
||||||
|
// The OLD approach counted connections in pair-loop order, which depended on the
|
||||||
|
// per-chunk room ordering → non-deterministic across chunks. Instead we gather
|
||||||
|
// ALL rooms backbone-linked to origin (window-invariant given the COLLECT region),
|
||||||
|
// rank them by a deterministic pair hash, and keep only the top N as forced.
|
||||||
|
// The rest are DOWNGRADED to the random TunnelDensity path (connectivity not
|
||||||
|
// broken, only the "guaranteed" aspect is limited).
|
||||||
|
TSet<int32> OriginDowngraded;
|
||||||
|
const int32 MaxOriginConn = Params.OriginRoomMaxConnections;
|
||||||
|
if (OriginIdx >= 0 && MaxOriginConn > 0)
|
||||||
|
{
|
||||||
|
// Collect origin backbone candidates with a deterministic ranking key.
|
||||||
|
TArray<TPair<uint32, int32>, TInlineAllocator<32>> OriginLinks;
|
||||||
|
for (int32 I = 0; I < NumRooms; I++)
|
||||||
|
{
|
||||||
|
if (I == OriginIdx) continue;
|
||||||
|
const bool bLinked = (NearestNeighbor[I] == OriginIdx) || (NearestNeighbor[OriginIdx] == I);
|
||||||
|
if (!bLinked) continue;
|
||||||
|
const uint32 Key = VoxelHash::Pair(
|
||||||
|
BuildRooms[OriginIdx].CellX, BuildRooms[OriginIdx].CellY,
|
||||||
|
BuildRooms[I].CellX, BuildRooms[I].CellY,
|
||||||
|
StrateSeed ^ 0x031A1Eu);
|
||||||
|
OriginLinks.Add(TPair<uint32, int32>(Key, I));
|
||||||
|
}
|
||||||
|
// Stable deterministic order by (hash, then index for tie-break).
|
||||||
|
OriginLinks.Sort([](const TPair<uint32, int32>& A, const TPair<uint32, int32>& B)
|
||||||
|
{
|
||||||
|
return A.Key != B.Key ? A.Key < B.Key : A.Value < B.Value;
|
||||||
|
});
|
||||||
|
for (int32 R = MaxOriginConn; R < OriginLinks.Num(); ++R)
|
||||||
|
{
|
||||||
|
OriginDowngraded.Add(OriginLinks[R].Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// Resolve tunnel connections, pre-compute geometry, store chunk-relevant ones
|
||||||
|
//=========================================================================
|
||||||
|
// A tunnel exists if EITHER:
|
||||||
|
// 1. One is the other's nearest neighbor (backbone — guarantees connectivity),
|
||||||
|
// and (for origin links) it survived the origin cap, OR
|
||||||
|
// 2. The pair hash passes TunnelDensity (random extra loops).
|
||||||
|
// Both must pass the distance check (MaxTunnelLength). Only tunnels whose bounding
|
||||||
|
// sphere reaches the search box are stored for the per-voxel loop.
|
||||||
|
for (int32 I = 0; I < NumRooms; I++)
|
||||||
|
{
|
||||||
|
for (int32 J = I + 1; J < NumRooms; J++)
|
||||||
|
{
|
||||||
|
const FBuildRoom& RoomA = BuildRooms[I];
|
||||||
|
const FBuildRoom& RoomB = BuildRooms[J];
|
||||||
|
bool bBackbone = (NearestNeighbor[I] == J) || (NearestNeighbor[J] == I);
|
||||||
|
|
||||||
|
// Origin cap: downgrade backbone links beyond the deterministic top-N.
|
||||||
|
if (bBackbone && (RoomA.bIsOrigin || RoomB.bIsOrigin))
|
||||||
|
{
|
||||||
|
const int32 Other = RoomA.bIsOrigin ? J : I;
|
||||||
|
if (OriginDowngraded.Contains(Other))
|
||||||
|
{
|
||||||
|
bBackbone = false; // Let TunnelDensity decide instead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DISTANCE CHECK ---
|
||||||
|
const float EuclidDist = FVector::Dist(RoomA.Center, RoomB.Center);
|
||||||
|
float CheckDist = EuclidDist;
|
||||||
|
|
||||||
|
// Horizontal bias: penalize vertical separation for non-backbone tunnels
|
||||||
|
if (!bBackbone && Params.TunnelHorizontalBias > 0.0f)
|
||||||
|
{
|
||||||
|
const float VertSep = FMath::Abs(RoomA.Center.Z - RoomB.Center.Z);
|
||||||
|
CheckDist += VertSep * Params.TunnelHorizontalBias * 5.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CheckDist > Params.MaxTunnelLength) continue;
|
||||||
|
|
||||||
|
// --- CONNECTION DECISION ---
|
||||||
|
if (!bBackbone)
|
||||||
|
{
|
||||||
|
const uint32 PairHash = VoxelHash::Pair(
|
||||||
|
RoomA.CellX, RoomA.CellY,
|
||||||
|
RoomB.CellX, RoomB.CellY,
|
||||||
|
StrateSeed
|
||||||
|
);
|
||||||
|
const float ConnectChance = VoxelHash::ToFloat01(PairHash);
|
||||||
|
if (ConnectChance >= Params.TunnelDensity) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TUNNEL HASH (for deriving all tunnel properties) ---
|
||||||
|
const uint32 TunnelHash = VoxelHash::Pair(
|
||||||
|
RoomA.CellX, RoomA.CellY,
|
||||||
|
RoomB.CellX, RoomB.CellY,
|
||||||
|
StrateSeed ^ 0xDECAF001u
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- RADIUS ---
|
||||||
|
const float FactorA = VoxelHash::ToFloat01(VoxelHash::Mix(TunnelHash ^ 0xBAADF00Du));
|
||||||
|
const float FactorB = VoxelHash::ToFloat01(VoxelHash::Mix(TunnelHash ^ 0x8BADF00Du));
|
||||||
|
const float RadA = FMath::Lerp(Params.TunnelMinRadius, Params.TunnelMaxRadius, FactorA);
|
||||||
|
const float RadB = FMath::Lerp(Params.TunnelMinRadius, Params.TunnelMaxRadius, FactorB);
|
||||||
|
|
||||||
|
// --- ENDPOINT Z OFFSET ---
|
||||||
|
const float ZOffsetA = VoxelHash::ToFloatSigned(VoxelHash::Mix(TunnelHash ^ 0xA1B2C3D4u))
|
||||||
|
* RoomA.RadiusZ * Params.TunnelEndpointZOffset;
|
||||||
|
const float ZOffsetB = VoxelHash::ToFloatSigned(VoxelHash::Mix(TunnelHash ^ 0xD4C3B2A1u))
|
||||||
|
* RoomB.RadiusZ * Params.TunnelEndpointZOffset;
|
||||||
|
|
||||||
|
FVector EndA = RoomA.Center + FVector(0.0f, 0.0f, ZOffsetA);
|
||||||
|
FVector EndB = RoomB.Center + FVector(0.0f, 0.0f, ZOffsetB);
|
||||||
|
|
||||||
|
// --- BUILD CACHED TUNNEL ---
|
||||||
|
FCachedTunnel CT;
|
||||||
|
CT.EndpointA = EndA;
|
||||||
|
CT.EndpointB = EndB;
|
||||||
|
CT.RadiusA = RadA;
|
||||||
|
CT.RadiusB = RadB;
|
||||||
|
|
||||||
|
// --- PATH WARPING ---
|
||||||
|
const float TunnelLength = FVector::Dist(EndA, EndB);
|
||||||
|
|
||||||
|
if (Params.TunnelWarpStrength > 0.0f && TunnelLength > 1.0f)
|
||||||
|
{
|
||||||
|
FVector TunnelDir = (EndB - EndA).GetSafeNormal();
|
||||||
|
FVector PerpH = FVector(-TunnelDir.Y, TunnelDir.X, 0.0f);
|
||||||
|
|
||||||
|
float MaxWarp = FMath::Min(Params.TunnelWarpStrength, TunnelLength * 0.25f);
|
||||||
|
float WarpH = VoxelHash::ToFloatSigned(VoxelHash::Mix(TunnelHash ^ 0x1234ABCDu)) * MaxWarp;
|
||||||
|
float WarpV = VoxelHash::ToFloatSigned(VoxelHash::Mix(TunnelHash ^ 0x5678EF01u)) * MaxWarp * 0.3f;
|
||||||
|
|
||||||
|
FVector Mid = (EndA + EndB) * 0.5f;
|
||||||
|
Mid += PerpH * WarpH + FVector(0.0f, 0.0f, WarpV);
|
||||||
|
|
||||||
|
CT.Midpoint = Mid;
|
||||||
|
CT.RadiusMid = (RadA + RadB) * 0.5f;
|
||||||
|
CT.bHasMidpoint = true;
|
||||||
|
|
||||||
|
// Bounding sphere: encloses all 3 control points + max radius
|
||||||
|
CT.BoundCenter = (EndA + Mid + EndB) / 3.0f;
|
||||||
|
float MaxR = FMath::Max3(RadA, RadB, CT.RadiusMid) + BlendK;
|
||||||
|
float DistA = FVector::Dist(CT.BoundCenter, EndA);
|
||||||
|
float DistM = FVector::Dist(CT.BoundCenter, Mid);
|
||||||
|
float DistB = FVector::Dist(CT.BoundCenter, EndB);
|
||||||
|
float BoundR = FMath::Max3(DistA, DistM, DistB) + MaxR;
|
||||||
|
CT.BoundRadiusSq = BoundR * BoundR;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CT.Midpoint = FVector::ZeroVector;
|
||||||
|
CT.RadiusMid = 0.0f;
|
||||||
|
CT.bHasMidpoint = false;
|
||||||
|
|
||||||
|
// Bounding sphere: encloses both endpoints + max radius
|
||||||
|
CT.BoundCenter = (EndA + EndB) * 0.5f;
|
||||||
|
float MaxR = FMath::Max(RadA, RadB) + BlendK;
|
||||||
|
float HalfLen = TunnelLength * 0.5f;
|
||||||
|
float BoundR = HalfLen + MaxR;
|
||||||
|
CT.BoundRadiusSq = BoundR * BoundR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store only if this tunnel can actually reach a voxel in this chunk.
|
||||||
|
if (SphereTouchesSearchXY(CT.BoundCenter, CT.BoundRadiusSq))
|
||||||
|
{
|
||||||
|
OutCache.Tunnels.Add(CT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// Copy STORE-relevant rooms to cache (cull radii + per-room terrain ops),
|
||||||
|
// then pre-bake their pits / chimneys / columns.
|
||||||
|
//=========================================================================
|
||||||
|
// Build terrain op pool stats once (outside the per-room loop).
|
||||||
|
// TerrainOps is the strate's probability pool; each entry has a Probability
|
||||||
|
// in [0,1]. We do a weighted random draw per room using the room's hash.
|
||||||
|
//
|
||||||
|
// PROBABILITY MODEL:
|
||||||
|
// - Entries are checked cumulatively (like drawing from a bucket).
|
||||||
|
// - The "no op" slot takes the remaining probability space (if sum < 1.0).
|
||||||
|
// - If sum >= 1.0, every room gets an op (normalized selection).
|
||||||
|
float TotalOpProb = 0.0f;
|
||||||
|
if (TerrainOps)
|
||||||
|
{
|
||||||
|
for (const FStrateTerrainOpEntry& E : *TerrainOps)
|
||||||
|
TotalOpProb += FMath::Max(E.Probability, 0.0f);
|
||||||
|
}
|
||||||
|
const float NormFactor = (TotalOpProb > 1.0f) ? (1.0f / TotalOpProb) : 1.0f;
|
||||||
|
|
||||||
|
// Temporary params struct for reading op fields (PitDensity, PitMinRadius, etc.)
|
||||||
|
FStrateGenerationParams OpParams;
|
||||||
|
|
||||||
|
OutCache.Rooms.Reserve(NumRooms);
|
||||||
|
|
||||||
|
for (const FBuildRoom& BR : BuildRooms)
|
||||||
|
{
|
||||||
|
if (!BR.bStore) continue; // Far room — collected for connectivity only
|
||||||
|
|
||||||
|
FCachedRoom CR;
|
||||||
|
CR.Center = BR.Center;
|
||||||
|
CR.RadiusXY = BR.RadiusXY;
|
||||||
|
CR.RadiusZ = BR.RadiusZ;
|
||||||
|
CR.Hash = BR.Hash;
|
||||||
|
CR.bIsOrigin = BR.bIsOrigin;
|
||||||
|
|
||||||
|
// Cull radius: max extent the room can reach + blend margin.
|
||||||
|
// 1.5x accounts for capsule shapes extending beyond nominal radius.
|
||||||
|
float MaxExtent = FMath::Max(BR.RadiusXY * 1.5f, BR.RadiusZ) + BlendK * 3.0f;
|
||||||
|
CR.CullRadiusSq = MaxExtent * MaxExtent;
|
||||||
|
|
||||||
|
// Flat floor cut: soft floor plane per room, hash-rolled from [Min, Max].
|
||||||
|
// SmoothMax applied in EvaluateSDFCached so tunnels/pits don't create hard seams.
|
||||||
|
// Sentinel -FLT_MAX means "no cut" so the per-voxel check is a single compare.
|
||||||
|
{
|
||||||
|
const float FloorRoll = VoxelHash::ToFloat01(VoxelHash::Mix(BR.Hash ^ 0xF100F2u));
|
||||||
|
const float FloorCut = FMath::Lerp(
|
||||||
|
FMath::Min(Params.RoomFloorCutMin, Params.RoomFloorCutMax),
|
||||||
|
FMath::Max(Params.RoomFloorCutMin, Params.RoomFloorCutMax),
|
||||||
|
FloorRoll
|
||||||
|
);
|
||||||
|
|
||||||
|
if (FloorCut < 1.0f)
|
||||||
|
{
|
||||||
|
CR.FloorCutZ = CR.Center.Z - CR.RadiusZ * FloorCut;
|
||||||
|
CR.FloorReliefStrength = Params.FloorReliefStrength;
|
||||||
|
CR.FloorReliefFrequency = Params.FloorReliefFrequency;
|
||||||
|
CR.FloorSeed = VoxelHash::Mix(BR.Hash ^ 0xF100F1u);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CR.FloorCutZ = -FLT_MAX;
|
||||||
|
CR.FloorReliefStrength = 0.0f;
|
||||||
|
CR.FloorReliefFrequency = 0.015f;
|
||||||
|
CR.FloorSeed = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PER-ROOM TERRAIN OP SELECTION ---
|
||||||
|
if (TerrainOps && TerrainOps->Num() > 0 && TotalOpProb > 0.0f)
|
||||||
|
{
|
||||||
|
const uint32 OpHash = VoxelHash::Mix(BR.Hash ^ 0x0FEED00u);
|
||||||
|
const float Roll = VoxelHash::ToFloat01(OpHash);
|
||||||
|
|
||||||
|
float Cursor = 0.0f;
|
||||||
|
for (const FStrateTerrainOpEntry& E : *TerrainOps)
|
||||||
|
{
|
||||||
|
Cursor += FMath::Max(E.Probability, 0.0f) * NormFactor;
|
||||||
|
if (Roll < Cursor)
|
||||||
|
{
|
||||||
|
const UVoxelTerrainOpDefinition* Op = E.Operation.Get();
|
||||||
|
if (Op)
|
||||||
|
{
|
||||||
|
CR.RoomOp = Op;
|
||||||
|
CR.RoomOpWeight = E.Weight;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OutCache.Rooms.Add(CR);
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// PRE-BAKE: PITS, CHIMNEYS, COLUMNS for this room
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// Pre-baking makes features independent of NearestRoomIdx (no thin "lid"
|
||||||
|
// when the owning room flips mid-shaft). Only stored rooms are baked —
|
||||||
|
// a far room's features can't reach this chunk anyway.
|
||||||
|
if (!CR.RoomOp) continue;
|
||||||
|
|
||||||
|
OpParams = FStrateGenerationParams{};
|
||||||
|
CR.RoomOp->ApplyTo(OpParams, CR.RoomOpWeight);
|
||||||
|
|
||||||
|
// PITS
|
||||||
|
if (OpParams.PitDensity > 0.0f)
|
||||||
|
{
|
||||||
|
const int32 MaxPits = 2;
|
||||||
|
for (int32 i = 0; i < MaxPits; i++)
|
||||||
|
{
|
||||||
|
uint32 PH = VoxelHash::Mix(BR.Hash ^ (0xDE1A7Eu + (uint32)i * 6271u));
|
||||||
|
if (VoxelHash::ToFloat01(PH) > OpParams.PitDensity) continue;
|
||||||
|
|
||||||
|
uint32 PH2 = VoxelHash::Mix(PH ^ 0xABCDu);
|
||||||
|
uint32 PH3 = VoxelHash::Mix(PH2 ^ 0x5EEDu);
|
||||||
|
uint32 PH4 = VoxelHash::Mix(PH3 ^ 0xF00Du);
|
||||||
|
|
||||||
|
float PX = CR.Center.X + VoxelHash::ToFloatSigned(PH2) * CR.RadiusXY * 0.6f;
|
||||||
|
float PY = CR.Center.Y + VoxelHash::ToFloatSigned(VoxelHash::Mix(PH2)) * CR.RadiusXY * 0.6f;
|
||||||
|
|
||||||
|
float PitRadius = FMath::Lerp(OpParams.PitMinRadius, OpParams.PitMaxRadius,
|
||||||
|
VoxelHash::ToFloat01(PH3));
|
||||||
|
|
||||||
|
float PitTopZ = CR.Center.Z - CR.RadiusZ * 0.5f
|
||||||
|
+ VoxelHash::ToFloat01(PH4) * CR.RadiusZ * 0.2f;
|
||||||
|
|
||||||
|
FCachedPit Pit;
|
||||||
|
Pit.CenterX = PX;
|
||||||
|
Pit.CenterY = PY;
|
||||||
|
Pit.TopZ = PitTopZ;
|
||||||
|
Pit.Radius = PitRadius;
|
||||||
|
Pit.Depth = OpParams.PitDepth;
|
||||||
|
Pit.FlareDist = PitRadius * 2.0f;
|
||||||
|
Pit.FlareExtra = PitRadius * 1.0f;
|
||||||
|
Pit.BaseDensity = Params.BaseDensity;
|
||||||
|
Pit.BlendK = Params.SDFBlendRadius;
|
||||||
|
float MaxXYR = PitRadius + PitRadius + Params.SDFBlendRadius + 4.0f;
|
||||||
|
Pit.BoundXYRadiusSq = MaxXYR * MaxXYR;
|
||||||
|
OutCache.Pits.Add(Pit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHIMNEYS
|
||||||
|
if (OpParams.ChimneyDensity > 0.0f)
|
||||||
|
{
|
||||||
|
const int32 MaxChimneys = 2;
|
||||||
|
for (int32 i = 0; i < MaxChimneys; i++)
|
||||||
|
{
|
||||||
|
uint32 CH = VoxelHash::Mix(BR.Hash ^ (0xC4F007u + (uint32)i * 7919u));
|
||||||
|
if (VoxelHash::ToFloat01(CH) > OpParams.ChimneyDensity) continue;
|
||||||
|
|
||||||
|
uint32 CH2 = VoxelHash::Mix(CH ^ 0x1337u);
|
||||||
|
uint32 CH3 = VoxelHash::Mix(CH2 ^ 0xCAFEu);
|
||||||
|
uint32 CH4 = VoxelHash::Mix(CH3 ^ 0xD00Du);
|
||||||
|
|
||||||
|
float CX = CR.Center.X + VoxelHash::ToFloatSigned(CH2) * CR.RadiusXY * 0.6f;
|
||||||
|
float CY = CR.Center.Y + VoxelHash::ToFloatSigned(VoxelHash::Mix(CH2)) * CR.RadiusXY * 0.6f;
|
||||||
|
|
||||||
|
float ChmRadius = FMath::Lerp(OpParams.ChimneyMinRadius, OpParams.ChimneyMaxRadius,
|
||||||
|
VoxelHash::ToFloat01(CH3));
|
||||||
|
|
||||||
|
float ChmBottomZ = CR.Center.Z + CR.RadiusZ * 0.5f
|
||||||
|
- VoxelHash::ToFloat01(CH4) * CR.RadiusZ * 0.2f;
|
||||||
|
|
||||||
|
FCachedChimney Chim;
|
||||||
|
Chim.CenterX = CX;
|
||||||
|
Chim.CenterY = CY;
|
||||||
|
Chim.BottomZ = ChmBottomZ;
|
||||||
|
Chim.Radius = ChmRadius;
|
||||||
|
Chim.Height = OpParams.ChimneyHeight;
|
||||||
|
Chim.FlareDist = ChmRadius * 2.0f;
|
||||||
|
Chim.FlareExtra = ChmRadius * 1.0f;
|
||||||
|
Chim.BaseDensity = Params.BaseDensity;
|
||||||
|
Chim.BlendK = Params.SDFBlendRadius;
|
||||||
|
float MaxXYR = ChmRadius + ChmRadius + Params.SDFBlendRadius + 4.0f;
|
||||||
|
Chim.BoundXYRadiusSq = MaxXYR * MaxXYR;
|
||||||
|
OutCache.Chimneys.Add(Chim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// COLUMNS
|
||||||
|
if (OpParams.ColumnDensity > 0.0f)
|
||||||
|
{
|
||||||
|
const int32 MaxCols = 4;
|
||||||
|
for (int32 i = 0; i < MaxCols; i++)
|
||||||
|
{
|
||||||
|
uint32 H = VoxelHash::Mix(BR.Hash ^ (0xC01C01u + (uint32)i * 3571u));
|
||||||
|
if (VoxelHash::ToFloat01(H) > OpParams.ColumnDensity) continue;
|
||||||
|
|
||||||
|
uint32 H2 = VoxelHash::Mix(H ^ 0x1A2B3Cu);
|
||||||
|
|
||||||
|
float ColX = CR.Center.X + VoxelHash::ToFloatSigned(H2) * CR.RadiusXY * 0.75f;
|
||||||
|
float ColY = CR.Center.Y + VoxelHash::ToFloatSigned(VoxelHash::Mix(H2)) * CR.RadiusXY * 0.75f;
|
||||||
|
|
||||||
|
uint32 H3 = VoxelHash::Mix(H2 ^ 0xBEEFu);
|
||||||
|
float ColR = FMath::Lerp(OpParams.ColumnMinRadius, OpParams.ColumnMaxRadius,
|
||||||
|
VoxelHash::ToFloat01(H3));
|
||||||
|
|
||||||
|
FCachedColumn Col;
|
||||||
|
Col.CenterX = ColX;
|
||||||
|
Col.CenterY = ColY;
|
||||||
|
Col.Radius = ColR;
|
||||||
|
Col.BaseDensity = Params.BaseDensity;
|
||||||
|
float MaxXYR = ColR + 6.0f;
|
||||||
|
Col.BoundXYRadiusSq = MaxXYR * MaxXYR;
|
||||||
|
OutCache.Columns.Add(Col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// PHASE 2: EVALUATE SDF WITH CACHED DATA
|
||||||
|
//=============================================================================
|
||||||
|
// Pure SDF math — no hashing, no array building, no backbone computation.
|
||||||
|
// Just loops through the pre-built rooms and tunnels, evaluates distance,
|
||||||
|
// and smooth-mins everything together. Distance culling skips primitives
|
||||||
|
// that are clearly too far to contribute.
|
||||||
|
|
||||||
|
float VoxelCaveMorphology::EvaluateSDFCached(
|
||||||
|
float WorldX, float WorldY, float WorldZ,
|
||||||
|
const FChunkSDFCache& Cache,
|
||||||
|
float SDFBlendRadius,
|
||||||
|
float RoomShapeVariety,
|
||||||
|
int32* OutNearestRoomIdx)
|
||||||
|
{
|
||||||
|
float MinSDF = FLT_MAX;
|
||||||
|
const float BlendK = SDFBlendRadius;
|
||||||
|
const FVector Pos(WorldX, WorldY, WorldZ);
|
||||||
|
|
||||||
|
// Track which room contributes the smallest (most-inside) raw SDF.
|
||||||
|
// This is used by the terrain ops system to find the "owning" room for
|
||||||
|
// this voxel and apply that room's per-room terrain operation.
|
||||||
|
// We track raw room SDF (before SmoothMin) so tunnel SDFs don't interfere.
|
||||||
|
float NearestRoomRawSDF = FLT_MAX;
|
||||||
|
int32 NearestIdx = -1;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// Room SDFs
|
||||||
|
//=========================================================================
|
||||||
|
for (int32 RoomIdx = 0; RoomIdx < Cache.Rooms.Num(); ++RoomIdx)
|
||||||
|
{
|
||||||
|
const FCachedRoom& Room = Cache.Rooms[RoomIdx];
|
||||||
|
|
||||||
|
// --- DISTANCE CULL ---
|
||||||
|
const float DistSq = FVector::DistSquared(Pos, Room.Center);
|
||||||
|
if (DistSq > Room.CullRadiusSq) continue;
|
||||||
|
|
||||||
|
// --- SHAPE SELECTION ---
|
||||||
|
float RoomSDF;
|
||||||
|
const uint32 ShapeHash = VoxelHash::Mix(Room.Hash ^ 0xDEADBEEFu);
|
||||||
|
const float ShapeRoll = Room.bIsOrigin ? 0.0f : VoxelHash::ToFloat01(ShapeHash);
|
||||||
|
|
||||||
|
// Thresholds: Variety=0 → all ellipsoid. Variety=1 → 50/30/20 split.
|
||||||
|
const float BoxThreshold = 1.0f - RoomShapeVariety * 0.5f;
|
||||||
|
const float CapsuleThreshold = 1.0f - RoomShapeVariety * 0.2f;
|
||||||
|
|
||||||
|
if (ShapeRoll >= BoxThreshold && ShapeRoll < CapsuleThreshold)
|
||||||
|
{
|
||||||
|
// ROUNDED BOX: angular chamber with smooth corners
|
||||||
|
FVector HalfExtent(
|
||||||
|
Room.RadiusXY * 0.8f,
|
||||||
|
Room.RadiusXY * 0.8f,
|
||||||
|
Room.RadiusZ * 0.8f
|
||||||
|
);
|
||||||
|
float Rounding = Room.RadiusXY * 0.25f;
|
||||||
|
RoomSDF = VoxelSDF::RoundedBox(Pos, Room.Center, HalfExtent, Rounding);
|
||||||
|
}
|
||||||
|
else if (ShapeRoll >= CapsuleThreshold)
|
||||||
|
{
|
||||||
|
// ELONGATED CAPSULE: stretched hall/corridor-room
|
||||||
|
float DirAngle = VoxelHash::ToFloat01(VoxelHash::Mix(Room.Hash ^ 0xCAFEBABEu)) * 2.0f * PI;
|
||||||
|
float StretchDist = Room.RadiusXY * 0.7f;
|
||||||
|
FVector Dir(FMath::Cos(DirAngle), FMath::Sin(DirAngle), 0.0f);
|
||||||
|
FVector EndA = Room.Center + Dir * StretchDist;
|
||||||
|
FVector EndB = Room.Center - Dir * StretchDist;
|
||||||
|
float CapsuleR = FMath::Min(Room.RadiusXY * 0.6f, Room.RadiusZ);
|
||||||
|
RoomSDF = VoxelSDF::Capsule(Pos, EndA, EndB, CapsuleR);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// ELLIPSOID (default): smooth oval chamber
|
||||||
|
const FVector Radii(Room.RadiusXY, Room.RadiusXY, Room.RadiusZ);
|
||||||
|
RoomSDF = VoxelSDF::Ellipsoid(Pos, Room.Center, Radii);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soft floor: SmoothMax of the room SDF and the floor half-space.
|
||||||
|
if (Room.FloorCutZ > -FLT_MAX)
|
||||||
|
{
|
||||||
|
float FloorZ = Room.FloorCutZ;
|
||||||
|
|
||||||
|
if (Room.FloorReliefStrength > 0.0f)
|
||||||
|
{
|
||||||
|
const float RF = Room.FloorReliefFrequency;
|
||||||
|
const float SF = (float)Room.FloorSeed * 0.00001f;
|
||||||
|
|
||||||
|
float N = FMath::PerlinNoise2D(FVector2D(Pos.X * RF + SF, Pos.Y * RF + SF * 1.7f)) * 0.65f
|
||||||
|
+ FMath::PerlinNoise2D(FVector2D(Pos.X * RF * 2.3f + SF * 3.1f, Pos.Y * RF * 2.3f + SF * 5.3f)) * 0.35f;
|
||||||
|
N *= VOXEL_NOISE_SCALE;
|
||||||
|
FloorZ += N * Room.FloorReliefStrength;
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomSDF = VoxelSDF::SmoothMax(RoomSDF, FloorZ - Pos.Z, BlendK * 0.35f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track the room whose SDF is smallest (most inside).
|
||||||
|
if (OutNearestRoomIdx && RoomSDF < NearestRoomRawSDF)
|
||||||
|
{
|
||||||
|
NearestRoomRawSDF = RoomSDF;
|
||||||
|
NearestIdx = RoomIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
MinSDF = VoxelSDF::SmoothMin(MinSDF, RoomSDF, BlendK);
|
||||||
|
}
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// Tunnel SDFs
|
||||||
|
//=========================================================================
|
||||||
|
for (const FCachedTunnel& Tunnel : Cache.Tunnels)
|
||||||
|
{
|
||||||
|
// --- BOUNDING SPHERE CULL ---
|
||||||
|
const float DistSq = FVector::DistSquared(Pos, Tunnel.BoundCenter);
|
||||||
|
if (DistSq > Tunnel.BoundRadiusSq) continue;
|
||||||
|
|
||||||
|
float TunnelSDF;
|
||||||
|
|
||||||
|
if (Tunnel.bHasMidpoint)
|
||||||
|
{
|
||||||
|
// Two-segment curved tunnel: A→Mid and Mid→B
|
||||||
|
float SegA = VoxelSDF::TaperedCapsule(Pos, Tunnel.EndpointA, Tunnel.Midpoint,
|
||||||
|
Tunnel.RadiusA, Tunnel.RadiusMid);
|
||||||
|
float SegB = VoxelSDF::TaperedCapsule(Pos, Tunnel.Midpoint, Tunnel.EndpointB,
|
||||||
|
Tunnel.RadiusMid, Tunnel.RadiusB);
|
||||||
|
TunnelSDF = FMath::Min(SegA, SegB);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Straight single-segment tunnel
|
||||||
|
TunnelSDF = VoxelSDF::TaperedCapsule(Pos, Tunnel.EndpointA, Tunnel.EndpointB,
|
||||||
|
Tunnel.RadiusA, Tunnel.RadiusB);
|
||||||
|
}
|
||||||
|
|
||||||
|
MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write nearest room index for the caller (terrain ops system)
|
||||||
|
if (OutNearestRoomIdx)
|
||||||
|
{
|
||||||
|
*OutNearestRoomIdx = NearestIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MinSDF;
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// CONVENIENCE WRAPPER (backward compatible)
|
||||||
|
//=============================================================================
|
||||||
|
// Builds a temporary cache for a single point, then evaluates.
|
||||||
|
// For chunk generation, use BuildChunkCache + EvaluateSDFCached directly.
|
||||||
|
// The search box around the point only needs a MaxInfluence margin — BuildChunkCache
|
||||||
|
// internally widens the COLLECT region to (2*MaxTunnelLength + MaxInfluence) so the
|
||||||
|
// graph it builds is the same one the chunk path would build at this point.
|
||||||
|
|
||||||
|
float VoxelCaveMorphology::EvaluateSDF(
|
||||||
|
float WorldX, float WorldY, float WorldZ,
|
||||||
|
const FStrateGenerationParams& Params,
|
||||||
|
uint32 Seed, int32 StrateIndex)
|
||||||
|
{
|
||||||
|
const float Margin = FMath::Max(
|
||||||
|
Params.MaxRoomRadius,
|
||||||
|
Params.TunnelWarpStrength + Params.TunnelMaxRadius
|
||||||
|
) + Params.SDFBlendRadius;
|
||||||
|
|
||||||
|
FChunkSDFCache TempCache;
|
||||||
|
BuildChunkCache(
|
||||||
|
TempCache,
|
||||||
|
WorldX - Margin, WorldY - Margin,
|
||||||
|
WorldX + Margin, WorldY + Margin,
|
||||||
|
Params, Seed, StrateIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
return EvaluateSDFCached(
|
||||||
|
WorldX, WorldY, WorldZ,
|
||||||
|
TempCache, Params.SDFBlendRadius, Params.RoomShapeVariety
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
// VoxelContentManager.cpp
|
||||||
|
// Deterministic per-chunk decoration scatter + aesthetic water surfaces.
|
||||||
|
|
||||||
|
#include "VoxelContentManager.h"
|
||||||
|
#include "VoxelStrateManager.h"
|
||||||
|
#include "VoxelStrateDefinition.h"
|
||||||
|
#include "VoxelCaveMorphology.h" // VoxelHash
|
||||||
|
#include "Components/StaticMeshComponent.h"
|
||||||
|
#include "Engine/StaticMesh.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "Materials/MaterialInterface.h"
|
||||||
|
|
||||||
|
// Global safety cap on spawned decorations per chunk (across all entries).
|
||||||
|
static constexpr int32 GMaxDecorationsPerChunk = 400;
|
||||||
|
|
||||||
|
void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InStrateManager, int32 InSeed)
|
||||||
|
{
|
||||||
|
Owner = InOwner;
|
||||||
|
StrateManager = InStrateManager;
|
||||||
|
Seed = InSeed;
|
||||||
|
|
||||||
|
// Engine unit plane — reused (scaled) for every water surface.
|
||||||
|
if (!PlaneMesh)
|
||||||
|
{
|
||||||
|
PlaneMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Plane.Plane"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deterministic placement hash: pure function of (chunk, surface index, entry, salt, seed).
|
||||||
|
static FORCEINLINE uint32 PlacementHash(const FIntVector& C, int32 SurfIdx, int32 EntryIdx, uint32 Seed, uint32 Salt)
|
||||||
|
{
|
||||||
|
uint32 H = VoxelHash::Cell(C.X, C.Y, Seed ^ Salt);
|
||||||
|
H ^= VoxelHash::Mix((uint32)(C.Z * 0x9E3779B1u));
|
||||||
|
H ^= VoxelHash::Mix((uint32)SurfIdx * 2654435761u);
|
||||||
|
H ^= VoxelHash::Mix((uint32)EntryIdx * 40503u + 1u);
|
||||||
|
return VoxelHash::Mix(H);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVoxelContentManager::PopulateChunk(const FIntVector& ChunkCoord, const FVoxelMeshData& MeshData, int32 LODLevel)
|
||||||
|
{
|
||||||
|
// Re-meshing a chunk calls this again — start clean.
|
||||||
|
ClearChunk(ChunkCoord);
|
||||||
|
|
||||||
|
if (!StrateManager) return;
|
||||||
|
const UVoxelStrateDefinition* Def = StrateManager->GetStrateForChunk(ChunkCoord);
|
||||||
|
if (!Def) return;
|
||||||
|
|
||||||
|
// Decorations only on near (full-detail) chunks — they're game-thread actor spawns
|
||||||
|
// and pointless on distant low-poly chunks. Water is one cheap plane, place it at any LOD.
|
||||||
|
if (LODLevel == 0)
|
||||||
|
{
|
||||||
|
TArray<TWeakObjectPtr<AActor>> Spawned;
|
||||||
|
SpawnDecorations(ChunkCoord, MeshData, Def, Spawned);
|
||||||
|
if (Spawned.Num() > 0)
|
||||||
|
{
|
||||||
|
SpawnedActors.Add(ChunkCoord, MoveTemp(Spawned));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SpawnWater(ChunkCoord, Def);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVoxelContentManager::SpawnDecorations(const FIntVector& ChunkCoord, const FVoxelMeshData& MeshData,
|
||||||
|
const UVoxelStrateDefinition* Def, TArray<TWeakObjectPtr<AActor>>& Out)
|
||||||
|
{
|
||||||
|
if (Def->Decorations.Num() == 0) return;
|
||||||
|
|
||||||
|
AActor* OwnerActor = Owner.Get();
|
||||||
|
if (!OwnerActor) return;
|
||||||
|
UWorld* World = OwnerActor->GetWorld();
|
||||||
|
if (!World) return;
|
||||||
|
|
||||||
|
const int32 NumVerts = MeshData.Vertices.Num();
|
||||||
|
if (NumVerts == 0) return;
|
||||||
|
|
||||||
|
const FTransform OwnerXf = OwnerActor->GetActorTransform();
|
||||||
|
|
||||||
|
// Water world-Z for the water-relative placement rule (voxel coords → world units).
|
||||||
|
const float WaterVoxelZ = StrateManager->GetWaterLevelWorldZForChunk(ChunkCoord);
|
||||||
|
const bool bHasWater = (WaterVoxelZ != -FLT_MAX);
|
||||||
|
const float WaterWorldZ = bHasWater ? WaterVoxelZ * VOXEL_SIZE : -FLT_MAX;
|
||||||
|
|
||||||
|
int32 TotalSpawned = 0;
|
||||||
|
|
||||||
|
for (int32 DecoIdx = 0; DecoIdx < Def->Decorations.Num(); ++DecoIdx)
|
||||||
|
{
|
||||||
|
const FStrateDecoration& Deco = Def->Decorations[DecoIdx];
|
||||||
|
if (!Deco.ActorClass) continue;
|
||||||
|
if (Deco.SpawnDensity <= 0.0f) continue;
|
||||||
|
|
||||||
|
int32 SpawnedThisEntry = 0;
|
||||||
|
|
||||||
|
for (int32 i = 0; i < NumVerts; ++i)
|
||||||
|
{
|
||||||
|
if (TotalSpawned >= GMaxDecorationsPerChunk) return;
|
||||||
|
if (SpawnedThisEntry >= Deco.MaxPerChunk) break;
|
||||||
|
|
||||||
|
const FVector NormalWorld = MeshData.Normals.IsValidIndex(i)
|
||||||
|
? OwnerXf.TransformVectorNoScale(MeshData.Normals[i]).GetSafeNormal()
|
||||||
|
: FVector::UpVector;
|
||||||
|
|
||||||
|
// Surface classification by normal Z.
|
||||||
|
const bool bFloor = NormalWorld.Z > 0.5f;
|
||||||
|
const bool bCeiling = NormalWorld.Z < -0.5f;
|
||||||
|
const bool bWall = !bFloor && !bCeiling;
|
||||||
|
bool bMatches = true;
|
||||||
|
switch (Deco.SurfacePlacement)
|
||||||
|
{
|
||||||
|
case ESurfaceType::Floor: bMatches = bFloor; break;
|
||||||
|
case ESurfaceType::Wall: bMatches = bWall; break;
|
||||||
|
case ESurfaceType::Ceiling: bMatches = bCeiling; break;
|
||||||
|
case ESurfaceType::Any: bMatches = true; break;
|
||||||
|
default: bMatches = true; break;
|
||||||
|
}
|
||||||
|
if (!bMatches) continue;
|
||||||
|
|
||||||
|
// Deterministic spawn gate.
|
||||||
|
const uint32 H = PlacementHash(ChunkCoord, i, DecoIdx, (uint32)Seed, 0xDEC0u);
|
||||||
|
if (VoxelHash::ToFloat01(H) > Deco.SpawnDensity) continue;
|
||||||
|
|
||||||
|
const FVector PosWorld = OwnerXf.TransformPosition(MeshData.Vertices[i]);
|
||||||
|
|
||||||
|
// Water-relative rule.
|
||||||
|
if (Deco.bRequireWaterRelative && bHasWater)
|
||||||
|
{
|
||||||
|
const bool bBelow = PosWorld.Z < WaterWorldZ;
|
||||||
|
if (bBelow != Deco.bPlaceBelowWater) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform.
|
||||||
|
const FVector SpawnPos = PosWorld + NormalWorld * Deco.SurfaceOffset;
|
||||||
|
|
||||||
|
FQuat BaseQ = Deco.bAlignToSurface
|
||||||
|
? FRotationMatrix::MakeFromZ(NormalWorld).ToQuat()
|
||||||
|
: FQuat::Identity;
|
||||||
|
if (Deco.bRandomYaw)
|
||||||
|
{
|
||||||
|
const float Yaw = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x59415721u)) * 2.0f * PI;
|
||||||
|
const FVector Axis = Deco.bAlignToSurface ? NormalWorld : FVector::UpVector;
|
||||||
|
BaseQ = FQuat(Axis, Yaw) * BaseQ;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float ScaleT = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x5CA1E000u));
|
||||||
|
const float Scale = FMath::Lerp(Deco.MinScale, Deco.MaxScale, ScaleT);
|
||||||
|
|
||||||
|
FActorSpawnParameters SpawnParams;
|
||||||
|
SpawnParams.Owner = OwnerActor;
|
||||||
|
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
|
|
||||||
|
AActor* NewActor = World->SpawnActor<AActor>(
|
||||||
|
Deco.ActorClass,
|
||||||
|
FTransform(BaseQ, SpawnPos, FVector(Scale)),
|
||||||
|
SpawnParams);
|
||||||
|
|
||||||
|
if (NewActor)
|
||||||
|
{
|
||||||
|
Out.Add(NewActor);
|
||||||
|
++SpawnedThisEntry;
|
||||||
|
++TotalSpawned;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVoxelContentManager::SpawnWater(const FIntVector& ChunkCoord, const UVoxelStrateDefinition* Def)
|
||||||
|
{
|
||||||
|
if (!Def->bHasWater || !PlaneMesh) return;
|
||||||
|
|
||||||
|
AActor* OwnerActor = Owner.Get();
|
||||||
|
if (!OwnerActor) return;
|
||||||
|
|
||||||
|
const float WaterVoxelZ = StrateManager->GetWaterLevelWorldZForChunk(ChunkCoord);
|
||||||
|
if (WaterVoxelZ == -FLT_MAX) return;
|
||||||
|
|
||||||
|
// Only the chunk whose vertical span contains the water surface gets a plane.
|
||||||
|
const float ChunkVoxelZMin = (float)(ChunkCoord.Z) * CHUNK_SIZE;
|
||||||
|
const float ChunkVoxelZMax = ChunkVoxelZMin + CHUNK_SIZE;
|
||||||
|
if (WaterVoxelZ < ChunkVoxelZMin || WaterVoxelZ > ChunkVoxelZMax) return;
|
||||||
|
|
||||||
|
// World-space center of this chunk's XY footprint, at the water surface Z.
|
||||||
|
const float CenterVoxelX = ((float)ChunkCoord.X + 0.5f) * CHUNK_SIZE;
|
||||||
|
const float CenterVoxelY = ((float)ChunkCoord.Y + 0.5f) * CHUNK_SIZE;
|
||||||
|
const FVector LocalCenter(CenterVoxelX * VOXEL_SIZE, CenterVoxelY * VOXEL_SIZE, WaterVoxelZ * VOXEL_SIZE);
|
||||||
|
const FVector WorldCenter = OwnerActor->GetActorTransform().TransformPosition(LocalCenter);
|
||||||
|
|
||||||
|
UStaticMeshComponent* Plane = NewObject<UStaticMeshComponent>(OwnerActor);
|
||||||
|
Plane->SetStaticMesh(PlaneMesh);
|
||||||
|
Plane->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||||
|
Plane->SetCastShadow(false);
|
||||||
|
Plane->RegisterComponent();
|
||||||
|
Plane->AttachToComponent(OwnerActor->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);
|
||||||
|
Plane->SetWorldLocation(WorldCenter);
|
||||||
|
|
||||||
|
// Engine plane is 100 uu square → scale to one chunk's world footprint.
|
||||||
|
const float ChunkWorld = (float)CHUNK_SIZE * VOXEL_SIZE;
|
||||||
|
Plane->SetWorldScale3D(FVector(ChunkWorld / 100.0f, ChunkWorld / 100.0f, 1.0f));
|
||||||
|
|
||||||
|
if (Def->WaterMaterial)
|
||||||
|
{
|
||||||
|
Plane->SetMaterial(0, Def->WaterMaterial);
|
||||||
|
}
|
||||||
|
|
||||||
|
WaterPlanes.Add(ChunkCoord, Plane);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVoxelContentManager::ClearChunk(const FIntVector& ChunkCoord)
|
||||||
|
{
|
||||||
|
if (TArray<TWeakObjectPtr<AActor>>* Actors = SpawnedActors.Find(ChunkCoord))
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<AActor>& A : *Actors)
|
||||||
|
{
|
||||||
|
if (AActor* Act = A.Get())
|
||||||
|
{
|
||||||
|
Act->Destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpawnedActors.Remove(ChunkCoord);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UStaticMeshComponent** Plane = WaterPlanes.Find(ChunkCoord))
|
||||||
|
{
|
||||||
|
if (*Plane)
|
||||||
|
{
|
||||||
|
(*Plane)->DestroyComponent();
|
||||||
|
}
|
||||||
|
WaterPlanes.Remove(ChunkCoord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVoxelContentManager::ClearAll()
|
||||||
|
{
|
||||||
|
TArray<FIntVector> Coords;
|
||||||
|
SpawnedActors.GetKeys(Coords);
|
||||||
|
for (const FIntVector& C : Coords)
|
||||||
|
{
|
||||||
|
ClearChunk(C);
|
||||||
|
}
|
||||||
|
// Any water planes without decorations.
|
||||||
|
TArray<FIntVector> WaterCoords;
|
||||||
|
WaterPlanes.GetKeys(WaterCoords);
|
||||||
|
for (const FIntVector& C : WaterCoords)
|
||||||
|
{
|
||||||
|
ClearChunk(C);
|
||||||
|
}
|
||||||
|
SpawnedActors.Empty();
|
||||||
|
WaterPlanes.Empty();
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
// VoxelDiffLayer.cpp
|
||||||
|
// Runtime terrain modification storage and evaluation.
|
||||||
|
|
||||||
|
#include "VoxelDiffLayer.h"
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// BUDGET CONFIGURATION
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
void UVoxelDiffLayer::SetBudget(int32 InMaxMods, float InMaxRadius, float InMaxVolume)
|
||||||
|
{
|
||||||
|
BudgetMaxMods = InMaxMods;
|
||||||
|
BudgetMaxRadius = InMaxRadius;
|
||||||
|
BudgetMaxVolume = InMaxVolume;
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("[DiffLayer] Budget set: MaxMods=%d, MaxRadius=%.1f, MaxVolume=%.0f (0=unlimited)"),
|
||||||
|
BudgetMaxMods, BudgetMaxRadius, BudgetMaxVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UVoxelDiffLayer::CanModify(float Radius) const
|
||||||
|
{
|
||||||
|
// Check radius limit (always enforced, even if "unlimited" count/volume)
|
||||||
|
if (Radius > BudgetMaxRadius)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check modification count limit
|
||||||
|
if (BudgetMaxMods > 0 && ModificationCount >= BudgetMaxMods)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check volume limit: would this brush push us over?
|
||||||
|
if (BudgetMaxVolume > 0.0f)
|
||||||
|
{
|
||||||
|
const float BrushVolume = (4.0f / 3.0f) * PI * Radius * Radius * Radius;
|
||||||
|
if (AccumulatedVolume + BrushVolume > BudgetMaxVolume)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UVoxelDiffLayer::GetRemainingModifications() const
|
||||||
|
{
|
||||||
|
if (BudgetMaxMods <= 0) return -1; // Unlimited
|
||||||
|
return FMath::Max(0, BudgetMaxMods - ModificationCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
float UVoxelDiffLayer::GetRemainingVolume() const
|
||||||
|
{
|
||||||
|
if (BudgetMaxVolume <= 0.0f) return -1.0f; // Unlimited
|
||||||
|
return FMath::Max(0.0f, BudgetMaxVolume - AccumulatedVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// APPLY MODIFICATION
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TArray<FIntVector> UVoxelDiffLayer::ApplyModification(const FVoxelModification& Mod)
|
||||||
|
{
|
||||||
|
TArray<FIntVector> AffectedChunks;
|
||||||
|
|
||||||
|
// --- Budget enforcement ---
|
||||||
|
// Clamp radius to max allowed
|
||||||
|
float ClampedRadius = FMath::Min(Mod.Radius, BudgetMaxRadius);
|
||||||
|
|
||||||
|
if (!CanModify(ClampedRadius))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning,
|
||||||
|
TEXT("[DiffLayer] Modification REJECTED — budget exceeded (count=%d/%d, volume=%.0f/%.0f)"),
|
||||||
|
ModificationCount, BudgetMaxMods, AccumulatedVolume, BudgetMaxVolume);
|
||||||
|
return AffectedChunks; // Empty = rejected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use clamped radius for the actual modification
|
||||||
|
FVoxelModification ClampedMod = Mod;
|
||||||
|
ClampedMod.Radius = ClampedRadius;
|
||||||
|
|
||||||
|
// Update budget counters
|
||||||
|
ModificationCount++;
|
||||||
|
const float BrushVolume = (4.0f / 3.0f) * PI * ClampedRadius * ClampedRadius * ClampedRadius;
|
||||||
|
AccumulatedVolume += BrushVolume;
|
||||||
|
|
||||||
|
// Find every chunk the brush can touch, from its shape-aware world AABB.
|
||||||
|
FVector BoundsMin, BoundsMax;
|
||||||
|
ClampedMod.GetWorldBounds(BoundsMin, BoundsMax);
|
||||||
|
const int32 MinCX = FMath::FloorToInt(BoundsMin.X / CHUNK_SIZE);
|
||||||
|
const int32 MaxCX = FMath::FloorToInt(BoundsMax.X / CHUNK_SIZE);
|
||||||
|
const int32 MinCY = FMath::FloorToInt(BoundsMin.Y / CHUNK_SIZE);
|
||||||
|
const int32 MaxCY = FMath::FloorToInt(BoundsMax.Y / CHUNK_SIZE);
|
||||||
|
const int32 MinCZ = FMath::FloorToInt(BoundsMin.Z / CHUNK_SIZE);
|
||||||
|
const int32 MaxCZ = FMath::FloorToInt(BoundsMax.Z / CHUNK_SIZE);
|
||||||
|
|
||||||
|
for (int32 CZ = MinCZ; CZ <= MaxCZ; CZ++)
|
||||||
|
{
|
||||||
|
for (int32 CY = MinCY; CY <= MaxCY; CY++)
|
||||||
|
{
|
||||||
|
for (int32 CX = MinCX; CX <= MaxCX; CX++)
|
||||||
|
{
|
||||||
|
FIntVector ChunkCoord(CX, CY, CZ);
|
||||||
|
|
||||||
|
// Store the modification in this chunk's list
|
||||||
|
ChunkMods.FindOrAdd(ChunkCoord).Add(ClampedMod);
|
||||||
|
|
||||||
|
// Track which chunks need re-meshing
|
||||||
|
AffectedChunks.Add(ChunkCoord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log,
|
||||||
|
TEXT("[DiffLayer] Modification #%d at (%.0f, %.0f, %.0f) R=%.1f S=%.1f -> %d chunks (budget: %d/%d mods, %.0f/%.0f vol)"),
|
||||||
|
ModificationCount,
|
||||||
|
ClampedMod.Center.X, ClampedMod.Center.Y, ClampedMod.Center.Z,
|
||||||
|
ClampedMod.Radius, ClampedMod.Strength,
|
||||||
|
AffectedChunks.Num(),
|
||||||
|
ModificationCount, BudgetMaxMods,
|
||||||
|
AccumulatedVolume, BudgetMaxVolume);
|
||||||
|
|
||||||
|
return AffectedChunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// DENSITY EVALUATION
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
float UVoxelDiffLayer::GetDensityOffset(const FIntVector& ChunkCoord,
|
||||||
|
float WorldX, float WorldY, float WorldZ) const
|
||||||
|
{
|
||||||
|
// Fast path: if this chunk has no modifications, return 0
|
||||||
|
const TArray<FVoxelModification>* Mods = ChunkMods.Find(ChunkCoord);
|
||||||
|
if (!Mods || Mods->Num() == 0) return 0.0f;
|
||||||
|
|
||||||
|
float TotalOffset = 0.0f;
|
||||||
|
const FVector Pos(WorldX, WorldY, WorldZ);
|
||||||
|
|
||||||
|
// smoothstep helper (full strength when t<=0, zero when t>=1).
|
||||||
|
auto Smooth01 = [](float T) -> float
|
||||||
|
{
|
||||||
|
T = FMath::Clamp(T, 0.0f, 1.0f);
|
||||||
|
return 1.0f - T * T * (3.0f - 2.0f * T);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const FVoxelModification& Mod : *Mods)
|
||||||
|
{
|
||||||
|
float Falloff = 0.0f;
|
||||||
|
|
||||||
|
switch (Mod.Shape)
|
||||||
|
{
|
||||||
|
case EVoxelBrushShape::Box:
|
||||||
|
{
|
||||||
|
// Box SDF (negative inside), then fade over the Falloff band outside.
|
||||||
|
const FVector Q = (Pos - Mod.Center).GetAbs() - Mod.BoxExtent;
|
||||||
|
const float Outside = FVector(FMath::Max(Q.X, 0.0f), FMath::Max(Q.Y, 0.0f), FMath::Max(Q.Z, 0.0f)).Size();
|
||||||
|
const float Inside = FMath::Min(FMath::Max3(Q.X, Q.Y, Q.Z), 0.0f);
|
||||||
|
const float S = Outside + Inside; // <=0 inside the box
|
||||||
|
const float Band = FMath::Max(Mod.Falloff, 0.01f);
|
||||||
|
if (S >= Band) continue;
|
||||||
|
Falloff = Smooth01(FMath::Max(S, 0.0f) / Band);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EVoxelBrushShape::Capsule:
|
||||||
|
{
|
||||||
|
// Distance to the segment minus the tube radius, faded over Falloff.
|
||||||
|
const FVector AB = Mod.CapsuleEnd - Mod.Center;
|
||||||
|
const FVector AP = Pos - Mod.Center;
|
||||||
|
const float T = FMath::Clamp(FVector::DotProduct(AP, AB) / FMath::Max(FVector::DotProduct(AB, AB), KINDA_SMALL_NUMBER), 0.0f, 1.0f);
|
||||||
|
const float SegDist = FVector::Dist(Pos, Mod.Center + AB * T);
|
||||||
|
const float S = SegDist - Mod.Radius; // <=0 inside the tube
|
||||||
|
const float Band = FMath::Max(Mod.Falloff, 0.01f);
|
||||||
|
if (S >= Band) continue;
|
||||||
|
Falloff = Smooth01(FMath::Max(S, 0.0f) / Band);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EVoxelBrushShape::Sphere:
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
const float Dist = FVector::Dist(Pos, Mod.Center);
|
||||||
|
if (Dist >= Mod.Radius) continue;
|
||||||
|
Falloff = Smooth01(Dist / Mod.Radius);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TotalOffset += Mod.Strength * Falloff;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TotalOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UVoxelDiffLayer::HasModifications(const FIntVector& ChunkCoord) const
|
||||||
|
{
|
||||||
|
const TArray<FVoxelModification>* Mods = ChunkMods.Find(ChunkCoord);
|
||||||
|
return Mods && Mods->Num() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// MANAGEMENT
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
void UVoxelDiffLayer::Clear()
|
||||||
|
{
|
||||||
|
int32 Count = GetTotalModificationCount();
|
||||||
|
ChunkMods.Empty();
|
||||||
|
|
||||||
|
// Reset budget counters — player gets a fresh budget after clear/season reset
|
||||||
|
ModificationCount = 0;
|
||||||
|
AccumulatedVolume = 0.0f;
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("[DiffLayer] Cleared %d modifications, budget reset"), Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UVoxelDiffLayer::GetTotalModificationCount() const
|
||||||
|
{
|
||||||
|
int32 Total = 0;
|
||||||
|
for (const auto& Pair : ChunkMods)
|
||||||
|
{
|
||||||
|
Total += Pair.Value.Num();
|
||||||
|
}
|
||||||
|
return Total;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UVoxelDiffLayer::GetModifiedChunkCount() const
|
||||||
|
{
|
||||||
|
return ChunkMods.Num();
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// VoxelForgeModule.cpp
|
||||||
|
// Module implementation - boilerplate
|
||||||
|
|
||||||
|
#include "VoxelForgeModule.h"
|
||||||
|
|
||||||
|
// This macro registers our module with Unreal
|
||||||
|
// "VoxelForge" must match the module name in VoxelForge.Build.cs and .uplugin
|
||||||
|
IMPLEMENT_MODULE(FVoxelForgeModule, VoxelForge)
|
||||||
|
|
||||||
|
void FVoxelForgeModule::StartupModule()
|
||||||
|
{
|
||||||
|
// Called when plugin loads
|
||||||
|
// We don't need to do anything here for now
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("VoxelForge module started!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void FVoxelForgeModule::ShutdownModule()
|
||||||
|
{
|
||||||
|
// Called when plugin unloads
|
||||||
|
// Cleanup would go here if we had any global resources
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("VoxelForge module shutdown."));
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,252 @@
|
|||||||
|
// VoxelMarchingCubesMesher.cpp
|
||||||
|
// Implémentation du marching cubes density-only.
|
||||||
|
|
||||||
|
#include "VoxelMarchingCubesMesher.h"
|
||||||
|
#include "MarchingCubesTables.h"
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// DENSITY SAMPLING
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
float UVoxelMarchingCubesMesher::GetDensity(const FVoxelChunk& Chunk, int32 X, int32 Y, int32 Z) const
|
||||||
|
{
|
||||||
|
// On n'utilise plus de stockage de blocs — densité demandée directement
|
||||||
|
// au générateur, qui produit la valeur pour TOUTE coordonnée monde.
|
||||||
|
// Si le générateur manque, le chunk est considéré tout-air (IsoLevel par défaut = 0).
|
||||||
|
if (!Generator) return 0.0f;
|
||||||
|
|
||||||
|
const float WorldX = Chunk.ChunkCoord.X * CHUNK_SIZE + X;
|
||||||
|
const float WorldY = Chunk.ChunkCoord.Y * CHUNK_SIZE + Y;
|
||||||
|
const float WorldZ = Chunk.ChunkCoord.Z * CHUNK_SIZE + Z;
|
||||||
|
return Generator->GetDensityAt(WorldX, WorldY, WorldZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// EDGE INTERPOLATION
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
FVector UVoxelMarchingCubesMesher::InterpolateEdge(
|
||||||
|
const FVector& P1, const FVector& P2,
|
||||||
|
float D1, float D2) const
|
||||||
|
{
|
||||||
|
// Densités quasi-égales → on prend le milieu (évite division par ~0).
|
||||||
|
if (FMath::Abs(D2 - D1) < KINDA_SMALL_NUMBER)
|
||||||
|
{
|
||||||
|
return (P1 + P2) * 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// t = 0 → surface en P1; t = 1 → surface en P2.
|
||||||
|
float T = (IsoLevel - D1) / (D2 - D1);
|
||||||
|
T = FMath::Clamp(T, 0.0f, 1.0f);
|
||||||
|
return P1 + T * (P2 - P1);
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// NORMAL (gradient central de densité)
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
FVector UVoxelMarchingCubesMesher::ComputeGradientNormal(float WorldX, float WorldY, float WorldZ) const
|
||||||
|
{
|
||||||
|
// Convention: densité négative = solide, positive = air.
|
||||||
|
// Le gradient pointe solide→air = vers l'extérieur de la surface.
|
||||||
|
// Pas de négation à faire.
|
||||||
|
const float Dx = Generator->GetDensityAt(WorldX + GradientOffset, WorldY, WorldZ)
|
||||||
|
- Generator->GetDensityAt(WorldX - GradientOffset, WorldY, WorldZ);
|
||||||
|
const float Dy = Generator->GetDensityAt(WorldX, WorldY + GradientOffset, WorldZ)
|
||||||
|
- Generator->GetDensityAt(WorldX, WorldY - GradientOffset, WorldZ);
|
||||||
|
const float Dz = Generator->GetDensityAt(WorldX, WorldY, WorldZ + GradientOffset)
|
||||||
|
- Generator->GetDensityAt(WorldX, WorldY, WorldZ - GradientOffset);
|
||||||
|
|
||||||
|
FVector Normal(Dx, Dy, Dz);
|
||||||
|
Normal.Normalize();
|
||||||
|
|
||||||
|
// Fallback si le gradient est dégénéré (zone plate).
|
||||||
|
if (Normal.IsNearlyZero())
|
||||||
|
{
|
||||||
|
Normal = FVector(0.0f, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
return Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// MAIN ALGORITHM
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(const FVoxelChunk& Chunk, int32 Step)
|
||||||
|
{
|
||||||
|
FVoxelMeshData MeshData;
|
||||||
|
|
||||||
|
// Step valide = puissance de 2 dans [1, 4]
|
||||||
|
Step = FMath::Clamp(Step, 1, 4);
|
||||||
|
|
||||||
|
const FVector ChunkWorldPos = Chunk.GetWorldPosition();
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// VERTEX DEDUPLICATION MAP
|
||||||
|
//=========================================================================
|
||||||
|
// Clé = position quantifiée au 0.01 unité (FIntVector).
|
||||||
|
// Valeur = index dans MeshData.Vertices.
|
||||||
|
// Les vertices partagés permettent des normales lisses et réduisent le count ~3x.
|
||||||
|
TMap<FIntVector, int32> VertexMap;
|
||||||
|
|
||||||
|
auto GetOrCreateVertex = [&](const FVector& WorldPos) -> int32
|
||||||
|
{
|
||||||
|
const FIntVector Key(
|
||||||
|
FMath::RoundToInt(WorldPos.X * 100.0f),
|
||||||
|
FMath::RoundToInt(WorldPos.Y * 100.0f),
|
||||||
|
FMath::RoundToInt(WorldPos.Z * 100.0f)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (int32* Existing = VertexMap.Find(Key))
|
||||||
|
{
|
||||||
|
return *Existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 NewIndex = MeshData.Vertices.Num();
|
||||||
|
VertexMap.Add(Key, NewIndex);
|
||||||
|
|
||||||
|
MeshData.Vertices.Add(WorldPos);
|
||||||
|
|
||||||
|
// Normale par gradient de densité (shading lisse).
|
||||||
|
if (Generator)
|
||||||
|
{
|
||||||
|
const float VoxelX = WorldPos.X / VOXEL_SIZE;
|
||||||
|
const float VoxelY = WorldPos.Y / VOXEL_SIZE;
|
||||||
|
const float VoxelZ = WorldPos.Z / VOXEL_SIZE;
|
||||||
|
MeshData.Normals.Add(ComputeGradientNormal(VoxelX, VoxelY, VoxelZ));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MeshData.Normals.Add(FVector(0.0f, 0.0f, 1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// UVs planaires — le triplanar mapping se fait dans le matériau.
|
||||||
|
MeshData.UVs.Add(FVector2D(WorldPos.X / VOXEL_SIZE, WorldPos.Y / VOXEL_SIZE));
|
||||||
|
|
||||||
|
return NewIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// CORNER + EDGE TABLES
|
||||||
|
//=========================================================================
|
||||||
|
// 4-------5
|
||||||
|
// /| /|
|
||||||
|
// / | / |
|
||||||
|
// 7-------6 |
|
||||||
|
// | 0----|--1
|
||||||
|
// | / | /
|
||||||
|
// |/ |/
|
||||||
|
// 3-------2
|
||||||
|
static const FIntVector CornerOffsets[8] = {
|
||||||
|
{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0},
|
||||||
|
{0, 0, 1}, {1, 0, 1}, {1, 1, 1}, {0, 1, 1},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const int32 EdgeCorners[12][2] = {
|
||||||
|
{0, 1}, {1, 2}, {2, 3}, {3, 0}, // Bas (0-3)
|
||||||
|
{4, 5}, {5, 6}, {6, 7}, {7, 4}, // Haut (4-7)
|
||||||
|
{0, 4}, {1, 5}, {2, 6}, {3, 7}, // Verticales (8-11)
|
||||||
|
};
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// PRÉ-CALCUL DE LA GRILLE DE DENSITÉ
|
||||||
|
//=========================================================================
|
||||||
|
// Les cellules adjacentes partagent leurs coins : en échantillonnant par
|
||||||
|
// cellule (8 coins) on appelle GetDensityAt ~8× de trop pour chaque point.
|
||||||
|
// On échantillonne donc chaque point de grille UNE SEULE FOIS dans un tableau
|
||||||
|
// plat, puis le balayage des cellules y lit ses coins. GetDensityAt est une
|
||||||
|
// fonction pure de la coordonnée monde, donc le maillage est identique au bit
|
||||||
|
// près — c'est juste ~7× moins d'appels au LOD0 (33³ au lieu de 32³×8).
|
||||||
|
//
|
||||||
|
// CellsPerAxis = nombre de cellules par axe ; GridDim = points de grille (+1).
|
||||||
|
// Le point de grille (gx,gy,gz) correspond au voxel monde (gx,gy,gz)*Step.
|
||||||
|
// Ordre de remplissage z→y→x : garde le cache SDF (search-box) bien chaud.
|
||||||
|
const int32 CellsPerAxis = CHUNK_SIZE / Step;
|
||||||
|
const int32 GridDim = CellsPerAxis + 1;
|
||||||
|
|
||||||
|
TArray<float> DensityGrid;
|
||||||
|
DensityGrid.SetNumUninitialized(GridDim * GridDim * GridDim);
|
||||||
|
for (int32 gz = 0; gz < GridDim; gz++)
|
||||||
|
{
|
||||||
|
for (int32 gy = 0; gy < GridDim; gy++)
|
||||||
|
{
|
||||||
|
for (int32 gx = 0; gx < GridDim; gx++)
|
||||||
|
{
|
||||||
|
DensityGrid[(gz * GridDim + gy) * GridDim + gx] =
|
||||||
|
GetDensity(Chunk, gx * Step, gy * Step, gz * Step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// ITÉRATION SUR LES CELLULES
|
||||||
|
//=========================================================================
|
||||||
|
// On itère sur les CELLULES (indices de grille), pas sur les voxels monde.
|
||||||
|
// Coin i de la cellule = point de grille (cx+ox, cy+oy, cz+oz) ; coord voxel
|
||||||
|
// monde = ce point × Step. LOD0 Step=1 → full res ; LOD1 Step=2 → ~4× moins.
|
||||||
|
for (int32 cz = 0; cz < CellsPerAxis; cz++)
|
||||||
|
{
|
||||||
|
for (int32 cy = 0; cy < CellsPerAxis; cy++)
|
||||||
|
{
|
||||||
|
for (int32 cx = 0; cx < CellsPerAxis; cx++)
|
||||||
|
{
|
||||||
|
// Densités + positions aux 8 coins (lues dans la grille pré-calculée)
|
||||||
|
float Densities[8];
|
||||||
|
FVector Positions[8];
|
||||||
|
|
||||||
|
for (int32 i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
const int32 GX = cx + CornerOffsets[i].X;
|
||||||
|
const int32 GY = cy + CornerOffsets[i].Y;
|
||||||
|
const int32 GZ = cz + CornerOffsets[i].Z;
|
||||||
|
|
||||||
|
Densities[i] = DensityGrid[(GZ * GridDim + GY) * GridDim + GX];
|
||||||
|
Positions[i] = ChunkWorldPos
|
||||||
|
+ FVector(GX * Step, GY * Step, GZ * Step) * VOXEL_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index de cas MC (8 bits, un par coin)
|
||||||
|
int32 CaseIndex = 0;
|
||||||
|
for (int32 i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
if (Densities[i] >= IsoLevel)
|
||||||
|
{
|
||||||
|
CaseIndex |= (1 << i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MCEdgeTable[CaseIndex] == 0) continue; // Pas de surface ici
|
||||||
|
|
||||||
|
// Interpolation des positions sur les arêtes traversées
|
||||||
|
FVector EdgeVertices[12];
|
||||||
|
for (int32 i = 0; i < 12; i++)
|
||||||
|
{
|
||||||
|
if (MCEdgeTable[CaseIndex] & (1 << i))
|
||||||
|
{
|
||||||
|
const int32 A = EdgeCorners[i][0];
|
||||||
|
const int32 B = EdgeCorners[i][1];
|
||||||
|
EdgeVertices[i] = InterpolateEdge(
|
||||||
|
Positions[A], Positions[B],
|
||||||
|
Densities[A], Densities[B]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Génère les triangles avec vertices dédupliqués.
|
||||||
|
// Ordre 0, 2, 1 (pas 0, 1, 2) pour le winding attendu par RealtimeMesh.
|
||||||
|
for (int32 i = 0; MCTriTable[CaseIndex][i] != -1; i += 3)
|
||||||
|
{
|
||||||
|
const int32 Idx0 = GetOrCreateVertex(EdgeVertices[MCTriTable[CaseIndex][i]]);
|
||||||
|
const int32 Idx1 = GetOrCreateVertex(EdgeVertices[MCTriTable[CaseIndex][i + 1]]);
|
||||||
|
const int32 Idx2 = GetOrCreateVertex(EdgeVertices[MCTriTable[CaseIndex][i + 2]]);
|
||||||
|
|
||||||
|
MeshData.Triangles.Add(Idx0);
|
||||||
|
MeshData.Triangles.Add(Idx2);
|
||||||
|
MeshData.Triangles.Add(Idx1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MeshData;
|
||||||
|
}
|
||||||
@@ -0,0 +1,852 @@
|
|||||||
|
// VoxelStrateManager.cpp
|
||||||
|
// Runtime strate layout generation and queries.
|
||||||
|
|
||||||
|
#include "VoxelStrateManager.h"
|
||||||
|
#include "VoxelSettings.h"
|
||||||
|
#include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord
|
||||||
|
#include "VoxelCaveMorphology.h" // For VoxelSDF and VoxelHash
|
||||||
|
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
||||||
|
|
||||||
|
// Fractal Brownian Motion (layered Perlin) along a 1D parameter, ~[-1,1].
|
||||||
|
// Independent octaves at increasing frequency / decreasing amplitude give an organic,
|
||||||
|
// non-repeating wander — the key to a worm that SQUIRMS instead of zig-zagging (1D) or
|
||||||
|
// orbiting (single-octave 2-channel = a spiral). Each axis samples this with its own seed.
|
||||||
|
static float PassageFBM(float X, float Seed)
|
||||||
|
{
|
||||||
|
float Total = 0.0f, Amp = 1.0f, Freq = 1.0f, MaxV = 0.0f;
|
||||||
|
for (int32 O = 0; O < 4; ++O)
|
||||||
|
{
|
||||||
|
Total += FMath::PerlinNoise3D(FVector(X * Freq + Seed, Seed * 1.7f + O * 13.0f, O * 5.0f)) * Amp;
|
||||||
|
MaxV += Amp;
|
||||||
|
Amp *= 0.5f;
|
||||||
|
Freq *= 2.0f;
|
||||||
|
}
|
||||||
|
return (MaxV > 0.0f) ? (Total / MaxV) : 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||||
|
{
|
||||||
|
if (!Settings)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Error, TEXT("[StrateManager] No settings provided!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StrateLayout.Empty();
|
||||||
|
|
||||||
|
const int32 TotalStrates = Settings->TotalStrates;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// STEP 1: Build shuffled pool (seed-based randomization)
|
||||||
|
//=========================================================================
|
||||||
|
// Copy the pool and shuffle it deterministically using the world seed.
|
||||||
|
// Fixed strates are excluded from the shuffle — they always use their
|
||||||
|
// assigned definition regardless of seed.
|
||||||
|
|
||||||
|
TArray<UVoxelStrateDefinition*> ShuffledPool;
|
||||||
|
for (const TSoftObjectPtr<UVoxelStrateDefinition>& SoftPtr : Settings->StratePool)
|
||||||
|
{
|
||||||
|
// Load the asset (synchronous for now — could be async later)
|
||||||
|
UVoxelStrateDefinition* Def = SoftPtr.LoadSynchronous();
|
||||||
|
if (Def)
|
||||||
|
{
|
||||||
|
ShuffledPool.Add(Def);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed-based shuffle using Fisher-Yates
|
||||||
|
// FRandomStream gives us deterministic random numbers from a seed
|
||||||
|
FRandomStream Rng(WorldSeed);
|
||||||
|
for (int32 i = ShuffledPool.Num() - 1; i > 0; i--)
|
||||||
|
{
|
||||||
|
int32 j = Rng.RandRange(0, i);
|
||||||
|
ShuffledPool.Swap(i, j);
|
||||||
|
}
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// STEP 2: Assign definitions to each strate slot
|
||||||
|
//=========================================================================
|
||||||
|
// Walk through strate indices 0..TotalStrates-1.
|
||||||
|
// Fixed strates use their pinned definition.
|
||||||
|
// Random strates cycle through the shuffled pool.
|
||||||
|
|
||||||
|
int32 PoolCursor = 0; // Current position in the shuffled pool
|
||||||
|
|
||||||
|
// Pre-load fixed strate definitions
|
||||||
|
TMap<int32, UVoxelStrateDefinition*> LoadedFixed;
|
||||||
|
for (auto& Pair : Settings->FixedStrates)
|
||||||
|
{
|
||||||
|
UVoxelStrateDefinition* Def = Pair.Value.LoadSynchronous();
|
||||||
|
if (Def)
|
||||||
|
{
|
||||||
|
LoadedFixed.Add(Pair.Key, Def);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current Z position (in chunks). Starts at 0 and goes downward (negative).
|
||||||
|
int32 CurrentTopZ = 0;
|
||||||
|
|
||||||
|
for (int32 i = 0; i < TotalStrates; i++)
|
||||||
|
{
|
||||||
|
FStrateSlot Slot;
|
||||||
|
Slot.StrateIndex = i;
|
||||||
|
|
||||||
|
// Pick definition: fixed or from pool
|
||||||
|
UVoxelStrateDefinition** FixedDef = LoadedFixed.Find(i);
|
||||||
|
if (FixedDef && *FixedDef)
|
||||||
|
{
|
||||||
|
Slot.Definition = *FixedDef;
|
||||||
|
}
|
||||||
|
else if (ShuffledPool.Num() > 0)
|
||||||
|
{
|
||||||
|
// Cycle through the pool (wraps around if more strates than pool entries)
|
||||||
|
Slot.Definition = ShuffledPool[PoolCursor % ShuffledPool.Num()];
|
||||||
|
PoolCursor++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("[StrateManager] No strate definitions available for slot %d!"), i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute Z range from definition's height
|
||||||
|
Slot.HeightInChunks = Slot.Definition->StrateHeightInChunks;
|
||||||
|
Slot.TopChunkZ = CurrentTopZ;
|
||||||
|
Slot.BottomChunkZ = CurrentTopZ - (Slot.HeightInChunks - 1);
|
||||||
|
|
||||||
|
// Move the cursor down for the next strate, leaving a solid-bedrock gap of
|
||||||
|
// InterStrateGapChunks chunks between this strate and the next.
|
||||||
|
CurrentTopZ = Slot.BottomChunkZ - 1 - FMath::Max(0, Settings->InterStrateGapChunks);
|
||||||
|
|
||||||
|
StrateLayout.Add(Slot);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("[StrateManager] Strate %d: '%s' | Z chunks [%d to %d] | %d chunks tall"),
|
||||||
|
i,
|
||||||
|
*Slot.Definition->StrateName.ToString(),
|
||||||
|
Slot.TopChunkZ,
|
||||||
|
Slot.BottomChunkZ,
|
||||||
|
Slot.HeightInChunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
CachedSeed = WorldSeed;
|
||||||
|
bOpenSurfaceEntry = Settings->bOpenSurfaceEntry;
|
||||||
|
OriginSpineRadius = Settings->OriginSpineRadius;
|
||||||
|
InterStrateGapChunks = FMath::Max(0, Settings->InterStrateGapChunks);
|
||||||
|
// Passage shape/count is per-strate now (UVoxelStrateDefinition::PassageConfig).
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// STEP 3: Load terrain operation assets
|
||||||
|
//=========================================================================
|
||||||
|
// Each strate definition references terrain ops as soft pointers.
|
||||||
|
// We load them synchronously here so they're available during generation.
|
||||||
|
// Without this, BuildParamsFromDefinition's Entry.Operation.Get() would
|
||||||
|
// return null if the assets haven't been loaded yet.
|
||||||
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
|
{
|
||||||
|
if (!Slot.Definition) continue;
|
||||||
|
|
||||||
|
for (const FStrateTerrainOpEntry& Entry : Slot.Definition->TerrainOperations)
|
||||||
|
{
|
||||||
|
if (!Entry.Operation.IsNull())
|
||||||
|
{
|
||||||
|
Entry.Operation.LoadSynchronous();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("[StrateManager] Initialized %d strates (seed=%d)"),
|
||||||
|
StrateLayout.Num(), WorldSeed);
|
||||||
|
|
||||||
|
// Generate passages between consecutive strates
|
||||||
|
GeneratePassages();
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// PASSAGE GENERATION
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
void UVoxelStrateManager::GeneratePassages()
|
||||||
|
{
|
||||||
|
Passages.Empty();
|
||||||
|
|
||||||
|
if (StrateLayout.Num() < 1) return;
|
||||||
|
|
||||||
|
// Deterministic RNG from world seed
|
||||||
|
FRandomStream Rng(CachedSeed ^ 0x50A55A6E); // XOR with "PASSAGE" hash
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// INTER-STRATE PASSAGES: tunnels connecting consecutive strates.
|
||||||
|
// Each passage is randomly assigned one of 5 types, which determines
|
||||||
|
// its shape, radius, and control point layout.
|
||||||
|
//=========================================================================
|
||||||
|
for (int32 i = 0; i < StrateLayout.Num() - 1; i++)
|
||||||
|
{
|
||||||
|
const FStrateSlot& Upper = StrateLayout[i];
|
||||||
|
const FStrateSlot& Lower = StrateLayout[i + 1];
|
||||||
|
|
||||||
|
// This (upper) strate's PassageConfig controls the descent tunnels to the layer
|
||||||
|
// below. The (0,0) spine descent is separate (player-dug); these are the shortcuts.
|
||||||
|
const UVoxelStrateDefinition* UpperDef = Upper.Definition;
|
||||||
|
if (!UpperDef) continue;
|
||||||
|
const FStratePassageConfig& Cfg = UpperDef->PassageConfig;
|
||||||
|
|
||||||
|
// Upper strate floor and lower strate ceiling (differ when there's a bedrock gap).
|
||||||
|
const float UpperBottomZ = (float)(Upper.BottomChunkZ) * CHUNK_SIZE;
|
||||||
|
const float LowerTopZ = (float)(Lower.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
const float UpperMax = (float)Upper.HeightInChunks * CHUNK_SIZE * 0.9f;
|
||||||
|
const float LowerMax = (float)Lower.HeightInChunks * CHUNK_SIZE * 0.9f;
|
||||||
|
|
||||||
|
const float DistLo = FMath::Min(Cfg.DistanceMin, Cfg.DistanceMax);
|
||||||
|
const float DistHi = FMath::Max(Cfg.DistanceMin, Cfg.DistanceMax);
|
||||||
|
|
||||||
|
const int32 Conns = FMath::Max(0, Cfg.Connections);
|
||||||
|
for (int32 c = 0; c < Conns; c++)
|
||||||
|
{
|
||||||
|
FVoxelPassage Passage;
|
||||||
|
Passage.UpperStrateIndex = i;
|
||||||
|
Passage.LowerStrateIndex = i + 1;
|
||||||
|
|
||||||
|
// PLACEMENT: random angle, distance from the (0,0) spine within config range.
|
||||||
|
const float Angle = Rng.FRandRange(0.0f, 2.0f * PI);
|
||||||
|
const float Distance = Rng.FRandRange(DistLo, DistHi);
|
||||||
|
const float PX = FMath::Cos(Angle) * Distance;
|
||||||
|
const float PY = FMath::Sin(Angle) * Distance;
|
||||||
|
|
||||||
|
// LENGTH: reach into each strate, capped to the interior.
|
||||||
|
const float UpperReach = FMath::Min(Rng.FRandRange(Cfg.ReachMin, Cfg.ReachMax), UpperMax);
|
||||||
|
const float LowerReach = FMath::Min(Rng.FRandRange(Cfg.ReachMin, Cfg.ReachMax), LowerMax);
|
||||||
|
const float TopZ = UpperBottomZ + UpperReach;
|
||||||
|
const float BottomZ = LowerTopZ - LowerReach;
|
||||||
|
|
||||||
|
const int32 Segments = FMath::Clamp(Cfg.Segments, 1, 48);
|
||||||
|
Passage.ControlPoints.Reset();
|
||||||
|
Passage.ControlRadii.Reset();
|
||||||
|
Passage.ControlPoints.Reserve(Segments + 1);
|
||||||
|
Passage.ControlRadii.Reserve(Segments + 1);
|
||||||
|
|
||||||
|
// WIDTH profile: mouth radius at the ends, mid radius in the centre (taper/bulge).
|
||||||
|
auto RadiusAt = [&](float t) { return FMath::Lerp(Cfg.MouthRadius, Cfg.MidRadius, FMath::Sin(t * PI)); };
|
||||||
|
|
||||||
|
// Per-passage shape seeds.
|
||||||
|
const float WormFreq = Rng.FRandRange(0.8f, 1.8f); // (vertical wobble only)
|
||||||
|
const float NSeedX = Rng.FRandRange(0.0f, 500.0f);
|
||||||
|
const float NSeedY = Rng.FRandRange(0.0f, 500.0f);
|
||||||
|
const float NSeedZ = Rng.FRandRange(0.0f, 500.0f);
|
||||||
|
const float PhaseA = Rng.FRandRange(0.0f, 2.0f * PI);
|
||||||
|
// Base fBM frequency for the worm's wander (octaves add finer detail on top).
|
||||||
|
const float BendFreq = Rng.FRandRange(1.5f, 2.5f);
|
||||||
|
|
||||||
|
for (int32 s = 0; s <= Segments; s++)
|
||||||
|
{
|
||||||
|
const float T = (float)s / (float)Segments;
|
||||||
|
float Z = FMath::Lerp(TopZ, BottomZ, T);
|
||||||
|
const float Env = FMath::Sin(T * PI); // 0 at both ends → mouths stay anchored
|
||||||
|
float OX = 0.0f, OY = 0.0f;
|
||||||
|
|
||||||
|
switch (Cfg.Style)
|
||||||
|
{
|
||||||
|
case EVoxelPassageStyle::Straight:
|
||||||
|
break; // pure vertical
|
||||||
|
|
||||||
|
case EVoxelPassageStyle::Spiral:
|
||||||
|
{
|
||||||
|
const float Ang = PhaseA + T * Cfg.SpiralTurns * 2.0f * PI;
|
||||||
|
OX = FMath::Cos(Ang) * Cfg.SpiralRadius * Env;
|
||||||
|
OY = FMath::Sin(Ang) * Cfg.SpiralRadius * Env;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EVoxelPassageStyle::Cascading:
|
||||||
|
{
|
||||||
|
// Switchback staircase: each tread offsets in a new deterministic direction.
|
||||||
|
const int32 Steps = FMath::Clamp(Cfg.CascadeSteps, 1, 16);
|
||||||
|
const int32 Idx = FMath::Min((int32)(T * Steps), Steps - 1);
|
||||||
|
const float SA = PhaseA + (float)Idx * 2.39996f; // golden-angle spread
|
||||||
|
OX = FMath::Cos(SA) * Cfg.CascadeLedge * Env;
|
||||||
|
OY = FMath::Sin(SA) * Cfg.CascadeLedge * Env;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EVoxelPassageStyle::Worm:
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
// SQUIRM: displace the descent independently on X and Y with multi-octave
|
||||||
|
// fBM (different seeds → uncorrelated). Independent fBM per axis is a true
|
||||||
|
// 2D organic wander — it curls and meanders "here and there" rather than
|
||||||
|
// oscillating along one line (zig-zag) or orbiting the axis (spiral).
|
||||||
|
// Flat-top envelope keeps full motion along the length but anchors the mouths.
|
||||||
|
const float WormEnv = FMath::Clamp(FMath::Sin(T * PI) * 3.0f, 0.0f, 1.0f);
|
||||||
|
OX = PassageFBM(T * BendFreq, NSeedX) * VOXEL_NOISE_SCALE * Cfg.Wander * WormEnv;
|
||||||
|
OY = PassageFBM(T * BendFreq, NSeedY + 53.0f) * VOXEL_NOISE_SCALE * Cfg.Wander * WormEnv;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical wobble (all styles): dips/rises along the descent, anchored at ends.
|
||||||
|
if (Cfg.VerticalWobble > 0.0f)
|
||||||
|
{
|
||||||
|
const float NZ = FMath::PerlinNoise3D(FVector(T * WormFreq * 1.3f + NSeedZ, NSeedZ * 0.5f, 27.0f));
|
||||||
|
Z += NZ * VOXEL_NOISE_SCALE * Cfg.VerticalWobble * Env;
|
||||||
|
}
|
||||||
|
|
||||||
|
Passage.ControlPoints.Add(FVector(PX + OX, PY + OY, Z));
|
||||||
|
Passage.ControlRadii.Add(RadiusAt(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
Passage.bHasMidPoint = false;
|
||||||
|
Passage.UpperPoint = Passage.ControlPoints[0];
|
||||||
|
Passage.LowerPoint = Passage.ControlPoints.Last();
|
||||||
|
Passage.Radius = FMath::Max(Cfg.MouthRadius, Cfg.MidRadius); // fallback / bounds
|
||||||
|
|
||||||
|
// Bounding sphere over all control points (+ widest radius + blend) for culling.
|
||||||
|
{
|
||||||
|
FVector Center = FVector::ZeroVector;
|
||||||
|
for (const FVector& CP : Passage.ControlPoints) Center += CP;
|
||||||
|
Center /= (float)Passage.ControlPoints.Num();
|
||||||
|
float MaxDistSq = 0.0f;
|
||||||
|
for (const FVector& CP : Passage.ControlPoints)
|
||||||
|
MaxDistSq = FMath::Max(MaxDistSq, (float)FVector::DistSquared(Center, CP));
|
||||||
|
const float R = FMath::Sqrt(MaxDistSq) + Passage.Radius + 4.0f;
|
||||||
|
Passage.BoundCenter = Center;
|
||||||
|
Passage.BoundRadiusSq = R * R;
|
||||||
|
}
|
||||||
|
|
||||||
|
Passages.Add(Passage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// SURFACE ENTRY SHAFT — the one auto-opened (0,0) connection.
|
||||||
|
// A straight vertical shaft at (0,0) piercing the TOP seal of the topmost
|
||||||
|
// strate, so the world begins with "a hole opened to the surface". All other
|
||||||
|
// (0,0) descents between strates remain player-dug.
|
||||||
|
//=========================================================================
|
||||||
|
if (bOpenSurfaceEntry && StrateLayout.Num() > 0)
|
||||||
|
{
|
||||||
|
const FStrateSlot& Top = StrateLayout[0];
|
||||||
|
const float TopZ = (float)(Top.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
|
||||||
|
FVoxelPassage Entry;
|
||||||
|
Entry.UpperStrateIndex = 0;
|
||||||
|
Entry.LowerStrateIndex = 0;
|
||||||
|
Entry.PassageType = EVoxelPassageType::VerticalShaft;
|
||||||
|
Entry.Radius = FMath::Max(OriginSpineRadius * 0.7f, 4.0f);
|
||||||
|
// From a little above the strate top (open air outside all strates) down
|
||||||
|
// past the seal into the interior, so the seal at (0,0) is breached.
|
||||||
|
Entry.UpperPoint = FVector(0.0f, 0.0f, TopZ + CHUNK_SIZE);
|
||||||
|
Entry.LowerPoint = FVector(0.0f, 0.0f, TopZ - CHUNK_SIZE);
|
||||||
|
Entry.bHasMidPoint = false;
|
||||||
|
{
|
||||||
|
const FVector C = (Entry.UpperPoint + Entry.LowerPoint) * 0.5f;
|
||||||
|
const float R = (float)FVector::Dist(C, Entry.UpperPoint) + Entry.Radius + 4.0f;
|
||||||
|
Entry.BoundCenter = C;
|
||||||
|
Entry.BoundRadiusSq = R * R;
|
||||||
|
}
|
||||||
|
Passages.Add(Entry);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("[StrateManager] Surface entry shaft at (0,0) topZ=%.0f R=%.1f"),
|
||||||
|
TopZ, Entry.Radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// MODIFIER SDF (inter-strate passages)
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float WorldZ) const
|
||||||
|
{
|
||||||
|
float MinSDF = FLT_MAX;
|
||||||
|
const float BlendK = 3.0f; // Smooth blend for passage junctions
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// PASSAGES — tapered capsule chains between strates (per-strate PassageConfig).
|
||||||
|
// Each passage is a control-point chain with per-point radii; the (0,0) surface
|
||||||
|
// entry is a simple straight tube. A bounding-sphere reject skips far passages.
|
||||||
|
//=========================================================================
|
||||||
|
const FVector Pos(WorldX, WorldY, WorldZ);
|
||||||
|
for (const FVoxelPassage& P : Passages)
|
||||||
|
{
|
||||||
|
// BOUNDING-SPHERE REJECT: skip passages this voxel can't possibly be inside.
|
||||||
|
// EvaluateModifierSDF runs PER VOXEL and used to evaluate every passage's full
|
||||||
|
// capsule chain unconditionally — the dominant lag source once passages became
|
||||||
|
// 12-segment worms. Now far passages cost a single squared-distance compare.
|
||||||
|
if (FVector::DistSquared(Pos, P.BoundCenter) > P.BoundRadiusSq) continue;
|
||||||
|
|
||||||
|
if (P.ControlPoints.Num() >= 2)
|
||||||
|
{
|
||||||
|
// Tapered capsule chain along the control points. ControlRadii (if present)
|
||||||
|
// gives the per-point width so the tunnel can flare at the mouths and pinch
|
||||||
|
// in the middle; otherwise the uniform Radius is used.
|
||||||
|
const bool bTaper = (P.ControlRadii.Num() == P.ControlPoints.Num());
|
||||||
|
float PassageSDF = FLT_MAX;
|
||||||
|
for (int32 j = 0; j < P.ControlPoints.Num() - 1; j++)
|
||||||
|
{
|
||||||
|
const float rA = bTaper ? P.ControlRadii[j] : P.Radius;
|
||||||
|
const float rB = bTaper ? P.ControlRadii[j + 1] : P.Radius;
|
||||||
|
const float SegSDF = VoxelSDF::TaperedCapsule(
|
||||||
|
Pos, P.ControlPoints[j], P.ControlPoints[j + 1], rA, rB);
|
||||||
|
PassageSDF = VoxelSDF::SmoothMin(PassageSDF, SegSDF, BlendK);
|
||||||
|
}
|
||||||
|
MinSDF = VoxelSDF::SmoothMin(MinSDF, PassageSDF, BlendK);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback: straight uniform tube Upper→Lower (e.g. the (0,0) surface entry).
|
||||||
|
const float PassageSDF = VoxelSDF::Capsule(Pos, P.UpperPoint, P.LowerPoint, P.Radius);
|
||||||
|
MinSDF = VoxelSDF::SmoothMin(MinSDF, PassageSDF, BlendK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MinSDF;
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// QUERIES
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
int32 UVoxelStrateManager::FindSlotIndexForChunkZ(int32 ChunkZ) const
|
||||||
|
{
|
||||||
|
// Linear search through strate layout.
|
||||||
|
// With ~10-20 strates this is fine. If we ever have hundreds,
|
||||||
|
// switch to binary search (layout is sorted by Z).
|
||||||
|
for (int32 i = 0; i < StrateLayout.Num(); i++)
|
||||||
|
{
|
||||||
|
const FStrateSlot& Slot = StrateLayout[i];
|
||||||
|
if (ChunkZ <= Slot.TopChunkZ && ChunkZ >= Slot.BottomChunkZ)
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
UVoxelStrateDefinition* UVoxelStrateManager::GetStrateAt(float WorldZ) const
|
||||||
|
{
|
||||||
|
// Convert world Z to chunk Z coordinate
|
||||||
|
int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / CHUNK_SIZE);
|
||||||
|
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
||||||
|
if (SlotIdx >= 0)
|
||||||
|
{
|
||||||
|
return StrateLayout[SlotIdx].Definition;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UVoxelStrateManager::GetStrateIndex(float WorldZ) const
|
||||||
|
{
|
||||||
|
int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / CHUNK_SIZE);
|
||||||
|
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
||||||
|
if (SlotIdx >= 0)
|
||||||
|
{
|
||||||
|
return StrateLayout[SlotIdx].StrateIndex;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
UVoxelStrateDefinition* UVoxelStrateManager::GetStrateForChunk(const FIntVector& ChunkCoord) const
|
||||||
|
{
|
||||||
|
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||||
|
if (SlotIdx >= 0)
|
||||||
|
{
|
||||||
|
return StrateLayout[SlotIdx].Definition;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ECaveGeneratorType UVoxelStrateManager::GetGeneratorTypeForChunk(const FIntVector& ChunkCoord) const
|
||||||
|
{
|
||||||
|
// Look up which slot this chunk falls into.
|
||||||
|
// If outside all strates (above or below), default to TunnelNetwork —
|
||||||
|
// the fallback density path will produce solid rock anyway.
|
||||||
|
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||||
|
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition)
|
||||||
|
{
|
||||||
|
return ECaveGeneratorType::TunnelNetwork;
|
||||||
|
}
|
||||||
|
|
||||||
|
return StrateLayout[SlotIdx].Definition->GeneratorType;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UVoxelStrateManager::IsGapChunk(const FIntVector& ChunkCoord) const
|
||||||
|
{
|
||||||
|
if (StrateLayout.Num() == 0) return false;
|
||||||
|
|
||||||
|
// Above the top strate or below the bottom strate = open air, NOT a gap.
|
||||||
|
const int32 StackTop = StrateLayout[0].TopChunkZ;
|
||||||
|
const int32 StackBottom = StrateLayout.Last().BottomChunkZ;
|
||||||
|
if (ChunkCoord.Z > StackTop || ChunkCoord.Z < StackBottom) return false;
|
||||||
|
|
||||||
|
// Inside the stack's Z span but not in any strate slot → it's a bedrock gap.
|
||||||
|
return FindSlotIndexForChunkZ(ChunkCoord.Z) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
FSlabGenerationParams UVoxelStrateManager::GetSlabParamsForChunk(const FIntVector& ChunkCoord) const
|
||||||
|
{
|
||||||
|
// Fallback: empty params with BaseDensity < 0 → all-air outside strate range.
|
||||||
|
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||||
|
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition)
|
||||||
|
{
|
||||||
|
FSlabGenerationParams Empty;
|
||||||
|
Empty.BaseDensity = -1.0f;
|
||||||
|
return Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
|
|
||||||
|
// Copy the designer-authored slab params from the strate definition.
|
||||||
|
FSlabGenerationParams Result = Slot.Definition->SlabParams;
|
||||||
|
|
||||||
|
// Fill in the runtime Z bounds (voxel coordinates, same convention as
|
||||||
|
// FStrateGenerationParams::StrateTopWorldZ / StrateBottomWorldZ).
|
||||||
|
// TopChunkZ+1 because the top chunk's CEILING is at (TopChunkZ+1)*CHUNK_SIZE.
|
||||||
|
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
|
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// PER-ARCHETYPE PARAM GETTERS
|
||||||
|
//=============================================================================
|
||||||
|
// Each mirrors GetSlabParamsForChunk: copy designer params, fill runtime Z bounds.
|
||||||
|
// No cross-boundary blending — archetypes meet at Hard boundaries. A macro keeps
|
||||||
|
// the boilerplate (slot lookup + fallback + Z bounds) in one place.
|
||||||
|
|
||||||
|
#define VF_ARCHETYPE_PARAMS_GETTER(FnName, StructType, DefMember) \
|
||||||
|
StructType UVoxelStrateManager::FnName(const FIntVector& ChunkCoord) const \
|
||||||
|
{ \
|
||||||
|
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z); \
|
||||||
|
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition) \
|
||||||
|
{ \
|
||||||
|
StructType Empty; \
|
||||||
|
Empty.BaseDensity = -1.0f; \
|
||||||
|
return Empty; \
|
||||||
|
} \
|
||||||
|
const FStrateSlot& Slot = StrateLayout[SlotIdx]; \
|
||||||
|
StructType Result = Slot.Definition->DefMember; \
|
||||||
|
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE; \
|
||||||
|
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE; \
|
||||||
|
return Result; \
|
||||||
|
}
|
||||||
|
|
||||||
|
VF_ARCHETYPE_PARAMS_GETTER(GetMazeParamsForChunk, FMazeGenerationParams, MazeParams)
|
||||||
|
VF_ARCHETYPE_PARAMS_GETTER(GetSurfaceParamsForChunk, FSurfaceGenerationParams, SurfaceParams)
|
||||||
|
VF_ARCHETYPE_PARAMS_GETTER(GetVerticalShaftParamsForChunk, FVerticalShaftParams, VerticalShaftParams)
|
||||||
|
VF_ARCHETYPE_PARAMS_GETTER(GetFloatingIslandParamsForChunk, FFloatingIslandParams, FloatingIslandParams)
|
||||||
|
|
||||||
|
#undef VF_ARCHETYPE_PARAMS_GETTER
|
||||||
|
|
||||||
|
bool UVoxelStrateManager::GetStrateUnrealZRange(float WorldZ, float& OutTopZ, float& OutBottomZ) const
|
||||||
|
{
|
||||||
|
const int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / CHUNK_SIZE);
|
||||||
|
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
||||||
|
if (SlotIdx < 0) return false;
|
||||||
|
|
||||||
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
|
// Voxel-space Z bounds → Unreal units. Ceiling = top chunk's upper edge.
|
||||||
|
OutTopZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE * VOXEL_SIZE;
|
||||||
|
OutBottomZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE * VOXEL_SIZE;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FStrateDisturbanceParams UVoxelStrateManager::GetDisturbanceParamsForChunk(const FIntVector& ChunkCoord) const
|
||||||
|
{
|
||||||
|
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||||
|
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition)
|
||||||
|
{
|
||||||
|
return FStrateDisturbanceParams(); // all features disabled
|
||||||
|
}
|
||||||
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
|
FStrateDisturbanceParams Result = Slot.Definition->Disturbances;
|
||||||
|
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
float UVoxelStrateManager::GetWaterLevelWorldZForChunk(const FIntVector& ChunkCoord) const
|
||||||
|
{
|
||||||
|
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||||
|
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition) return -FLT_MAX;
|
||||||
|
|
||||||
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
|
const UVoxelStrateDefinition* Def = Slot.Definition;
|
||||||
|
if (!Def->bHasWater) return -FLT_MAX;
|
||||||
|
|
||||||
|
// Pull the relative level from whichever archetype owns water.
|
||||||
|
float Rel = 0.0f;
|
||||||
|
switch (Def->GeneratorType)
|
||||||
|
{
|
||||||
|
case ECaveGeneratorType::SurfaceWorld: Rel = Def->SurfaceParams.WaterLevelRelative; break;
|
||||||
|
case ECaveGeneratorType::Underwater: Rel = Def->GenerationParams.WaterLevelRelative; break;
|
||||||
|
default: Rel = Def->GenerationParams.WaterLevelRelative; break;
|
||||||
|
}
|
||||||
|
if (Rel <= 0.0f) return -FLT_MAX;
|
||||||
|
|
||||||
|
const float BottomZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
|
const float TopZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
return FMath::Lerp(BottomZ, TopZ, FMath::Clamp(Rel, 0.0f, 1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVector& ChunkCoord) const
|
||||||
|
{
|
||||||
|
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||||
|
|
||||||
|
// If outside all strates, return negative density → guaranteed air.
|
||||||
|
// BaseDensity must be < 0 because IsoLevel is 0.0 and density >= IsoLevel = solid.
|
||||||
|
if (SlotIdx < 0)
|
||||||
|
{
|
||||||
|
FStrateGenerationParams Empty;
|
||||||
|
Empty.BaseDensity = -1.0f; // Negative → air after negation
|
||||||
|
Empty.WormStrength = 0.0f;
|
||||||
|
Empty.RoomDensity = 0.0f; // No rooms outside strates
|
||||||
|
return Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
|
FStrateGenerationParams BaseParams = BuildParamsFromDefinition(Slot.Definition);
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// SET STRATE BOUNDARY Z VALUES
|
||||||
|
//=========================================================================
|
||||||
|
// The density function needs to know the strate's Z range (in voxel coords)
|
||||||
|
// to seal the top and bottom with solid rock. This prevents caves from
|
||||||
|
// carving through strate boundaries.
|
||||||
|
//
|
||||||
|
// TopChunkZ=0, CHUNK_SIZE=32: top of the strate = chunk 0's top edge = voxel Z=32
|
||||||
|
// BottomChunkZ=-3: bottom of the strate = chunk -3's bottom edge = voxel Z=-3*32 = -96
|
||||||
|
BaseParams.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
BaseParams.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// BOUNDARY BLENDING (transition-type aware)
|
||||||
|
//=========================================================================
|
||||||
|
// If this chunk is near a strate boundary, apply the appropriate transition
|
||||||
|
// based on the UPPER strate's TransitionType setting.
|
||||||
|
//
|
||||||
|
// Three transition styles:
|
||||||
|
//
|
||||||
|
// GRADIENT (default):
|
||||||
|
// Classic linear lerp of all params across BlendChunks. Smooth,
|
||||||
|
// invisible boundary. Cave shape morphs gradually from one strate
|
||||||
|
// to the next over several chunks.
|
||||||
|
//
|
||||||
|
// HARD:
|
||||||
|
// No blending at all — params switch instantly at the boundary.
|
||||||
|
// The abrupt change in density, room size, roughness, etc. creates
|
||||||
|
// a natural cliff, ledge, or visible material discontinuity.
|
||||||
|
// BlendChunks is ignored (effectively 0).
|
||||||
|
//
|
||||||
|
// INTERLEAVED:
|
||||||
|
// 3D Perlin noise warps the effective boundary Z position per XY column.
|
||||||
|
// Some columns transition early (fingers of the lower strate reach UP),
|
||||||
|
// others late (fingers of the upper strate reach DOWN). The Z frequency
|
||||||
|
// is intentionally low so the fingers are horizontal — wide, flat
|
||||||
|
// intrusions rather than vertical spikes.
|
||||||
|
//
|
||||||
|
// WHICH STRATE'S TRANSITION TYPE IS USED:
|
||||||
|
// At the bottom boundary of strate N (between N and N+1), we use
|
||||||
|
// strate N's (the upper strate's) TransitionType. This is consistent:
|
||||||
|
// each strate definition controls what happens at its lower edge.
|
||||||
|
// At the top boundary of strate N (between N-1 and N), we use
|
||||||
|
// strate N-1's TransitionType (the strate above controls its lower edge).
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// CHECK BOTTOM BOUNDARY (transitioning to strate below)
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// DistFromBottom = how many chunks above the bottom edge of this strate.
|
||||||
|
// When 0, we're right at the boundary. When == BlendChunks, we're at
|
||||||
|
// the outer edge of the transition zone.
|
||||||
|
int32 DistFromBottom = ChunkCoord.Z - Slot.BottomChunkZ;
|
||||||
|
|
||||||
|
if (SlotIdx + 1 < StrateLayout.Num())
|
||||||
|
{
|
||||||
|
// The upper strate (this one) controls the transition type at its lower edge
|
||||||
|
const EVoxelStrateTransition TransType = Slot.Definition->TransitionType;
|
||||||
|
|
||||||
|
// Per-definition blend distance (overrides the manager's default BlendChunks)
|
||||||
|
const int32 EffectiveBlend = Slot.Definition->TransitionBlendChunks;
|
||||||
|
|
||||||
|
// Prepare the neighbor's params (only used for Gradient and Interleaved)
|
||||||
|
const FStrateSlot& BelowSlot = StrateLayout[SlotIdx + 1];
|
||||||
|
|
||||||
|
switch (TransType)
|
||||||
|
{
|
||||||
|
case EVoxelStrateTransition::Hard:
|
||||||
|
{
|
||||||
|
// HARD TRANSITION: No blending. The current strate's params apply
|
||||||
|
// all the way to the boundary with zero transition zone.
|
||||||
|
// The abrupt param change (different densities, room sizes, etc.)
|
||||||
|
// creates a natural cliff or ledge — no special density boost needed.
|
||||||
|
// We simply skip blending and fall through to the "return BaseParams" below.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EVoxelStrateTransition::Gradient:
|
||||||
|
{
|
||||||
|
// GRADIENT TRANSITION: Classic smooth lerp across the blend zone.
|
||||||
|
// Alpha goes from 0 (at the outer edge of the zone) to 1 (right at boundary).
|
||||||
|
if (DistFromBottom < EffectiveBlend)
|
||||||
|
{
|
||||||
|
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
||||||
|
BelowParams.StrateTopWorldZ = (float)(BelowSlot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
BelowParams.StrateBottomWorldZ = (float)(BelowSlot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
|
|
||||||
|
// Linear alpha: 0 at EffectiveBlend chunks away, 1 at the boundary
|
||||||
|
float Alpha = 1.0f - ((float)DistFromBottom / (float)EffectiveBlend);
|
||||||
|
Alpha = FMath::Clamp(Alpha, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
return FStrateGenerationParams::Lerp(BaseParams, BelowParams, Alpha);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EVoxelStrateTransition::Interleaved:
|
||||||
|
{
|
||||||
|
// INTERLEAVED TRANSITION: 3D noise warps the effective boundary Z.
|
||||||
|
//
|
||||||
|
// Instead of a flat boundary plane, the boundary becomes a wavy 3D surface.
|
||||||
|
// For each XY position, a Perlin noise sample offsets the boundary Z by
|
||||||
|
// up to ±2 chunks. Where the noise pushes the boundary UP, the lower strate's
|
||||||
|
// params appear earlier (its "fingers" reach into the upper strate). Where
|
||||||
|
// the noise pushes DOWN, the upper strate's params persist longer.
|
||||||
|
//
|
||||||
|
// The Z frequency is intentionally 3x lower than XY frequency so the fingers
|
||||||
|
// are horizontal slabs rather than vertical spikes — this matches how real
|
||||||
|
// geological intrusions look (wide, flat, layered).
|
||||||
|
//
|
||||||
|
// WarpAmplitude of 2.0 means the boundary can shift ±2 chunks from its
|
||||||
|
// true position. Combined with EffectiveBlend for the transition width,
|
||||||
|
// we need to check a wider zone: EffectiveBlend + WarpAmplitude.
|
||||||
|
const float WarpAmplitude = 2.0f; // Max boundary offset in chunks
|
||||||
|
const int32 CheckRange = EffectiveBlend + FMath::CeilToInt(WarpAmplitude);
|
||||||
|
|
||||||
|
if (DistFromBottom < CheckRange)
|
||||||
|
{
|
||||||
|
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
||||||
|
BelowParams.StrateTopWorldZ = (float)(BelowSlot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
BelowParams.StrateBottomWorldZ = (float)(BelowSlot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
|
|
||||||
|
// Sample 3D Perlin noise to warp the boundary position.
|
||||||
|
// XY frequency 0.15 gives medium-scale variation (~6-7 chunks per cycle).
|
||||||
|
// Z frequency 0.05 gives slow vertical change — horizontal finger shapes.
|
||||||
|
// CachedSeed offsets ensure each world has unique finger patterns.
|
||||||
|
float WarpNoise = FMath::PerlinNoise3D(FVector(
|
||||||
|
ChunkCoord.X * 0.15f + CachedSeed * 0.01f,
|
||||||
|
ChunkCoord.Y * 0.15f + CachedSeed * 0.017f,
|
||||||
|
ChunkCoord.Z * 0.05f // Lower Z frequency for horizontal "fingers"
|
||||||
|
)) * VOXEL_NOISE_SCALE;
|
||||||
|
|
||||||
|
// Offset the distance from boundary by the noise * amplitude.
|
||||||
|
// Positive noise → boundary pushed up → lower strate appears earlier.
|
||||||
|
// Negative noise → boundary pushed down → upper strate persists longer.
|
||||||
|
float WarpedDist = (float)DistFromBottom + WarpNoise * WarpAmplitude;
|
||||||
|
|
||||||
|
// Compute alpha from the warped distance (same formula as Gradient,
|
||||||
|
// but using the noise-displaced distance instead of the true distance)
|
||||||
|
float Alpha = 1.0f - FMath::Clamp(WarpedDist / (float)EffectiveBlend, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
// Only blend if alpha > 0 (we're inside the warped transition zone)
|
||||||
|
if (Alpha > 0.0f)
|
||||||
|
{
|
||||||
|
return FStrateGenerationParams::Lerp(BaseParams, BelowParams, Alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// CHECK TOP BOUNDARY (transitioning to strate above)
|
||||||
|
//---------------------------------------------------------------------
|
||||||
|
// Mirror logic: the ABOVE strate's TransitionType controls its lower edge,
|
||||||
|
// which is this strate's upper edge. So we read from StrateLayout[SlotIdx-1].
|
||||||
|
int32 DistFromTop = Slot.TopChunkZ - ChunkCoord.Z;
|
||||||
|
|
||||||
|
if (SlotIdx > 0)
|
||||||
|
{
|
||||||
|
// The strate ABOVE controls the transition at its lower edge (= our upper edge)
|
||||||
|
const FStrateSlot& AboveSlot = StrateLayout[SlotIdx - 1];
|
||||||
|
const EVoxelStrateTransition TransType = AboveSlot.Definition->TransitionType;
|
||||||
|
const int32 EffectiveBlend = AboveSlot.Definition->TransitionBlendChunks;
|
||||||
|
|
||||||
|
switch (TransType)
|
||||||
|
{
|
||||||
|
case EVoxelStrateTransition::Hard:
|
||||||
|
{
|
||||||
|
// No blending — fall through to return BaseParams
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EVoxelStrateTransition::Gradient:
|
||||||
|
{
|
||||||
|
if (DistFromTop < EffectiveBlend)
|
||||||
|
{
|
||||||
|
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
||||||
|
AboveParams.StrateTopWorldZ = (float)(AboveSlot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
AboveParams.StrateBottomWorldZ = (float)(AboveSlot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
|
|
||||||
|
float Alpha = 1.0f - ((float)DistFromTop / (float)EffectiveBlend);
|
||||||
|
Alpha = FMath::Clamp(Alpha, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
return FStrateGenerationParams::Lerp(BaseParams, AboveParams, Alpha);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EVoxelStrateTransition::Interleaved:
|
||||||
|
{
|
||||||
|
const float WarpAmplitude = 2.0f;
|
||||||
|
const int32 CheckRange = EffectiveBlend + FMath::CeilToInt(WarpAmplitude);
|
||||||
|
|
||||||
|
if (DistFromTop < CheckRange)
|
||||||
|
{
|
||||||
|
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
||||||
|
AboveParams.StrateTopWorldZ = (float)(AboveSlot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
AboveParams.StrateBottomWorldZ = (float)(AboveSlot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
|
|
||||||
|
// Same noise function but with a different seed offset to avoid
|
||||||
|
// symmetry between top and bottom boundaries of adjacent strates
|
||||||
|
float WarpNoise = FMath::PerlinNoise3D(FVector(
|
||||||
|
ChunkCoord.X * 0.15f + CachedSeed * 0.013f,
|
||||||
|
ChunkCoord.Y * 0.15f + CachedSeed * 0.023f,
|
||||||
|
ChunkCoord.Z * 0.05f
|
||||||
|
)) * VOXEL_NOISE_SCALE;
|
||||||
|
|
||||||
|
float WarpedDist = (float)DistFromTop + WarpNoise * WarpAmplitude;
|
||||||
|
float Alpha = 1.0f - FMath::Clamp(WarpedDist / (float)EffectiveBlend, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
if (Alpha > 0.0f)
|
||||||
|
{
|
||||||
|
return FStrateGenerationParams::Lerp(BaseParams, AboveParams, Alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not near any boundary (or Hard transition) — use this strate's params directly
|
||||||
|
return BaseParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// BUILD PARAMS FROM DEFINITION
|
||||||
|
//=============================================================================
|
||||||
|
// Returns the definition's base GenerationParams (cave shape, SDF, roughness, etc.).
|
||||||
|
//
|
||||||
|
// NOTE: Terrain op fields (TerraceStepHeight, ColumnDensity, etc.) are no longer
|
||||||
|
// merged here. They default to 0 (disabled) in the base params, and are applied
|
||||||
|
// per-room during BuildChunkCache() via FCachedRoom::RoomOp — each room hash-rolls
|
||||||
|
// one op from the strate's probability pool (FStrateTerrainOpEntry::Probability).
|
||||||
|
//
|
||||||
|
// Assets are still pre-loaded in Initialize() so BuildChunkCache can resolve
|
||||||
|
// soft pointers (Entry.Operation.Get()) without a disk read during generation.
|
||||||
|
|
||||||
|
FStrateGenerationParams UVoxelStrateManager::BuildParamsFromDefinition(const UVoxelStrateDefinition* Definition)
|
||||||
|
{
|
||||||
|
if (!Definition) return FStrateGenerationParams();
|
||||||
|
|
||||||
|
// Base params only — terrain op fields stay 0 until per-room assignment.
|
||||||
|
return Definition->GenerationParams;
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// VoxelTerrainOpDefinition.cpp
|
||||||
|
// Applies a terrain operation's parameters to the generation params struct.
|
||||||
|
|
||||||
|
#include "VoxelTerrainOpDefinition.h"
|
||||||
|
|
||||||
|
void UVoxelTerrainOpDefinition::ApplyTo(FStrateGenerationParams& OutParams, float Weight) const
|
||||||
|
{
|
||||||
|
// Weight scales the "primary" field of each op type — the one that controls
|
||||||
|
// whether the op is active and how strong it is. Other fields (radii, spacing,
|
||||||
|
// ratios) are copied directly since they define shape, not intensity.
|
||||||
|
|
||||||
|
switch (Type)
|
||||||
|
{
|
||||||
|
case EVoxelTerrainOpType::Terrace:
|
||||||
|
// TerraceStepHeight is the activation field (0 = disabled)
|
||||||
|
OutParams.TerraceStepHeight = TerraceStepHeight * Weight;
|
||||||
|
OutParams.TerraceHardness = TerraceHardness;
|
||||||
|
OutParams.TerraceNoiseDisplacement = TerraceNoiseDisplacement;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EVoxelTerrainOpType::LayerLines:
|
||||||
|
// LayerLineSpacing is the activation field (0 = disabled)
|
||||||
|
OutParams.LayerLineSpacing = LayerLineSpacing * Weight;
|
||||||
|
OutParams.LayerLineDepth = LayerLineDepth;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EVoxelTerrainOpType::Ribbing:
|
||||||
|
// RibbingSpacing is the activation field (0 = disabled)
|
||||||
|
OutParams.RibbingSpacing = RibbingSpacing * Weight;
|
||||||
|
OutParams.RibbingDepth = RibbingDepth;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EVoxelTerrainOpType::Cliff:
|
||||||
|
OutParams.CliffStrength = CliffStrength * Weight;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EVoxelTerrainOpType::Scallop:
|
||||||
|
OutParams.ScallopStrength = ScallopStrength * Weight;
|
||||||
|
OutParams.ScallopFrequency = ScallopFrequency;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EVoxelTerrainOpType::Overhang:
|
||||||
|
OutParams.OverhangStrength = OverhangStrength * Weight;
|
||||||
|
OutParams.OverhangDepth = OverhangDepth;
|
||||||
|
OutParams.OverhangFrequency = OverhangFrequency;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EVoxelTerrainOpType::Arch:
|
||||||
|
OutParams.ArchDensity = ArchDensity * Weight;
|
||||||
|
OutParams.ArchMinRadius = ArchMinRadius;
|
||||||
|
OutParams.ArchMaxRadius = ArchMaxRadius;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EVoxelTerrainOpType::Column:
|
||||||
|
OutParams.ColumnDensity = ColumnDensity * Weight;
|
||||||
|
OutParams.ColumnMinRadius = ColumnMinRadius;
|
||||||
|
OutParams.ColumnMaxRadius = ColumnMaxRadius;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EVoxelTerrainOpType::Pit:
|
||||||
|
OutParams.PitDensity = PitDensity * Weight;
|
||||||
|
OutParams.PitMinRadius = PitMinRadius;
|
||||||
|
OutParams.PitMaxRadius = PitMaxRadius;
|
||||||
|
OutParams.PitDepth = PitDepth;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EVoxelTerrainOpType::Chimney:
|
||||||
|
OutParams.ChimneyDensity = ChimneyDensity * Weight;
|
||||||
|
OutParams.ChimneyMinRadius = ChimneyMinRadius;
|
||||||
|
OutParams.ChimneyMaxRadius = ChimneyMaxRadius;
|
||||||
|
OutParams.ChimneyHeight = ChimneyHeight;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EVoxelTerrainOpType::Dome:
|
||||||
|
OutParams.DomeDensity = DomeDensity * Weight;
|
||||||
|
OutParams.DomeMinRadius = DomeMinRadius;
|
||||||
|
OutParams.DomeMaxRadius = DomeMaxRadius;
|
||||||
|
OutParams.DomeHeightRatio = DomeHeightRatio;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EVoxelTerrainOpType::Pinch:
|
||||||
|
OutParams.PinchDensity = PinchDensity * Weight;
|
||||||
|
OutParams.PinchStrength = PinchStrength;
|
||||||
|
OutParams.PinchLength = PinchLength;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,344 @@
|
|||||||
|
// MarchingCubesTables.h
|
||||||
|
// Standard lookup tables for the Marching Cubes algorithm
|
||||||
|
//
|
||||||
|
// These tables are reference data — everyone uses the same ones.
|
||||||
|
// Originally from Paul Bourke's Marching Cubes implementation.
|
||||||
|
//
|
||||||
|
// CUBE CORNER LAYOUT:
|
||||||
|
// -------------------
|
||||||
|
// 4-------5
|
||||||
|
// /| /|
|
||||||
|
// / | / |
|
||||||
|
// 7-------6 |
|
||||||
|
// | 0----|--1
|
||||||
|
// | / | /
|
||||||
|
// |/ |/
|
||||||
|
// 3-------2
|
||||||
|
//
|
||||||
|
// Corner indices and their positions relative to (X, Y, Z):
|
||||||
|
// 0: (0, 0, 0) 4: (0, 0, 1)
|
||||||
|
// 1: (1, 0, 0) 5: (1, 0, 1)
|
||||||
|
// 2: (1, 1, 0) 6: (1, 1, 1)
|
||||||
|
// 3: (0, 1, 0) 7: (0, 1, 1)
|
||||||
|
//
|
||||||
|
// EDGE INDICES:
|
||||||
|
// -------------
|
||||||
|
// Edge 0: corner 0 - corner 1 (bottom front)
|
||||||
|
// Edge 1: corner 1 - corner 2 (bottom right)
|
||||||
|
// Edge 2: corner 2 - corner 3 (bottom back)
|
||||||
|
// Edge 3: corner 3 - corner 0 (bottom left)
|
||||||
|
// Edge 4: corner 4 - corner 5 (top front)
|
||||||
|
// Edge 5: corner 5 - corner 6 (top right)
|
||||||
|
// Edge 6: corner 6 - corner 7 (top back)
|
||||||
|
// Edge 7: corner 7 - corner 4 (top left)
|
||||||
|
// Edge 8: corner 0 - corner 4 (vertical left-front)
|
||||||
|
// Edge 9: corner 1 - corner 5 (vertical right-front)
|
||||||
|
// Edge 10: corner 2 - corner 6 (vertical right-back)
|
||||||
|
// Edge 11: corner 3 - corner 7 (vertical left-back)
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
|
||||||
|
// EdgeTable[caseIndex] = 12-bit bitmask
|
||||||
|
// Each bit tells you if that edge is intersected by the surface.
|
||||||
|
// Example: EdgeTable[1] = 0x109 = 0000 0001 0000 1001
|
||||||
|
// → edges 0, 3, and 8 are intersected
|
||||||
|
static const int32 MCEdgeTable[256] = {
|
||||||
|
0x000, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
|
||||||
|
0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
|
||||||
|
0x190, 0x099, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
|
||||||
|
0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90,
|
||||||
|
0x230, 0x339, 0x033, 0x13a, 0x636, 0x73f, 0x435, 0x53c,
|
||||||
|
0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
|
||||||
|
0x3a0, 0x2a9, 0x1a3, 0x0aa, 0x7a6, 0x6af, 0x5a5, 0x4ac,
|
||||||
|
0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0,
|
||||||
|
0x460, 0x569, 0x663, 0x76a, 0x066, 0x16f, 0x265, 0x36c,
|
||||||
|
0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60,
|
||||||
|
0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0x0ff, 0x3f5, 0x2fc,
|
||||||
|
0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
|
||||||
|
0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x055, 0x15c,
|
||||||
|
0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950,
|
||||||
|
0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0x0cc,
|
||||||
|
0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0,
|
||||||
|
0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc,
|
||||||
|
0x0cc, 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
|
||||||
|
0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c,
|
||||||
|
0x15c, 0x055, 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650,
|
||||||
|
0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
|
||||||
|
0x2fc, 0x3f5, 0x0ff, 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0,
|
||||||
|
0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c,
|
||||||
|
0x36c, 0x265, 0x16f, 0x066, 0x76a, 0x663, 0x569, 0x460,
|
||||||
|
0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac,
|
||||||
|
0x4ac, 0x5a5, 0x6af, 0x7a6, 0x0aa, 0x1a3, 0x2a9, 0x3a0,
|
||||||
|
0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c,
|
||||||
|
0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x033, 0x339, 0x230,
|
||||||
|
0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
|
||||||
|
0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x099, 0x190,
|
||||||
|
0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
|
||||||
|
0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x000
|
||||||
|
};
|
||||||
|
|
||||||
|
// TriTable[caseIndex][up to 16]
|
||||||
|
// Lists edge indices that form triangles, in groups of 3.
|
||||||
|
// -1 terminates the list.
|
||||||
|
// Example: TriTable[1] = {0, 8, 3, -1, ...}
|
||||||
|
// → one triangle using edges 0, 8, and 3
|
||||||
|
static const int32 MCTriTable[256][16] = {
|
||||||
|
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 2, 8, 3, 2, 10, 8, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 11, 2, 8, 11, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 11, 2, 1, 9, 11, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 3, 10, 1, 11, 10, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 10, 1, 0, 8, 10, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 3, 9, 0, 3, 11, 9, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 4, 3, 0, 7, 3, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 1, 9, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 4, 1, 9, 4, 7, 1, 7, 3, 1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 2, 10, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 3, 4, 7, 3, 0, 4, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 2, 10, 9, 0, 2, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4, -1, -1, -1, -1},
|
||||||
|
{ 8, 4, 7, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{11, 4, 7, 11, 2, 4, 2, 0, 4, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 0, 1, 8, 4, 7, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1, -1, -1, -1, -1},
|
||||||
|
{ 3, 10, 1, 3, 11, 10, 7, 8, 4, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4, -1, -1, -1, -1},
|
||||||
|
{ 4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3, -1, -1, -1, -1},
|
||||||
|
{ 4, 7, 11, 4, 11, 9, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 5, 4, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 5, 4, 1, 5, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 8, 5, 4, 8, 3, 5, 3, 1, 5, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 2, 10, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 3, 0, 8, 1, 2, 10, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 5, 2, 10, 5, 4, 2, 4, 0, 2, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8, -1, -1, -1, -1},
|
||||||
|
{ 9, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 11, 2, 0, 8, 11, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 5, 4, 0, 1, 5, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5, -1, -1, -1, -1},
|
||||||
|
{10, 3, 11, 10, 1, 3, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10, -1, -1, -1, -1},
|
||||||
|
{ 5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3, -1, -1, -1, -1},
|
||||||
|
{ 5, 4, 8, 5, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 7, 8, 5, 7, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 3, 0, 9, 5, 3, 5, 7, 3, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 7, 8, 0, 1, 7, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 7, 8, 9, 5, 7, 10, 1, 2, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3, -1, -1, -1, -1},
|
||||||
|
{ 8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2, -1, -1, -1, -1},
|
||||||
|
{ 2, 10, 5, 2, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 7, 9, 5, 7, 8, 9, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11, -1, -1, -1, -1},
|
||||||
|
{ 2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7, -1, -1, -1, -1},
|
||||||
|
{11, 2, 1, 11, 1, 7, 7, 1, 5, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11, -1, -1, -1, -1},
|
||||||
|
{ 5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0, -1},
|
||||||
|
{11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0, -1},
|
||||||
|
{11, 10, 5, 7, 11, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 0, 1, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 8, 3, 1, 9, 8, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 6, 5, 2, 6, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 6, 5, 1, 2, 6, 3, 0, 8, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 6, 5, 9, 0, 6, 0, 2, 6, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8, -1, -1, -1, -1},
|
||||||
|
{ 2, 3, 11, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{11, 0, 8, 11, 2, 0, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 1, 9, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11, -1, -1, -1, -1},
|
||||||
|
{ 6, 3, 11, 6, 5, 3, 5, 1, 3, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6, -1, -1, -1, -1},
|
||||||
|
{ 3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9, -1, -1, -1, -1},
|
||||||
|
{ 6, 5, 9, 6, 9, 11, 11, 9, 8, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 5, 10, 6, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 4, 3, 0, 4, 7, 3, 6, 5, 10, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 9, 0, 5, 10, 6, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4, -1, -1, -1, -1},
|
||||||
|
{ 6, 1, 2, 6, 5, 1, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7, -1, -1, -1, -1},
|
||||||
|
{ 8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6, -1, -1, -1, -1},
|
||||||
|
{ 7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9, -1},
|
||||||
|
{ 3, 11, 2, 7, 8, 4, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11, -1, -1, -1, -1},
|
||||||
|
{ 0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1},
|
||||||
|
{ 9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6, -1},
|
||||||
|
{ 8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6, -1, -1, -1, -1},
|
||||||
|
{ 5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11, -1},
|
||||||
|
{ 0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7, -1},
|
||||||
|
{ 6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9, -1, -1, -1, -1},
|
||||||
|
{10, 4, 9, 6, 4, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 4, 10, 6, 4, 9, 10, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{10, 0, 1, 10, 6, 0, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10, -1, -1, -1, -1},
|
||||||
|
{ 1, 4, 9, 1, 2, 4, 2, 6, 4, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4, -1, -1, -1, -1},
|
||||||
|
{ 0, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 8, 3, 2, 8, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{10, 4, 9, 10, 6, 4, 11, 2, 3, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6, -1, -1, -1, -1},
|
||||||
|
{ 3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10, -1, -1, -1, -1},
|
||||||
|
{ 6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1, -1},
|
||||||
|
{ 9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3, -1, -1, -1, -1},
|
||||||
|
{ 8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1, -1},
|
||||||
|
{ 3, 11, 6, 3, 6, 0, 0, 6, 4, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 6, 4, 8, 11, 6, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 7, 10, 6, 7, 8, 10, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10, -1, -1, -1, -1},
|
||||||
|
{10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0, -1, -1, -1, -1},
|
||||||
|
{10, 6, 7, 10, 7, 1, 1, 7, 3, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7, -1, -1, -1, -1},
|
||||||
|
{ 2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9, -1},
|
||||||
|
{ 7, 8, 0, 7, 0, 6, 6, 0, 2, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 7, 3, 2, 6, 7, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7, -1, -1, -1, -1},
|
||||||
|
{ 2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7, -1},
|
||||||
|
{ 1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11, -1},
|
||||||
|
{11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1, -1, -1, -1, -1},
|
||||||
|
{ 8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6, -1},
|
||||||
|
{ 0, 9, 1, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0, -1, -1, -1, -1},
|
||||||
|
{ 7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 3, 0, 8, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 1, 9, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 8, 1, 9, 8, 3, 1, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{10, 1, 2, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 2, 10, 3, 0, 8, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 2, 9, 0, 2, 10, 9, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8, -1, -1, -1, -1},
|
||||||
|
{ 7, 2, 3, 6, 2, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 7, 0, 8, 7, 6, 0, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 2, 7, 6, 2, 3, 7, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6, -1, -1, -1, -1},
|
||||||
|
{10, 7, 6, 10, 1, 7, 1, 3, 7, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8, -1, -1, -1, -1},
|
||||||
|
{ 0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7, -1, -1, -1, -1},
|
||||||
|
{ 7, 6, 10, 7, 10, 8, 8, 10, 9, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 6, 8, 4, 11, 8, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 3, 6, 11, 3, 0, 6, 0, 4, 6, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 8, 6, 11, 8, 4, 6, 9, 0, 1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6, -1, -1, -1, -1},
|
||||||
|
{ 6, 8, 4, 6, 11, 8, 2, 10, 1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6, -1, -1, -1, -1},
|
||||||
|
{ 4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9, -1, -1, -1, -1},
|
||||||
|
{10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3, -1},
|
||||||
|
{ 8, 2, 3, 8, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8, -1, -1, -1, -1},
|
||||||
|
{ 1, 9, 4, 1, 4, 2, 2, 4, 6, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1, -1, -1, -1, -1},
|
||||||
|
{10, 1, 0, 10, 0, 6, 6, 0, 4, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3, -1},
|
||||||
|
{10, 9, 4, 6, 10, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 4, 9, 5, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 8, 3, 4, 9, 5, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 5, 0, 1, 5, 4, 0, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5, -1, -1, -1, -1},
|
||||||
|
{ 9, 5, 4, 10, 1, 2, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5, -1, -1, -1, -1},
|
||||||
|
{ 7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2, -1, -1, -1, -1},
|
||||||
|
{ 3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6, -1},
|
||||||
|
{ 7, 2, 3, 7, 6, 2, 5, 4, 9, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7, -1, -1, -1, -1},
|
||||||
|
{ 3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0, -1, -1, -1, -1},
|
||||||
|
{ 6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8, -1},
|
||||||
|
{ 9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7, -1, -1, -1, -1},
|
||||||
|
{ 1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4, -1},
|
||||||
|
{ 4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10, -1},
|
||||||
|
{ 7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10, -1, -1, -1, -1},
|
||||||
|
{ 6, 9, 5, 6, 11, 9, 11, 8, 9, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5, -1, -1, -1, -1},
|
||||||
|
{ 0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11, -1, -1, -1, -1},
|
||||||
|
{ 6, 11, 3, 6, 3, 5, 5, 3, 1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6, -1, -1, -1, -1},
|
||||||
|
{ 0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10, -1},
|
||||||
|
{11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5, -1},
|
||||||
|
{ 6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3, -1, -1, -1, -1},
|
||||||
|
{ 5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2, -1, -1, -1, -1},
|
||||||
|
{ 9, 5, 6, 9, 6, 0, 0, 6, 2, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8, -1},
|
||||||
|
{ 1, 5, 6, 2, 1, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6, -1},
|
||||||
|
{10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0, -1, -1, -1, -1},
|
||||||
|
{ 0, 3, 8, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{11, 5, 10, 7, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{11, 5, 10, 11, 7, 5, 8, 3, 0, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 5, 11, 7, 5, 10, 11, 1, 9, 0, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1, -1, -1, -1, -1},
|
||||||
|
{11, 1, 2, 11, 7, 1, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11, -1, -1, -1, -1},
|
||||||
|
{ 9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7, -1, -1, -1, -1},
|
||||||
|
{ 7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2, -1},
|
||||||
|
{ 2, 5, 10, 2, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5, -1, -1, -1, -1},
|
||||||
|
{ 9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2, -1, -1, -1, -1},
|
||||||
|
{ 9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2, -1},
|
||||||
|
{ 1, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 8, 7, 0, 7, 1, 1, 7, 5, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 0, 3, 9, 3, 5, 5, 3, 7, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 8, 7, 5, 9, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 5, 8, 4, 5, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0, -1, -1, -1, -1},
|
||||||
|
{ 0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5, -1, -1, -1, -1},
|
||||||
|
{10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4, -1},
|
||||||
|
{ 2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8, -1, -1, -1, -1},
|
||||||
|
{ 0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11, -1},
|
||||||
|
{ 0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5, -1},
|
||||||
|
{ 9, 4, 5, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4, -1, -1, -1, -1},
|
||||||
|
{ 5, 10, 2, 5, 2, 4, 4, 2, 0, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9, -1},
|
||||||
|
{ 5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2, -1, -1, -1, -1},
|
||||||
|
{ 8, 4, 5, 8, 5, 3, 3, 5, 1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 4, 5, 1, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5, -1, -1, -1, -1},
|
||||||
|
{ 9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 4, 11, 7, 4, 9, 11, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11, -1, -1, -1, -1},
|
||||||
|
{ 1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11, -1, -1, -1, -1},
|
||||||
|
{ 3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4, -1},
|
||||||
|
{ 4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2, -1, -1, -1, -1},
|
||||||
|
{ 9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3, -1},
|
||||||
|
{11, 7, 4, 11, 4, 2, 2, 4, 0, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4, -1, -1, -1, -1},
|
||||||
|
{ 2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9, -1, -1, -1, -1},
|
||||||
|
{ 9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7, -1},
|
||||||
|
{ 3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10, -1},
|
||||||
|
{ 1, 10, 2, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 4, 9, 1, 4, 1, 7, 7, 1, 3, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1, -1, -1, -1, -1},
|
||||||
|
{ 4, 0, 3, 7, 4, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 3, 0, 9, 3, 9, 11, 11, 9, 10, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 1, 10, 0, 10, 8, 8, 10, 11, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 3, 1, 10, 11, 3, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 2, 11, 1, 11, 9, 9, 11, 8, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9, -1, -1, -1, -1},
|
||||||
|
{ 0, 2, 11, 8, 0, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 2, 3, 8, 2, 8, 10, 10, 8, 9, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 9, 10, 2, 0, 9, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8, -1, -1, -1, -1},
|
||||||
|
{ 1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 1, 3, 8, 9, 1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{ 0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
|
||||||
|
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}
|
||||||
|
};
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// VoxelAtmosphereManager.h
|
||||||
|
// Whole-strate ambiance: a managed height fog + skylight driven by the strate the
|
||||||
|
// PLAYER is in, plus persistent ceiling/floor "layer" actors (e.g. seas of clouds)
|
||||||
|
// that follow the player in XY and hug the strate boundaries.
|
||||||
|
//
|
||||||
|
// Unlike the per-chunk content spawner, this is PLAYER-scoped: it updates only when
|
||||||
|
// the player changes strate, and the layer actors are single persistent instances —
|
||||||
|
// no spawn/unload churn with chunk streaming.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "VoxelAtmosphereManager.generated.h"
|
||||||
|
|
||||||
|
class UVoxelStrateManager;
|
||||||
|
class UVoxelStrateDefinition;
|
||||||
|
class UExponentialHeightFogComponent;
|
||||||
|
class USkyLightComponent;
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class VOXELFORGE_API UVoxelAtmosphereManager : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Create the managed fog + skylight components on the owner actor. */
|
||||||
|
void Initialize(AActor* InOwner, UVoxelStrateManager* InStrateManager);
|
||||||
|
|
||||||
|
/** Call each frame with the player's world position. Cheap: only reacts on strate change. */
|
||||||
|
void UpdateForPlayer(const FVector& PlayerWorldPos);
|
||||||
|
|
||||||
|
/** Tear down spawned layer actors + reset (season reset / shutdown). */
|
||||||
|
void Reset();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ApplyStrate(const UVoxelStrateDefinition* Def);
|
||||||
|
|
||||||
|
TWeakObjectPtr<AActor> Owner;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
UVoxelStrateManager* StrateManager = nullptr;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
UExponentialHeightFogComponent* Fog = nullptr;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
USkyLightComponent* Sky = nullptr;
|
||||||
|
|
||||||
|
// Fully-authored atmosphere BP instance (replaces managed fog/sky when present).
|
||||||
|
UPROPERTY()
|
||||||
|
AActor* AtmosphereActorInstance = nullptr;
|
||||||
|
|
||||||
|
// Persistent per-strate layer actor instances (single each).
|
||||||
|
UPROPERTY()
|
||||||
|
AActor* CeilingActor = nullptr;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
AActor* FloorActor = nullptr;
|
||||||
|
|
||||||
|
// Which strate's atmosphere is currently applied (INT32_MIN = none yet).
|
||||||
|
int32 CurrentStrateIndex = INT32_MIN;
|
||||||
|
};
|
||||||
@@ -0,0 +1,395 @@
|
|||||||
|
// VoxelCaveMorphology.h
|
||||||
|
// Hash-based cave room/tunnel generation and SDF evaluation.
|
||||||
|
//
|
||||||
|
// HOW IT WORKS:
|
||||||
|
// =============
|
||||||
|
// The world is divided into a coarse 2D grid (cell size = RoomSpacing).
|
||||||
|
// Each cell deterministically either contains a room or doesn't, based on
|
||||||
|
// a hash of (CellX, CellY, Seed, StrateIndex). No pre-computation needed —
|
||||||
|
// room positions are derived on-the-fly from the hash, so this works for
|
||||||
|
// infinite worlds without storing anything.
|
||||||
|
//
|
||||||
|
// ROOMS:
|
||||||
|
// - Origin room: guaranteed large room at (0,0) per strate — the hub
|
||||||
|
// - Hash rooms: ellipsoids, rounded boxes, or elongated capsules
|
||||||
|
// - All blended with smooth-min for organic junctions
|
||||||
|
//
|
||||||
|
// TUNNELS:
|
||||||
|
// - Connect pairs of rooms based on distance and probability
|
||||||
|
// - Tapered (variable min/max radius at each endpoint)
|
||||||
|
// - Curved (midpoint displaced perpendicular to tunnel direction)
|
||||||
|
// - Horizontal bias (vertical connections penalized)
|
||||||
|
// - Endpoint Z offset (tunnels enter rooms at different heights)
|
||||||
|
// - Origin room forces connections to all nearby rooms
|
||||||
|
//
|
||||||
|
// PIPELINE POSITION:
|
||||||
|
// Step 3 (cave warp) bends the SDF query coordinates before we evaluate.
|
||||||
|
// This step (Step 4) builds the SDF skeleton of rooms + tunnels.
|
||||||
|
// Surface roughness (Step 4b), terrain ops, and worm tunnels layer on top.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
struct FStrateGenerationParams;
|
||||||
|
struct FStrateTerrainOpEntry;
|
||||||
|
class UVoxelTerrainOpDefinition;
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// SDF PRIMITIVES
|
||||||
|
//=============================================================================
|
||||||
|
// Signed Distance Field functions.
|
||||||
|
// Return value: negative = inside the shape, positive = outside.
|
||||||
|
// The "distance" is how far from the surface — 0.0 = exactly on the surface.
|
||||||
|
|
||||||
|
namespace VoxelSDF
|
||||||
|
{
|
||||||
|
// Distance from point P to the surface of a sphere at Center with Radius.
|
||||||
|
// Negative when P is inside the sphere.
|
||||||
|
FORCEINLINE float Sphere(const FVector& P, const FVector& Center, float Radius)
|
||||||
|
{
|
||||||
|
return FVector::Dist(P, Center) - Radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance from P to an ellipsoid (squished sphere).
|
||||||
|
// Radii = (RadiusX, RadiusY, RadiusZ) — different size per axis.
|
||||||
|
// We scale the point into a unit sphere, compute distance, then scale back.
|
||||||
|
// This isn't an exact SDF (distances are approximate near the surface),
|
||||||
|
// but it's good enough for marching cubes density evaluation.
|
||||||
|
FORCEINLINE float Ellipsoid(const FVector& P, const FVector& Center, const FVector& Radii)
|
||||||
|
{
|
||||||
|
// Transform to unit sphere space
|
||||||
|
FVector Scaled = (P - Center) / Radii;
|
||||||
|
float ScaledDist = Scaled.Size();
|
||||||
|
// Approximate: scale the distance back by the average radius
|
||||||
|
float AvgRadius = (Radii.X + Radii.Y + Radii.Z) / 3.0f;
|
||||||
|
return (ScaledDist - 1.0f) * AvgRadius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance from P to a capsule (line segment with thickness).
|
||||||
|
// A and B are the endpoints, Radius is the tube thickness.
|
||||||
|
// This is an EXACT SDF — used for tunnels.
|
||||||
|
FORCEINLINE float Capsule(const FVector& P, const FVector& A, const FVector& B, float Radius)
|
||||||
|
{
|
||||||
|
FVector AB = B - A;
|
||||||
|
FVector AP = P - A;
|
||||||
|
// Project P onto line AB, clamped to [0,1] (stays within segment)
|
||||||
|
float T = FMath::Clamp(FVector::DotProduct(AP, AB) / FMath::Max(FVector::DotProduct(AB, AB), KINDA_SMALL_NUMBER), 0.0f, 1.0f);
|
||||||
|
// Closest point on the segment
|
||||||
|
FVector Closest = A + AB * T;
|
||||||
|
return FVector::Dist(P, Closest) - Radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance from P to a rounded box (box with rounded edges).
|
||||||
|
// Center = box center, HalfExtent = half-size per axis, Rounding = edge radius.
|
||||||
|
// This creates angular chambers (rectangular rooms with smooth corners).
|
||||||
|
// Without rounding (Rounding=0), it's a sharp box.
|
||||||
|
// With rounding, edges and corners are smoothed — essential for MC quality.
|
||||||
|
FORCEINLINE float RoundedBox(const FVector& P, const FVector& Center, const FVector& HalfExtent, float Rounding)
|
||||||
|
{
|
||||||
|
// Vector from center to P, take absolute value (box symmetry)
|
||||||
|
FVector D = FVector(
|
||||||
|
FMath::Abs(P.X - Center.X),
|
||||||
|
FMath::Abs(P.Y - Center.Y),
|
||||||
|
FMath::Abs(P.Z - Center.Z)
|
||||||
|
) - HalfExtent;
|
||||||
|
|
||||||
|
// Distance outside the box (positive when outside)
|
||||||
|
float Outside = FVector(
|
||||||
|
FMath::Max(D.X, 0.0f),
|
||||||
|
FMath::Max(D.Y, 0.0f),
|
||||||
|
FMath::Max(D.Z, 0.0f)
|
||||||
|
).Size();
|
||||||
|
|
||||||
|
// Distance inside the box (negative when inside)
|
||||||
|
float Inside = FMath::Min(FMath::Max(D.X, FMath::Max(D.Y, D.Z)), 0.0f);
|
||||||
|
|
||||||
|
return Outside + Inside - Rounding;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance from P to a tapered capsule (cone-like tube with spherical caps).
|
||||||
|
// A and B are endpoints, RadiusA and RadiusB are the tube radius at each end.
|
||||||
|
// When RadiusA != RadiusB, the tube narrows/widens along its length.
|
||||||
|
// This creates natural-looking corridors that aren't perfectly uniform.
|
||||||
|
FORCEINLINE float TaperedCapsule(const FVector& P, const FVector& A, const FVector& B, float RadiusA, float RadiusB)
|
||||||
|
{
|
||||||
|
FVector AB = B - A;
|
||||||
|
FVector AP = P - A;
|
||||||
|
float LenSq = FVector::DotProduct(AB, AB);
|
||||||
|
// T = how far along AB the closest point is (0 = at A, 1 = at B)
|
||||||
|
float T = (LenSq > KINDA_SMALL_NUMBER)
|
||||||
|
? FMath::Clamp(FVector::DotProduct(AP, AB) / LenSq, 0.0f, 1.0f)
|
||||||
|
: 0.0f;
|
||||||
|
FVector Closest = A + AB * T;
|
||||||
|
// Radius interpolates from RadiusA at A to RadiusB at B
|
||||||
|
float R = FMath::Lerp(RadiusA, RadiusB, T);
|
||||||
|
return FVector::Dist(P, Closest) - R;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polynomial smooth minimum — blends two SDF shapes together.
|
||||||
|
// K controls the blend radius: higher K = rounder, smoother junctions.
|
||||||
|
// With K=0, this is just FMath::Min(A, B) (hard intersection).
|
||||||
|
// With K=4, room/tunnel junctions look organic and natural.
|
||||||
|
FORCEINLINE float SmoothMin(float A, float B, float K)
|
||||||
|
{
|
||||||
|
if (K <= 0.0f) return FMath::Min(A, B);
|
||||||
|
float H = FMath::Max(K - FMath::Abs(A - B), 0.0f) / K;
|
||||||
|
return FMath::Min(A, B) - H * H * H * K * (1.0f / 6.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polynomial smooth maximum — dual of SmoothMin, enforces the larger value softly.
|
||||||
|
// Used for soft SDF intersections (e.g., floor cut planes) so the boundary
|
||||||
|
// rounds off near tunnel/pit openings instead of creating a hard lip.
|
||||||
|
// With K=0, this is just FMath::Max(A, B).
|
||||||
|
FORCEINLINE float SmoothMax(float A, float B, float K)
|
||||||
|
{
|
||||||
|
return -SmoothMin(-A, -B, K);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// HASH FUNCTIONS
|
||||||
|
//=============================================================================
|
||||||
|
// Deterministic integer hashing for room placement.
|
||||||
|
// Given (CellX, CellY, Seed), always returns the same result.
|
||||||
|
// No randomness — fully reproducible across sessions and clients.
|
||||||
|
|
||||||
|
namespace VoxelHash
|
||||||
|
{
|
||||||
|
// Mix a single uint32 — scrambles bits to reduce patterns
|
||||||
|
FORCEINLINE uint32 Mix(uint32 X)
|
||||||
|
{
|
||||||
|
X ^= X >> 16;
|
||||||
|
X *= 0x45d9f3bu;
|
||||||
|
X ^= X >> 16;
|
||||||
|
X *= 0x45d9f3bu;
|
||||||
|
X ^= X >> 16;
|
||||||
|
return X;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash a 2D cell coordinate with a seed → deterministic uint32
|
||||||
|
FORCEINLINE uint32 Cell(int32 CellX, int32 CellY, uint32 Seed)
|
||||||
|
{
|
||||||
|
uint32 H = Seed;
|
||||||
|
H ^= Mix((uint32)(CellX + 0x7FFFFFFF));
|
||||||
|
H ^= Mix((uint32)(CellY + 0x7FFFFFFF) * 2654435761u);
|
||||||
|
return Mix(H);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash a pair of cells (for tunnel connectivity decisions).
|
||||||
|
// Order-independent: Cell(A,B) == Cell(B,A) so each tunnel is evaluated once.
|
||||||
|
FORCEINLINE uint32 Pair(int32 AX, int32 AY, int32 BX, int32 BY, uint32 Seed)
|
||||||
|
{
|
||||||
|
// Sort the pair so (A,B) and (B,A) give the same hash
|
||||||
|
int32 MinX = FMath::Min(AX, BX), MinY = FMath::Min(AY, BY);
|
||||||
|
int32 MaxX = FMath::Max(AX, BX), MaxY = FMath::Max(AY, BY);
|
||||||
|
uint32 H = Seed ^ 0xDEADBEEF;
|
||||||
|
H ^= Mix((uint32)(MinX + 0x7FFFFFFF));
|
||||||
|
H ^= Mix((uint32)(MinY + 0x7FFFFFFF) * 2654435761u);
|
||||||
|
H ^= Mix((uint32)(MaxX + 0x7FFFFFFF) * 374761393u);
|
||||||
|
H ^= Mix((uint32)(MaxY + 0x7FFFFFFF) * 668265263u);
|
||||||
|
return Mix(H);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a hash to a float in [0, 1) — uniform distribution
|
||||||
|
FORCEINLINE float ToFloat01(uint32 Hash)
|
||||||
|
{
|
||||||
|
return (float)(Hash & 0xFFFFFF) / (float)0x1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a hash to a float in [-1, 1) — for centering
|
||||||
|
FORCEINLINE float ToFloatSigned(uint32 Hash)
|
||||||
|
{
|
||||||
|
return ToFloat01(Hash) * 2.0f - 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// PER-CHUNK SDF CACHE
|
||||||
|
//=============================================================================
|
||||||
|
// The room list and tunnel connections are IDENTICAL for all voxels in a chunk.
|
||||||
|
// Without caching, EvaluateSDF rebuilds these 32,768 times per 32³ chunk:
|
||||||
|
// - Hash every grid cell in the search area
|
||||||
|
// - Determine room existence, position, size
|
||||||
|
// - Run O(N²) nearest-neighbor backbone
|
||||||
|
// - Decide O(N²) tunnel connections with hash lookups
|
||||||
|
// - Compute tunnel radii, Z offsets, midpoint warping
|
||||||
|
//
|
||||||
|
// With caching: build once, evaluate 32K times with just SDF math.
|
||||||
|
// This is the single biggest CPU performance win for cave generation.
|
||||||
|
|
||||||
|
// A room in the cache — everything needed for per-voxel SDF evaluation.
|
||||||
|
// Internal details (CellX, CellY) used during cache building are NOT stored here.
|
||||||
|
struct FCachedRoom
|
||||||
|
{
|
||||||
|
FVector Center; // World position of room center
|
||||||
|
float RadiusXY; // Horizontal radius (used for SDF + shape selection)
|
||||||
|
float RadiusZ; // Vertical radius (usually squished: RadiusXY * HeightRatio)
|
||||||
|
uint32 Hash; // Cell hash — used for deterministic shape selection per voxel
|
||||||
|
bool bIsOrigin; // True = origin room at (0,0), always uses ellipsoid shape
|
||||||
|
float CullRadiusSq; // Squared distance beyond which this room can't affect a voxel
|
||||||
|
|
||||||
|
// Flat floor cut: base world Z of the soft floor plane.
|
||||||
|
// Applied via SmoothMax so tunnels/pits pass through without a hard lip seam.
|
||||||
|
// Set to -FLT_MAX when the hash-rolled floor cut = 1.0 (full bubble, no cut).
|
||||||
|
float FloorCutZ;
|
||||||
|
|
||||||
|
// Floor relief: noise amplitude (voxels) and frequency for floor undulation.
|
||||||
|
// 0 strength = perfectly flat floor (just the cut plane, no variation).
|
||||||
|
// Stored per-room so EvaluateSDFCached can apply it without a params lookup.
|
||||||
|
float FloorReliefStrength;
|
||||||
|
float FloorReliefFrequency;
|
||||||
|
uint32 FloorSeed; // Deterministic seed offset for this room's floor noise
|
||||||
|
|
||||||
|
// Per-room terrain operation, hash-rolled during BuildChunkCache from the
|
||||||
|
// strate's probability pool (FStrateTerrainOpEntry::Probability).
|
||||||
|
// null = this room has no terrain op (the "no op" slot in the probability pool).
|
||||||
|
// Raw pointer is safe — assets stay loaded for the entire generation session.
|
||||||
|
const UVoxelTerrainOpDefinition* RoomOp = nullptr;
|
||||||
|
|
||||||
|
// Intensity scale for this room's op (from FStrateTerrainOpEntry::Weight).
|
||||||
|
// 1.0 = use op as configured, 0.5 = half intensity, 2.0 = double.
|
||||||
|
float RoomOpWeight = 1.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A pre-computed tunnel segment — all connection decisions and hash-derived
|
||||||
|
// properties (radius, Z offset, midpoint warp) are resolved during cache build.
|
||||||
|
struct FCachedTunnel
|
||||||
|
{
|
||||||
|
FVector EndpointA; // First room's connection point (with Z offset)
|
||||||
|
FVector EndpointB; // Second room's connection point (with Z offset)
|
||||||
|
float RadiusA; // Tube radius at endpoint A
|
||||||
|
float RadiusB; // Tube radius at endpoint B
|
||||||
|
// Warped midpoint — only used if bHasMidpoint is true.
|
||||||
|
// Creates a two-segment curved path instead of a straight tube.
|
||||||
|
FVector Midpoint;
|
||||||
|
float RadiusMid; // Radius at the midpoint (average of A and B)
|
||||||
|
bool bHasMidpoint; // True when TunnelWarpStrength > 0 and tunnel is long enough
|
||||||
|
// Bounding sphere for quick per-voxel rejection
|
||||||
|
FVector BoundCenter; // Center of the bounding sphere
|
||||||
|
float BoundRadiusSq; // Squared radius — if voxel is further, skip this tunnel
|
||||||
|
};
|
||||||
|
|
||||||
|
// A pre-baked pit shaft — position and dimensions resolved during BuildChunkCache.
|
||||||
|
//
|
||||||
|
// Pits are included in the main SDF evaluation (SmoothMin alongside rooms and
|
||||||
|
// tunnels) rather than as a separate density subtraction. This lets SmoothMin
|
||||||
|
// handle the pit-to-room junction organically — same mechanism as tunnel junctions.
|
||||||
|
// No more hard seam at PitTopZ. BlendK drives the junction roundness and comes
|
||||||
|
// from the strate's SDFBlendRadius, making it tweakable in the data asset.
|
||||||
|
struct FCachedPit
|
||||||
|
{
|
||||||
|
float CenterX, CenterY; // XY center in world coords (unwarped)
|
||||||
|
float TopZ; // World Z where the pit starts (anchored inside room air)
|
||||||
|
float Radius; // Shaft cylinder radius
|
||||||
|
float Depth; // Max depth the pit reaches below TopZ
|
||||||
|
float FlareDist; // Over how many voxels the opening flares (Radius * 2)
|
||||||
|
float FlareExtra; // Extra radius at the lip (Radius * 1)
|
||||||
|
float BaseDensity; // Carving strength — matches the strate's BaseDensity
|
||||||
|
float BlendK; // SmoothMin blend radius — set from Params.SDFBlendRadius
|
||||||
|
float BoundXYRadiusSq; // XY rejection: skip if (dx^2+dy^2) > this
|
||||||
|
};
|
||||||
|
|
||||||
|
// A pre-baked chimney shaft — inverse of a pit (carves upward from room ceiling).
|
||||||
|
struct FCachedChimney
|
||||||
|
{
|
||||||
|
float CenterX, CenterY;
|
||||||
|
float BottomZ; // World Z where the chimney starts (anchored inside room air)
|
||||||
|
float Radius;
|
||||||
|
float Height; // How far the chimney reaches above BottomZ
|
||||||
|
float FlareDist;
|
||||||
|
float FlareExtra;
|
||||||
|
float BaseDensity;
|
||||||
|
float BlendK; // SmoothMin blend radius — set from Params.SDFBlendRadius
|
||||||
|
float BoundXYRadiusSq;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A pre-baked column — vertical solid cylinder inside a room.
|
||||||
|
// Pre-baking fixes the NearestRoomIdx ownership issue for columns too:
|
||||||
|
// a column's XY position is fixed, but which room "owns" the voxel can change
|
||||||
|
// with depth, which would cause columns to appear/disappear mid-height.
|
||||||
|
struct FCachedColumn
|
||||||
|
{
|
||||||
|
float CenterX, CenterY;
|
||||||
|
float Radius;
|
||||||
|
float BaseDensity;
|
||||||
|
float BoundXYRadiusSq;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The complete SDF cache for a chunk region.
|
||||||
|
// Built once per chunk by BuildChunkCache(), then passed to EvaluateSDFCached()
|
||||||
|
// for every voxel in the chunk. Typically contains 5-15 rooms and 10-30 tunnels.
|
||||||
|
struct FChunkSDFCache
|
||||||
|
{
|
||||||
|
TArray<FCachedRoom> Rooms;
|
||||||
|
TArray<FCachedTunnel> Tunnels;
|
||||||
|
TArray<FCachedPit> Pits;
|
||||||
|
TArray<FCachedChimney> Chimneys;
|
||||||
|
TArray<FCachedColumn> Columns;
|
||||||
|
};
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// CAVE MORPHOLOGY EVALUATOR
|
||||||
|
//=============================================================================
|
||||||
|
// Two-phase evaluation: BuildChunkCache (once per chunk) + EvaluateSDFCached (per voxel).
|
||||||
|
// The original EvaluateSDF is kept as a convenience wrapper for backward compatibility.
|
||||||
|
|
||||||
|
namespace VoxelCaveMorphology
|
||||||
|
{
|
||||||
|
// PHASE 1: Build the SDF cache for a rectangular region.
|
||||||
|
// Collects all rooms, computes nearest-neighbor backbone, decides tunnel
|
||||||
|
// connections, and pre-computes all tunnel geometry (radii, offsets, midpoints).
|
||||||
|
// Also hash-rolls a terrain op per room from the optional probability pool.
|
||||||
|
//
|
||||||
|
// Call once per chunk before the voxel loop. The region bounds should cover
|
||||||
|
// the chunk's XY extent PLUS CaveWarpStrength (warped queries can shift)
|
||||||
|
// PLUS a small margin for gradient normal sampling (~2 voxels).
|
||||||
|
// MaxInfluence (room/tunnel reach) is computed internally from Params.
|
||||||
|
//
|
||||||
|
// @param OutCache — filled with rooms and tunnels for this region
|
||||||
|
// @param SearchMinX/Y, SearchMaxX/Y — XY world bounds to search (expanded by caller)
|
||||||
|
// @param Params — strate generation parameters
|
||||||
|
// @param Seed — world seed
|
||||||
|
// @param StrateIndex — which strate (offsets hash for variety)
|
||||||
|
// @param TerrainOps — optional probability pool; null = no per-room ops
|
||||||
|
void BuildChunkCache(
|
||||||
|
FChunkSDFCache& OutCache,
|
||||||
|
float SearchMinX, float SearchMinY,
|
||||||
|
float SearchMaxX, float SearchMaxY,
|
||||||
|
const FStrateGenerationParams& Params,
|
||||||
|
uint32 Seed, int32 StrateIndex,
|
||||||
|
const TArray<FStrateTerrainOpEntry>* TerrainOps = nullptr
|
||||||
|
);
|
||||||
|
|
||||||
|
// PHASE 2: Evaluate the SDF at a single world position using cached data.
|
||||||
|
// Loops through cached rooms and tunnels, computes SDF primitives, applies
|
||||||
|
// SmoothMin. Includes per-room/tunnel distance culling to skip primitives
|
||||||
|
// that are too far to contribute.
|
||||||
|
//
|
||||||
|
// @param WorldX, WorldY, WorldZ — position in voxel coordinates (may be warped)
|
||||||
|
// @param Cache — pre-built cache from BuildChunkCache
|
||||||
|
// @param SDFBlendRadius — SmoothMin blend radius (from Params.SDFBlendRadius)
|
||||||
|
// @param RoomShapeVariety — shape variety factor (from Params.RoomShapeVariety)
|
||||||
|
// @param OutNearestRoomIdx — optional out: index of the room with minimum SDF
|
||||||
|
// contribution. -1 if no room passed the cull test.
|
||||||
|
// Used by the terrain ops system to look up the
|
||||||
|
// per-room terrain op assigned to this voxel's room.
|
||||||
|
// @return negative = inside cave, positive = solid rock
|
||||||
|
float EvaluateSDFCached(
|
||||||
|
float WorldX, float WorldY, float WorldZ,
|
||||||
|
const FChunkSDFCache& Cache,
|
||||||
|
float SDFBlendRadius,
|
||||||
|
float RoomShapeVariety,
|
||||||
|
int32* OutNearestRoomIdx = nullptr
|
||||||
|
);
|
||||||
|
|
||||||
|
// CONVENIENCE WRAPPER: builds a temporary cache and evaluates in one call.
|
||||||
|
// Use this for one-off queries (debug visualization, single-point sampling).
|
||||||
|
// For chunk generation, use BuildChunkCache + EvaluateSDFCached instead.
|
||||||
|
float EvaluateSDF(
|
||||||
|
float WorldX, float WorldY, float WorldZ,
|
||||||
|
const FStrateGenerationParams& Params,
|
||||||
|
uint32 Seed, int32 StrateIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// VoxelChunk.h
|
||||||
|
// Identifiant léger de chunk.
|
||||||
|
//
|
||||||
|
// Rôle: dans un monde density-only (pas de blocs), le chunk n'a plus rien
|
||||||
|
// à stocker — la densité est évaluée à la volée par le générateur à partir
|
||||||
|
// des coordonnées monde. On garde un struct fin pour:
|
||||||
|
// - Servir de clé/valeur dans les collections de AVoxelWorld (Chunks, FChunkResult)
|
||||||
|
// - Fournir l'helper GetWorldPosition() au mesher
|
||||||
|
// - Laisser une place si on veut cacher des infos par chunk plus tard
|
||||||
|
// (index de strate, LOD courant, etc.)
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "VoxelTypes.h"
|
||||||
|
#include "VoxelChunk.generated.h"
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FVoxelChunk
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
// Coordonnée de chunk dans la grille mondiale (peut être négative).
|
||||||
|
FIntVector ChunkCoord = FIntVector::ZeroValue;
|
||||||
|
|
||||||
|
FVoxelChunk() = default;
|
||||||
|
explicit FVoxelChunk(const FIntVector& InCoord) : ChunkCoord(InCoord) {}
|
||||||
|
|
||||||
|
// Coin (0,0,0) du chunk en espace monde (cm).
|
||||||
|
FVector GetWorldPosition() const
|
||||||
|
{
|
||||||
|
return ChunkToWorldPos(ChunkCoord);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// VoxelContentManager.h
|
||||||
|
// Per-chunk content population: deterministic decoration/actor scatter from the
|
||||||
|
// strate content pools, plus self-contained aesthetic water surfaces.
|
||||||
|
//
|
||||||
|
// LIFECYCLE (driven by AVoxelWorld on the GAME THREAD):
|
||||||
|
// - PopulateChunk(coord, meshdata) after a chunk's mesh is applied
|
||||||
|
// - ClearChunk(coord) when a chunk unloads / is re-meshed
|
||||||
|
// - ClearAll() on regenerate / season reset
|
||||||
|
//
|
||||||
|
// DETERMINISM: every placement decision is a pure hash of (chunk, surface index,
|
||||||
|
// entry index, seed), so the same world re-populates identically. Spawning itself
|
||||||
|
// must run on the game thread (UWorld::SpawnActor), which it does — ApplyMeshToChunk
|
||||||
|
// is game-thread.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "VoxelTypes.h"
|
||||||
|
#include "VoxelContentManager.generated.h"
|
||||||
|
|
||||||
|
class UVoxelStrateManager;
|
||||||
|
class UVoxelStrateDefinition;
|
||||||
|
class UStaticMesh;
|
||||||
|
class UStaticMeshComponent;
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class VOXELFORGE_API UVoxelContentManager : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Wire up services. Owner is the AVoxelWorld actor that owns spawned content. */
|
||||||
|
void Initialize(AActor* InOwner, UVoxelStrateManager* InStrateManager, int32 InSeed);
|
||||||
|
|
||||||
|
/** Update the seed used for placement hashing (season reset). */
|
||||||
|
void SetSeed(int32 InSeed) { Seed = InSeed; }
|
||||||
|
|
||||||
|
/** Populate decorations + water for a chunk. Clears any previous content first.
|
||||||
|
* Decorations are only scattered at LOD 0 (near chunks); water is placed at any LOD. */
|
||||||
|
void PopulateChunk(const FIntVector& ChunkCoord, const FVoxelMeshData& MeshData, int32 LODLevel = 0);
|
||||||
|
|
||||||
|
/** Destroy all spawned content (actors + water plane) for one chunk. */
|
||||||
|
void ClearChunk(const FIntVector& ChunkCoord);
|
||||||
|
|
||||||
|
/** Destroy all spawned content for every chunk. */
|
||||||
|
void ClearAll();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void SpawnDecorations(const FIntVector& ChunkCoord, const FVoxelMeshData& MeshData,
|
||||||
|
const UVoxelStrateDefinition* Def, TArray<TWeakObjectPtr<AActor>>& Out);
|
||||||
|
void SpawnWater(const FIntVector& ChunkCoord, const UVoxelStrateDefinition* Def);
|
||||||
|
|
||||||
|
TWeakObjectPtr<AActor> Owner;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
UVoxelStrateManager* StrateManager = nullptr;
|
||||||
|
|
||||||
|
int32 Seed = 0;
|
||||||
|
|
||||||
|
// Engine unit plane (/Engine/BasicShapes/Plane) reused for every water surface.
|
||||||
|
UPROPERTY()
|
||||||
|
UStaticMesh* PlaneMesh = nullptr;
|
||||||
|
|
||||||
|
// Spawned decoration/ambient actors per chunk (weak — they live in the level).
|
||||||
|
TMap<FIntVector, TArray<TWeakObjectPtr<AActor>>> SpawnedActors;
|
||||||
|
|
||||||
|
// Water surface component per chunk.
|
||||||
|
UPROPERTY()
|
||||||
|
TMap<FIntVector, UStaticMeshComponent*> WaterPlanes;
|
||||||
|
};
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
// VoxelDiffLayer.h
|
||||||
|
// Runtime terrain modification system (player carving & filling).
|
||||||
|
//
|
||||||
|
// HOW IT WORKS:
|
||||||
|
// =============
|
||||||
|
// The procedural density function generates cave terrain deterministically.
|
||||||
|
// When the player carves or fills terrain, we DON'T modify the procedural
|
||||||
|
// output — instead, we store modifications as a "diff layer" on top.
|
||||||
|
//
|
||||||
|
// During density evaluation:
|
||||||
|
// Final density = Procedural density + Diff offset
|
||||||
|
//
|
||||||
|
// This keeps procedural generation deterministic while supporting freeform editing.
|
||||||
|
// Modifications are stored spatially (per-chunk) for O(1) lookup.
|
||||||
|
//
|
||||||
|
// MODIFICATION LIFECYCLE:
|
||||||
|
// 1. Player uses a tool at position P with radius R and strength S
|
||||||
|
// 2. ApplyModification() stores the mod in all overlapping chunks
|
||||||
|
// 3. Returns the list of affected chunk coords → world re-meshes those chunks
|
||||||
|
// 4. During re-meshing, GetDensityOffset() returns the combined diff at each voxel
|
||||||
|
//
|
||||||
|
// PERSISTENCE:
|
||||||
|
// Diffs are stored in memory. The game layer is responsible for saving/loading
|
||||||
|
// them to disk (server-side) and clearing them on season reset.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "VoxelTypes.h"
|
||||||
|
#include "VoxelDiffLayer.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EVoxelBrushShape — geometry of a modification brush.
|
||||||
|
* Sphere : Center + Radius. Classic round brush.
|
||||||
|
* Box : Center + BoxExtent (half-extents) + Falloff band.
|
||||||
|
* Capsule : segment Center→CapsuleEnd + Radius (tube) + Falloff band. Good for tunnels.
|
||||||
|
*/
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EVoxelBrushShape : uint8
|
||||||
|
{
|
||||||
|
Sphere UMETA(DisplayName = "Sphere"),
|
||||||
|
Box UMETA(DisplayName = "Box"),
|
||||||
|
Capsule UMETA(DisplayName = "Capsule")
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FVoxelModification — A single edit to the density field.
|
||||||
|
*
|
||||||
|
* Represents one brush stroke at a world position. Multiple overlapping
|
||||||
|
* modifications combine additively.
|
||||||
|
*
|
||||||
|
* Strength convention (INTERNAL density, before MC negate):
|
||||||
|
* Negative → subtract density → create air → CARVE (remove rock)
|
||||||
|
* Positive → add density → create solid → FILL (add rock)
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct VOXELFORGE_API FVoxelModification
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
// World-space center of the brush (endpoint A for Capsule).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification")
|
||||||
|
FVector Center = FVector::ZeroVector;
|
||||||
|
|
||||||
|
// Sphere radius / capsule tube radius, in voxels.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification", meta = (ClampMin = "0.5"))
|
||||||
|
float Radius = 3.0f;
|
||||||
|
|
||||||
|
// Density change: negative = carve (remove rock), positive = fill (add rock).
|
||||||
|
// -10 → strong carve (punches through BaseDensity=8) · +10 → strong fill
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification")
|
||||||
|
float Strength = -10.0f;
|
||||||
|
|
||||||
|
// Brush geometry.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification")
|
||||||
|
EVoxelBrushShape Shape = EVoxelBrushShape::Sphere;
|
||||||
|
|
||||||
|
// Box half-extents in voxels (Box shape only).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification",
|
||||||
|
meta = (EditCondition = "Shape == EVoxelBrushShape::Box"))
|
||||||
|
FVector BoxExtent = FVector(5.0f, 5.0f, 5.0f);
|
||||||
|
|
||||||
|
// Capsule second endpoint in world voxel coords (Capsule shape only).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification",
|
||||||
|
meta = (EditCondition = "Shape == EVoxelBrushShape::Capsule"))
|
||||||
|
FVector CapsuleEnd = FVector::ZeroVector;
|
||||||
|
|
||||||
|
// Soft falloff band (voxels) around the Box/Capsule surface for smooth edges.
|
||||||
|
// Sphere ignores this — its whole Radius is the falloff.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification", meta = (ClampMin = "0.1"))
|
||||||
|
float Falloff = 2.0f;
|
||||||
|
|
||||||
|
// Conservative world-space AABB the brush can affect (used to find overlapping chunks).
|
||||||
|
void GetWorldBounds(FVector& OutMin, FVector& OutMax) const
|
||||||
|
{
|
||||||
|
switch (Shape)
|
||||||
|
{
|
||||||
|
case EVoxelBrushShape::Box:
|
||||||
|
{
|
||||||
|
const FVector E = BoxExtent + FVector(Falloff);
|
||||||
|
OutMin = Center - E;
|
||||||
|
OutMax = Center + E;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EVoxelBrushShape::Capsule:
|
||||||
|
{
|
||||||
|
const float Pad = Radius + Falloff;
|
||||||
|
OutMin = FVector(FMath::Min(Center.X, CapsuleEnd.X), FMath::Min(Center.Y, CapsuleEnd.Y), FMath::Min(Center.Z, CapsuleEnd.Z)) - FVector(Pad);
|
||||||
|
OutMax = FVector(FMath::Max(Center.X, CapsuleEnd.X), FMath::Max(Center.Y, CapsuleEnd.Y), FMath::Max(Center.Z, CapsuleEnd.Z)) + FVector(Pad);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EVoxelBrushShape::Sphere:
|
||||||
|
default:
|
||||||
|
OutMin = Center - FVector(Radius);
|
||||||
|
OutMax = Center + FVector(Radius);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UVoxelDiffLayer — Stores all player modifications as a spatial diff.
|
||||||
|
*
|
||||||
|
* Owned by AVoxelWorld, passed to the generator for density evaluation.
|
||||||
|
* Modifications are grouped by chunk coord for fast spatial lookup —
|
||||||
|
* when evaluating density for a chunk, we only check modifications
|
||||||
|
* stored in that chunk (no global scan).
|
||||||
|
*
|
||||||
|
* When a modification sphere overlaps multiple chunks, it's stored in ALL
|
||||||
|
* of them so each chunk's lookup is self-contained.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class VOXELFORGE_API UVoxelDiffLayer : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
//=========================================================================
|
||||||
|
// BUDGET CONFIGURATION
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the budget limits for player modifications.
|
||||||
|
* Called by AVoxelWorld during BeginPlay from VoxelSettings values.
|
||||||
|
* A value of 0 means unlimited (no cap).
|
||||||
|
*
|
||||||
|
* @param InMaxMods - Max number of operations (0 = unlimited)
|
||||||
|
* @param InMaxRadius - Max brush radius per operation
|
||||||
|
* @param InMaxVolume - Max cumulative brush volume in cubic voxels (0 = unlimited)
|
||||||
|
*/
|
||||||
|
void SetBudget(int32 InMaxMods, float InMaxRadius, float InMaxVolume);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a modification would be allowed under the current budget.
|
||||||
|
* Does NOT consume budget — use this for UI feedback (greyed-out tool, etc.)
|
||||||
|
*
|
||||||
|
* @param Radius - The brush radius the player wants to use
|
||||||
|
* @return true if the modification is allowed
|
||||||
|
*/
|
||||||
|
bool CanModify(float Radius) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get remaining modification count (how many more operations the player can do).
|
||||||
|
* Returns -1 if unlimited.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Voxel Diff Layer")
|
||||||
|
int32 GetRemainingModifications() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get remaining volume budget (cubic voxels of modification still allowed).
|
||||||
|
* Returns -1.0 if unlimited.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Voxel Diff Layer")
|
||||||
|
float GetRemainingVolume() const;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// APPLY MODIFICATIONS
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a carve or fill operation.
|
||||||
|
*
|
||||||
|
* Stores the modification in every chunk the brush sphere overlaps.
|
||||||
|
* Returns the list of affected chunk coordinates so the world knows
|
||||||
|
* which chunks need re-meshing.
|
||||||
|
*
|
||||||
|
* Budget enforcement: if the modification would exceed any configured
|
||||||
|
* limit (count, radius, or volume), it is REJECTED and an empty array
|
||||||
|
* is returned. Check CanModify() first for UI feedback.
|
||||||
|
*
|
||||||
|
* @param Mod - The modification to apply
|
||||||
|
* @return Array of chunk coordinates that need re-meshing (empty if rejected)
|
||||||
|
*/
|
||||||
|
TArray<FIntVector> ApplyModification(const FVoxelModification& Mod);
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// DENSITY EVALUATION
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the combined density offset from all modifications at a world position.
|
||||||
|
*
|
||||||
|
* Called per-voxel by the generator during density evaluation.
|
||||||
|
* Only checks modifications stored for the given chunk coordinate,
|
||||||
|
* so it's fast when most chunks have no modifications.
|
||||||
|
*
|
||||||
|
* Each modification contributes with a smoothstep falloff from center to edge.
|
||||||
|
* Multiple overlapping modifications combine additively.
|
||||||
|
*
|
||||||
|
* @param ChunkCoord - The chunk being evaluated (for spatial lookup)
|
||||||
|
* @param WorldX, WorldY, WorldZ - The voxel position
|
||||||
|
* @return Total density offset (negative = carve, positive = fill)
|
||||||
|
*/
|
||||||
|
float GetDensityOffset(const FIntVector& ChunkCoord,
|
||||||
|
float WorldX, float WorldY, float WorldZ) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a chunk has any modifications (fast reject for hot path).
|
||||||
|
*
|
||||||
|
* @param ChunkCoord - The chunk to check
|
||||||
|
* @return true if there are modifications affecting this chunk
|
||||||
|
*/
|
||||||
|
bool HasModifications(const FIntVector& ChunkCoord) const;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// MANAGEMENT
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/** Clear ALL modifications (e.g., on season reset). */
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
/** Get total number of stored modification entries (for debug/stats). */
|
||||||
|
int32 GetTotalModificationCount() const;
|
||||||
|
|
||||||
|
/** Get number of chunks that have modifications. */
|
||||||
|
int32 GetModifiedChunkCount() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
//=========================================================================
|
||||||
|
// STORAGE
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Modifications grouped by chunk coordinate.
|
||||||
|
// Each chunk's list contains ALL modifications that overlap it
|
||||||
|
// (including mods centered in neighboring chunks whose radius reaches here).
|
||||||
|
TMap<FIntVector, TArray<FVoxelModification>> ChunkMods;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// BUDGET TRACKING
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Configured limits (0 = unlimited)
|
||||||
|
int32 BudgetMaxMods = 0;
|
||||||
|
float BudgetMaxRadius = 50.0f;
|
||||||
|
float BudgetMaxVolume = 0.0f;
|
||||||
|
|
||||||
|
// Running totals — reset when Clear() is called
|
||||||
|
int32 ModificationCount = 0; // Total operations applied
|
||||||
|
float AccumulatedVolume = 0.0f; // Sum of all brush sphere volumes (4/3 * PI * R^3)
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// VoxelForgeModule.h
|
||||||
|
// Module interface - this is boilerplate that every Unreal plugin needs
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FVoxelForgeModule
|
||||||
|
*
|
||||||
|
* This is the module class for VoxelForge. Every Unreal plugin needs one.
|
||||||
|
* It handles startup and shutdown of the plugin.
|
||||||
|
*
|
||||||
|
* For our purposes, this is just boilerplate - the real code is elsewhere.
|
||||||
|
*/
|
||||||
|
class FVoxelForgeModule : public IModuleInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Called when the plugin loads (game/editor starts)
|
||||||
|
virtual void StartupModule() override;
|
||||||
|
|
||||||
|
// Called when the plugin unloads (game/editor closes)
|
||||||
|
virtual void ShutdownModule() override;
|
||||||
|
};
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
// VoxelGenerator.h
|
||||||
|
// Fournit le champ de densité du monde (positif = solide, négatif = air).
|
||||||
|
//
|
||||||
|
// Pipeline density-only: plus de grille de blocs, plus de surface heightfield.
|
||||||
|
// Le mesher appelle GetDensityAt() par voxel pour reconstruire la géométrie.
|
||||||
|
// Toute la logique de caves vit dans GetDensityWithParams / GetSlabDensity,
|
||||||
|
// pilotée par les params de strate récupérés auprès du StrateManager.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "VoxelTypes.h"
|
||||||
|
#include "VoxelStrateTypes.h"
|
||||||
|
#include "VoxelGenerator.generated.h"
|
||||||
|
|
||||||
|
// Forward decls (évite les includes transitifs)
|
||||||
|
class UVoxelSettings;
|
||||||
|
class UVoxelStrateManager;
|
||||||
|
class UVoxelDiffLayer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UVoxelGenerator
|
||||||
|
*
|
||||||
|
* Objet UObject léger — ne stocke pas de données lourdes.
|
||||||
|
* Tient juste un pointeur sur les settings (pour le seed) et les services
|
||||||
|
* dont il a besoin (StrateManager pour les params par chunk, DiffLayer pour
|
||||||
|
* les carves du joueur).
|
||||||
|
*/
|
||||||
|
UCLASS(BlueprintType)
|
||||||
|
class VOXELFORGE_API UVoxelGenerator : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
//=========================================================================
|
||||||
|
// SEED (source unique: Settings->Seed)
|
||||||
|
//=========================================================================
|
||||||
|
// On copie juste le seed au démarrage pour éviter un déréférencement
|
||||||
|
// par voxel. Tout le reste vient des params de strate.
|
||||||
|
int32 Seed = 0;
|
||||||
|
|
||||||
|
// Radius (voxels) of the guaranteed open vertical landing column carved at world
|
||||||
|
// XY (0,0) in every strate — the (0,0) descent spine. Copied from VoxelSettings.
|
||||||
|
// 0 disables the spine carve.
|
||||||
|
float OriginSpineRadius = 14.0f;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// SERVICES (injectés par AVoxelWorld::BeginPlay)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Manager des strates — fournit les params de cave par chunk.
|
||||||
|
// Peut être nullptr: dans ce cas on utilise des params par défaut.
|
||||||
|
UPROPERTY()
|
||||||
|
const UVoxelStrateManager* StrateManager = nullptr;
|
||||||
|
|
||||||
|
// Diff layer des modifications joueur (carve/fill).
|
||||||
|
// Peut être nullptr: dans ce cas pas de modifs appliquées.
|
||||||
|
UPROPERTY()
|
||||||
|
const UVoxelDiffLayer* DiffLayer = nullptr;
|
||||||
|
|
||||||
|
void SetStrateManager(const UVoxelStrateManager* InManager) { StrateManager = InManager; }
|
||||||
|
void SetDiffLayer(const UVoxelDiffLayer* InDiffLayer) { DiffLayer = InDiffLayer; }
|
||||||
|
|
||||||
|
// Copie le seed depuis les settings. Appelé au démarrage et lors d'un ChangeSeed.
|
||||||
|
void InitializeSettings(const UVoxelSettings* Settings);
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// API DE DENSITÉ
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Densité combinée (strates + diff du joueur).
|
||||||
|
*
|
||||||
|
* Convention de sortie (MC): négatif = solide, positif = air.
|
||||||
|
* C'est ce que le marching cubes attend comme champ scalaire.
|
||||||
|
*
|
||||||
|
* @param WorldX, WorldY, WorldZ - coordonnées monde en voxels (pas cm)
|
||||||
|
*/
|
||||||
|
float GetDensityAt(float WorldX, float WorldY, float WorldZ) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Densité pour une strate TunnelNetwork (rooms + tunnels + worm noise).
|
||||||
|
* Utilisée en interne par GetDensityAt quand la strate est de ce type.
|
||||||
|
* Exposée pour permettre des tests isolés avec des params custom.
|
||||||
|
*/
|
||||||
|
float GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
|
||||||
|
const FStrateGenerationParams& Params) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Densité pour une strate Slab (FlatPlain / CrystalChamber).
|
||||||
|
* Vide horizontal entre un sol bruité et un plafond bruité.
|
||||||
|
*/
|
||||||
|
float GetSlabDensity(float WorldX, float WorldY, float WorldZ,
|
||||||
|
const FSlabGenerationParams& Params) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Densité pour une strate Maze — couloirs étroits sur un treillis 3D déterministe.
|
||||||
|
* Pas de cache: évalué par voxel sur les quelques cellules voisines.
|
||||||
|
*/
|
||||||
|
float GetMazeDensity(float WorldX, float WorldY, float WorldZ,
|
||||||
|
const FMazeGenerationParams& Params) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Densité pour une strate SurfaceWorld — terrain à ciel ouvert (collines,
|
||||||
|
* montagnes, plages) sous un plafond solide, avec nappe d'eau optionnelle.
|
||||||
|
*/
|
||||||
|
float GetSurfaceDensity(float WorldX, float WorldY, float WorldZ,
|
||||||
|
const FSurfaceGenerationParams& Params) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Densité pour une strate VerticalShafts — puits verticaux pleine hauteur,
|
||||||
|
* vires horizontales, et connecteurs horizontaux occasionnels.
|
||||||
|
*/
|
||||||
|
float GetVerticalShaftDensity(float WorldX, float WorldY, float WorldZ,
|
||||||
|
const FVerticalShaftParams& Params) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Densité pour une strate FloatingIslands — masses de terre suspendues dans
|
||||||
|
* un grand vide ouvert (îles flottantes), sommets aplatis, dessous rugueux.
|
||||||
|
*/
|
||||||
|
float GetFloatingIslandDensity(float WorldX, float WorldY, float WorldZ,
|
||||||
|
const FFloatingIslandParams& Params) const;
|
||||||
|
};
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// VoxelMarchingCubesMesher.h
|
||||||
|
// Mesh lisse à partir du champ de densité du générateur.
|
||||||
|
//
|
||||||
|
// Principe:
|
||||||
|
// 1. Chaque cellule du chunk (cube 2x2x2 voxels) lit la densité aux 8 coins.
|
||||||
|
// 2. Un index 8 bits indique quels coins sont au-dessus du IsoLevel.
|
||||||
|
// 3. Les tables MC (EdgeTable + TriTable) donnent les triangles à générer.
|
||||||
|
// 4. On interpole la position du vertex le long de chaque arête traversée.
|
||||||
|
//
|
||||||
|
// LOD: Step > 1 échantillonne moins souvent → moins de triangles à distance.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc.
|
||||||
|
#include "VoxelChunk.h"
|
||||||
|
#include "VoxelGenerator.h"
|
||||||
|
#include "VoxelMarchingCubesMesher.generated.h"
|
||||||
|
|
||||||
|
UCLASS(BlueprintType)
|
||||||
|
class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Génère le mesh d'un chunk.
|
||||||
|
*
|
||||||
|
* @param Chunk - Le chunk à mesher (on n'utilise que ChunkCoord pour
|
||||||
|
* calculer les positions monde; la densité vient du générateur).
|
||||||
|
* @param Step - Pas d'échantillonnage (1=LOD0, 2=LOD1, 4=LOD2).
|
||||||
|
*/
|
||||||
|
FVoxelMeshData GenerateMesh(const FVoxelChunk& Chunk, int32 Step = 1);
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// SERVICES (injectés par AVoxelWorld)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Le générateur fournit la densité par voxel.
|
||||||
|
UPROPERTY()
|
||||||
|
const UVoxelGenerator* Generator = nullptr;
|
||||||
|
|
||||||
|
void SetGenerator(const UVoxelGenerator* InGenerator) { Generator = InGenerator; }
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// SETTINGS
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Seuil de l'isosurface dans le champ de densité.
|
||||||
|
// Convention MC: densité < IsoLevel = solide, >= = air.
|
||||||
|
float IsoLevel = 0.0f;
|
||||||
|
|
||||||
|
// Distance d'échantillonnage (en voxels) pour calculer la normale par
|
||||||
|
// différence centrée du gradient. Plus petit = plus détaillé mais bruité.
|
||||||
|
float GradientOffset = 1.0f;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
//=========================================================================
|
||||||
|
// DENSITY + NORMAL SAMPLING
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Lit la densité à une position locale (via le générateur en coords monde).
|
||||||
|
float GetDensity(const FVoxelChunk& Chunk, int32 X, int32 Y, int32 Z) const;
|
||||||
|
|
||||||
|
// Normale lissée: gradient central du champ de densité (pointe solide→air).
|
||||||
|
FVector ComputeGradientNormal(float WorldX, float WorldY, float WorldZ) const;
|
||||||
|
|
||||||
|
// Interpolation linéaire le long d'une arête: trouve où la surface
|
||||||
|
// traverse entre P1 (densité D1) et P2 (densité D2).
|
||||||
|
FVector InterpolateEdge(const FVector& P1, const FVector& P2, float D1, float D2) const;
|
||||||
|
};
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
// VoxelSettings.h
|
||||||
|
// Settings du plugin VoxelForge.
|
||||||
|
//
|
||||||
|
// Data asset unique (assigné sur AVoxelWorld) qui regroupe tous les tuning
|
||||||
|
// du monde: seed, streaming, LOD, pool de strates, budgets de carving.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Engine/DataAsset.h"
|
||||||
|
#include "VoxelStrateDefinition.h"
|
||||||
|
#include "VoxelSettings.generated.h"
|
||||||
|
|
||||||
|
UCLASS(BlueprintType)
|
||||||
|
class UVoxelSettings : public UPrimaryDataAsset
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// STREAMING (distance de vue)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
|
||||||
|
int32 ViewDistanceXY = 16;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
|
||||||
|
int32 ViewDistanceUp = 5;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
|
||||||
|
int32 ViewDistanceDown = 5;
|
||||||
|
|
||||||
|
// Nombre max de tâches de génération/mesh en parallèle.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
|
||||||
|
int32 MaxConcurrentTasks = 16;
|
||||||
|
|
||||||
|
// Nombre max de meshes appliqués par frame (évite les stutters d'upload GPU).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
|
||||||
|
int32 MaxMeshAppliesPerFrame = 4;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// LOD
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Distance en chunks pour LOD0 (pleine résolution, step=1).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|LOD")
|
||||||
|
int32 LOD0Distance = 4;
|
||||||
|
|
||||||
|
// Distance en chunks pour LOD1 (demi-résolution, step=2).
|
||||||
|
// Au-delà → LOD2 (quart-résolution, step=4).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|LOD")
|
||||||
|
int32 LOD1Distance = 8;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// RENDERING
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Matériau global par défaut (peut être override par strate).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Rendering")
|
||||||
|
UMaterialInterface* VoxelMaterial;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// STRATES
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Seed du monde. Pilote toute la génération procédurale.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates")
|
||||||
|
int32 Seed = 0;
|
||||||
|
|
||||||
|
// Numéro de saison (incrementé par ChangeSeed). Purement informatif côté gameplay.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates")
|
||||||
|
int32 CurrentSeason = 1;
|
||||||
|
|
||||||
|
// Pool de définitions de strates disponibles pour l'assignation aléatoire.
|
||||||
|
// Chaque définition a sa propre hauteur — les strates ne sont PAS uniformes.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates")
|
||||||
|
TArray<TSoftObjectPtr<UVoxelStrateDefinition>> StratePool;
|
||||||
|
|
||||||
|
// Strates fixées: {index → définition}. Ex: {5, CrystalCaverns} =
|
||||||
|
// la strate 5 est toujours CrystalCaverns. Les autres sont tirées du pool.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates")
|
||||||
|
TMap<int32, TSoftObjectPtr<UVoxelStrateDefinition>> FixedStrates;
|
||||||
|
|
||||||
|
// Nombre total de strates à générer vers le bas.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "1"))
|
||||||
|
int32 TotalStrates = 10;
|
||||||
|
|
||||||
|
// Vertical SEPARATION between consecutive strates, in chunks of solid bedrock.
|
||||||
|
// 0 = strates touch (just the seals between them). Higher = a thick rock layer
|
||||||
|
// between each biome that the player must dig through at (0,0), and that the
|
||||||
|
// auto-carved passages tunnel across.
|
||||||
|
// 0 → adjacent layers (default) · 1-2 → a real bedrock band · 4+ → deep separation
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "0", ClampMax = "16"))
|
||||||
|
int32 InterStrateGapChunks = 0;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// SPINE & INTER-STRATE CONNECTIONS (the (0,0) descent axis)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Radius (voxels) of the guaranteed open vertical "landing" column carved at
|
||||||
|
// world XY (0,0) in EVERY strate, regardless of archetype. This is the prepared
|
||||||
|
// space the player digs THROUGH the seal into when descending. 0 = disabled.
|
||||||
|
// 10-14 → cozy shaft (default) · 20+ → wide landing chamber
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Spine", meta = (ClampMin = "0.0"))
|
||||||
|
float OriginSpineRadius = 14.0f;
|
||||||
|
|
||||||
|
// Auto-open the very top seal at (0,0) so the world starts with a hole to the
|
||||||
|
// surface (the entry shaft). Lower boundaries are NOT auto-opened — the player
|
||||||
|
// digs those. Disable for a fully sealed world the player must breach from above.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Spine")
|
||||||
|
bool bOpenSurfaceEntry = true;
|
||||||
|
|
||||||
|
// NOTE: inter-strate tunnel count + shape (style, width, length, placement, worming,
|
||||||
|
// spiral, cascade) are now PER-STRATE — see UVoxelStrateDefinition::PassageConfig.
|
||||||
|
// Each strate controls its own descent tunnels to the layer below it.
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// CARVING (modifications du joueur)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Nombre max d'opérations de modification par saison. 0 = illimité.
|
||||||
|
// 500 = édition légère, 2000 = mining modéré, 0 = créatif.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Carving",
|
||||||
|
meta = (ClampMin = "0"))
|
||||||
|
int32 MaxModifications = 0;
|
||||||
|
|
||||||
|
// Rayon max d'un brush (en voxels). Empêche les édits géants.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Carving",
|
||||||
|
meta = (ClampMin = "1.0"))
|
||||||
|
float MaxBrushRadius = 15.0f;
|
||||||
|
|
||||||
|
// Volume total maximum (somme des volumes de brush). 0 = illimité.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Carving",
|
||||||
|
meta = (ClampMin = "0.0"))
|
||||||
|
float MaxTotalVolume = 0.0f;
|
||||||
|
};
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
// VoxelStrateDefinition.h
|
||||||
|
// Data asset defining everything about a single strate (layer) type.
|
||||||
|
//
|
||||||
|
// HOW TO USE:
|
||||||
|
// -----------
|
||||||
|
// 1. Right-click in Content Browser → Miscellaneous → Data Asset
|
||||||
|
// 2. Pick "VoxelStrateDefinition" as the class
|
||||||
|
// 3. Fill in the fields — cave shape, visuals, content lists, audio, gameplay tags
|
||||||
|
// 4. Reference it from VoxelSettings (StratePool or FixedStrates)
|
||||||
|
//
|
||||||
|
// Each asset is a strate TYPE (e.g., "CrystalCaverns").
|
||||||
|
// Multiple strate slots can use the same definition.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Engine/DataAsset.h"
|
||||||
|
#include "GameplayTagContainer.h"
|
||||||
|
#include "VoxelStrateTypes.h"
|
||||||
|
#include "VoxelStrateDefinition.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UVoxelStrateDefinition — The content bag for a strate type.
|
||||||
|
*
|
||||||
|
* This is the central piece of the strate system. Every field here
|
||||||
|
* defines what makes a strate unique: how its caves form, what it
|
||||||
|
* looks like, what lives in it, how it sounds, and what gameplay
|
||||||
|
* rules apply.
|
||||||
|
*
|
||||||
|
* EXAMPLE STRATES:
|
||||||
|
* - "Crystal Caverns": big open caves, blue fog, crystal decorations
|
||||||
|
* - "Tight Tunnels": narrow caves, dark, spider creatures
|
||||||
|
* - "Flooded Galleries": flat floors, water, diving required
|
||||||
|
*/
|
||||||
|
UCLASS(BlueprintType)
|
||||||
|
class VOXELFORGE_API UVoxelStrateDefinition : public UPrimaryDataAsset
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
//=========================================================================
|
||||||
|
// IDENTIFICATION
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Human-readable name for this strate type (shown in editor and debug)
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Strate")
|
||||||
|
FText StrateName;
|
||||||
|
|
||||||
|
// Short description for editor tooltips
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Strate", meta = (MultiLine = true))
|
||||||
|
FText StrateDescription;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// DIMENSIONS
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// How many chunks tall this strate is.
|
||||||
|
// Each strate can have a different height!
|
||||||
|
// Big open caverns = 6-8 chunks, tight tunnels = 3, vertical shaft = 12+
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (ClampMin = "1", ClampMax = "32"))
|
||||||
|
int32 StrateHeightInChunks = 4;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// BOUNDARY TRANSITION
|
||||||
|
//=========================================================================
|
||||||
|
// Controls how this strate blends with the strate BELOW it.
|
||||||
|
// Each boundary between two strates uses the UPPER strate's transition type.
|
||||||
|
//
|
||||||
|
// Gradient: Smooth param lerp — no visible boundary (default).
|
||||||
|
// Hard: Abrupt switch — creates a cliff/ledge at the boundary.
|
||||||
|
// Interleaved: 3D noise warps the boundary — fingers of one strate
|
||||||
|
// reach into the other for an organic, interlocking look.
|
||||||
|
//
|
||||||
|
// NOTE: The BOTTOM-MOST strate's transition type is unused (nothing below it).
|
||||||
|
|
||||||
|
// Which transition style to use at this strate's lower boundary
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
|
||||||
|
meta = (ToolTip = "How this strate transitions to the strate below it. Gradient = smooth blend, Hard = sharp cliff, Interleaved = noisy interlocking fingers."))
|
||||||
|
EVoxelStrateTransition TransitionType = EVoxelStrateTransition::Gradient;
|
||||||
|
|
||||||
|
// How many chunks the transition zone spans (used by Gradient and Interleaved).
|
||||||
|
// For Hard transitions this is ignored (effectively 0).
|
||||||
|
// Higher = wider, more gradual transition zone.
|
||||||
|
// 1 → very narrow transition (almost hard)
|
||||||
|
// 2 → moderate (good default)
|
||||||
|
// 4 → wide, gentle blend across many chunks
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
|
||||||
|
meta = (ClampMin = "1", ClampMax = "8", EditCondition = "TransitionType != EVoxelStrateTransition::Hard"))
|
||||||
|
int32 TransitionBlendChunks = 2;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// GENERATOR TYPE
|
||||||
|
//=========================================================================
|
||||||
|
// Controls which density strategy this strate uses.
|
||||||
|
//
|
||||||
|
// TunnelNetwork → classic rooms + tunnels + worm noise (FStrateGenerationParams below)
|
||||||
|
// FlatPlain → horizontal void between floor and ceiling (FSlabGenerationParams below)
|
||||||
|
// CrystalChamber → same as FlatPlain but ceiling has heavy downward formations
|
||||||
|
//
|
||||||
|
// Changing this shows/hides the relevant parameter sections below.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation")
|
||||||
|
ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// TUNNEL NETWORK PARAMS (shown only for TunnelNetwork generator type)
|
||||||
|
//=========================================================================
|
||||||
|
// These params control how the density field is shaped for chunks in this strate.
|
||||||
|
// The generator reads these values to produce unique cave geometry per layer.
|
||||||
|
// Only relevant when GeneratorType == TunnelNetwork.
|
||||||
|
|
||||||
|
// Used by TunnelNetwork (rooms+tunnels) AND Underwater (same rock, flooded).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation",
|
||||||
|
meta = (EditCondition = "GeneratorType == ECaveGeneratorType::TunnelNetwork || GeneratorType == ECaveGeneratorType::Underwater"))
|
||||||
|
FStrateGenerationParams GenerationParams;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// SLAB PARAMS (shown only for FlatPlain and CrystalChamber generator types)
|
||||||
|
//=========================================================================
|
||||||
|
// Controls the floor height, ceiling height, roughness, and column density
|
||||||
|
// for open-void strates. Only relevant when GeneratorType is FlatPlain or
|
||||||
|
// CrystalChamber. Other generator types ignore these entirely.
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation",
|
||||||
|
meta = (EditCondition = "GeneratorType == ECaveGeneratorType::FlatPlain || GeneratorType == ECaveGeneratorType::CrystalChamber"))
|
||||||
|
FSlabGenerationParams SlabParams;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// ARCHETYPE PARAMS (each shown only for its generator type)
|
||||||
|
//=========================================================================
|
||||||
|
// Tight branching corridors on a 3D lattice.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation",
|
||||||
|
meta = (EditCondition = "GeneratorType == ECaveGeneratorType::Maze"))
|
||||||
|
FMazeGenerationParams MazeParams;
|
||||||
|
|
||||||
|
// Open-sky terrain: hills/mountains/plains/beaches under a high ceiling, with water.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation",
|
||||||
|
meta = (EditCondition = "GeneratorType == ECaveGeneratorType::SurfaceWorld"))
|
||||||
|
FSurfaceGenerationParams SurfaceParams;
|
||||||
|
|
||||||
|
// Mostly-vertical shafts with ledges and sparse horizontal links.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation",
|
||||||
|
meta = (EditCondition = "GeneratorType == ECaveGeneratorType::VerticalShafts"))
|
||||||
|
FVerticalShaftParams VerticalShaftParams;
|
||||||
|
|
||||||
|
// Suspended land masses floating in a large open void.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation",
|
||||||
|
meta = (EditCondition = "GeneratorType == ECaveGeneratorType::FloatingIslands"))
|
||||||
|
FFloatingIslandParams FloatingIslandParams;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// DISTURBANCES (the "wow" layer — applies on top of ANY archetype)
|
||||||
|
//=========================================================================
|
||||||
|
// Chasms, natural bridges and rock ridges layered onto whatever the archetype
|
||||||
|
// produces, for surprise and variety. All features default to disabled.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Disturbances")
|
||||||
|
FStrateDisturbanceParams Disturbances;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// INTER-STRATE PASSAGES (how THIS strate connects DOWN to the next)
|
||||||
|
//=========================================================================
|
||||||
|
// Auto-carved descent tunnels from this strate to the one below it: count, style
|
||||||
|
// (straight / worm / spiral / cascade), tapered width, length, placement. The (0,0)
|
||||||
|
// spine descent is separate (player-dug); these are the extra shortcuts.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Passages")
|
||||||
|
FStratePassageConfig PassageConfig;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// WATER RENDERING (self-contained, aesthetic — no swim/flow simulation)
|
||||||
|
//=========================================================================
|
||||||
|
// The water table HEIGHT comes from the active generator params
|
||||||
|
// (FSurfaceGenerationParams::WaterLevelRelative or FStrateGenerationParams::
|
||||||
|
// WaterLevelRelative). These fields control whether/how it is rendered.
|
||||||
|
|
||||||
|
// Master switch for the water surface in this strate.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Water")
|
||||||
|
bool bHasWater = false;
|
||||||
|
|
||||||
|
// Translucent material applied to the generated water surface planes.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Water",
|
||||||
|
meta = (EditCondition = "bHasWater"))
|
||||||
|
UMaterialInterface* WaterMaterial = nullptr;
|
||||||
|
|
||||||
|
// Water tint/colour, available to the material as a parameter if it reads it.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Water",
|
||||||
|
meta = (EditCondition = "bHasWater"))
|
||||||
|
FLinearColor WaterColor = FLinearColor(0.02f, 0.08f, 0.12f, 0.8f);
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// TERRAIN OPERATIONS
|
||||||
|
//=========================================================================
|
||||||
|
// Weighted list of terrain operation data assets to apply in this strate.
|
||||||
|
//
|
||||||
|
// HOW TO USE:
|
||||||
|
// 1. Create terrain op assets (Content → Data Asset → VoxelTerrainOpDefinition)
|
||||||
|
// 2. Add entries here — pick an asset and set its Weight
|
||||||
|
// 3. Weight scales the op's intensity: 1.0 = as configured, 0.5 = half, 2.0 = double
|
||||||
|
//
|
||||||
|
// EXAMPLE:
|
||||||
|
// "Crystal Caverns" strate might reference:
|
||||||
|
// - DA_Op_TallColumns (Weight 1.0) → floor-to-ceiling pillars
|
||||||
|
// - DA_Op_WideDomes (Weight 0.8) → cathedral ceilings
|
||||||
|
// - DA_Op_LightScallop (Weight 0.5) → subtle erosion texture
|
||||||
|
//
|
||||||
|
// The same op asset can be shared across multiple strate definitions.
|
||||||
|
// At generation time, these are merged into FStrateGenerationParams
|
||||||
|
// before being passed to the density pipeline.
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Terrain Operations")
|
||||||
|
TArray<FStrateTerrainOpEntry> TerrainOperations;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// VISUALS
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Material override for this strate (null = use the default VoxelMaterial)
|
||||||
|
// Allows each strate to have its own rock/ground look
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Visuals")
|
||||||
|
UMaterialInterface* OverrideMaterial = nullptr;
|
||||||
|
|
||||||
|
// Fog color for this strate (used by a future fog system or post-process)
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Visuals")
|
||||||
|
FLinearColor FogColor = FLinearColor(0.05f, 0.05f, 0.1f, 1.0f);
|
||||||
|
|
||||||
|
// Fog density (0 = no fog, 1 = very thick)
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Visuals", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float FogDensity = 0.3f;
|
||||||
|
|
||||||
|
// Ambient light tint (color of the "base" underground light)
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Visuals")
|
||||||
|
FLinearColor AmbientLightColor = FLinearColor(0.1f, 0.1f, 0.15f, 1.0f);
|
||||||
|
|
||||||
|
// Ambient light brightness
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Visuals", meta = (ClampMin = "0.0"))
|
||||||
|
float AmbientLightIntensity = 0.5f;
|
||||||
|
|
||||||
|
// Render the managed height fog volumetrically (softer, light-shaft-friendly, costlier).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Visuals")
|
||||||
|
bool bVolumetricFog = false;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// ATMOSPHERE LAYERS (persistent ceiling / floor actors — e.g. seas of clouds)
|
||||||
|
//=========================================================================
|
||||||
|
// These spawn ONCE when the player enters this strate (NOT per chunk, so they
|
||||||
|
// don't churn with streaming) and follow the player in XY while hugging the
|
||||||
|
// strate's ceiling / floor. Perfect for the "sky island" look: open space
|
||||||
|
// between two cloud seas. Author the look as a Blueprint (a large plane with a
|
||||||
|
// cloud/fog material, or a Niagara system). Leave null to disable.
|
||||||
|
// This is also how you get "fog emitting from ceiling and floor".
|
||||||
|
|
||||||
|
// FULL atmosphere override: a Blueprint you author with your own ExponentialHeightFog,
|
||||||
|
// SkyLight, PostProcessVolume, SkyAtmosphere, etc. — tuned exactly how you want.
|
||||||
|
// When set, this REPLACES the simple Fog/Ambient knobs above for this strate (the
|
||||||
|
// plugin's managed fog + skylight are disabled so there's no double-up). The plugin
|
||||||
|
// spawns one instance when you enter the strate and keeps it anchored to the player.
|
||||||
|
// Leave null to use the simple knobs instead.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Atmosphere")
|
||||||
|
TSubclassOf<AActor> AtmosphereActor;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Atmosphere")
|
||||||
|
TSubclassOf<AActor> CeilingLayerActor;
|
||||||
|
|
||||||
|
// Vertical offset (Unreal units) from the strate ceiling. Negative = below the ceiling.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Atmosphere",
|
||||||
|
meta = (EditCondition = "CeilingLayerActor != nullptr"))
|
||||||
|
float CeilingLayerZOffset = -200.0f;
|
||||||
|
|
||||||
|
// Rotation applied to the ceiling layer actor each update.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Atmosphere",
|
||||||
|
meta = (EditCondition = "CeilingLayerActor != nullptr"))
|
||||||
|
FRotator CeilingLayerRotation = FRotator::ZeroRotator;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Atmosphere")
|
||||||
|
TSubclassOf<AActor> FloorLayerActor;
|
||||||
|
|
||||||
|
// Vertical offset (Unreal units) from the strate floor. Positive = above the floor.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Atmosphere",
|
||||||
|
meta = (EditCondition = "FloorLayerActor != nullptr"))
|
||||||
|
float FloorLayerZOffset = 200.0f;
|
||||||
|
|
||||||
|
// Rotation applied to the floor layer actor each update.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Atmosphere",
|
||||||
|
meta = (EditCondition = "FloorLayerActor != nullptr"))
|
||||||
|
FRotator FloorLayerRotation = FRotator::ZeroRotator;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// CONTENT LISTS
|
||||||
|
//=========================================================================
|
||||||
|
// These arrays define what gets spawned in this strate.
|
||||||
|
// Future systems (decoration placer, creature spawner) consume these lists.
|
||||||
|
|
||||||
|
// Decorations: actors placed ON cave surfaces (stalactites, mushrooms, crystals)
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Content")
|
||||||
|
TArray<FStrateDecoration> Decorations;
|
||||||
|
|
||||||
|
// Ambient actors: things floating in cave space (fog volumes, particles, lights)
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Content")
|
||||||
|
TArray<FStrateAmbientActor> AmbientActors;
|
||||||
|
|
||||||
|
// Creatures: enemies/NPCs that spawn in this strate
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Content")
|
||||||
|
TArray<FStrateCreature> Creatures;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// AUDIO
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Ambient sound loop for this strate (dripping water, wind, etc.)
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Audio")
|
||||||
|
USoundBase* AmbientSound = nullptr;
|
||||||
|
|
||||||
|
// Background music for this strate
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Audio")
|
||||||
|
USoundBase* Music = nullptr;
|
||||||
|
|
||||||
|
// Music volume multiplier
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Audio", meta = (ClampMin = "0.0", ClampMax = "2.0"))
|
||||||
|
float MusicVolume = 1.0f;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// GAMEPLAY TAGS
|
||||||
|
//=========================================================================
|
||||||
|
// Extensible tag system for gameplay rules.
|
||||||
|
// Game code checks these tags to apply effects (damage, movement restrictions, etc.)
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// "Strate.Hazard.Flooded" → water everywhere, need diving gear
|
||||||
|
// "Strate.Hazard.Toxic" → air damages without mask
|
||||||
|
// "Strate.Lighting.Dark" → almost no ambient light
|
||||||
|
// "Strate.Rule.NeedEquipment.Rope" → climbing areas
|
||||||
|
//
|
||||||
|
// The plugin doesn't act on these — it just stores them.
|
||||||
|
// Your game code reads them and applies the appropriate effects.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Gameplay")
|
||||||
|
FGameplayTagContainer GameplayTags;
|
||||||
|
};
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
// VoxelStrateManager.h
|
||||||
|
// Runtime system that maps world Z coordinates to strate definitions.
|
||||||
|
//
|
||||||
|
// HOW IT WORKS:
|
||||||
|
// -------------
|
||||||
|
// 1. At world startup, Initialize() builds the strate layout:
|
||||||
|
// - Fixed strates go into their assigned slots
|
||||||
|
// - Random strates are shuffled from a pool using the world seed
|
||||||
|
// - Each strate's height (in chunks) is accumulated to compute Z ranges
|
||||||
|
//
|
||||||
|
// 2. During gameplay, GetStrateAt(WorldZ) returns which strate definition
|
||||||
|
// applies at a given depth. GetGenerationParams() returns blended params
|
||||||
|
// (with smooth transitions at strate boundaries).
|
||||||
|
//
|
||||||
|
// 3. The generator, world manager, and future systems (decorations, creatures,
|
||||||
|
// audio) all query this manager to know "what goes here."
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "VoxelStrateTypes.h"
|
||||||
|
#include "VoxelStrateDefinition.h"
|
||||||
|
#include "VoxelStrateManager.generated.h"
|
||||||
|
|
||||||
|
class UVoxelSettings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FVoxelPassage — A navigable connection between two strates.
|
||||||
|
*
|
||||||
|
* Generated deterministically from the world seed during Initialize().
|
||||||
|
* Each passage is a path from a point in strate A down through the
|
||||||
|
* boundary seal to a point in strate B. It's carved as a series of
|
||||||
|
* capsule SDFs in the density function.
|
||||||
|
*
|
||||||
|
* Passages are the PRIMARY progression path — players find these
|
||||||
|
* through exploration. The elevator is a FAST TRAVEL shortcut unlocked later.
|
||||||
|
*/
|
||||||
|
USTRUCT()
|
||||||
|
struct FVoxelPassage
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
// Which strates this passage connects (indices into StrateLayout)
|
||||||
|
int32 UpperStrateIndex = 0; // The strate above
|
||||||
|
int32 LowerStrateIndex = 0; // The strate below
|
||||||
|
|
||||||
|
// World-space positions of the passage endpoints
|
||||||
|
FVector UpperPoint = FVector::ZeroVector; // Entry in upper strate
|
||||||
|
FVector LowerPoint = FVector::ZeroVector; // Exit in lower strate
|
||||||
|
|
||||||
|
// An optional midpoint for non-straight passages (sloped, curved).
|
||||||
|
// Used by SlopedTunnel and CrackCrevice types. Ignored when ControlPoints is populated.
|
||||||
|
FVector MidPoint = FVector::ZeroVector;
|
||||||
|
|
||||||
|
// Passage dimensions — how wide the carved tunnel is (in voxels).
|
||||||
|
// Varies by type: VerticalShaft ~7-8, SpiralDescent ~4, CrackCrevice ~2-3, others ~5.
|
||||||
|
float Radius = 5.0f;
|
||||||
|
|
||||||
|
// Whether this passage uses a midpoint (curved/sloped) or is straight.
|
||||||
|
// Only relevant when ControlPoints is empty — if ControlPoints has entries,
|
||||||
|
// the passage is evaluated as a capsule chain along those points instead.
|
||||||
|
bool bHasMidPoint = false;
|
||||||
|
|
||||||
|
// The shape/style of this passage. Determines how control points are generated
|
||||||
|
// and how the passage feels to navigate (shaft, spiral, ledges, crack, etc.).
|
||||||
|
EVoxelPassageType PassageType = EVoxelPassageType::SlopedTunnel;
|
||||||
|
|
||||||
|
// Multi-segment control points for complex passage shapes.
|
||||||
|
// Used by SpiralDescent (helix points) and CascadingDrops (ledge+drop points).
|
||||||
|
// When non-empty, the SDF evaluator walks this array as a capsule chain
|
||||||
|
// instead of using Upper→Mid→Lower logic.
|
||||||
|
// Empty for simple types (SlopedTunnel, VerticalShaft, CrackCrevice).
|
||||||
|
TArray<FVector> ControlPoints;
|
||||||
|
|
||||||
|
// Per-control-point tube radius (parallel to ControlPoints) for tapered tunnels —
|
||||||
|
// wider chambers at the mouths, a squeeze in the middle, etc. If shorter than
|
||||||
|
// ControlPoints, the uniform Radius is used as a fallback.
|
||||||
|
TArray<float> ControlRadii;
|
||||||
|
|
||||||
|
// Bounding sphere enclosing the whole passage (+ radius + blend), in voxel coords.
|
||||||
|
// Computed once in GeneratePassages; lets EvaluateModifierSDF reject far voxels with
|
||||||
|
// a single squared-distance test instead of walking every segment per voxel.
|
||||||
|
FVector BoundCenter = FVector::ZeroVector;
|
||||||
|
float BoundRadiusSq = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FStrateSlot — Runtime info for one strate in the layout.
|
||||||
|
*
|
||||||
|
* Created during Initialize() and stored in the StrateLayout array.
|
||||||
|
* Each slot knows its definition, its Z-range in chunk coordinates,
|
||||||
|
* and its index in the sequence.
|
||||||
|
*/
|
||||||
|
USTRUCT()
|
||||||
|
struct FStrateSlot
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
// The strate definition asset (content bag)
|
||||||
|
UPROPERTY()
|
||||||
|
UVoxelStrateDefinition* Definition = nullptr;
|
||||||
|
|
||||||
|
// Strate index (0 = topmost, increases downward)
|
||||||
|
int32 StrateIndex = 0;
|
||||||
|
|
||||||
|
// Z chunk range (inclusive). TopZ > BottomZ since Z decreases downward.
|
||||||
|
// Example: TopZ = 0, BottomZ = -3 means this strate spans chunks Z=0 to Z=-3
|
||||||
|
int32 TopChunkZ = 0;
|
||||||
|
int32 BottomChunkZ = 0;
|
||||||
|
|
||||||
|
// Height in chunks (from the definition)
|
||||||
|
int32 HeightInChunks = 4;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UVoxelStrateManager — Maps depth to strate definitions at runtime.
|
||||||
|
*/
|
||||||
|
UCLASS(BlueprintType)
|
||||||
|
class VOXELFORGE_API UVoxelStrateManager : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
//=========================================================================
|
||||||
|
// INITIALIZATION
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the strate layout from settings and seed.
|
||||||
|
*
|
||||||
|
* STEPS:
|
||||||
|
* 1. For each strate index (0 to TotalStrates-1):
|
||||||
|
* - If it's in FixedStrates → use that definition
|
||||||
|
* - Else → pick from the shuffled pool (seeded random)
|
||||||
|
* 2. Stack strates top-to-bottom, accumulating heights
|
||||||
|
* 3. Store the layout in StrateLayout array
|
||||||
|
*
|
||||||
|
* @param Settings - VoxelSettings with pool/fixed strate config
|
||||||
|
* @param WorldSeed - Seed for randomizing non-fixed strates
|
||||||
|
*/
|
||||||
|
void Initialize(UVoxelSettings* Settings, int32 WorldSeed);
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// QUERIES
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the strate definition at a world Z coordinate.
|
||||||
|
*
|
||||||
|
* @param WorldZ - Z position in world space (Unreal units)
|
||||||
|
* @return The strate definition, or nullptr if above/below all strates
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Strate")
|
||||||
|
UVoxelStrateDefinition* GetStrateAt(float WorldZ) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the strate index at a world Z coordinate.
|
||||||
|
*
|
||||||
|
* @param WorldZ - Z position in world space
|
||||||
|
* @return Strate index (0 = topmost), or -1 if outside strate range
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Strate")
|
||||||
|
int32 GetStrateIndex(float WorldZ) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the strate definition for a specific chunk coordinate.
|
||||||
|
*
|
||||||
|
* @param ChunkCoord - The chunk position
|
||||||
|
* @return The strate definition, or nullptr if outside strate range
|
||||||
|
*/
|
||||||
|
UVoxelStrateDefinition* GetStrateForChunk(const FIntVector& ChunkCoord) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get generation params for a chunk, with boundary blending.
|
||||||
|
*
|
||||||
|
* BLENDING CONCEPT:
|
||||||
|
* When a chunk is near a strate boundary (within BlendChunks of the edge),
|
||||||
|
* the params are lerped between the two adjacent strates. This prevents
|
||||||
|
* hard visual seams where one strate ends and another begins.
|
||||||
|
*
|
||||||
|
* @param ChunkCoord - The chunk position
|
||||||
|
* @return Blended generation params for this chunk
|
||||||
|
*/
|
||||||
|
FStrateGenerationParams GetGenerationParams(const FIntVector& ChunkCoord) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the generator type for a specific chunk.
|
||||||
|
*
|
||||||
|
* Returns TunnelNetwork if the chunk is outside all strates or if the
|
||||||
|
* strate definition is null. The generator uses this to pick which
|
||||||
|
* density function to call (GetDensityWithParams vs GetSlabDensity).
|
||||||
|
*
|
||||||
|
* @param ChunkCoord - The chunk position
|
||||||
|
* @return The ECaveGeneratorType for the strate containing this chunk
|
||||||
|
*/
|
||||||
|
ECaveGeneratorType GetGeneratorTypeForChunk(const FIntVector& ChunkCoord) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if this chunk is in the solid-bedrock GAP between two strates (inside the
|
||||||
|
* overall stack's Z range but not in any strate slot). Chunks above the top strate
|
||||||
|
* or below the bottom strate are NOT gaps (they're open air). Driven by
|
||||||
|
* VoxelSettings::InterStrateGapChunks.
|
||||||
|
*/
|
||||||
|
bool IsGapChunk(const FIntVector& ChunkCoord) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get slab generation params for a chunk, with runtime Z bounds filled in.
|
||||||
|
*
|
||||||
|
* Used when GetGeneratorTypeForChunk returns FlatPlain or CrystalChamber.
|
||||||
|
* Does NOT blend between adjacent strates — slab strates use Hard transition
|
||||||
|
* at their boundaries (blending between fundamentally different generator types
|
||||||
|
* is not meaningful).
|
||||||
|
*
|
||||||
|
* @param ChunkCoord - The chunk position
|
||||||
|
* @return FSlabGenerationParams with StrateTopWorldZ / StrateBottomWorldZ set
|
||||||
|
*/
|
||||||
|
FSlabGenerationParams GetSlabParamsForChunk(const FIntVector& ChunkCoord) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-archetype param getters. Each copies the designer params from the strate
|
||||||
|
* definition and fills in the runtime Z bounds (voxel coords). Like the slab
|
||||||
|
* getter, these do NOT blend across boundaries — fundamentally different
|
||||||
|
* archetypes meet at Hard boundaries.
|
||||||
|
*/
|
||||||
|
FMazeGenerationParams GetMazeParamsForChunk(const FIntVector& ChunkCoord) const;
|
||||||
|
FSurfaceGenerationParams GetSurfaceParamsForChunk(const FIntVector& ChunkCoord) const;
|
||||||
|
FVerticalShaftParams GetVerticalShaftParamsForChunk(const FIntVector& ChunkCoord) const;
|
||||||
|
FFloatingIslandParams GetFloatingIslandParamsForChunk(const FIntVector& ChunkCoord) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* World-space Z (voxel coords) of this strate's water surface, or -FLT_MAX if the
|
||||||
|
* strate has no water. Derived from the active archetype's WaterLevelRelative and
|
||||||
|
* the strate's Z range. Used by the water render system.
|
||||||
|
*/
|
||||||
|
float GetWaterLevelWorldZForChunk(const FIntVector& ChunkCoord) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unreal-unit (cm) world Z range of the strate containing WorldZ (Unreal units).
|
||||||
|
* OutTopZ = ceiling, OutBottomZ = floor. Returns false if WorldZ is outside all strates.
|
||||||
|
* Used by the atmosphere system to glue ceiling/floor layers to the strate.
|
||||||
|
*/
|
||||||
|
bool GetStrateUnrealZRange(float WorldZ, float& OutTopZ, float& OutBottomZ) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disturbance params for a chunk (chasms/bridges/ridges), with runtime Z bounds
|
||||||
|
* filled in. Returns an all-disabled default when outside the strate range.
|
||||||
|
*/
|
||||||
|
FStrateDisturbanceParams GetDisturbanceParamsForChunk(const FIntVector& ChunkCoord) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the full strate layout (for debug display or UI).
|
||||||
|
*/
|
||||||
|
const TArray<FStrateSlot>& GetLayout() const { return StrateLayout; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total number of strates in the layout.
|
||||||
|
*/
|
||||||
|
int32 GetNumStrates() const { return StrateLayout.Num(); }
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// PASSAGES & ELEVATOR
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate the SDF for all passages and the elevator shaft at a world position.
|
||||||
|
* Called by the density function to carve these modifiers into the terrain.
|
||||||
|
*
|
||||||
|
* Returns: negative = inside a passage/shaft, positive = solid rock.
|
||||||
|
* FLT_MAX = no modifier nearby.
|
||||||
|
*/
|
||||||
|
float EvaluateModifierSDF(float WorldX, float WorldY, float WorldZ) const;
|
||||||
|
|
||||||
|
/** Get all generated passages (for debug display). */
|
||||||
|
const TArray<FVoxelPassage>& GetPassages() const { return Passages; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
//=========================================================================
|
||||||
|
// INTERNAL DATA
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// The stacked strate layout (index 0 = topmost strate)
|
||||||
|
UPROPERTY()
|
||||||
|
TArray<FStrateSlot> StrateLayout;
|
||||||
|
|
||||||
|
// Passages connecting consecutive strates
|
||||||
|
TArray<FVoxelPassage> Passages;
|
||||||
|
|
||||||
|
// How many chunks at strate boundaries are blended (transition zone)
|
||||||
|
int32 BlendChunks = 2;
|
||||||
|
|
||||||
|
// World seed (stored for passage generation)
|
||||||
|
int32 CachedSeed = 0;
|
||||||
|
|
||||||
|
// Whether to auto-open the (0,0) entry shaft through the top of strate 0.
|
||||||
|
// Copied from VoxelSettings::bOpenSurfaceEntry during Initialize().
|
||||||
|
bool bOpenSurfaceEntry = true;
|
||||||
|
|
||||||
|
// Radius (voxels) of the surface entry shaft at (0,0). Copied from
|
||||||
|
// VoxelSettings::OriginSpineRadius so the entry matches the spine landing.
|
||||||
|
float OriginSpineRadius = 14.0f;
|
||||||
|
|
||||||
|
// Solid-bedrock gap between consecutive strates, in chunks. Copied from
|
||||||
|
// VoxelSettings::InterStrateGapChunks during Initialize().
|
||||||
|
int32 InterStrateGapChunks = 0;
|
||||||
|
|
||||||
|
// Per-passage tunnel shape now lives on each UVoxelStrateDefinition::PassageConfig
|
||||||
|
// (the upper strate of each boundary controls its own descent tunnels).
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// HELPERS
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Find which slot a chunk Z coordinate falls into.
|
||||||
|
// Returns INDEX into StrateLayout, or -1 if not found.
|
||||||
|
int32 FindSlotIndexForChunkZ(int32 ChunkZ) const;
|
||||||
|
|
||||||
|
// Build FStrateGenerationParams from a strate definition:
|
||||||
|
// copies base GenerationParams, then applies all referenced terrain op assets.
|
||||||
|
// This is the single point where terrain op data assets are merged into params.
|
||||||
|
static FStrateGenerationParams BuildParamsFromDefinition(const UVoxelStrateDefinition* Definition);
|
||||||
|
|
||||||
|
// Generate passages between consecutive strates. Called from Initialize().
|
||||||
|
void GeneratePassages();
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,350 @@
|
|||||||
|
// VoxelTerrainOpDefinition.h
|
||||||
|
// Individual terrain operation data asset — one per operation variant.
|
||||||
|
//
|
||||||
|
// DESIGN:
|
||||||
|
// ========
|
||||||
|
// Each terrain operation (pit, arch, terrace, etc.) is its own asset.
|
||||||
|
// The strate definition holds a weighted array of references to these.
|
||||||
|
// This allows:
|
||||||
|
// - Reuse: "DA_Op_DeepPit" can be referenced by multiple strates
|
||||||
|
// - Composition: mix different ops per strate by drag-and-drop
|
||||||
|
// - Tuning: tweak one asset, all strates using it update
|
||||||
|
//
|
||||||
|
// HOW PARAMS FLOW:
|
||||||
|
// At generation time, the strate manager builds FStrateGenerationParams by:
|
||||||
|
// 1. Starting from the strate definition's base params (terrain op fields = 0)
|
||||||
|
// 2. For each terrain op reference: calling ApplyTo() which copies the op's
|
||||||
|
// fields into the params, scaled by the entry's Weight.
|
||||||
|
// 3. The generator reads the final params as usual — no code change needed.
|
||||||
|
//
|
||||||
|
// EDITOR UX:
|
||||||
|
// The Type enum controls which param group is visible via EditCondition.
|
||||||
|
// When you select "Pit", only pit-related fields appear in the Details panel.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Engine/DataAsset.h"
|
||||||
|
#include "VoxelStrateTypes.h"
|
||||||
|
#include "VoxelTerrainOpDefinition.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EVoxelTerrainOpType — Which terrain operation this asset configures.
|
||||||
|
* Each type maps to a specific density modification in the generation pipeline.
|
||||||
|
*/
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EVoxelTerrainOpType : uint8
|
||||||
|
{
|
||||||
|
// Surface treatments (modify cave wall character)
|
||||||
|
Terrace UMETA(DisplayName = "Terrace / Ledges"),
|
||||||
|
LayerLines UMETA(DisplayName = "Layer Lines"),
|
||||||
|
Ribbing UMETA(DisplayName = "Ribbing"),
|
||||||
|
Cliff UMETA(DisplayName = "Cliff Sharpening"),
|
||||||
|
Scallop UMETA(DisplayName = "Scallop / Erosion"),
|
||||||
|
Overhang UMETA(DisplayName = "Overhang / Shelf"),
|
||||||
|
|
||||||
|
// Structural features (place geometry in caves)
|
||||||
|
Arch UMETA(DisplayName = "Arch / Bridge"),
|
||||||
|
Column UMETA(DisplayName = "Column / Pillar"),
|
||||||
|
Pit UMETA(DisplayName = "Pit / Shaft (downward)"),
|
||||||
|
Chimney UMETA(DisplayName = "Chimney / Shaft (upward)"),
|
||||||
|
Dome UMETA(DisplayName = "Dome (cathedral ceiling)"),
|
||||||
|
Pinch UMETA(DisplayName = "Pinch / Bottleneck"),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UVoxelTerrainOpDefinition — A single terrain operation, configured as a data asset.
|
||||||
|
*
|
||||||
|
* Create these in your Content folder (e.g., Content/TerrainOps/DA_Op_DeepPit).
|
||||||
|
* Then reference them from your strate definitions with a weight.
|
||||||
|
*
|
||||||
|
* USAGE EXAMPLE:
|
||||||
|
* 1. Create: Right-click Content → Miscellaneous → Data Asset → VoxelTerrainOpDefinition
|
||||||
|
* 2. Set Type to "Pit", configure depth/radius/taper
|
||||||
|
* 3. In your strate definition, add this asset to TerrainOperations with Weight=1.0
|
||||||
|
*/
|
||||||
|
UCLASS(BlueprintType)
|
||||||
|
class VOXELFORGE_API UVoxelTerrainOpDefinition : public UPrimaryDataAsset
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// OPERATION TYPE
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Which terrain operation this asset configures.
|
||||||
|
// Changing this shows/hides the relevant parameter group below.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Terrain Operation")
|
||||||
|
EVoxelTerrainOpType Type = EVoxelTerrainOpType::Terrace;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// TERRACE PARAMS (visible when Type == Terrace)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Vertical spacing between terrace steps (in voxels). 0 = disabled.
|
||||||
|
// 3-5 → tight steps (staircase feel)
|
||||||
|
// 8-12 → wide ledges (platforming scale)
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Terrace",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Terrace"))
|
||||||
|
float TerraceStepHeight = 5.0f;
|
||||||
|
|
||||||
|
// Edge sharpness: 0 = rounded steps, 1 = razor sharp edges.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Terrace",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Terrace",
|
||||||
|
ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float TerraceHardness = 0.5f;
|
||||||
|
|
||||||
|
// Noise displacement on step edges (organic variation).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Terrace",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Terrace",
|
||||||
|
ClampMin = "0.0", ClampMax = "2.0"))
|
||||||
|
float TerraceNoiseDisplacement = 0.5f;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// LAYER LINES PARAMS (visible when Type == LayerLines)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Spacing between horizontal geological lines (in voxels). 0 = disabled.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Layer Lines",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::LayerLines"))
|
||||||
|
float LayerLineSpacing = 4.0f;
|
||||||
|
|
||||||
|
// Depth of the grooves (how much density is removed).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Layer Lines",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::LayerLines",
|
||||||
|
ClampMin = "0.0", ClampMax = "2.0"))
|
||||||
|
float LayerLineDepth = 0.3f;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// RIBBING PARAMS (visible when Type == Ribbing)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Spacing between parallel ribs along Z (in voxels). 0 = disabled.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ribbing",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Ribbing"))
|
||||||
|
float RibbingSpacing = 3.0f;
|
||||||
|
|
||||||
|
// Depth of the ridges.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ribbing",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Ribbing",
|
||||||
|
ClampMin = "0.0", ClampMax = "3.0"))
|
||||||
|
float RibbingDepth = 0.4f;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// CLIFF PARAMS (visible when Type == Cliff)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// How much to amplify vertical gradients near cave surfaces.
|
||||||
|
// 0 = no effect, 1 = maximum sheer cliff amplification.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cliff",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Cliff",
|
||||||
|
ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float CliffStrength = 0.5f;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// SCALLOP PARAMS (visible when Type == Scallop)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Strength of the erosion bowl patterns (cellular noise subtraction).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Scallop",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Scallop",
|
||||||
|
ClampMin = "0.0", ClampMax = "2.0"))
|
||||||
|
float ScallopStrength = 0.5f;
|
||||||
|
|
||||||
|
// Frequency of the cellular noise (smaller = larger bowls).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Scallop",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Scallop"))
|
||||||
|
float ScallopFrequency = 0.1f;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// OVERHANG PARAMS (visible when Type == Overhang)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Probability/strength of overhang generation (0 = none, 1 = max).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Overhang",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Overhang",
|
||||||
|
ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float OverhangStrength = 0.5f;
|
||||||
|
|
||||||
|
// How far the overhang protrudes from the wall (in voxels).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Overhang",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Overhang"))
|
||||||
|
float OverhangDepth = 5.0f;
|
||||||
|
|
||||||
|
// Frequency of the noise that creates overhangs (lower = broader shelves).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Overhang",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Overhang"))
|
||||||
|
float OverhangFrequency = 0.06f;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// ARCH PARAMS (visible when Type == Arch)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Probability per grid cell (0 = none, 0.3 = max recommended).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arch",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Arch",
|
||||||
|
ClampMin = "0.0", ClampMax = "0.3"))
|
||||||
|
float ArchDensity = 0.1f;
|
||||||
|
|
||||||
|
// Minimum arch thickness (radius of the capsule SDF).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arch",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Arch",
|
||||||
|
ClampMin = "1.0"))
|
||||||
|
float ArchMinRadius = 3.0f;
|
||||||
|
|
||||||
|
// Maximum arch thickness.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arch",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Arch",
|
||||||
|
ClampMin = "1.0"))
|
||||||
|
float ArchMaxRadius = 6.0f;
|
||||||
|
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// COLUMN PARAMS (visible when Type == Column)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Probability per grid cell (0 = none, 1 = every cell).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Column",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Column",
|
||||||
|
ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float ColumnDensity = 0.15f;
|
||||||
|
|
||||||
|
// Minimum column radius.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Column",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Column",
|
||||||
|
ClampMin = "0.5"))
|
||||||
|
float ColumnMinRadius = 2.0f;
|
||||||
|
|
||||||
|
// Maximum column radius.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Column",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Column",
|
||||||
|
ClampMin = "1.0"))
|
||||||
|
float ColumnMaxRadius = 5.0f;
|
||||||
|
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// PIT PARAMS (visible when Type == Pit)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Probability per grid cell.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pit",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Pit",
|
||||||
|
ClampMin = "0.0", ClampMax = "0.5"))
|
||||||
|
float PitDensity = 0.1f;
|
||||||
|
|
||||||
|
// Minimum pit radius at the top.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pit",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Pit",
|
||||||
|
ClampMin = "1.0"))
|
||||||
|
float PitMinRadius = 4.0f;
|
||||||
|
|
||||||
|
// Maximum pit radius at the top.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pit",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Pit",
|
||||||
|
ClampMin = "2.0"))
|
||||||
|
float PitMaxRadius = 10.0f;
|
||||||
|
|
||||||
|
// How deep the pit extends downward (in voxels).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pit",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Pit",
|
||||||
|
ClampMin = "5.0"))
|
||||||
|
float PitDepth = 25.0f;
|
||||||
|
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// CHIMNEY PARAMS (visible when Type == Chimney)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Probability per grid cell.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Chimney",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Chimney",
|
||||||
|
ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float ChimneyDensity = 0.1f;
|
||||||
|
|
||||||
|
// Minimum chimney radius.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Chimney",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Chimney",
|
||||||
|
ClampMin = "0.5"))
|
||||||
|
float ChimneyMinRadius = 2.0f;
|
||||||
|
|
||||||
|
// Maximum chimney radius.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Chimney",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Chimney",
|
||||||
|
ClampMin = "1.0"))
|
||||||
|
float ChimneyMaxRadius = 5.0f;
|
||||||
|
|
||||||
|
// How high the chimney extends upward (in voxels).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Chimney",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Chimney",
|
||||||
|
ClampMin = "5.0"))
|
||||||
|
float ChimneyHeight = 20.0f;
|
||||||
|
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// DOME PARAMS (visible when Type == Dome)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Probability per grid cell.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dome",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Dome",
|
||||||
|
ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float DomeDensity = 0.15f;
|
||||||
|
|
||||||
|
// Minimum dome radius.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dome",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Dome",
|
||||||
|
ClampMin = "3.0"))
|
||||||
|
float DomeMinRadius = 8.0f;
|
||||||
|
|
||||||
|
// Maximum dome radius.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dome",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Dome",
|
||||||
|
ClampMin = "4.0"))
|
||||||
|
float DomeMaxRadius = 15.0f;
|
||||||
|
|
||||||
|
// Height-to-radius ratio (0.5 = flat, 1.0 = hemisphere, 1.5 = tall/gothic).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dome",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Dome",
|
||||||
|
ClampMin = "0.3", ClampMax = "2.0"))
|
||||||
|
float DomeHeightRatio = 0.8f;
|
||||||
|
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// PINCH PARAMS (visible when Type == Pinch)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Probability per grid cell.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pinch",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Pinch",
|
||||||
|
ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float PinchDensity = 0.15f;
|
||||||
|
|
||||||
|
// How much the passage narrows (voxels of added density from each side).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pinch",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Pinch",
|
||||||
|
ClampMin = "1.0"))
|
||||||
|
float PinchStrength = 5.0f;
|
||||||
|
|
||||||
|
// Length of the narrowed section along the passage.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pinch",
|
||||||
|
meta = (EditCondition = "Type == EVoxelTerrainOpType::Pinch",
|
||||||
|
ClampMin = "3.0"))
|
||||||
|
float PinchLength = 12.0f;
|
||||||
|
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// APPLY TO GENERATION PARAMS
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy this operation's parameters into a FStrateGenerationParams struct.
|
||||||
|
* Only the fields matching this operation's Type are written.
|
||||||
|
* Weight scales the primary "strength" or "density" field for intensity control.
|
||||||
|
*
|
||||||
|
* @param OutParams - The generation params to write into
|
||||||
|
* @param Weight - Multiplier for the op's intensity (1.0 = full strength)
|
||||||
|
*/
|
||||||
|
void ApplyTo(FStrateGenerationParams& OutParams, float Weight = 1.0f) const;
|
||||||
|
};
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
// VoxelTypes.h
|
||||||
|
// Core type definitions, coordinate conversions, and mesh data.
|
||||||
|
//
|
||||||
|
// Ce header est le socle du plugin — tout le monde l'inclut.
|
||||||
|
// Pas de dépendance sur des UClasses ici (on reste léger).
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// CHUNK CONSTANTS
|
||||||
|
//=============================================================================
|
||||||
|
//
|
||||||
|
// Taille de chunk: 32^3 = 32 768 voxels.
|
||||||
|
// Pourquoi 32 ? Puissance de 2 → astuces bit à bit + bon alignement GPU.
|
||||||
|
// VOXEL_SIZE = 25 cm/voxel (Unreal travaille en centimètres).
|
||||||
|
|
||||||
|
constexpr int32 CHUNK_SIZE = 32;
|
||||||
|
constexpr int32 CHUNK_SIZE_SQUARED = CHUNK_SIZE * CHUNK_SIZE; // 1024
|
||||||
|
constexpr int32 CHUNK_VOLUME = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE; // 32768
|
||||||
|
|
||||||
|
constexpr float VOXEL_SIZE = 25.0f;
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// FACE DIRECTIONS
|
||||||
|
//=============================================================================
|
||||||
|
//
|
||||||
|
// Les 6 faces d'un cube. Gardé ici car le mesher peut encore en avoir besoin
|
||||||
|
// (ex. normales orientées), et les strates pourraient s'en servir pour des
|
||||||
|
// décorations "sur le sol" vs "au plafond".
|
||||||
|
|
||||||
|
enum class EVoxelFace : uint8
|
||||||
|
{
|
||||||
|
PositiveX, // East (+X)
|
||||||
|
NegativeX, // West (-X)
|
||||||
|
PositiveY, // North (+Y)
|
||||||
|
NegativeY, // South (-Y)
|
||||||
|
PositiveZ, // Up (+Z)
|
||||||
|
NegativeZ, // Down (-Z)
|
||||||
|
};
|
||||||
|
|
||||||
|
inline FIntVector GetFaceDirection(EVoxelFace Face)
|
||||||
|
{
|
||||||
|
switch (Face)
|
||||||
|
{
|
||||||
|
case EVoxelFace::PositiveX: return FIntVector( 1, 0, 0);
|
||||||
|
case EVoxelFace::NegativeX: return FIntVector(-1, 0, 0);
|
||||||
|
case EVoxelFace::PositiveY: return FIntVector( 0, 1, 0);
|
||||||
|
case EVoxelFace::NegativeY: return FIntVector( 0, -1, 0);
|
||||||
|
case EVoxelFace::PositiveZ: return FIntVector( 0, 0, 1);
|
||||||
|
case EVoxelFace::NegativeZ: return FIntVector( 0, 0, -1);
|
||||||
|
default: return FIntVector( 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline FVector GetFaceNormal(EVoxelFace Face)
|
||||||
|
{
|
||||||
|
const FIntVector Dir = GetFaceDirection(Face);
|
||||||
|
return FVector(Dir.X, Dir.Y, Dir.Z);
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// COORDINATE CONVERSION
|
||||||
|
//=============================================================================
|
||||||
|
//
|
||||||
|
// Trois espaces:
|
||||||
|
// WORLD (FVector, cm) → position Unreal
|
||||||
|
// CHUNK (FIntVector) → quel chunk (32 voxels)
|
||||||
|
// LOCAL (FIntVector 0-31) → position dans le chunk
|
||||||
|
//
|
||||||
|
// Relation: WorldPos = (ChunkCoord * CHUNK_SIZE + LocalPos) * VOXEL_SIZE
|
||||||
|
|
||||||
|
inline FIntVector WorldToChunkCoord(const FVector& WorldPos)
|
||||||
|
{
|
||||||
|
// FloorToInt (pas division int) pour gérer proprement les coords négatives:
|
||||||
|
// -5 / 32 = 0 (faux — on veut -1)
|
||||||
|
// floor(-5 / 32) = floor(-0.156) = -1 (correct)
|
||||||
|
return FIntVector(
|
||||||
|
FMath::FloorToInt((WorldPos.X / VOXEL_SIZE) / CHUNK_SIZE),
|
||||||
|
FMath::FloorToInt((WorldPos.Y / VOXEL_SIZE) / CHUNK_SIZE),
|
||||||
|
FMath::FloorToInt((WorldPos.Z / VOXEL_SIZE) / CHUNK_SIZE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline FIntVector WorldToLocalCoord(const FVector& WorldPos)
|
||||||
|
{
|
||||||
|
// ((x % n) + n) % n → modulo positif même pour x négatif.
|
||||||
|
// (-5 % 32) = -5 en C++, mais on veut 27.
|
||||||
|
return FIntVector(
|
||||||
|
((FMath::FloorToInt(WorldPos.X / VOXEL_SIZE) % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE,
|
||||||
|
((FMath::FloorToInt(WorldPos.Y / VOXEL_SIZE) % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE,
|
||||||
|
((FMath::FloorToInt(WorldPos.Z / VOXEL_SIZE) % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline FVector ChunkToWorldPos(const FIntVector& ChunkCoord)
|
||||||
|
{
|
||||||
|
return FVector(
|
||||||
|
ChunkCoord.X * CHUNK_SIZE * VOXEL_SIZE,
|
||||||
|
ChunkCoord.Y * CHUNK_SIZE * VOXEL_SIZE,
|
||||||
|
ChunkCoord.Z * CHUNK_SIZE * VOXEL_SIZE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index 3D → 1D pour un tableau plat (voir doc CLAUDE.md: "x + y*SizeX + z*SizeX*SizeY").
|
||||||
|
inline int32 LocalToIndex(int32 X, int32 Y, int32 Z)
|
||||||
|
{
|
||||||
|
return X + (Y * CHUNK_SIZE) + (Z * CHUNK_SIZE_SQUARED);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline FIntVector IndexToLocal(int32 Index)
|
||||||
|
{
|
||||||
|
return FIntVector(
|
||||||
|
Index % CHUNK_SIZE,
|
||||||
|
(Index / CHUNK_SIZE) % CHUNK_SIZE,
|
||||||
|
Index / CHUNK_SIZE_SQUARED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool IsValidLocalCoord(int32 X, int32 Y, int32 Z)
|
||||||
|
{
|
||||||
|
return X >= 0 && X < CHUNK_SIZE
|
||||||
|
&& Y >= 0 && Y < CHUNK_SIZE
|
||||||
|
&& Z >= 0 && Z < CHUNK_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool IsValidLocalCoord(const FIntVector& Coord)
|
||||||
|
{
|
||||||
|
return IsValidLocalCoord(Coord.X, Coord.Y, Coord.Z);
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// MATH HELPERS (partagés entre générateur, morphologie, manager)
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
// Smoothstep classique: 3x² - 2x³. Maps 0→0, 1→1, avec pente nulle aux bornes.
|
||||||
|
// Utilisé partout pour adoucir les transitions de densité (blend SDF, seal boundary).
|
||||||
|
// Entrée: x doit être dans [0, 1] (clamp avant si besoin).
|
||||||
|
inline float SmoothStep01(float x)
|
||||||
|
{
|
||||||
|
return x * x * (3.0f - 2.0f * x);
|
||||||
|
}
|
||||||
|
|
||||||
|
// UE's PerlinNoise3D renvoie ~[-0.8, 0.8] — ce facteur remet à ~[-1, 1]
|
||||||
|
// pour correspondre aux attentes des formules de densité.
|
||||||
|
constexpr float VOXEL_NOISE_SCALE = 1.25f;
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// MESH DATA
|
||||||
|
//=============================================================================
|
||||||
|
//
|
||||||
|
// Sortie du mesher: géométrie prête pour l'upload GPU.
|
||||||
|
// Struct plain C++ (pas de USTRUCT) — uniquement utilisé entre tâches C++.
|
||||||
|
// Si lighting est ajouté plus tard, ré-ajouter un champ Colors dédié.
|
||||||
|
|
||||||
|
struct FVoxelMeshData
|
||||||
|
{
|
||||||
|
TArray<FVector> Vertices; // Positions monde
|
||||||
|
TArray<int32> Triangles; // Indices, 3 par triangle
|
||||||
|
TArray<FVector2D> UVs; // Coords de texture (une par vertex)
|
||||||
|
TArray<FVector> Normals; // Normale lissée (gradient de densité)
|
||||||
|
|
||||||
|
void Clear()
|
||||||
|
{
|
||||||
|
Vertices.Empty();
|
||||||
|
Triangles.Empty();
|
||||||
|
UVs.Empty();
|
||||||
|
Normals.Empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsEmpty() const { return Vertices.Num() == 0; }
|
||||||
|
};
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
// VoxelWorld.h
|
||||||
|
// The main world manager - orchestrates chunk loading, generation, meshing, and rendering
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include <atomic>
|
||||||
|
#include "VoxelTypes.h"
|
||||||
|
#include "VoxelChunk.h"
|
||||||
|
#include "VoxelGenerator.h"
|
||||||
|
#include "VoxelMarchingCubesMesher.h"
|
||||||
|
#include "VoxelSettings.h"
|
||||||
|
#include "VoxelStrateManager.h"
|
||||||
|
#include "VoxelDiffLayer.h"
|
||||||
|
#include "VoxelWorld.generated.h"
|
||||||
|
|
||||||
|
// Forward declaration
|
||||||
|
class URealtimeMeshComponent;
|
||||||
|
class URealtimeMeshSimple;
|
||||||
|
class UVoxelDiffLayer;
|
||||||
|
class UVoxelContentManager;
|
||||||
|
class UVoxelAtmosphereManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AVoxelWorld - The main voxel terrain actor
|
||||||
|
*
|
||||||
|
* RESPONSIBILITIES:
|
||||||
|
* - Track which chunks should be loaded based on player position
|
||||||
|
* - Create/destroy chunks as player moves
|
||||||
|
* - Coordinate generation and meshing
|
||||||
|
* - Manage mesh components for rendering
|
||||||
|
*
|
||||||
|
* LIFECYCLE:
|
||||||
|
* - BeginPlay: Initialize generator, mesher
|
||||||
|
* - Tick: Update chunks around player position
|
||||||
|
*/
|
||||||
|
struct FChunkResult
|
||||||
|
{
|
||||||
|
FIntVector ChunkCoord;
|
||||||
|
FVoxelChunk Chunk;
|
||||||
|
FVoxelMeshData MeshData;
|
||||||
|
int32 LODLevel = 0; // 0=full, 1=half, 2=quarter resolution
|
||||||
|
uint32 Epoch = 0; // Generation epoch — discard if stale
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class VOXELFORGE_API AVoxelWorld : public AActor
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
AVoxelWorld();
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// SETTINGS
|
||||||
|
//=========================================================================
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World")
|
||||||
|
UVoxelSettings* Settings;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// COMPONENTS & REFERENCES
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/** The terrain generator */
|
||||||
|
UPROPERTY()
|
||||||
|
UVoxelGenerator* Generator;
|
||||||
|
|
||||||
|
/** The mesh builder */
|
||||||
|
UPROPERTY()
|
||||||
|
UVoxelMarchingCubesMesher* Mesher;
|
||||||
|
|
||||||
|
/** The strate manager — maps depth to strate definitions */
|
||||||
|
UPROPERTY()
|
||||||
|
UVoxelStrateManager* StrateManager;
|
||||||
|
|
||||||
|
/** Player terrain modifications (carving & filling).
|
||||||
|
* Stores density diffs on top of procedural terrain.
|
||||||
|
* Created in BeginPlay, passed to Generator for density evaluation. */
|
||||||
|
UPROPERTY()
|
||||||
|
UVoxelDiffLayer* DiffLayer;
|
||||||
|
|
||||||
|
/** Spawns per-chunk decorations/actors from the strate content pools and the
|
||||||
|
* aesthetic water surfaces. Created in BeginPlay. */
|
||||||
|
UPROPERTY()
|
||||||
|
UVoxelContentManager* ContentManager;
|
||||||
|
|
||||||
|
/** Drives per-strate fog + ambient + persistent ceiling/floor layer actors
|
||||||
|
* (e.g. seas of clouds) based on the strate the player is in. Created in BeginPlay
|
||||||
|
* when bManageAtmosphere is true. */
|
||||||
|
UPROPERTY()
|
||||||
|
UVoxelAtmosphereManager* AtmosphereManager;
|
||||||
|
|
||||||
|
/** When true, VoxelForge spawns & drives its own height fog + skylight + ceiling/floor
|
||||||
|
* layer actors from each strate's settings. Turn OFF if you manage fog/lighting
|
||||||
|
* yourself in the level (avoids a duplicate ExponentialHeightFog). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World")
|
||||||
|
bool bManageAtmosphere = true;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// CHUNK STORAGE
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/** All currently loaded chunks, keyed by chunk coordinate */
|
||||||
|
TMap<FIntVector, FVoxelChunk> Chunks;
|
||||||
|
|
||||||
|
/** Mesh components for each chunk, keyed by chunk coordinate */
|
||||||
|
UPROPERTY()
|
||||||
|
TMap<FIntVector, URealtimeMeshComponent*> ChunkMeshes;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// TERRAIN MODIFICATION (player carving & filling)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carve terrain at a world position (remove rock, create air).
|
||||||
|
* Applies a spherical brush that subtracts density.
|
||||||
|
*
|
||||||
|
* @param Position - World-space center of the carve brush
|
||||||
|
* @param Radius - Brush radius in voxels (default 3)
|
||||||
|
* @param Strength - How aggressively to carve (default 10, higher = deeper)
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||||
|
void CarveAtPosition(FVector Position, float Radius = 3.0f, float Strength = 10.0f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill terrain at a world position (add rock, seal holes).
|
||||||
|
* Applies a spherical brush that adds density.
|
||||||
|
*
|
||||||
|
* @param Position - World-space center of the fill brush
|
||||||
|
* @param Radius - Brush radius in voxels (default 3)
|
||||||
|
* @param Strength - How aggressively to fill (default 10, higher = more solid)
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||||
|
void FillAtPosition(FVector Position, float Radius = 3.0f, float Strength = 10.0f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Box brush carve/fill. Position is world-space (Unreal units); ExtentVoxels is the
|
||||||
|
* box half-size in voxels. Strength magnitude controls aggressiveness (sign forced).
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||||
|
void CarveBox(FVector Position, FVector ExtentVoxels, float Strength = 12.0f);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||||
|
void FillBox(FVector Position, FVector ExtentVoxels, float Strength = 12.0f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capsule brush carve/fill between two world-space points (Unreal units), with a
|
||||||
|
* tube radius in voxels. Great for boring tunnels or laying solid pillars/walls.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||||
|
void CarveCapsule(FVector WorldA, FVector WorldB, float RadiusVoxels = 3.0f, float Strength = 12.0f);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||||
|
void FillCapsule(FVector WorldA, FVector WorldB, float RadiusVoxels = 3.0f, float Strength = 12.0f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a fully-specified modification (any shape). Centers/endpoints are expected
|
||||||
|
* in VOXEL coordinates here (this is the low-level entry the helpers build on).
|
||||||
|
* Returns nothing; re-meshes affected chunks.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||||
|
void ApplyModification(const FVoxelModification& Modification);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all player modifications (e.g., on season reset).
|
||||||
|
* Regenerates all currently loaded chunks to restore procedural terrain.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||||
|
void ClearAllModifications();
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// SEED / SEASON MANAGEMENT
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the world seed and regenerate everything.
|
||||||
|
*
|
||||||
|
* This is the "season reset" operation:
|
||||||
|
* 1. Updates the seed in Settings, Generator, and StrateManager
|
||||||
|
* 2. Clears ALL player modifications (carvings are meaningless in a new world)
|
||||||
|
* 3. Resets the elevator depth (player must re-discover strates)
|
||||||
|
* 4. Increments the season counter
|
||||||
|
* 5. Unloads all chunks and lets Tick reload them with the new seed
|
||||||
|
*
|
||||||
|
* The game layer is responsible for calling this at season boundaries,
|
||||||
|
* saving/loading the season counter, and any pre-reset cleanup (inventory, etc.)
|
||||||
|
*
|
||||||
|
* @param NewSeed - The new world seed
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Season")
|
||||||
|
void ChangeSeed(int32 NewSeed);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current world seed.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Voxel World|Season")
|
||||||
|
int32 GetCurrentSeed() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current season number (incremented each time ChangeSeed is called).
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Voxel World|Season")
|
||||||
|
int32 GetCurrentSeason() const;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// STRATE QUERIES (gameplay integration)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get which strate index a world position is in.
|
||||||
|
*
|
||||||
|
* @param WorldPosition - Position in world space (Unreal units)
|
||||||
|
* @return Strate index (0 = topmost), or -1 if outside all strates
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
|
||||||
|
int32 GetStrateAtPosition(FVector WorldPosition) const;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// LIVE EDIT (debug tuning in PIE)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/** When true, editing your Strate Definition data assets during PIE
|
||||||
|
* will automatically regenerate all chunks so you see the result live.
|
||||||
|
* Also adds a "Regenerate" button in Details for manual refresh. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Live Edit")
|
||||||
|
bool bLiveEditStrates = false;
|
||||||
|
|
||||||
|
/** Click to force-regenerate all chunks right now (useful during PIE). */
|
||||||
|
UFUNCTION(CallInEditor, BlueprintCallable, Category = "Live Edit")
|
||||||
|
void RegenerateAllChunks();
|
||||||
|
|
||||||
|
/** Re-read ALL of VoxelSettings (strate layout, inter-strate gap, passages, spine)
|
||||||
|
* and rebuild from scratch, then regenerate every chunk. Use this after changing
|
||||||
|
* passage / gap / spine settings so they apply live without restarting PIE —
|
||||||
|
* RegenerateAllChunks alone keeps the existing layout & passages. */
|
||||||
|
UFUNCTION(CallInEditor, BlueprintCallable, Category = "Live Edit")
|
||||||
|
void RebuildStrates();
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// EDITOR BRUSH (manual carve/fill from the Details panel, works in PIE)
|
||||||
|
//=========================================================================
|
||||||
|
// The diff layer only exists while playing, so these buttons act during PIE.
|
||||||
|
|
||||||
|
/** World-space center (Unreal units) for the editor carve/fill buttons below. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush")
|
||||||
|
FVector EditorBrushCenter = FVector::ZeroVector;
|
||||||
|
|
||||||
|
/** Brush radius in voxels for the editor buttons. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush", meta = (ClampMin = "0.5"))
|
||||||
|
float EditorBrushRadius = 6.0f;
|
||||||
|
|
||||||
|
/** Brush strength for the editor buttons (sign forced by the button). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush", meta = (ClampMin = "0.1"))
|
||||||
|
float EditorBrushStrength = 12.0f;
|
||||||
|
|
||||||
|
/** Carve a sphere at EditorBrushCenter using the brush settings above. */
|
||||||
|
UFUNCTION(CallInEditor, BlueprintCallable, Category = "Editor Brush")
|
||||||
|
void EditorCarveSphere();
|
||||||
|
|
||||||
|
/** Fill a sphere at EditorBrushCenter using the brush settings above. */
|
||||||
|
UFUNCTION(CallInEditor, BlueprintCallable, Category = "Editor Brush")
|
||||||
|
void EditorFillSphere();
|
||||||
|
|
||||||
|
/** Generation counter — incremented each time we regenerate.
|
||||||
|
* Async tasks carry this value; stale results are discarded. */
|
||||||
|
uint32 GenerationEpoch = 0;
|
||||||
|
|
||||||
|
/** Draw the inter-strate passages each frame (lines along the path + endpoint
|
||||||
|
* spheres) so you can see where they spawned and verify they carve. PIE only. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Debug")
|
||||||
|
bool bDebugDrawPassages = false;
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
|
||||||
|
|
||||||
|
/** Called by FCoreUObjectDelegates::OnObjectModified when ANY UObject is edited.
|
||||||
|
* We filter for UVoxelStrateDefinition changes and regenerate if live edit is on. */
|
||||||
|
void OnObjectModifiedInEditor(UObject* ModifiedObject);
|
||||||
|
|
||||||
|
/** Handle to unbind the delegate when EndPlay is called */
|
||||||
|
FDelegateHandle OnObjectModifiedHandle;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// ACTOR LIFECYCLE
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
virtual void BeginPlay() override;
|
||||||
|
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||||
|
virtual void Tick(float DeltaTime) override;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// CHUNK MANAGEMENT - YOU IMPLEMENT THESE
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update which chunks are loaded based on a world position.
|
||||||
|
*
|
||||||
|
* CONCEPT:
|
||||||
|
* - Figure out which chunk the position is in (the "center")
|
||||||
|
* - Determine which chunks SHOULD exist (within view distance)
|
||||||
|
* - Load any chunks that should exist but don't
|
||||||
|
* - Unload any chunks that exist but shouldn't
|
||||||
|
*
|
||||||
|
* @param CenterPosition - Usually the player's position
|
||||||
|
*/
|
||||||
|
void UpdateChunksAroundPosition(const FVector& CenterPosition);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a single chunk at the given coordinate.
|
||||||
|
*
|
||||||
|
* CONCEPT:
|
||||||
|
* - Create chunk data
|
||||||
|
* - Generate terrain
|
||||||
|
* - Build mesh
|
||||||
|
* - Create visual component
|
||||||
|
*
|
||||||
|
* @param ChunkCoord - Which chunk to load
|
||||||
|
*/
|
||||||
|
void LoadChunk(const FIntVector& ChunkCoord);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unload a single chunk.
|
||||||
|
*
|
||||||
|
* CONCEPT:
|
||||||
|
* - Remove and destroy the mesh component
|
||||||
|
* - Remove chunk data from storage
|
||||||
|
*
|
||||||
|
* @param ChunkCoord - Which chunk to unload
|
||||||
|
*/
|
||||||
|
void UnloadChunk(const FIntVector& ChunkCoord);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply mesh data to a RealtimeMesh component.
|
||||||
|
*
|
||||||
|
* CONCEPT:
|
||||||
|
* - Get or create the mesh component for this chunk
|
||||||
|
* - Set the mesh data (vertices, triangles, etc.)
|
||||||
|
*
|
||||||
|
* @param ChunkCoord - Which chunk this mesh belongs to
|
||||||
|
* @param MeshData - The generated mesh data
|
||||||
|
*/
|
||||||
|
void ApplyMeshToChunk(const FIntVector& ChunkCoord, const FVoxelMeshData& MeshData);
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// HELPERS
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/** Get the current player position (or zero if no player) */
|
||||||
|
FVector GetPlayerPosition() const;
|
||||||
|
|
||||||
|
/** Check if a chunk coordinate is within view distance of a center chunk */
|
||||||
|
bool IsChunkInRange(const FIntVector& ChunkCoord, const FIntVector& CenterChunk) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine LOD level for a chunk based on its distance from the center.
|
||||||
|
*
|
||||||
|
* LOD CONCEPT:
|
||||||
|
* Chunks close to the player get full resolution (LOD0, Step=1).
|
||||||
|
* Chunks further away get coarser resolution (LOD1=Step 2, LOD2=Step 4).
|
||||||
|
* This dramatically reduces triangle count for distant terrain without
|
||||||
|
* visible quality loss (they're far away!).
|
||||||
|
*
|
||||||
|
* @param ChunkCoord - The chunk to evaluate
|
||||||
|
* @param CenterChunk - The player's current chunk
|
||||||
|
* @return LOD level: 0 (full), 1 (half), 2 (quarter)
|
||||||
|
*/
|
||||||
|
int32 GetLODForChunk(const FIntVector& ChunkCoord, const FIntVector& CenterChunk) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert LOD level to marching cubes step size.
|
||||||
|
* LOD0 → Step 1 (every voxel)
|
||||||
|
* LOD1 → Step 2 (every 2nd voxel)
|
||||||
|
* LOD2 → Step 4 (every 4th voxel)
|
||||||
|
*/
|
||||||
|
static int32 LODToStep(int32 LODLevel);
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// ASYNC
|
||||||
|
//=========================================================================
|
||||||
|
// MPSC: up to MaxConcurrentTasks ChunkGen worker threads Enqueue concurrently,
|
||||||
|
// the game thread (ProcessPendingChunks) is the only consumer. The default Spsc
|
||||||
|
// mode is NOT safe for multiple producers — concurrent Enqueues race on the tail
|
||||||
|
// link and silently drop results, which leaks PendingChunkCoord slots until the
|
||||||
|
// budget is exhausted and streaming stalls permanently. Mpsc guards the producer side.
|
||||||
|
TQueue<FChunkResult, EQueueMode::Mpsc> ProcessQueue;
|
||||||
|
TSet<FIntVector> PendingChunkCoord;
|
||||||
|
|
||||||
|
// Set to true during EndPlay — async tasks check this before accessing UObjects
|
||||||
|
std::atomic<bool> bShuttingDown{false};
|
||||||
|
|
||||||
|
// Number of async tasks currently running — EndPlay waits for this to reach 0
|
||||||
|
std::atomic<int32> ActiveTaskCount{0};
|
||||||
|
|
||||||
|
// Last known player chunk coord — used by LoadChunk to compute LOD
|
||||||
|
FIntVector CurrentCenterChunk = FIntVector::ZeroValue;
|
||||||
|
|
||||||
|
// Track current LOD per loaded chunk (for LOD transitions)
|
||||||
|
TMap<FIntVector, int32> ChunkLODs;
|
||||||
|
|
||||||
|
// --- Streaming work-avoidance (perf) ---
|
||||||
|
// The desired chunk set only changes when the player crosses a chunk boundary.
|
||||||
|
// We cache it and only rebuild/cull/sort on a real move, and go idle once every
|
||||||
|
// desired chunk is streamed in — so a stationary player costs ~nothing per frame.
|
||||||
|
FIntVector LastUpdateCenter = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||||
|
bool bAllChunksLoaded = false;
|
||||||
|
TArray<FIntVector> DesiredSorted; // desired coords, nearest-first
|
||||||
|
TSet<FIntVector> DesiredSet; // O(1) membership for the cull pass
|
||||||
|
|
||||||
|
void ProcessPendingChunks();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-queue loaded chunks for async re-generation + re-meshing.
|
||||||
|
* Used after terrain modifications: the old mesh stays visible until
|
||||||
|
* the new async result comes back, so there's no visual pop.
|
||||||
|
*
|
||||||
|
* Chunks that aren't currently loaded are ignored (they'll generate
|
||||||
|
* fresh with the diff layer when loaded normally).
|
||||||
|
*
|
||||||
|
* @param DirtyCoords - Chunk coordinates that need re-meshing
|
||||||
|
*/
|
||||||
|
void RemeshDirtyChunks(const TArray<FIntVector>& DirtyCoords);
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// VoxelForge.Build.cs
|
||||||
|
// This file tells Unreal how to compile our plugin
|
||||||
|
|
||||||
|
using UnrealBuildTool;
|
||||||
|
|
||||||
|
public class VoxelForge : ModuleRules
|
||||||
|
{
|
||||||
|
public VoxelForge(ReadOnlyTargetRules Target) : base(Target)
|
||||||
|
{
|
||||||
|
// PCH = Precompiled Headers - speeds up compilation
|
||||||
|
// UseExplicitOrSharedPCHs is the modern recommended setting
|
||||||
|
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||||
|
|
||||||
|
// Modules we depend on:
|
||||||
|
// - Core: Basic types (TArray, FString, etc.)
|
||||||
|
// - CoreUObject: UObject system (UCLASS, UPROPERTY, etc.)
|
||||||
|
// - Engine: Game engine features (AActor, UWorld, etc.)
|
||||||
|
PublicDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
"Core",
|
||||||
|
"CoreUObject",
|
||||||
|
"Engine",
|
||||||
|
"GameplayTags", // For FGameplayTagContainer on strate definitions
|
||||||
|
});
|
||||||
|
|
||||||
|
// RealtimeMeshComponent - the rendering library we'll use
|
||||||
|
// This is a third-party plugin you should have installed
|
||||||
|
PublicDependencyModuleNames.Add("RealtimeMeshComponent");
|
||||||
|
|
||||||
|
// Private dependencies - only used in our .cpp files, not exposed in headers
|
||||||
|
PrivateDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
// None for now - we'll add more as needed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"FileVersion": 3,
|
||||||
|
"Version": 1,
|
||||||
|
"VersionName": "1.0",
|
||||||
|
"FriendlyName": "VoxelForge",
|
||||||
|
"Description": "A voxel terrain generation plugin - built from scratch to learn",
|
||||||
|
"Category": "Procedural Generation",
|
||||||
|
"CreatedBy": "You",
|
||||||
|
"CreatedByURL": "",
|
||||||
|
"DocsURL": "",
|
||||||
|
"MarketplaceURL": "",
|
||||||
|
"SupportURL": "",
|
||||||
|
"CanContainContent": true,
|
||||||
|
"IsBetaVersion": true,
|
||||||
|
"IsExperimentalVersion": false,
|
||||||
|
"Installed": false,
|
||||||
|
"Modules": [
|
||||||
|
{
|
||||||
|
"Name": "VoxelForge",
|
||||||
|
"Type": "Runtime",
|
||||||
|
"LoadingPhase": "Default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user