j
This commit is contained in:
+150
-21
@@ -4,7 +4,7 @@
|
|||||||
> without re-reading the whole plugin. Anchors are `File:line` — line numbers drift
|
> 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.
|
> 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.
|
> Plugin root: `Source/VoxelForge/` · ~8,300 lines of C++ across 25 files.
|
||||||
> Comments in the code are mixed **French + English**. UE module = `VoxelForge` (Runtime).
|
> Comments in the code are mixed **French + English**. UE module = `VoxelForge` (Runtime).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -83,7 +83,7 @@ Paths relative to `Source/VoxelForge/`. `Public/` = headers, `Private/` = impl.
|
|||||||
### 3.2 Foundational types — `Public/VoxelTypes.h` (no UClass, everyone includes it)
|
### 3.2 Foundational types — `Public/VoxelTypes.h` (no UClass, everyone includes it)
|
||||||
| Symbol | Line | Notes |
|
| Symbol | Line | Notes |
|
||||||
|--------|------|-------|
|
|--------|------|-------|
|
||||||
| `CHUNK_SIZE` (32), `CHUNK_SIZE_SQUARED`, `CHUNK_VOLUME` | 19-21 | Chunk dimensions. |
|
| `CHUNK_SIZE` (32), `CHUNK_SIZE_SQUARED`, `CHUNK_VOLUME` | 19-21 | Chunk dimensions. (64³ tried for fewer draws → reverted: streaming too bursty. fps is fixed render-side instead.) |
|
||||||
| `VOXEL_SIZE` (25.0f cm) | 23 | World scale. |
|
| `VOXEL_SIZE` (25.0f cm) | 23 | World scale. |
|
||||||
| `EVoxelFace` enum + `GetFaceDirection` / `GetFaceNormal` | 33-61 | 6 cube faces. |
|
| `EVoxelFace` enum + `GetFaceDirection` / `GetFaceNormal` | 33-61 | 6 cube faces. |
|
||||||
| `WorldToChunkCoord` / `WorldToLocalCoord` / `ChunkToWorldPos` | 74-104 | Coord-space conversions (handle negatives via floor/positive-modulo). |
|
| `WorldToChunkCoord` / `WorldToLocalCoord` / `ChunkToWorldPos` | 74-104 | Coord-space conversions (handle negatives via floor/positive-modulo). |
|
||||||
@@ -101,8 +101,8 @@ per-chunk info later.
|
|||||||
`UVoxelSettings : UPrimaryDataAsset` — the single tuning asset assigned on `AVoxelWorld`.
|
`UVoxelSettings : UPrimaryDataAsset` — the single tuning asset assigned on `AVoxelWorld`.
|
||||||
| Group | Fields (line) |
|
| Group | Fields (line) |
|
||||||
|-------|---------------|
|
|-------|---------------|
|
||||||
| Streaming | `ViewDistanceXY=16` (26), `ViewDistanceUp/Down=5` (29/32), `MaxConcurrentTasks=16` (36), `MaxMeshAppliesPerFrame=4` (40) |
|
| Streaming | `ViewDistanceXY=16`, `ViewDistanceUp/Down=5`, `MaxConcurrentTasks=16`, `MaxMeshAppliesPerFrame=4` (defaults — actual values live on the data asset) |
|
||||||
| LOD | `LOD0Distance=4` (48), `LOD1Distance=8` (53) |
|
| LOD | `LOD0Distance=4`, `LOD1Distance=8` |
|
||||||
| Rendering | `VoxelMaterial` (61) |
|
| Rendering | `VoxelMaterial` (61) |
|
||||||
| Strates | `Seed` (69), `CurrentSeason=1` (73), `StratePool` (78), `FixedStrates` map (83), `TotalStrates=10` (87) |
|
| 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. |
|
| Carving budget | `MaxModifications=0` (97), `MaxBrushRadius=15` (102), `MaxTotalVolume=0` (107). 0 = unlimited. |
|
||||||
@@ -155,6 +155,11 @@ per-chunk info later.
|
|||||||
| **`GetDensityAt`** | 218 | **Entry point.** Picks strate + generator type, dispatches, adds diff offset. |
|
| **`GetDensityAt`** | 218 | **Entry point.** Picks strate + generator type, dispatches, adds diff offset. |
|
||||||
| **`GetDensityWithParams`** | 277 | TunnelNetwork pipeline (~1000 lines). See §4. |
|
| **`GetDensityWithParams`** | 277 | TunnelNetwork pipeline (~1000 lines). See §4. |
|
||||||
| **`GetSlabDensity`** | 1306 | FlatPlain/CrystalChamber pipeline. See §4.2. |
|
| **`GetSlabDensity`** | 1306 | FlatPlain/CrystalChamber pipeline. See §4.2. |
|
||||||
|
| `ComputeSurfaceTerrainZ` / `GetSurfaceDensity` | — | SurfaceWorld heightfield → terrain Z, then density; biome **output-blend** lerps dominant/neighbour heights (`ParamsD`/`ParamsN`/weight). §8.14. |
|
||||||
|
| `SampleRelief` / `SampleMoisture` | — | Climate fields (pure XY, [0,1]). Relief = shared source of truth for the relief map M. §8.14. |
|
||||||
|
| `SampleBiomeAt` | — | Warped-Voronoi + climate biome query (dominant + neighbour + weight). Reference used by the preview bake + `GetDominantBiomeAt`. §8.14. |
|
||||||
|
| `ResolveBiomeSampleAt` / `RebuildBiomeGrid` | — | Hot-path biome resolve (FBiomeSample) via a box-validated per-chunk cell-grid cache. Bit-identical to `SampleBiomeAt`. §8.14, §8.10. |
|
||||||
|
| `GetDominantBiomeAt` | — | Game-thread query → dominant biome ASSET (content/atmosphere). §8.14. |
|
||||||
|
|
||||||
### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp`
|
### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp`
|
||||||
Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
||||||
@@ -168,7 +173,7 @@ Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
|||||||
- `namespace VoxelCaveMorphology`:
|
- `namespace VoxelCaveMorphology`:
|
||||||
| Function | .cpp line | Role |
|
| 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. |
|
| `BuildChunkCache` | 47 | **Phase 1** (once/chunk): collect rooms, guaranteed backbone (`bTunnelsFlowTowardOrigin`: tree rooted at the (0,0) hub — every room reachable, links flow inward; false = legacy NN forest), slope-aware link metric (`TunnelHorizontalBias` now applies to backbone too), decide tunnels, **cull zero-connection rooms** (no sealed bubbles), store rooms by their OWN reach (fixes origin-room clipping at `MaxInfluence`), 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. |
|
| `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. |
|
| `EvaluateSDF` | 738 | Convenience wrapper (builds temp cache) for one-off queries. |
|
||||||
|
|
||||||
@@ -192,7 +197,8 @@ Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
|||||||
**`Public/VoxelStrateDefinition.h`** — `UVoxelStrateDefinition : UPrimaryDataAsset`
|
**`Public/VoxelStrateDefinition.h`** — `UVoxelStrateDefinition : UPrimaryDataAsset`
|
||||||
(line 36). One asset = one strate *type*. Fields: identity, `StrateHeightInChunks`(60),
|
(line 36). One asset = one strate *type*. Fields: identity, `StrateHeightInChunks`(60),
|
||||||
`TransitionType`(79)/`TransitionBlendChunks`(89), `GeneratorType`(102),
|
`TransitionType`(79)/`TransitionBlendChunks`(89), `GeneratorType`(102),
|
||||||
`GenerationParams`(113), `SlabParams`(124), `TerrainOperations`(147), visuals/fog/light,
|
`GenerationParams`(113), `SlabParams`(124), `Biomes[]`+`BiomeMapParams` (the biome list +
|
||||||
|
field tuning — empty ⇒ unchanged world, §8.14), `TerrainOperations`(147), visuals/fog/light,
|
||||||
content lists, audio, `GameplayTags`(223). EditConditions show/hide param groups by generator type.
|
content lists, audio, `GameplayTags`(223). EditConditions show/hide param groups by generator type.
|
||||||
|
|
||||||
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:108).
|
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:108).
|
||||||
@@ -209,6 +215,7 @@ Maps depth→strate at runtime; owns passages.
|
|||||||
| `GetStrateForChunk` | 466 | Chunk → definition. |
|
| `GetStrateForChunk` | 466 | Chunk → definition. |
|
||||||
| `GetGeneratorTypeForChunk` | 476 | Chunk → generator type. |
|
| `GetGeneratorTypeForChunk` | 476 | Chunk → generator type. |
|
||||||
| `GetSlabParamsForChunk` | 490 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
|
| `GetSlabParamsForChunk` | 490 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
|
||||||
|
| `GetBiomeContextForChunk` | — | Flatten the strate's `Biomes[]` + `BiomeMapParams` into a POD `FBiomeContext` for the biome field. Empty ⇒ biomes disabled. §8.14. |
|
||||||
| `GetGenerationParams` | 515 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
|
| `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. |
|
| `BuildParamsFromDefinition` (static) | 771 | Base params + merge all referenced terrain op assets. The one place ops fold into params. |
|
||||||
|
|
||||||
@@ -218,6 +225,17 @@ Ribbing, Cliff, Scallop, Overhang, Arch, Column, Pit, Chimney, Dome, Pinch. Per-
|
|||||||
param groups gated by EditCondition. `ApplyTo(OutParams, Weight)` (.cpp:6) copies only
|
param groups gated by EditCondition. `ApplyTo(OutParams, Weight)` (.cpp:6) copies only
|
||||||
the active type's fields into `FStrateGenerationParams`, scaled by Weight.
|
the active type's fields into `FStrateGenerationParams`, scaled by Weight.
|
||||||
|
|
||||||
|
**`Public/VoxelBiomeTypes.h`** (NEW) — biome vocabulary. `FBiomeMapParams` (Voronoi cell size /
|
||||||
|
border warp+blend / climate field freqs), `EBiomePreviewChannel` (preview-bake selector), and plain
|
||||||
|
runtime PODs `FBiomeResolved` / `FBiomeContext` / `FBiomeSample` / `FChunkBiomeCache` (the
|
||||||
|
box-validated per-chunk grid cache). See §8.14.
|
||||||
|
|
||||||
|
**`Public/VoxelBiomeDefinition.h` + `.cpp`** (NEW) — `UVoxelBiomeDefinition : UPrimaryDataAsset`.
|
||||||
|
One asset = one biome: identity + `DebugColor`, climate placement box (`ReliefMin/Max`,
|
||||||
|
`MoistureMin/Max`), terrain override (`bOverrideTerrain` + `GeneratorType` + archetype params), content profile (`Decorations`/`AmbientActors`,
|
||||||
|
atmosphere override, `WaterMaterial`, reserved `MaterialPaletteIndex`), `GameplayTags`. Referenced from
|
||||||
|
`UVoxelStrateDefinition::Biomes[]`. Generator-agnostic (surface biomes now, cave biomes later). §8.14.
|
||||||
|
|
||||||
### 3.9 Player edits — `Public/VoxelDiffLayer.h` + `.cpp`
|
### 3.9 Player edits — `Public/VoxelDiffLayer.h` + `.cpp`
|
||||||
`UVoxelDiffLayer : UObject` (h:77). Stores `FVoxelModification` (h:43: Center/Radius/Strength;
|
`UVoxelDiffLayer : UObject` (h:77). Stores `FVoxelModification` (h:43: Center/Radius/Strength;
|
||||||
**negative Strength = carve, positive = fill**) grouped by chunk in `TMap ChunkMods`.
|
**negative Strength = carve, positive = fill**) grouped by chunk in `TMap ChunkMods`.
|
||||||
@@ -279,7 +297,7 @@ Stage order (negative=solid throughout). Each stage's anchor:
|
|||||||
| 4d — Columns | 1053 | Pre-baked vertical cylinders. |
|
| 4d — Columns | 1053 | Pre-baked vertical cylinders. |
|
||||||
| 4g — Domes | 1077 | Room-relative hemispherical ceilings. |
|
| 4g — Domes | 1077 | Room-relative hemispherical ceilings. |
|
||||||
| 4h — Pinch | 1142 | Passage bottlenecks. |
|
| 4h — Pinch | 1142 | Passage bottlenecks. |
|
||||||
| 5 — Worm tunnels | 1241 | abs(noise1)+abs(noise2) connectivity. |
|
| 5 — Worm tunnels | 1241 | abs(noise1)+abs(noise2), masked by distance-to-network (`WormNetworkRange`: braids hugging rooms/tunnels, no far-field speckle; 0 = legacy unmasked). |
|
||||||
| 6 — Boundary seal | 1274 | Solid top/bottom shells (`ApplyBoundarySeal`). |
|
| 6 — Boundary seal | 1274 | Solid top/bottom shells (`ApplyBoundarySeal`). |
|
||||||
| 7 — Inter-strate passages | 1281 | Carve passages/elevator (`ApplyPassageCarving`). |
|
| 7 — Inter-strate passages | 1281 | Carve passages/elevator (`ApplyPassageCarving`). |
|
||||||
|
|
||||||
@@ -312,6 +330,9 @@ Stage order (negative=solid throughout). Each stage's anchor:
|
|||||||
| Player carve/fill | `CarveAtPosition`/`FillAtPosition` VoxelWorld.cpp:691/709 → `UVoxelDiffLayer::ApplyModification` :63. |
|
| Player carve/fill | `CarveAtPosition`/`FillAtPosition` VoxelWorld.cpp:691/709 → `UVoxelDiffLayer::ApplyModification` :63. |
|
||||||
| Mesh smoothness / normals | `UVoxelMarchingCubesMesher::ComputeGradientNormal` :48, `IsoLevel`/`GradientOffset` (h:51/55). |
|
| Mesh smoothness / normals | `UVoxelMarchingCubesMesher::ComputeGradientNormal` :48, `IsoLevel`/`GradientOffset` (h:51/55). |
|
||||||
| New slab/flat-world generator | `GetSlabDensity` Generator.cpp:1306 + `FSlabGenerationParams` (StrateTypes.h:1019). |
|
| New slab/flat-world generator | `GetSlabDensity` Generator.cpp:1306 + `FSlabGenerationParams` (StrateTypes.h:1019). |
|
||||||
|
| Biome placement / layout | `BiomeMapParams` on the strate (cell size, warp, climate freqs) + each biome's climate box. Bake `AVoxelWorld::BakeBiomePreview` to tune. §8.14. |
|
||||||
|
| What a biome does to terrain | A full archetype param override on the biome (`bOverrideTerrain` + `SurfaceParams`); surface output-blends dominant/neighbour heights in `GetSurfaceDensity`. Caves = content/atmosphere only (determinism, §8.14). |
|
||||||
|
| Add a biome / biome content | New `UVoxelBiomeDefinition` asset → add to the strate's `Biomes[]`. §8.14 / §8.12. |
|
||||||
| Season reset | `AVoxelWorld::ChangeSeed` :740. |
|
| Season reset | `AVoxelWorld::ChangeSeed` :740. |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -357,7 +378,7 @@ by `GeneratorType`) and its own density function in `VoxelGenerator.cpp`, dispat
|
|||||||
| TunnelNetwork | `FStrateGenerationParams` | `GetDensityWithParams` | rooms+tunnels (original) |
|
| TunnelNetwork | `FStrateGenerationParams` | `GetDensityWithParams` | rooms+tunnels (original) |
|
||||||
| FlatPlain / CrystalChamber | `FSlabGenerationParams` | `GetSlabDensity` | floor/ceiling void (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) |
|
| 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 |
|
| SurfaceWorld | `FSurfaceGenerationParams` | `GetSurfaceDensity` | heightfield terrain: domain-warped continents+ridged mtns+detail, a low-freq **relief map** (`M`) that scales mountains/elevation for plains↔highland variety, opt-in plateau **terracing**, beaches at water line, high sky-cap ceiling. (`Surface|Macro` params = the cheap precursor to biomes; `ReliefStrength=0` ⇒ old uniform terrain.) |
|
||||||
| VerticalShafts | `FVerticalShaftParams` | `GetVerticalShaftDensity` | full-height shafts + horizontal connectors + partial ledges |
|
| 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 |
|
| 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 |
|
| Underwater | `FStrateGenerationParams` + water | (reuses `GetDensityWithParams`) | tunnel rock + high water table |
|
||||||
@@ -366,6 +387,8 @@ All density fns share the convention: internal **positive=solid**, apply origin
|
|||||||
boundary seal → inter-strate passages, then `return -Density` (MC: negative=solid).
|
boundary seal → inter-strate passages, then `return -Density` (MC: negative=solid).
|
||||||
StrateManager provides params per chunk via `GetMaze/Surface/VerticalShaft/FloatingIslandParamsForChunk`
|
StrateManager provides params per chunk via `GetMaze/Surface/VerticalShaft/FloatingIslandParamsForChunk`
|
||||||
(macro `VF_ARCHETYPE_PARAMS_GETTER`) — no cross-boundary blend (Hard transitions between archetypes).
|
(macro `VF_ARCHETYPE_PARAMS_GETTER`) — no cross-boundary blend (Hard transitions between archetypes).
|
||||||
|
On top of the archetype, an optional **biome** layer (§8.14) modulates terrain & content WITHIN a
|
||||||
|
strate via a window-invariant XY field — currently wired into SurfaceWorld.
|
||||||
|
|
||||||
### 8.2 (0,0) spine & hybrid connections
|
### 8.2 (0,0) spine & hybrid connections
|
||||||
- `ApplyOriginSpine` (VoxelGenerator.cpp, static helper) carves a guaranteed open vertical
|
- `ApplyOriginSpine` (VoxelGenerator.cpp, static helper) carves a guaranteed open vertical
|
||||||
@@ -395,12 +418,24 @@ max edge reach, and decisions must not depend on the stored window.**
|
|||||||
### 8.5 Content scatter & water — `VoxelContentManager.h/.cpp` (NEW)
|
### 8.5 Content scatter & water — `VoxelContentManager.h/.cpp` (NEW)
|
||||||
`UVoxelContentManager` (owned by `AVoxelWorld`, game-thread). Chunk lifecycle:
|
`UVoxelContentManager` (owned by `AVoxelWorld`, game-thread). Chunk lifecycle:
|
||||||
`PopulateChunk(coord, mesh, LOD)` in `ApplyMeshToChunk`, `ClearChunk` in `UnloadChunk`,
|
`PopulateChunk(coord, mesh, LOD)` in `ApplyMeshToChunk`, `ClearChunk` in `UnloadChunk`,
|
||||||
`ClearAll`/`SetSeed` in `ChangeSeed`. Deterministic decoration scatter on mesh vertices
|
`ClearAll`/`SetSeed` in `ChangeSeed`, `SetActiveStrate(GetStrateAtPosition(player))` each Tick.
|
||||||
(surface-type / water-relative / align-to-normal / random-yaw / scale / `MaxPerChunk`, from
|
Deterministic decoration scatter on mesh vertices (surface-type / water-relative /
|
||||||
`FStrateDecoration`) — **only at LOD 0** (perf). Aesthetic water = one scaled engine plane
|
align-to-normal / random-yaw / scale / `MaxPerChunk`, from `FStrateDecoration`).
|
||||||
(`/Engine/BasicShapes/Plane`) per water-surface chunk (any LOD), material
|
**Two render paths per entry:** `ActorClass` (real actors — lights/logic/interaction,
|
||||||
`UVoxelStrateDefinition::WaterMaterial`. Water Z: `bHasWater` + `WaterLevelRelative`
|
keep `MaxLODLevel=0`) vs `InstancedMesh` (batched per-chunk **HISM**, no tick/actor cost,
|
||||||
(Surface/tunnel params) → `StrateManager::GetWaterLevelWorldZForChunk`.
|
no collision; safe at `MaxLODLevel` 1-2 so visual props don't pop out with LOD0 — emissive
|
||||||
|
materials still glow at distance). Placement samples the LOD's vertices ⇒ instances
|
||||||
|
re-scatter slightly on LOD swap (masked by terrain pop). **Strate light culling:**
|
||||||
|
light components on decoration actors are visible only while the player is in the SAME
|
||||||
|
strate (`SetActiveStrate`, no-op until strate change) — analytical occlusion: seals+gap are
|
||||||
|
light-tight, and shadowless lights otherwise BLEED through rock (shadowed ones render black
|
||||||
|
at full cost). 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`.
|
||||||
|
**Per-biome content (§8.14):** when the chunk's dominant biome (`Generator::GetDominantBiomeAt`)
|
||||||
|
supplies decorations they REPLACE the strate's; biome `WaterMaterial` overrides the plane's material
|
||||||
|
(level stays strate-global). NOTE: `GetChunkStrateIndex` (light culling) feeds `GetStrateIndex` which
|
||||||
|
expects Unreal **cm** — it now passes `chunkZ*CHUNK_SIZE*VOXEL_SIZE` (was voxel-Z, a latent strate-match bug).
|
||||||
|
|
||||||
### 8.6 Atmosphere — `VoxelAtmosphereManager.h/.cpp` (NEW)
|
### 8.6 Atmosphere — `VoxelAtmosphereManager.h/.cpp` (NEW)
|
||||||
`UVoxelAtmosphereManager` (owned by `AVoxelWorld`, gated by `bManageAtmosphere`).
|
`UVoxelAtmosphereManager` (owned by `AVoxelWorld`, gated by `bManageAtmosphere`).
|
||||||
@@ -412,6 +447,10 @@ PERSISTENT ceiling/floor "layer" actors (`Def->CeilingLayerActor`/`FloorLayerAct
|
|||||||
`Def->AtmosphereActor` (a full BP with your own fog/sky/postprocess) OVERRIDES the managed
|
`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
|
fog+sky for that strate. `Reset()` on ChangeSeed/EndPlay. (Skylight ambient underground is
|
||||||
weak — captures a dark scene; fog is the strong visual.)
|
weak — captures a dark scene; fog is the strong visual.)
|
||||||
|
**Per-biome atmosphere (§8.14):** `UpdateForPlayer` also resolves the player's dominant biome and,
|
||||||
|
when the biome has `bOverrideAtmosphere`, its fog/sky beats the strate's (reacts on biome change, not
|
||||||
|
just strate change). `ApplyFogSky(Def, Biome)` is the shared path; layer actors + the full `AtmosphereActor`
|
||||||
|
BP stay strate-level. Needs the generator injected (`Initialize(..., Generator)`).
|
||||||
|
|
||||||
### 8.7 Inter-strate bedrock gap
|
### 8.7 Inter-strate bedrock gap
|
||||||
`VoxelSettings::InterStrateGapChunks` (N) inserts N chunks of SOLID bedrock between consecutive
|
`VoxelSettings::InterStrateGapChunks` (N) inserts N chunks of SOLID bedrock between consecutive
|
||||||
@@ -451,12 +490,50 @@ driven by `EditorBrush*` props.
|
|||||||
sampling must not thrash the (expensive) rebuild.
|
sampling must not thrash the (expensive) rebuild.
|
||||||
- **Per-chunk param cache** in `GetDensityAt`: GenType + param struct + disturbance cached
|
- **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.
|
thread-locally per chunk; don't move the fetch/blend back to per-voxel.
|
||||||
|
- **Biome cache** (`ResolveBiomeSampleAt`/`FChunkBiomeCache`, §8.14): validity is a world-XY BOX +
|
||||||
|
ChunkZ + Seed, NOT a chunk key — same reason as the SDF cache. The cell classification is
|
||||||
|
noise-heavy; a chunk-key would thrash it on gradient-normal / +X/+Y boundary samples. Keep
|
||||||
|
the box halo (≥ CHUNK_SIZE) + cell margin (warp + CellSize) so the 3x3 lookup never misses.
|
||||||
- **Passage cull** (§8.8) + **morphology two-region** (§8.4): both are per-voxel-cost critical.
|
- **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
|
- **Mesher density grid + margin ring** (`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
|
`(CHUNK_SIZE/Step + 1 + 2)³` array (the `+2` is a 1-point MARGIN ring, indices −1..GridDim, for
|
||||||
cells share corners, so per-cell sampling calls `GetDensityAt` ~8× too often (262k→36k at
|
T1.b normals). The cell loop reads 8 corners from it; per-cell sampling would call `GetDensityAt`
|
||||||
LOD0). Output is bit-identical (`GetDensityAt` is pure in world coords). Don't refactor
|
~8× too often. Geometry is bit-identical (edge positions unchanged). Don't refactor back to
|
||||||
back to per-corner `GetDensity` calls inside the cell loop.
|
per-corner `GetDensity` and don't drop the margin ring (normals + seamless borders need it).
|
||||||
|
- **Normals from the density grid (T1.b)** (`GenerateMesh`): corner gradients = central differences
|
||||||
|
on the (margin) grid; edge normals interpolate the two corner gradients by the SAME `t` as the
|
||||||
|
position → seamless across chunk borders (both sides use identical pure samples). NO per-vertex
|
||||||
|
`GetDensityAt` (was ~6/vertex, often as costly as the whole grid). `ComputeGradientNormal` is now
|
||||||
|
unused. Only NORMALS changed vs the old path; geometry is identical.
|
||||||
|
- **Surface column cache (T1.a)** (`FSurfaceColumnCache`, `GetDensityAt` SurfaceWorld branch):
|
||||||
|
the heightfield + sky-cap + biome blend are XY-only but sampled ~33× per column (once per Z
|
||||||
|
grid-point). Cached per integer XY (box-valid, like the SDF cache) and reused down the column.
|
||||||
|
Box `Halo = CHUNK_SIZE + 8` each side so the T1.b margin ring stays inside (no thrash). **Used
|
||||||
|
ONLY for integer-XY queries**; fractional queries compute directly → bit-identical. Don't key it
|
||||||
|
by chunk and don't feed it fractional coords.
|
||||||
|
- **Collision only at LOD0 (T1.c)** (`ApplyMeshToChunk`): `UpdateSectionConfig(..., LOD==0)`.
|
||||||
|
LOD1/2 chunks are unreachable (the §8.10 reconciliation hot-swaps to LOD0 before the player
|
||||||
|
arrives), so cooking their Chaos collision is waste. Don't force collision on for all LODs.
|
||||||
|
- **No shadows on LOD2 (draw-call cut)** (`ApplyMeshToChunk`): `MeshComp->SetCastShadow(LOD <= 1)`.
|
||||||
|
Every shadow-casting chunk emits a SECOND draw in the shadow-depth pass; the farthest tier
|
||||||
|
(LOD2, blocky) doesn't need it. Re-applied every apply (LOD hot-swaps reuse the component).
|
||||||
|
Pulling `LOD0Distance`/`LOD1Distance` inward pushes more chunks into the shadowless LOD2 band
|
||||||
|
→ fewer draws (free fps knob). Widen to `<= 0` (drop LOD1 shadows too) if still draw-bound.
|
||||||
|
NOTE: fps is a RENDER-side problem (draw calls ≈ visible chunk count × passes) — generation
|
||||||
|
cost (worker threads) and LOD *resolution* (cuts triangles, not draws) don't move it.
|
||||||
|
- **Insights scopes** `VoxelForge_GenerateMesh` / `VoxelForge_ApplyMeshToChunk` (Perf 0) bracket
|
||||||
|
the worker gen + game-thread apply — capture a trace to see if we're density- or upload-bound.
|
||||||
|
- **Float SIMD noise core (T2.a)** (`Public/VoxelNoise.h`): the density hot path uses
|
||||||
|
`VoxelNoise::Perlin3D` (single-sample, float, table-free hash-gradient) and `VoxelNoise::FBM` /
|
||||||
|
`Ridged` (octaves evaluated **4-wide via SSE** `Perlin3D_x4`) — NOT `FMath::PerlinNoise3D`
|
||||||
|
(double-precision, the old ~6.6 ms/chunk noise cost). `FractalNoise3D` / `RidgedNoise3D` in
|
||||||
|
`VoxelGenerator.cpp` are now thin wrappers over it; every call site is unchanged. It's a
|
||||||
|
DIFFERENT noise field than FMath's ⇒ a ONE-TIME world re-tune (fBm/Ridged contracts/[-1,1] are
|
||||||
|
identical). Pure function of (x,y,z) ⇒ every box-validity cache stays valid. Scalar `Perlin3D`
|
||||||
|
and SSE `Perlin3D_x4` are op-for-op identical (bit-identical on x86) — the SIMD path is a free
|
||||||
|
speedup; `#define VF_NOISE_USE_SIMD 0` falls back to scalar with no re-tune if a toolchain
|
||||||
|
rejects the SSE4.1 intrinsics. StrateManager's passage/transition Perlin calls were left on
|
||||||
|
`FMath` (layout-time, not per-voxel). Don't reintroduce `FMath::PerlinNoise3D` on the density path.
|
||||||
- **`ProcessQueue` MUST be `EQueueMode::Mpsc`** (`VoxelWorld.h`): up to `MaxConcurrentTasks`
|
- **`ProcessQueue` MUST be `EQueueMode::Mpsc`** (`VoxelWorld.h`): up to `MaxConcurrentTasks`
|
||||||
`ChunkGen` worker threads `Enqueue` concurrently; the game thread is the sole consumer.
|
`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
|
The default `Spsc` is single-producer — concurrent enqueues race the tail link and silently
|
||||||
@@ -476,14 +553,66 @@ driven by `EditorBrush*` props.
|
|||||||
4. Atmosphere: `FogColor/Density`, `AmbientLight*`, `bVolumetricFog`, or a full `AtmosphereActor` BP;
|
4. Atmosphere: `FogColor/Density`, `AmbientLight*`, `bVolumetricFog`, or a full `AtmosphereActor` BP;
|
||||||
`CeilingLayerActor`/`FloorLayerActor` (+offsets/rotations) for cloud seas.
|
`CeilingLayerActor`/`FloorLayerActor` (+offsets/rotations) for cloud seas.
|
||||||
5. `Decorations`/`AmbientActors` (placement rules) for content + lights.
|
5. `Decorations`/`AmbientActors` (placement rules) for content + lights.
|
||||||
6. Reference from `VoxelSettings` (`StratePool`/`FixedStrates`). Global knobs there:
|
6. (Optional) `Biomes[]` + `BiomeMapParams` to vary terrain/content within the strate (§8.14).
|
||||||
|
Author `UVoxelBiomeDefinition` assets (climate box + modulation + content), then tune layout
|
||||||
|
with `AVoxelWorld::BakeBiomePreview`. Turn `ReliefStrength` down when biomes drive elevation.
|
||||||
|
7. Reference from `VoxelSettings` (`StratePool`/`FixedStrates`). Global knobs there:
|
||||||
`OriginSpineRadius`, `bOpenSurfaceEntry`, `InterStrateGapChunks`, view distances, LOD, carving budget.
|
`OriginSpineRadius`, `bOpenSurfaceEntry`, `InterStrateGapChunks`, view distances, LOD, carving budget.
|
||||||
|
|
||||||
### 8.13 New files this redesign
|
### 8.13 New files this redesign
|
||||||
`Public/Private/VoxelContentManager.h/.cpp` (§8.5) · `Public/Private/VoxelAtmosphereManager.h/.cpp` (§8.6).
|
`Public/Private/VoxelContentManager.h/.cpp` (§8.5) · `Public/Private/VoxelAtmosphereManager.h/.cpp` (§8.6) ·
|
||||||
|
`Public/VoxelBiomeTypes.h` + `Public/VoxelBiomeDefinition.h`/`Private/VoxelBiomeDefinition.cpp` (§8.14).
|
||||||
Everything else extended existing files: `VoxelStrateTypes.h` (archetype params, disturbance,
|
Everything else extended existing files: `VoxelStrateTypes.h` (archetype params, disturbance,
|
||||||
`FStratePassageConfig`, enums), `VoxelStrateDefinition.h`, `VoxelGenerator.h/.cpp` (archetype
|
`FStratePassageConfig`, enums), `VoxelStrateDefinition.h`, `VoxelGenerator.h/.cpp` (archetype
|
||||||
density fns + spine/disturbance/param-cache), `VoxelStrateManager.h/.cpp` (per-archetype getters,
|
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,
|
passages, gap, atmosphere Z helper), `VoxelWorld.h/.cpp` (managers, streaming perf, brush API,
|
||||||
editor buttons), `VoxelDiffLayer.h/.cpp` (brush shapes), `VoxelSettings.h`, `VoxelCaveMorphology.cpp`
|
editor buttons), `VoxelDiffLayer.h/.cpp` (brush shapes), `VoxelSettings.h`, `VoxelCaveMorphology.cpp`
|
||||||
(two-region determinism). Status: compiles & runs in-editor.
|
(two-region determinism). Status: compiles & runs in-editor.
|
||||||
|
|
||||||
|
### 8.14 Biome system (Stage 1 — climate-driven, full-param overrides)
|
||||||
|
Biomes vary terrain **and** content WITHIN a strate. A biome is a **"mini-strate-variant"**: it
|
||||||
|
can carry a FULL archetype param override (its own `FSurfaceGenerationParams`, …) plus a content
|
||||||
|
profile, placed by a deterministic, window-invariant world-XY field. Empty `Biomes[]` ⇒ bit-identical
|
||||||
|
to the pre-biome world. (Replaces the earlier `FBiomeModulation` scalar bag — full params let a biome
|
||||||
|
change *anything*, e.g. frequencies, which scalar multipliers couldn't.)
|
||||||
|
|
||||||
|
- **Assets/data.** `UVoxelBiomeDefinition` (one per biome): `DebugColor`, climate box (relief,
|
||||||
|
moisture), `bOverrideTerrain` + `GeneratorType` + the matching archetype param struct (Surface
|
||||||
|
wired), content profile (decorations/atmosphere/water). + `UVoxelStrateDefinition::Biomes[]` &
|
||||||
|
`BiomeMapParams`. Types in `VoxelBiomeTypes.h` (§3.8).
|
||||||
|
- **The field (pure XY, window-invariant — §8.4).** `SampleBiomeAt` (VoxelGenerator.cpp): warped
|
||||||
|
**Voronoi** over a jittered grid → dominant cell + nearest neighbour (F1/F2) + border blend weight.
|
||||||
|
Each cell's biome is chosen by `ClassifyBiomeAtSite` from the site's **climate** = `SampleRelief`
|
||||||
|
(the relief map M, shared with SurfaceWorld terrain) + `SampleMoisture`, matched against each
|
||||||
|
biome's (relief, moisture) box → coherent geography. **Climate must vary much slower than
|
||||||
|
`CellSize`** (~4-6 cells/feature) or it's salt-and-pepper.
|
||||||
|
- **Per-chunk resolution (perf — §8.10).** `ResolveBiomeSampleAt`/`RebuildBiomeGrid` build a
|
||||||
|
`FChunkBiomeCache`: the expensive cell classification is done ONCE into a small grid; per voxel only
|
||||||
|
a warp + 3x3 lookup, returning `FBiomeSample` (dominant + neighbour + weight). **Cache validity is a
|
||||||
|
world-XY BOX + ChunkZ + Seed (NOT a chunk key)** — gradient-normal + boundary samples stay inside
|
||||||
|
the box and don't thrash the noise-heavy rebuild (same as the SDF cache). Bit-identical to
|
||||||
|
`SampleBiomeAt`, so the baked preview matches the terrain. `GetBiomeContextForChunk` supplies the
|
||||||
|
flattened POD context per chunk (thread-local `CP_BiomeCtx`).
|
||||||
|
- **Consumption — SURFACE (output-blend).** Per chunk, `CP_SurfaceBiomeParams[]` holds each biome's
|
||||||
|
resolved surface params (its override when `bOverrideTerrain` + GeneratorType matches, else the
|
||||||
|
strate's) with **structural fields forced from the strate** (Z bounds, seal, base density, water
|
||||||
|
level). Per voxel: `ResolveBiomeSampleAt` → dominant `PD` (+ neighbour `PN`); `GetSurfaceDensity`
|
||||||
|
computes `ComputeSurfaceTerrainZ` for `PD` and, in the border band, for `PN`, and **lerps the
|
||||||
|
resulting HEIGHTS**. Blending heights (not params) is seamless across *any* difference (frequencies
|
||||||
|
included) — what per-param blend never could. `PD==PN`, weight 0 ⇒ bit-identical, no biomes.
|
||||||
|
- **Consumption — CAVES: structural overrides are NOT applied (determinism).** Rooms/tunnels are
|
||||||
|
decided over a wide COLLECT region spanning chunks (§8.4); making room params vary by region would
|
||||||
|
need the biome sampled per *room site* inside `BuildChunkCache`, or it breaks window-invariance
|
||||||
|
(a room near a border resolves differently per querying chunk → seams/holes). So SDF archetypes
|
||||||
|
(Tunnel/Maze/Shaft/Islands) keep strate-level structure; biomes affect them via **content +
|
||||||
|
atmosphere only** (below). Per-room-site biome params = a future deep task.
|
||||||
|
- **Consumption (content/atmosphere).** `GetDominantBiomeAt(x,y,chunkZ)` (game-thread, uncached) →
|
||||||
|
biome ASSET. ContentManager: dominant biome's decorations (else strate's) + water material override.
|
||||||
|
AtmosphereManager: player's dominant biome fog/sky (`bOverrideAtmosphere`). Works for ANY archetype.
|
||||||
|
Water LEVEL stays strate-global (continuous plane); biomes retint material only.
|
||||||
|
- **Preview tool.** `AVoxelWorld::BakeBiomePreview()` (CallInEditor) bakes biome / relief / moisture
|
||||||
|
to `Saved/BiomePreview.png` via a transient generator (no PIE). Needs the `ImageWrapper` module.
|
||||||
|
- **Status:** A (field+asset+preview), B (terrain), C (content/atmosphere) verified in-editor.
|
||||||
|
Full-param redesign (surface output-blend) code-complete, pending build. Cave structural biomes
|
||||||
|
deferred (determinism, see above). Per-voxel biome warp (+2 Perlin) & content `GetDominantBiomeAt`
|
||||||
|
are future T1.a column-cache candidates.
|
||||||
|
|||||||
@@ -3,15 +3,19 @@
|
|||||||
#include "VoxelAtmosphereManager.h"
|
#include "VoxelAtmosphereManager.h"
|
||||||
#include "VoxelStrateManager.h"
|
#include "VoxelStrateManager.h"
|
||||||
#include "VoxelStrateDefinition.h"
|
#include "VoxelStrateDefinition.h"
|
||||||
|
#include "VoxelBiomeDefinition.h"
|
||||||
|
#include "VoxelGenerator.h"
|
||||||
|
#include "VoxelTypes.h" // CHUNK_SIZE / VOXEL_SIZE
|
||||||
#include "Components/ExponentialHeightFogComponent.h"
|
#include "Components/ExponentialHeightFogComponent.h"
|
||||||
#include "Components/SkyLightComponent.h"
|
#include "Components/SkyLightComponent.h"
|
||||||
#include "GameFramework/Actor.h"
|
#include "GameFramework/Actor.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
|
|
||||||
void UVoxelAtmosphereManager::Initialize(AActor* InOwner, UVoxelStrateManager* InStrateManager)
|
void UVoxelAtmosphereManager::Initialize(AActor* InOwner, UVoxelStrateManager* InStrateManager, UVoxelGenerator* InGenerator)
|
||||||
{
|
{
|
||||||
Owner = InOwner;
|
Owner = InOwner;
|
||||||
StrateManager = InStrateManager;
|
StrateManager = InStrateManager;
|
||||||
|
Generator = InGenerator;
|
||||||
|
|
||||||
if (!InOwner) return;
|
if (!InOwner) return;
|
||||||
USceneComponent* Root = InOwner->GetRootComponent();
|
USceneComponent* Root = InOwner->GetRootComponent();
|
||||||
@@ -41,11 +45,28 @@ void UVoxelAtmosphereManager::UpdateForPlayer(const FVector& PlayerWorldPos)
|
|||||||
const int32 Idx = StrateManager->GetStrateIndex(PlayerWorldPos.Z);
|
const int32 Idx = StrateManager->GetStrateIndex(PlayerWorldPos.Z);
|
||||||
const UVoxelStrateDefinition* Def = StrateManager->GetStrateAt(PlayerWorldPos.Z);
|
const UVoxelStrateDefinition* Def = StrateManager->GetStrateAt(PlayerWorldPos.Z);
|
||||||
|
|
||||||
// React only when the player changes strate (cheap on every other frame).
|
// Player's dominant biome (XY field) — atmosphere can vary by biome within a strate.
|
||||||
|
// Convention matches GetStrateAt: world cm = voxel * VOXEL_SIZE (actor at origin/identity).
|
||||||
|
const UVoxelBiomeDefinition* Biome = nullptr;
|
||||||
|
if (Generator && Def)
|
||||||
|
{
|
||||||
|
const int32 ChunkZ = FMath::FloorToInt((PlayerWorldPos.Z / VOXEL_SIZE) / CHUNK_SIZE);
|
||||||
|
Biome = Generator->GetDominantBiomeAt(PlayerWorldPos.X / VOXEL_SIZE,
|
||||||
|
PlayerWorldPos.Y / VOXEL_SIZE, ChunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
// React on strate change (full apply) OR biome change within a strate (fog/sky only —
|
||||||
|
// layer actors + atmosphere BP are strate-level). Cheap no-op otherwise.
|
||||||
if (Idx != CurrentStrateIndex)
|
if (Idx != CurrentStrateIndex)
|
||||||
{
|
{
|
||||||
CurrentStrateIndex = Idx;
|
CurrentStrateIndex = Idx;
|
||||||
ApplyStrate(Def);
|
CurrentBiome = Biome;
|
||||||
|
ApplyStrate(Def, Biome);
|
||||||
|
}
|
||||||
|
else if (Biome != CurrentBiome.Get())
|
||||||
|
{
|
||||||
|
CurrentBiome = Biome;
|
||||||
|
ApplyFogSky(Def, Biome);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anchor the full-atmosphere BP to the player (so any localized volumes/effects
|
// Anchor the full-atmosphere BP to the player (so any localized volumes/effects
|
||||||
@@ -78,7 +99,54 @@ void UVoxelAtmosphereManager::UpdateForPlayer(const FVector& PlayerWorldPos)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UVoxelAtmosphereManager::ApplyStrate(const UVoxelStrateDefinition* Def)
|
void UVoxelAtmosphereManager::ApplyFogSky(const UVoxelStrateDefinition* Def, const UVoxelBiomeDefinition* Biome)
|
||||||
|
{
|
||||||
|
// A strate's full atmosphere BP owns the entire look — managed fog/sky stay off.
|
||||||
|
const bool bUseOverride = (Def && Def->AtmosphereActor);
|
||||||
|
|
||||||
|
// Biome retint beats the strate's fog/sky when the biome opts in.
|
||||||
|
const bool bBiome = (Biome && Biome->bOverrideAtmosphere);
|
||||||
|
const FLinearColor FogCol = bBiome ? Biome->FogColor : (Def ? Def->FogColor : FLinearColor::Black);
|
||||||
|
const float FogDen = bBiome ? Biome->FogDensity : (Def ? Def->FogDensity : 0.0f);
|
||||||
|
const FLinearColor AmbCol = bBiome ? Biome->AmbientLightColor : (Def ? Def->AmbientLightColor : FLinearColor::Black);
|
||||||
|
const float AmbInt = bBiome ? Biome->AmbientLightIntensity : (Def ? Def->AmbientLightIntensity : 0.0f);
|
||||||
|
const bool bVol = Def ? Def->bVolumetricFog : false;
|
||||||
|
|
||||||
|
// ── Managed fog (only when NOT overridden by a full atmosphere BP) ──
|
||||||
|
if (Fog)
|
||||||
|
{
|
||||||
|
if (!bUseOverride && FogDen > 0.0f)
|
||||||
|
{
|
||||||
|
Fog->SetVisibility(true);
|
||||||
|
// FogDensity is authored 0..1; EHF density is tiny — scale into a sane range.
|
||||||
|
Fog->SetFogDensity(FogDen * 0.05f);
|
||||||
|
Fog->SetFogInscatteringColor(FogCol);
|
||||||
|
Fog->SetVolumetricFog(bVol);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Fog->SetVisibility(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Managed ambient skylight (only when NOT overridden) ──
|
||||||
|
if (Sky)
|
||||||
|
{
|
||||||
|
if (!bUseOverride && Def)
|
||||||
|
{
|
||||||
|
Sky->SetIntensity(AmbInt);
|
||||||
|
Sky->SetLightColor(AmbCol);
|
||||||
|
Sky->SetLowerHemisphereColor(AmbCol);
|
||||||
|
Sky->RecaptureSky();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Sky->SetIntensity(0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVoxelAtmosphereManager::ApplyStrate(const UVoxelStrateDefinition* Def, const UVoxelBiomeDefinition* Biome)
|
||||||
{
|
{
|
||||||
AActor* O = Owner.Get();
|
AActor* O = Owner.Get();
|
||||||
UWorld* W = O ? O->GetWorld() : nullptr;
|
UWorld* W = O ? O->GetWorld() : nullptr;
|
||||||
@@ -98,38 +166,8 @@ void UVoxelAtmosphereManager::ApplyStrate(const UVoxelStrateDefinition* Def)
|
|||||||
AtmosphereActorInstance = W->SpawnActor<AActor>(Def->AtmosphereActor, FTransform::Identity, Params);
|
AtmosphereActorInstance = W->SpawnActor<AActor>(Def->AtmosphereActor, FTransform::Identity, Params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Managed fog (only when NOT overridden) ──
|
// ── Managed fog + ambient skylight (biome-aware) ──
|
||||||
if (Fog)
|
ApplyFogSky(Def, Biome);
|
||||||
{
|
|
||||||
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 ──
|
// ── Ceiling / floor layer actors — destroy old, spawn new for this strate ──
|
||||||
// (Independent of the override — you can have cloud seas with either fog path.)
|
// (Independent of the override — you can have cloud seas with either fog path.)
|
||||||
@@ -155,5 +193,6 @@ void UVoxelAtmosphereManager::Reset()
|
|||||||
if (CeilingActor) { CeilingActor->Destroy(); CeilingActor = nullptr; }
|
if (CeilingActor) { CeilingActor->Destroy(); CeilingActor = nullptr; }
|
||||||
if (FloorActor) { FloorActor->Destroy(); FloorActor = nullptr; }
|
if (FloorActor) { FloorActor->Destroy(); FloorActor = nullptr; }
|
||||||
CurrentStrateIndex = INT32_MIN;
|
CurrentStrateIndex = INT32_MIN;
|
||||||
|
CurrentBiome = nullptr;
|
||||||
if (Fog) Fog->SetVisibility(false);
|
if (Fog) Fog->SetVisibility(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,14 +94,10 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
const float MaxTunnelLen = FMath::Max(Params.MaxTunnelLength, 0.0f);
|
const float MaxTunnelLen = FMath::Max(Params.MaxTunnelLength, 0.0f);
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// STORE region — what we keep for the per-voxel loop.
|
// STORE decision — what we keep for the per-voxel loop.
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// A room/tunnel whose influence overlaps the search box is RELEVANT to this
|
// A room/tunnel is stored iff its OWN influence sphere can overlap the search box
|
||||||
// chunk. STORE box = search box + MaxInfluence. (Per-voxel culling refines this.)
|
// (RoomReachesSearchBox / tunnel bounding spheres below). Per-voxel culling refines.
|
||||||
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.
|
// COLLECT region — what we hash into existence for the connectivity decision.
|
||||||
@@ -142,13 +138,19 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
|
|
||||||
const float BlendK = Params.SDFBlendRadius;
|
const float BlendK = Params.SDFBlendRadius;
|
||||||
|
|
||||||
// Conservative "this room can reach the STORE box" test. A room's cull sphere
|
// "This room can reach the search box" test, using the room's OWN reach — the same
|
||||||
// radius is <= MaxInfluence, and STORE box already includes a MaxInfluence
|
// extent formula as its per-voxel cull sphere (1.5x radius for capsule-stretched
|
||||||
// margin, so testing the center against the STORE box is a correct superset.
|
// variants + blend margin). The old test compared the center against a box inflated
|
||||||
auto CenterInStoreBox = [&](const FVector& C) -> bool
|
// by the SHARED MaxInfluence, which silently assumed every room's reach <= MaxInfluence.
|
||||||
|
// That's FALSE for the origin room (OriginRoomRadius >> MaxRoomRadius) — chunks inside
|
||||||
|
// the big room but > MaxInfluence from (0,0) didn't store it, so its carve clipped at an
|
||||||
|
// arbitrary chunk-aligned radius — and slightly false even for hash rooms (1.5x stretch).
|
||||||
|
auto RoomReachesSearchBox = [&](const FVector& C, float RadiusXY, float RadiusZ) -> bool
|
||||||
{
|
{
|
||||||
return C.X >= StoreMinX && C.X <= StoreMaxX
|
const float Reach = FMath::Max(RadiusXY * 1.5f, RadiusZ) + BlendK * 3.0f;
|
||||||
&& C.Y >= StoreMinY && C.Y <= StoreMaxY;
|
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) <= Reach * Reach;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sphere-vs-search-box test in XY (treated as infinite in Z; per-voxel culling
|
// Sphere-vs-search-box test in XY (treated as infinite in Z; per-voxel culling
|
||||||
@@ -170,8 +172,14 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
int32 OriginIdx = -1;
|
int32 OriginIdx = -1;
|
||||||
if (Params.OriginRoomRadius > 0.0f)
|
if (Params.OriginRoomRadius > 0.0f)
|
||||||
{
|
{
|
||||||
if (0.0f >= CollectMinX && 0.0f <= CollectMaxX &&
|
// Collected when (0,0) is in the COLLECT region (connectivity) OR when the room's
|
||||||
0.0f >= CollectMinY && 0.0f <= CollectMaxY)
|
// own body can reach this chunk (store) — its radius may exceed the collect margin.
|
||||||
|
const bool bOriginInCollect =
|
||||||
|
0.0f >= CollectMinX && 0.0f <= CollectMaxX &&
|
||||||
|
0.0f >= CollectMinY && 0.0f <= CollectMaxY;
|
||||||
|
const float OriginRZ = Params.OriginRoomRadius * Params.RoomHeightRatio;
|
||||||
|
if (bOriginInCollect ||
|
||||||
|
RoomReachesSearchBox(FVector(0.0f, 0.0f, StrateCenterZ), Params.OriginRoomRadius, OriginRZ))
|
||||||
{
|
{
|
||||||
FBuildRoom OriginRoom;
|
FBuildRoom OriginRoom;
|
||||||
OriginRoom.CellX = INT32_MAX; // Sentinel — never matches a real grid cell
|
OriginRoom.CellX = INT32_MAX; // Sentinel — never matches a real grid cell
|
||||||
@@ -181,7 +189,7 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
OriginRoom.RadiusXY = Params.OriginRoomRadius;
|
OriginRoom.RadiusXY = Params.OriginRoomRadius;
|
||||||
OriginRoom.RadiusZ = Params.OriginRoomRadius * Params.RoomHeightRatio;
|
OriginRoom.RadiusZ = Params.OriginRoomRadius * Params.RoomHeightRatio;
|
||||||
OriginRoom.bIsOrigin = true;
|
OriginRoom.bIsOrigin = true;
|
||||||
OriginRoom.bStore = CenterInStoreBox(OriginRoom.Center);
|
OriginRoom.bStore = RoomReachesSearchBox(OriginRoom.Center, OriginRoom.RadiusXY, OriginRoom.RadiusZ);
|
||||||
OriginIdx = BuildRooms.Add(OriginRoom);
|
OriginIdx = BuildRooms.Add(OriginRoom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,7 +224,7 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
Room.RadiusXY = FMath::Lerp(Params.MinRoomRadius, Params.MaxRoomRadius, SizeFactor);
|
Room.RadiusXY = FMath::Lerp(Params.MinRoomRadius, Params.MaxRoomRadius, SizeFactor);
|
||||||
Room.RadiusZ = Room.RadiusXY * Params.RoomHeightRatio;
|
Room.RadiusZ = Room.RadiusXY * Params.RoomHeightRatio;
|
||||||
Room.bIsOrigin = false;
|
Room.bIsOrigin = false;
|
||||||
Room.bStore = CenterInStoreBox(Room.Center);
|
Room.bStore = RoomReachesSearchBox(Room.Center, Room.RadiusXY, Room.RadiusZ);
|
||||||
|
|
||||||
BuildRooms.Add(Room);
|
BuildRooms.Add(Room);
|
||||||
}
|
}
|
||||||
@@ -226,35 +234,70 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
if (NumRooms == 0) return;
|
if (NumRooms == 0) return;
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// Window-invariant nearest-neighbor backbone
|
// Window-invariant guaranteed backbone
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// Each room's nearest neighbor (among rooms within MaxTunnelLength) is a
|
// Each room gets ONE guaranteed link, chosen among candidates within MaxTunnelLength
|
||||||
// GUARANTEED tunnel connection. Filtering candidates to <= MaxTunnelLength is
|
// (that reach filter is what keeps the decision identical across chunk windows).
|
||||||
// what makes the result identical across chunks: rooms beyond MaxTunnelLength
|
//
|
||||||
// can never be a tunnel anyway, so excluding them removes the only source of
|
// bTunnelsFlowTowardOrigin = true (default): the link target is the best candidate
|
||||||
// window dependence. This builds a connected tree backbone; TunnelDensity adds
|
// among rooms STRICTLY CLOSER to (0,0) in XY. Every chain of links then descends in
|
||||||
// loops on top.
|
// origin-distance and terminates at the origin room → the network is a TREE ROOTED AT
|
||||||
|
// THE SPINE HUB: every room is reachable, tunnels flow inward like tributaries.
|
||||||
|
// (Frontier rooms with no closer candidate in reach fall back to plain NN — a far
|
||||||
|
// cluster stays internally chained even when it can't bridge to the origin side.)
|
||||||
|
//
|
||||||
|
// bTunnelsFlowTowardOrigin = false (legacy): plain nearest-neighbor pairing. NOTE:
|
||||||
|
// despite what this comment used to claim, an NN-graph is a FOREST of small clusters,
|
||||||
|
// not a connected tree — isolated cave pockets are expected in this mode.
|
||||||
|
//
|
||||||
|
// Selection metric (not the reach filter) penalizes vertical separation via
|
||||||
|
// TunnelHorizontalBias, so the GUARANTEED links also prefer walkable slopes —
|
||||||
|
// previously only the random TunnelDensity extras were biased, which is why
|
||||||
|
// backbone tunnels could come out absurdly steep.
|
||||||
TArray<int32, TInlineAllocator<64>> NearestNeighbor;
|
TArray<int32, TInlineAllocator<64>> NearestNeighbor;
|
||||||
NearestNeighbor.SetNumUninitialized(NumRooms);
|
NearestNeighbor.SetNumUninitialized(NumRooms);
|
||||||
|
|
||||||
const float MaxTunnelLenSq = MaxTunnelLen * MaxTunnelLen;
|
const float MaxTunnelLenSq = MaxTunnelLen * MaxTunnelLen;
|
||||||
|
const bool bFlowToOrigin = Params.bTunnelsFlowTowardOrigin;
|
||||||
|
|
||||||
|
auto LinkMetric = [&](int32 I, int32 J) -> float
|
||||||
|
{
|
||||||
|
const float D = FVector::Dist(BuildRooms[I].Center, BuildRooms[J].Center);
|
||||||
|
const float VertSep = FMath::Abs(BuildRooms[I].Center.Z - BuildRooms[J].Center.Z);
|
||||||
|
return D + VertSep * Params.TunnelHorizontalBias * 5.0f;
|
||||||
|
};
|
||||||
|
// Squared XY distance to the (0,0) spine — the "inward" ordering. Purely positional,
|
||||||
|
// so it is window-invariant by construction.
|
||||||
|
auto OriginKeySq = [&](int32 I) -> float
|
||||||
|
{
|
||||||
|
const FVector& C = BuildRooms[I].Center;
|
||||||
|
return C.X * C.X + C.Y * C.Y;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ExcludeJ: used by the origin-cap redirect below (re-pick ignoring the origin room).
|
||||||
|
auto PickNeighbor = [&](int32 I, int32 ExcludeJ) -> int32
|
||||||
|
{
|
||||||
|
const float MyKeySq = OriginKeySq(I);
|
||||||
|
float BestInward = FLT_MAX; int32 BestInwardJ = -1;
|
||||||
|
float BestAny = FLT_MAX; int32 BestAnyJ = -1;
|
||||||
|
for (int32 J = 0; J < NumRooms; J++)
|
||||||
|
{
|
||||||
|
if (J == I || J == ExcludeJ) continue;
|
||||||
|
const float DSq = FVector::DistSquared(BuildRooms[I].Center, BuildRooms[J].Center);
|
||||||
|
if (DSq > MaxTunnelLenSq) continue; // out of reach — never a tunnel
|
||||||
|
const float M = LinkMetric(I, J);
|
||||||
|
if (M < BestAny) { BestAny = M; BestAnyJ = J; }
|
||||||
|
if (bFlowToOrigin && OriginKeySq(J) < MyKeySq && M < BestInward)
|
||||||
|
{
|
||||||
|
BestInward = M; BestInwardJ = J;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (bFlowToOrigin && BestInwardJ != -1) ? BestInwardJ : BestAnyJ;
|
||||||
|
};
|
||||||
|
|
||||||
for (int32 I = 0; I < NumRooms; I++)
|
for (int32 I = 0; I < NumRooms; I++)
|
||||||
{
|
{
|
||||||
float BestDistSq = FLT_MAX;
|
NearestNeighbor[I] = PickNeighbor(I, /*ExcludeJ=*/INDEX_NONE);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -293,6 +336,19 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
{
|
{
|
||||||
OriginDowngraded.Add(OriginLinks[R].Value);
|
OriginDowngraded.Add(OriginLinks[R].Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// REDIRECT, don't strand: a downgraded room whose guaranteed link pointed at the
|
||||||
|
// origin re-picks its best target EXCLUDING origin. It keeps a guaranteed link
|
||||||
|
// (chains to the hub through another room instead of directly), which matters
|
||||||
|
// doubly now that rooms with zero connections are culled below. Deterministic:
|
||||||
|
// same candidate set, same metric, one exclusion.
|
||||||
|
for (int32 DowngradedI : OriginDowngraded)
|
||||||
|
{
|
||||||
|
if (NearestNeighbor[DowngradedI] == OriginIdx)
|
||||||
|
{
|
||||||
|
NearestNeighbor[DowngradedI] = PickNeighbor(DowngradedI, /*ExcludeJ=*/OriginIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -304,6 +360,15 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
// 2. The pair hash passes TunnelDensity (random extra loops).
|
// 2. The pair hash passes TunnelDensity (random extra loops).
|
||||||
// Both must pass the distance check (MaxTunnelLength). Only tunnels whose bounding
|
// Both must pass the distance check (MaxTunnelLength). Only tunnels whose bounding
|
||||||
// sphere reaches the search box are stored for the per-voxel loop.
|
// sphere reaches the search box are stored for the per-voxel loop.
|
||||||
|
// Tracks whether each room ends up with at least one tunnel — DECIDED connections,
|
||||||
|
// independent of whether the tunnel itself is stored for this chunk (a room near the
|
||||||
|
// window edge may have all its tunnels outside the box; it's still "connected").
|
||||||
|
// Stored rooms with zero connections are culled at emission: they'd be sealed air
|
||||||
|
// pockets no tunnel ever reaches. Window-invariant: a stored room's full candidate
|
||||||
|
// set (and each candidate's own candidates) lies inside the COLLECT region.
|
||||||
|
TArray<bool, TInlineAllocator<64>> RoomConnected;
|
||||||
|
RoomConnected.Init(false, NumRooms);
|
||||||
|
|
||||||
for (int32 I = 0; I < NumRooms; I++)
|
for (int32 I = 0; I < NumRooms; I++)
|
||||||
{
|
{
|
||||||
for (int32 J = I + 1; J < NumRooms; J++)
|
for (int32 J = I + 1; J < NumRooms; J++)
|
||||||
@@ -347,6 +412,10 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
if (ConnectChance >= Params.TunnelDensity) continue;
|
if (ConnectChance >= Params.TunnelDensity) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Connection DECIDED (backbone or density roll) — both rooms are reachable.
|
||||||
|
RoomConnected[I] = true;
|
||||||
|
RoomConnected[J] = true;
|
||||||
|
|
||||||
// --- TUNNEL HASH (for deriving all tunnel properties) ---
|
// --- TUNNEL HASH (for deriving all tunnel properties) ---
|
||||||
const uint32 TunnelHash = VoxelHash::Pair(
|
const uint32 TunnelHash = VoxelHash::Pair(
|
||||||
RoomA.CellX, RoomA.CellY,
|
RoomA.CellX, RoomA.CellY,
|
||||||
@@ -451,10 +520,15 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
|
|
||||||
OutCache.Rooms.Reserve(NumRooms);
|
OutCache.Rooms.Reserve(NumRooms);
|
||||||
|
|
||||||
for (const FBuildRoom& BR : BuildRooms)
|
for (int32 RoomIdx = 0; RoomIdx < NumRooms; RoomIdx++)
|
||||||
{
|
{
|
||||||
|
const FBuildRoom& BR = BuildRooms[RoomIdx];
|
||||||
if (!BR.bStore) continue; // Far room — collected for connectivity only
|
if (!BR.bStore) continue; // Far room — collected for connectivity only
|
||||||
|
|
||||||
|
// Sealed-bubble cull: a room no tunnel ever reaches would be an isolated air
|
||||||
|
// pocket — don't carve it at all. The origin room is always kept (spine hub).
|
||||||
|
if (!RoomConnected[RoomIdx] && !BR.bIsOrigin) continue;
|
||||||
|
|
||||||
FCachedRoom CR;
|
FCachedRoom CR;
|
||||||
CR.Center = BR.Center;
|
CR.Center = BR.Center;
|
||||||
CR.RadiusXY = BR.RadiusXY;
|
CR.RadiusXY = BR.RadiusXY;
|
||||||
|
|||||||
@@ -4,20 +4,27 @@
|
|||||||
#include "VoxelContentManager.h"
|
#include "VoxelContentManager.h"
|
||||||
#include "VoxelStrateManager.h"
|
#include "VoxelStrateManager.h"
|
||||||
#include "VoxelStrateDefinition.h"
|
#include "VoxelStrateDefinition.h"
|
||||||
|
#include "VoxelBiomeDefinition.h"
|
||||||
|
#include "VoxelGenerator.h"
|
||||||
#include "VoxelCaveMorphology.h" // VoxelHash
|
#include "VoxelCaveMorphology.h" // VoxelHash
|
||||||
#include "Components/StaticMeshComponent.h"
|
#include "Components/StaticMeshComponent.h"
|
||||||
|
#include "Components/HierarchicalInstancedStaticMeshComponent.h"
|
||||||
|
#include "Components/LightComponent.h"
|
||||||
#include "Engine/StaticMesh.h"
|
#include "Engine/StaticMesh.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
#include "GameFramework/Actor.h"
|
#include "GameFramework/Actor.h"
|
||||||
#include "Materials/MaterialInterface.h"
|
#include "Materials/MaterialInterface.h"
|
||||||
|
|
||||||
// Global safety cap on spawned decorations per chunk (across all entries).
|
// Global safety cap on spawned decoration ACTORS per chunk (across all entries).
|
||||||
|
// HISM instances are exempt — they're batched render data, capped per entry by MaxPerChunk.
|
||||||
static constexpr int32 GMaxDecorationsPerChunk = 400;
|
static constexpr int32 GMaxDecorationsPerChunk = 400;
|
||||||
|
|
||||||
void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InStrateManager, int32 InSeed)
|
void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InStrateManager,
|
||||||
|
UVoxelGenerator* InGenerator, int32 InSeed)
|
||||||
{
|
{
|
||||||
Owner = InOwner;
|
Owner = InOwner;
|
||||||
StrateManager = InStrateManager;
|
StrateManager = InStrateManager;
|
||||||
|
Generator = InGenerator;
|
||||||
Seed = InSeed;
|
Seed = InSeed;
|
||||||
|
|
||||||
// Engine unit plane — reused (scaled) for every water surface.
|
// Engine unit plane — reused (scaled) for every water surface.
|
||||||
@@ -46,25 +53,43 @@ void UVoxelContentManager::PopulateChunk(const FIntVector& ChunkCoord, const FVo
|
|||||||
const UVoxelStrateDefinition* Def = StrateManager->GetStrateForChunk(ChunkCoord);
|
const UVoxelStrateDefinition* Def = StrateManager->GetStrateForChunk(ChunkCoord);
|
||||||
if (!Def) return;
|
if (!Def) return;
|
||||||
|
|
||||||
// Decorations only on near (full-detail) chunks — they're game-thread actor spawns
|
// Dominant biome for this chunk (center XY) selects the content + water overrides.
|
||||||
// and pointless on distant low-poly chunks. Water is one cheap plane, place it at any LOD.
|
// Per-chunk granularity: a chunk straddling a biome border takes its centre's biome
|
||||||
if (LODLevel == 0)
|
// for the whole decoration SET (fine — the set is a choice, not a per-vertex blend).
|
||||||
|
const UVoxelBiomeDefinition* Biome = nullptr;
|
||||||
|
if (Generator)
|
||||||
|
{
|
||||||
|
const float CenterVoxelX = ((float)ChunkCoord.X + 0.5f) * CHUNK_SIZE;
|
||||||
|
const float CenterVoxelY = ((float)ChunkCoord.Y + 0.5f) * CHUNK_SIZE;
|
||||||
|
Biome = Generator->GetDominantBiomeAt(CenterVoxelX, CenterVoxelY, ChunkCoord.Z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decorations come from the biome when it provides any, else the strate's list.
|
||||||
|
const TArray<FStrateDecoration>& Decos =
|
||||||
|
(Biome && Biome->Decorations.Num() > 0) ? Biome->Decorations : Def->Decorations;
|
||||||
|
|
||||||
|
// Per-entry LOD gate inside SpawnDecorations (FStrateDecoration::MaxLODLevel):
|
||||||
|
// actor entries usually stay LOD0-only; instanced entries may persist to LOD1-2.
|
||||||
|
// Water is one cheap plane, place it at any LOD.
|
||||||
{
|
{
|
||||||
TArray<TWeakObjectPtr<AActor>> Spawned;
|
TArray<TWeakObjectPtr<AActor>> Spawned;
|
||||||
SpawnDecorations(ChunkCoord, MeshData, Def, Spawned);
|
SpawnDecorations(ChunkCoord, MeshData, Decos, LODLevel, Spawned);
|
||||||
if (Spawned.Num() > 0)
|
if (Spawned.Num() > 0)
|
||||||
{
|
{
|
||||||
SpawnedActors.Add(ChunkCoord, MoveTemp(Spawned));
|
SpawnedActors.Add(ChunkCoord, MoveTemp(Spawned));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SpawnWater(ChunkCoord, Def);
|
// Water plane: biome material override (level stays strate-global, see SpawnWater).
|
||||||
|
UMaterialInterface* WaterMat = (Biome && Biome->WaterMaterial) ? Biome->WaterMaterial : Def->WaterMaterial;
|
||||||
|
SpawnWater(ChunkCoord, Def, WaterMat);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UVoxelContentManager::SpawnDecorations(const FIntVector& ChunkCoord, const FVoxelMeshData& MeshData,
|
void UVoxelContentManager::SpawnDecorations(const FIntVector& ChunkCoord, const FVoxelMeshData& MeshData,
|
||||||
const UVoxelStrateDefinition* Def, TArray<TWeakObjectPtr<AActor>>& Out)
|
const TArray<FStrateDecoration>& Decorations, int32 LODLevel,
|
||||||
|
TArray<TWeakObjectPtr<AActor>>& Out)
|
||||||
{
|
{
|
||||||
if (Def->Decorations.Num() == 0) return;
|
if (Decorations.Num() == 0) return;
|
||||||
|
|
||||||
AActor* OwnerActor = Owner.Get();
|
AActor* OwnerActor = Owner.Get();
|
||||||
if (!OwnerActor) return;
|
if (!OwnerActor) return;
|
||||||
@@ -81,19 +106,30 @@ void UVoxelContentManager::SpawnDecorations(const FIntVector& ChunkCoord, const
|
|||||||
const bool bHasWater = (WaterVoxelZ != -FLT_MAX);
|
const bool bHasWater = (WaterVoxelZ != -FLT_MAX);
|
||||||
const float WaterWorldZ = bHasWater ? WaterVoxelZ * VOXEL_SIZE : -FLT_MAX;
|
const float WaterWorldZ = bHasWater ? WaterVoxelZ * VOXEL_SIZE : -FLT_MAX;
|
||||||
|
|
||||||
|
// Strate light culling: lights only live in the player's strate. INT32_MIN = player
|
||||||
|
// strate not known yet (first frames) — leave lights on, the first SetActiveStrate
|
||||||
|
// sweep corrects them.
|
||||||
|
const int32 ChunkStrate = GetChunkStrateIndex(ChunkCoord);
|
||||||
|
const bool bLightsOn = (ActiveStrateIndex == INT32_MIN) || (ChunkStrate == ActiveStrateIndex);
|
||||||
|
|
||||||
int32 TotalSpawned = 0;
|
int32 TotalSpawned = 0;
|
||||||
|
|
||||||
for (int32 DecoIdx = 0; DecoIdx < Def->Decorations.Num(); ++DecoIdx)
|
for (int32 DecoIdx = 0; DecoIdx < Decorations.Num(); ++DecoIdx)
|
||||||
{
|
{
|
||||||
const FStrateDecoration& Deco = Def->Decorations[DecoIdx];
|
const FStrateDecoration& Deco = Decorations[DecoIdx];
|
||||||
if (!Deco.ActorClass) continue;
|
const bool bInstanced = (Deco.InstancedMesh != nullptr);
|
||||||
|
if (!bInstanced && !Deco.ActorClass) continue;
|
||||||
if (Deco.SpawnDensity <= 0.0f) continue;
|
if (Deco.SpawnDensity <= 0.0f) continue;
|
||||||
|
if (LODLevel > Deco.MaxLODLevel) continue; // entry not wanted at this LOD
|
||||||
|
|
||||||
|
// Lazily created on the first placement of this entry (avoids empty components).
|
||||||
|
UHierarchicalInstancedStaticMeshComponent* HISM = nullptr;
|
||||||
|
|
||||||
int32 SpawnedThisEntry = 0;
|
int32 SpawnedThisEntry = 0;
|
||||||
|
|
||||||
for (int32 i = 0; i < NumVerts; ++i)
|
for (int32 i = 0; i < NumVerts; ++i)
|
||||||
{
|
{
|
||||||
if (TotalSpawned >= GMaxDecorationsPerChunk) return;
|
if (!bInstanced && TotalSpawned >= GMaxDecorationsPerChunk) break; // actor cap
|
||||||
if (SpawnedThisEntry >= Deco.MaxPerChunk) break;
|
if (SpawnedThisEntry >= Deco.MaxPerChunk) break;
|
||||||
|
|
||||||
const FVector NormalWorld = MeshData.Normals.IsValidIndex(i)
|
const FVector NormalWorld = MeshData.Normals.IsValidIndex(i)
|
||||||
@@ -144,26 +180,101 @@ void UVoxelContentManager::SpawnDecorations(const FIntVector& ChunkCoord, const
|
|||||||
const float ScaleT = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x5CA1E000u));
|
const float ScaleT = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x5CA1E000u));
|
||||||
const float Scale = FMath::Lerp(Deco.MinScale, Deco.MaxScale, ScaleT);
|
const float Scale = FMath::Lerp(Deco.MinScale, Deco.MaxScale, ScaleT);
|
||||||
|
|
||||||
|
const FTransform SpawnXf(BaseQ, SpawnPos, FVector(Scale));
|
||||||
|
|
||||||
|
if (bInstanced)
|
||||||
|
{
|
||||||
|
// HISM path — batched instances, no actor. Pure visual: no collision,
|
||||||
|
// engine handles frustum/occlusion culling per cluster.
|
||||||
|
if (!HISM)
|
||||||
|
{
|
||||||
|
HISM = NewObject<UHierarchicalInstancedStaticMeshComponent>(OwnerActor);
|
||||||
|
HISM->SetStaticMesh(Deco.InstancedMesh);
|
||||||
|
HISM->SetMobility(EComponentMobility::Movable);
|
||||||
|
HISM->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||||
|
HISM->RegisterComponent();
|
||||||
|
HISM->AttachToComponent(OwnerActor->GetRootComponent(),
|
||||||
|
FAttachmentTransformRules::KeepRelativeTransform);
|
||||||
|
ChunkInstances.FindOrAdd(ChunkCoord).Add(HISM);
|
||||||
|
}
|
||||||
|
HISM->AddInstance(SpawnXf, /*bWorldSpace=*/true);
|
||||||
|
++SpawnedThisEntry;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
FActorSpawnParameters SpawnParams;
|
FActorSpawnParameters SpawnParams;
|
||||||
SpawnParams.Owner = OwnerActor;
|
SpawnParams.Owner = OwnerActor;
|
||||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
|
|
||||||
AActor* NewActor = World->SpawnActor<AActor>(
|
AActor* NewActor = World->SpawnActor<AActor>(Deco.ActorClass, SpawnXf, SpawnParams);
|
||||||
Deco.ActorClass,
|
|
||||||
FTransform(BaseQ, SpawnPos, FVector(Scale)),
|
|
||||||
SpawnParams);
|
|
||||||
|
|
||||||
if (NewActor)
|
if (NewActor)
|
||||||
{
|
{
|
||||||
|
// Strate light culling — wrong strate ⇒ lights start hidden.
|
||||||
|
if (!bLightsOn)
|
||||||
|
{
|
||||||
|
SetActorLightsEnabled(NewActor, false);
|
||||||
|
}
|
||||||
Out.Add(NewActor);
|
Out.Add(NewActor);
|
||||||
++SpawnedThisEntry;
|
++SpawnedThisEntry;
|
||||||
++TotalSpawned;
|
++TotalSpawned;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UVoxelContentManager::SpawnWater(const FIntVector& ChunkCoord, const UVoxelStrateDefinition* Def)
|
//=============================================================================
|
||||||
|
// STRATE LIGHT CULLING
|
||||||
|
//=============================================================================
|
||||||
|
// Seals + the bedrock gap make strates light-tight by construction, but a shadowless
|
||||||
|
// point light shines straight through rock (blocking IS shadowing), and a shadowed one
|
||||||
|
// pays full shadow cost to render black. Lights can therefore only legitimately matter
|
||||||
|
// in the player's own strate — toggle them analytically on strate change instead of
|
||||||
|
// asking the GPU to discover it per-pixel.
|
||||||
|
|
||||||
|
int32 UVoxelContentManager::GetChunkStrateIndex(const FIntVector& ChunkCoord) const
|
||||||
|
{
|
||||||
|
if (!StrateManager) return INT32_MIN;
|
||||||
|
// GetStrateIndex expects Unreal world units (cm) — it converts cm→chunkZ internally.
|
||||||
|
// (Previously this passed voxel-Z, so the strate match for light culling was wrong.)
|
||||||
|
const float CenterWorldZ = ((float)ChunkCoord.Z + 0.5f) * CHUNK_SIZE * VOXEL_SIZE;
|
||||||
|
return StrateManager->GetStrateIndex(CenterWorldZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVoxelContentManager::SetActorLightsEnabled(AActor* Actor, bool bEnabled)
|
||||||
|
{
|
||||||
|
TInlineComponentArray<ULightComponent*> Lights(Actor);
|
||||||
|
for (ULightComponent* Light : Lights)
|
||||||
|
{
|
||||||
|
if (Light)
|
||||||
|
{
|
||||||
|
Light->SetVisibility(bEnabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVoxelContentManager::SetActiveStrate(int32 PlayerStrateIndex)
|
||||||
|
{
|
||||||
|
if (PlayerStrateIndex == ActiveStrateIndex) return; // every-Tick fast path
|
||||||
|
ActiveStrateIndex = PlayerStrateIndex;
|
||||||
|
|
||||||
|
// Strate change (rare): sweep all populated chunks and toggle their actors' lights.
|
||||||
|
for (const auto& Pair : SpawnedActors)
|
||||||
|
{
|
||||||
|
const bool bEnable = (GetChunkStrateIndex(Pair.Key) == ActiveStrateIndex);
|
||||||
|
for (const TWeakObjectPtr<AActor>& A : Pair.Value)
|
||||||
|
{
|
||||||
|
if (AActor* Act = A.Get())
|
||||||
|
{
|
||||||
|
SetActorLightsEnabled(Act, bEnable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVoxelContentManager::SpawnWater(const FIntVector& ChunkCoord, const UVoxelStrateDefinition* Def,
|
||||||
|
UMaterialInterface* WaterMaterial)
|
||||||
{
|
{
|
||||||
if (!Def->bHasWater || !PlaneMesh) return;
|
if (!Def->bHasWater || !PlaneMesh) return;
|
||||||
|
|
||||||
@@ -196,9 +307,9 @@ void UVoxelContentManager::SpawnWater(const FIntVector& ChunkCoord, const UVoxel
|
|||||||
const float ChunkWorld = (float)CHUNK_SIZE * VOXEL_SIZE;
|
const float ChunkWorld = (float)CHUNK_SIZE * VOXEL_SIZE;
|
||||||
Plane->SetWorldScale3D(FVector(ChunkWorld / 100.0f, ChunkWorld / 100.0f, 1.0f));
|
Plane->SetWorldScale3D(FVector(ChunkWorld / 100.0f, ChunkWorld / 100.0f, 1.0f));
|
||||||
|
|
||||||
if (Def->WaterMaterial)
|
if (WaterMaterial)
|
||||||
{
|
{
|
||||||
Plane->SetMaterial(0, Def->WaterMaterial);
|
Plane->SetMaterial(0, WaterMaterial);
|
||||||
}
|
}
|
||||||
|
|
||||||
WaterPlanes.Add(ChunkCoord, Plane);
|
WaterPlanes.Add(ChunkCoord, Plane);
|
||||||
@@ -218,6 +329,18 @@ void UVoxelContentManager::ClearChunk(const FIntVector& ChunkCoord)
|
|||||||
SpawnedActors.Remove(ChunkCoord);
|
SpawnedActors.Remove(ChunkCoord);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (TArray<TWeakObjectPtr<UHierarchicalInstancedStaticMeshComponent>>* Comps = ChunkInstances.Find(ChunkCoord))
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<UHierarchicalInstancedStaticMeshComponent>& C : *Comps)
|
||||||
|
{
|
||||||
|
if (UHierarchicalInstancedStaticMeshComponent* Comp = C.Get())
|
||||||
|
{
|
||||||
|
Comp->DestroyComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChunkInstances.Remove(ChunkCoord);
|
||||||
|
}
|
||||||
|
|
||||||
if (UStaticMeshComponent** Plane = WaterPlanes.Find(ChunkCoord))
|
if (UStaticMeshComponent** Plane = WaterPlanes.Find(ChunkCoord))
|
||||||
{
|
{
|
||||||
if (*Plane)
|
if (*Plane)
|
||||||
@@ -236,7 +359,13 @@ void UVoxelContentManager::ClearAll()
|
|||||||
{
|
{
|
||||||
ClearChunk(C);
|
ClearChunk(C);
|
||||||
}
|
}
|
||||||
// Any water planes without decorations.
|
// Chunks that only have instances or water (no actors).
|
||||||
|
TArray<FIntVector> InstanceCoords;
|
||||||
|
ChunkInstances.GetKeys(InstanceCoords);
|
||||||
|
for (const FIntVector& C : InstanceCoords)
|
||||||
|
{
|
||||||
|
ClearChunk(C);
|
||||||
|
}
|
||||||
TArray<FIntVector> WaterCoords;
|
TArray<FIntVector> WaterCoords;
|
||||||
WaterPlanes.GetKeys(WaterCoords);
|
WaterPlanes.GetKeys(WaterCoords);
|
||||||
for (const FIntVector& C : WaterCoords)
|
for (const FIntVector& C : WaterCoords)
|
||||||
@@ -244,5 +373,6 @@ void UVoxelContentManager::ClearAll()
|
|||||||
ClearChunk(C);
|
ClearChunk(C);
|
||||||
}
|
}
|
||||||
SpawnedActors.Empty();
|
SpawnedActors.Empty();
|
||||||
|
ChunkInstances.Empty();
|
||||||
WaterPlanes.Empty();
|
WaterPlanes.Empty();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,34 @@
|
|||||||
#include "VoxelTerrainOpDefinition.h"
|
#include "VoxelTerrainOpDefinition.h"
|
||||||
#include "VoxelCaveMorphology.h"
|
#include "VoxelCaveMorphology.h"
|
||||||
#include "VoxelDiffLayer.h"
|
#include "VoxelDiffLayer.h"
|
||||||
|
#include "VoxelBiomeDefinition.h"
|
||||||
|
#include "VoxelNoise.h" // T2.a: float, SIMD-batched gradient-noise core
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// SURFACE COLUMN CACHE (T1.a) — kill the per-Z heightfield redundancy
|
||||||
|
//=============================================================================
|
||||||
|
// The SurfaceWorld heightfield + sky-cap + biome blend are functions of XY ONLY, but
|
||||||
|
// the mesher samples ~33 Z grid-points per column, each re-running that XY work. We cache
|
||||||
|
// the column (terrain Z + ceiling Z) once per integer XY and reuse it down the column.
|
||||||
|
// Used ONLY for integer-XY queries (the density grid); fractional queries (gradient
|
||||||
|
// normals at interpolated vertices) fall through and compute directly → bit-identical.
|
||||||
|
// Validity is a world-XY BOX (chunk footprint) + ChunkZ + Seed, same discipline as the
|
||||||
|
// SDF/biome caches (§8.10) so the +X/+Y boundary plane doesn't thrash it.
|
||||||
|
struct FSurfaceColumn { float TerrainZ = 0.0f; float CeilSurf = 0.0f; };
|
||||||
|
|
||||||
|
struct FSurfaceColumnCache
|
||||||
|
{
|
||||||
|
// Box covers a chunk footprint + a margin on every side (≥ the LOD margin ring the
|
||||||
|
// mesher samples for grid-based normals, ±Step ≤ 4, plus slack). Symmetric around the
|
||||||
|
// first (rebuild-triggering) sample so the whole chunk's column queries stay in one box.
|
||||||
|
static constexpr int32 Halo = CHUNK_SIZE + 8;
|
||||||
|
static constexpr int32 Dim = 2 * Halo + 1;
|
||||||
|
int32 BaseX = 0, BaseY = 0; // box origin (voxel coords)
|
||||||
|
int32 ChunkZ = MIN_int32, Seed = MIN_int32;
|
||||||
|
bool bValid = false;
|
||||||
|
FSurfaceColumn Cols[Dim * Dim];
|
||||||
|
bool Computed[Dim * Dim];
|
||||||
|
};
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// FRACTAL NOISE (fBm — fractional Brownian motion)
|
// FRACTAL NOISE (fBm — fractional Brownian motion)
|
||||||
@@ -22,23 +50,15 @@
|
|||||||
// Lacunarity = x freq par octave (2 = double à chaque fois)
|
// Lacunarity = x freq par octave (2 = double à chaque fois)
|
||||||
// Persistence = x amp par octave (0.5 = moitié)
|
// Persistence = x amp par octave (0.5 = moitié)
|
||||||
|
|
||||||
|
// NOTE (T2.a): the fBm/Ridged bodies moved to VoxelNoise.h, where octaves are evaluated
|
||||||
|
// 4-wide via SSE (Perlin3D_x4). These thin wrappers keep every call site unchanged. They
|
||||||
|
// sample a DIFFERENT (float hash-gradient) noise field than the old FMath::PerlinNoise3D,
|
||||||
|
// so worlds re-tune once — but the fBm/Ridged math/contracts ([-1,1]) are identical.
|
||||||
static float FractalNoise3D(const FVector& Position, int32 Octaves = 4,
|
static float FractalNoise3D(const FVector& Position, int32 Octaves = 4,
|
||||||
float Lacunarity = 2.0f, float Persistence = 0.5f)
|
float Lacunarity = 2.0f, float Persistence = 0.5f)
|
||||||
{
|
{
|
||||||
float Total = 0.0f;
|
return VoxelNoise::FBM((float)Position.X, (float)Position.Y, (float)Position.Z,
|
||||||
float Frequency = 1.0f;
|
Octaves, Lacunarity, Persistence);
|
||||||
float Amplitude = 1.0f;
|
|
||||||
float MaxValue = 0.0f;
|
|
||||||
|
|
||||||
for (int32 i = 0; i < Octaves; i++)
|
|
||||||
{
|
|
||||||
Total += FMath::PerlinNoise3D(Position * Frequency) * Amplitude;
|
|
||||||
MaxValue += Amplitude;
|
|
||||||
Frequency *= Lacunarity;
|
|
||||||
Amplitude *= Persistence;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Total / MaxValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -55,32 +75,8 @@ static float FractalNoise3D(const FVector& Position, int32 Octaves = 4,
|
|||||||
static float RidgedNoise3D(const FVector& Position, int32 Octaves = 4,
|
static float RidgedNoise3D(const FVector& Position, int32 Octaves = 4,
|
||||||
float Lacunarity = 2.0f, float Persistence = 0.5f)
|
float Lacunarity = 2.0f, float Persistence = 0.5f)
|
||||||
{
|
{
|
||||||
// UE's PerlinNoise3D returns ~[-0.8, 0.8]; scale to [-1, 1]
|
return VoxelNoise::Ridged((float)Position.X, (float)Position.Y, (float)Position.Z,
|
||||||
static constexpr float NS = 1.25f;
|
Octaves, Lacunarity, Persistence);
|
||||||
|
|
||||||
float Total = 0.0f;
|
|
||||||
float Frequency = 1.0f;
|
|
||||||
float Amplitude = 1.0f;
|
|
||||||
float MaxValue = 0.0f;
|
|
||||||
float Weight = 1.0f; // Weight feedback from previous octave
|
|
||||||
|
|
||||||
for (int32 i = 0; i < Octaves; i++)
|
|
||||||
{
|
|
||||||
// Sample Perlin and fold at zero → ridge at zero-crossings
|
|
||||||
float N = FMath::PerlinNoise3D(Position * Frequency) * NS;
|
|
||||||
N = 1.0f - FMath::Abs(N); // Fold: [−1,1] → [0,1] with ridges at N=0
|
|
||||||
N = N * N; // Sharpen ridges (quadratic falloff)
|
|
||||||
N *= Weight; // Weight by previous octave → detail follows ridges
|
|
||||||
Weight = FMath::Clamp(N * 2.0f, 0.0f, 1.0f); // Feedback for next octave
|
|
||||||
|
|
||||||
Total += N * Amplitude;
|
|
||||||
MaxValue += Amplitude;
|
|
||||||
Frequency *= Lacunarity;
|
|
||||||
Amplitude *= Persistence;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shift from [0, 1] to [-1, 1] to match FractalNoise3D's range
|
|
||||||
return (Total / MaxValue) * 2.0f - 1.0f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -401,6 +397,13 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
thread_local FVerticalShaftParams CP_Vert;
|
thread_local FVerticalShaftParams CP_Vert;
|
||||||
thread_local FFloatingIslandParams CP_Float;
|
thread_local FFloatingIslandParams CP_Float;
|
||||||
thread_local FStrateDisturbanceParams CP_Dist;
|
thread_local FStrateDisturbanceParams CP_Dist;
|
||||||
|
// Biomes (SurfaceWorld for now): CP_BiomeCtx is the cheap per-chunk flatten;
|
||||||
|
// CP_BiomeCache is the box-validated grid; CP_SurfaceBiomeParams holds each biome's
|
||||||
|
// resolved surface params (override or strate fallback) parallel to CP_BiomeCtx.Biomes.
|
||||||
|
thread_local FBiomeContext CP_BiomeCtx;
|
||||||
|
thread_local FChunkBiomeCache CP_BiomeCache;
|
||||||
|
thread_local TArray<FSurfaceGenerationParams> CP_SurfaceBiomeParams;
|
||||||
|
thread_local FSurfaceColumnCache CP_SurfCol; // T1.a per-column surface cache
|
||||||
|
|
||||||
if (ChunkCoord != CP_Chunk)
|
if (ChunkCoord != CP_Chunk)
|
||||||
{
|
{
|
||||||
@@ -414,7 +417,37 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
case ECaveGeneratorType::Maze:
|
case ECaveGeneratorType::Maze:
|
||||||
CP_Maze = StrateManager->GetMazeParamsForChunk(ChunkCoord); break;
|
CP_Maze = StrateManager->GetMazeParamsForChunk(ChunkCoord); break;
|
||||||
case ECaveGeneratorType::SurfaceWorld:
|
case ECaveGeneratorType::SurfaceWorld:
|
||||||
CP_Surface = StrateManager->GetSurfaceParamsForChunk(ChunkCoord); break;
|
{
|
||||||
|
CP_Surface = StrateManager->GetSurfaceParamsForChunk(ChunkCoord);
|
||||||
|
CP_BiomeCtx = StrateManager->GetBiomeContextForChunk(ChunkCoord);
|
||||||
|
|
||||||
|
// Per-biome surface params: each biome's override (when it overrides
|
||||||
|
// SurfaceWorld) else the strate's, with STRUCTURAL fields forced from the
|
||||||
|
// strate (Z bounds, seal, base, water level) so seals/spine/water stay intact.
|
||||||
|
CP_SurfaceBiomeParams.Reset();
|
||||||
|
if (CP_BiomeCtx.IsValid())
|
||||||
|
{
|
||||||
|
const UVoxelStrateDefinition* Def = StrateManager->GetStrateForChunk(ChunkCoord);
|
||||||
|
CP_SurfaceBiomeParams.Reserve(CP_BiomeCtx.Biomes.Num());
|
||||||
|
for (const FBiomeResolved& BR : CP_BiomeCtx.Biomes)
|
||||||
|
{
|
||||||
|
FSurfaceGenerationParams P = CP_Surface; // strate base (+ structural)
|
||||||
|
const UVoxelBiomeDefinition* B =
|
||||||
|
(Def && Def->Biomes.IsValidIndex(BR.Index)) ? Def->Biomes[BR.Index] : nullptr;
|
||||||
|
if (B && B->bOverrideTerrain && B->GeneratorType == ECaveGeneratorType::SurfaceWorld)
|
||||||
|
{
|
||||||
|
P = B->SurfaceParams; // biome shape
|
||||||
|
P.StrateTopWorldZ = CP_Surface.StrateTopWorldZ;
|
||||||
|
P.StrateBottomWorldZ = CP_Surface.StrateBottomWorldZ;
|
||||||
|
P.BoundarySealThickness = CP_Surface.BoundarySealThickness;
|
||||||
|
P.BaseDensity = CP_Surface.BaseDensity;
|
||||||
|
P.WaterLevelRelative = CP_Surface.WaterLevelRelative; // shared water plane
|
||||||
|
}
|
||||||
|
CP_SurfaceBiomeParams.Add(P);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case ECaveGeneratorType::VerticalShafts:
|
case ECaveGeneratorType::VerticalShafts:
|
||||||
CP_Vert = StrateManager->GetVerticalShaftParamsForChunk(ChunkCoord); break;
|
CP_Vert = StrateManager->GetVerticalShaftParamsForChunk(ChunkCoord); break;
|
||||||
case ECaveGeneratorType::FloatingIslands:
|
case ECaveGeneratorType::FloatingIslands:
|
||||||
@@ -433,7 +466,69 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
case ECaveGeneratorType::Maze:
|
case ECaveGeneratorType::Maze:
|
||||||
Result = GetMazeDensity(WorldX, WorldY, WorldZ, CP_Maze); break;
|
Result = GetMazeDensity(WorldX, WorldY, WorldZ, CP_Maze); break;
|
||||||
case ECaveGeneratorType::SurfaceWorld:
|
case ECaveGeneratorType::SurfaceWorld:
|
||||||
Result = GetSurfaceDensity(WorldX, WorldY, WorldZ, CP_Surface); break;
|
{
|
||||||
|
// The XY-only surface field (biome-blended terrain Z + sky-cap ceiling) for one
|
||||||
|
// column. Resolves the biome sample here so it's cached per column too.
|
||||||
|
auto ComputeColumn = [&](float X, float Y) -> FSurfaceColumn
|
||||||
|
{
|
||||||
|
const FSurfaceGenerationParams* PD = &CP_Surface;
|
||||||
|
const FSurfaceGenerationParams* PN = &CP_Surface;
|
||||||
|
float W = 0.0f;
|
||||||
|
if (CP_BiomeCtx.IsValid() && CP_SurfaceBiomeParams.Num() > 0)
|
||||||
|
{
|
||||||
|
const FBiomeSample Smp = ResolveBiomeSampleAt(X, Y, ChunkCoord.Z, CP_BiomeCtx, CP_BiomeCache);
|
||||||
|
const int32 Di = CP_SurfaceBiomeParams.IsValidIndex(Smp.DominantIndex) ? Smp.DominantIndex : 0;
|
||||||
|
PD = &CP_SurfaceBiomeParams[Di];
|
||||||
|
if (Smp.NeighborWeight > 0.0f && CP_SurfaceBiomeParams.IsValidIndex(Smp.NeighborIndex))
|
||||||
|
{
|
||||||
|
PN = &CP_SurfaceBiomeParams[Smp.NeighborIndex];
|
||||||
|
W = Smp.NeighborWeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FSurfaceColumn Col;
|
||||||
|
Col.TerrainZ = ComputeSurfaceTerrainZ(X, Y, *PD);
|
||||||
|
if (W > 0.0f) Col.TerrainZ = FMath::Lerp(Col.TerrainZ, ComputeSurfaceTerrainZ(X, Y, *PN), W);
|
||||||
|
Col.CeilSurf = ComputeSurfaceCeiling(X, Y, *PD);
|
||||||
|
return Col;
|
||||||
|
};
|
||||||
|
|
||||||
|
FSurfaceColumn Col;
|
||||||
|
// Integer XY (the density grid) → reuse the column down its whole Z extent.
|
||||||
|
// Fractional XY (gradient-normal samples) → compute directly (no cache key).
|
||||||
|
if (WorldX == FMath::FloorToFloat(WorldX) && WorldY == FMath::FloorToFloat(WorldY))
|
||||||
|
{
|
||||||
|
const int32 IX = (int32)WorldX, IY = (int32)WorldY;
|
||||||
|
const bool bInBox = CP_SurfCol.bValid
|
||||||
|
&& CP_SurfCol.ChunkZ == ChunkCoord.Z && CP_SurfCol.Seed == Seed
|
||||||
|
&& IX >= CP_SurfCol.BaseX && IX < CP_SurfCol.BaseX + FSurfaceColumnCache::Dim
|
||||||
|
&& IY >= CP_SurfCol.BaseY && IY < CP_SurfCol.BaseY + FSurfaceColumnCache::Dim;
|
||||||
|
if (!bInBox)
|
||||||
|
{
|
||||||
|
// Centre the box on this (first / boundary-crossing) sample so the rest of
|
||||||
|
// the chunk's column queries — including the ±Step margin ring — fall inside.
|
||||||
|
CP_SurfCol.BaseX = IX - FSurfaceColumnCache::Halo;
|
||||||
|
CP_SurfCol.BaseY = IY - FSurfaceColumnCache::Halo;
|
||||||
|
CP_SurfCol.ChunkZ = ChunkCoord.Z;
|
||||||
|
CP_SurfCol.Seed = Seed;
|
||||||
|
CP_SurfCol.bValid = true;
|
||||||
|
FMemory::Memzero(CP_SurfCol.Computed, sizeof(CP_SurfCol.Computed));
|
||||||
|
}
|
||||||
|
const int32 CI = (IY - CP_SurfCol.BaseY) * FSurfaceColumnCache::Dim + (IX - CP_SurfCol.BaseX);
|
||||||
|
if (!CP_SurfCol.Computed[CI])
|
||||||
|
{
|
||||||
|
CP_SurfCol.Cols[CI] = ComputeColumn(WorldX, WorldY);
|
||||||
|
CP_SurfCol.Computed[CI] = true;
|
||||||
|
}
|
||||||
|
Col = CP_SurfCol.Cols[CI];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Col = ComputeColumn(WorldX, WorldY);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result = SurfaceDensityFromColumn(WorldX, WorldY, WorldZ, Col.TerrainZ, Col.CeilSurf, CP_Surface);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case ECaveGeneratorType::VerticalShafts:
|
case ECaveGeneratorType::VerticalShafts:
|
||||||
Result = GetVerticalShaftDensity(WorldX, WorldY, WorldZ, CP_Vert); break;
|
Result = GetVerticalShaftDensity(WorldX, WorldY, WorldZ, CP_Vert); break;
|
||||||
case ECaveGeneratorType::FloatingIslands:
|
case ECaveGeneratorType::FloatingIslands:
|
||||||
@@ -549,15 +644,15 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
|||||||
// Three independent Perlin fields offset by irrational-ish numbers
|
// Three independent Perlin fields offset by irrational-ish numbers
|
||||||
// so the X/Y/Z warp channels don't correlate with each other.
|
// so the X/Y/Z warp channels don't correlate with each other.
|
||||||
// Single octave to keep per-voxel cost low (3 Perlin calls total).
|
// Single octave to keep per-voxel cost low (3 Perlin calls total).
|
||||||
WarpedX += FMath::PerlinNoise3D(FVector(
|
WarpedX += VoxelNoise::Perlin3D(FVector(
|
||||||
WorldX * WF + SeedF * 0.37f,
|
WorldX * WF + SeedF * 0.37f,
|
||||||
WorldY * WF + 1.3f,
|
WorldY * WF + 1.3f,
|
||||||
EffectiveZ * WF + 5.7f)) * VOXEL_NOISE_SCALE * WS;
|
EffectiveZ * WF + 5.7f)) * VOXEL_NOISE_SCALE * WS;
|
||||||
WarpedY += FMath::PerlinNoise3D(FVector(
|
WarpedY += VoxelNoise::Perlin3D(FVector(
|
||||||
WorldX * WF + 7.1f,
|
WorldX * WF + 7.1f,
|
||||||
WorldY * WF + SeedF * 0.59f,
|
WorldY * WF + SeedF * 0.59f,
|
||||||
EffectiveZ * WF + 2.3f)) * VOXEL_NOISE_SCALE * WS;
|
EffectiveZ * WF + 2.3f)) * VOXEL_NOISE_SCALE * WS;
|
||||||
WarpedZ += FMath::PerlinNoise3D(FVector(
|
WarpedZ += VoxelNoise::Perlin3D(FVector(
|
||||||
WorldX * WF + 11.3f,
|
WorldX * WF + 11.3f,
|
||||||
WorldY * WF + 9.7f,
|
WorldY * WF + 9.7f,
|
||||||
EffectiveZ * WF + SeedF * 0.41f)) * VOXEL_NOISE_SCALE * WS;
|
EffectiveZ * WF + SeedF * 0.41f)) * VOXEL_NOISE_SCALE * WS;
|
||||||
@@ -814,19 +909,19 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
|||||||
float WS = Params.DomainWarpStrength;
|
float WS = Params.DomainWarpStrength;
|
||||||
|
|
||||||
// Sample three independent noise fields for X, Y, Z warp
|
// Sample three independent noise fields for X, Y, Z warp
|
||||||
float WarpX = FMath::PerlinNoise3D(FVector(
|
float WarpX = VoxelNoise::Perlin3D(FVector(
|
||||||
WorldX * WF + SeedF * 5.2f,
|
WorldX * WF + SeedF * 5.2f,
|
||||||
WorldY * WF + SeedF * 1.3f,
|
WorldY * WF + SeedF * 1.3f,
|
||||||
EffectiveZ * WF + SeedF * 9.7f
|
EffectiveZ * WF + SeedF * 9.7f
|
||||||
)) * VOXEL_NOISE_SCALE * WS;
|
)) * VOXEL_NOISE_SCALE * WS;
|
||||||
|
|
||||||
float WarpY = FMath::PerlinNoise3D(FVector(
|
float WarpY = VoxelNoise::Perlin3D(FVector(
|
||||||
WorldX * WF + 100.0f + SeedF * 7.7f,
|
WorldX * WF + 100.0f + SeedF * 7.7f,
|
||||||
WorldY * WF + 200.0f + SeedF * 3.1f,
|
WorldY * WF + 200.0f + SeedF * 3.1f,
|
||||||
EffectiveZ * WF + 300.0f
|
EffectiveZ * WF + 300.0f
|
||||||
)) * VOXEL_NOISE_SCALE * WS;
|
)) * VOXEL_NOISE_SCALE * WS;
|
||||||
|
|
||||||
float WarpZ = FMath::PerlinNoise3D(FVector(
|
float WarpZ = VoxelNoise::Perlin3D(FVector(
|
||||||
WorldX * WF + 400.0f,
|
WorldX * WF + 400.0f,
|
||||||
WorldY * WF + 500.0f + SeedF * 11.9f,
|
WorldY * WF + 500.0f + SeedF * 11.9f,
|
||||||
EffectiveZ * WF + 600.0f + SeedF * 13.3f
|
EffectiveZ * WF + 600.0f + SeedF * 13.3f
|
||||||
@@ -1149,7 +1244,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
|||||||
// Near the surface (CaveSDF ≈ 0), the sign of CaveSDF tells us which
|
// Near the surface (CaveSDF ≈ 0), the sign of CaveSDF tells us which
|
||||||
// side we're on: negative = inside cave, positive = solid rock.
|
// side we're on: negative = inside cave, positive = solid rock.
|
||||||
// We use a noise-modulated vertical gradient to detect steep faces.
|
// We use a noise-modulated vertical gradient to detect steep faces.
|
||||||
float VertGrad = FMath::PerlinNoise3D(FVector(
|
float VertGrad = VoxelNoise::Perlin3D(FVector(
|
||||||
WorldX * 0.05f + SeedF * 71.3f,
|
WorldX * 0.05f + SeedF * 71.3f,
|
||||||
WorldY * 0.05f + SeedF * 73.7f,
|
WorldY * 0.05f + SeedF * 73.7f,
|
||||||
EffectiveZ * 0.15f + SeedF * 79.1f // 3x faster in Z → detects vertical features
|
EffectiveZ * 0.15f + SeedF * 79.1f // 3x faster in Z → detects vertical features
|
||||||
@@ -1458,17 +1553,40 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
|||||||
// produce natural winding paths that complement the room-and-corridor structure.
|
// produce natural winding paths that complement the room-and-corridor structure.
|
||||||
//
|
//
|
||||||
// HORIZONTAL BIAS: Z frequency is scaled up so tunnels prefer horizontal paths.
|
// HORIZONTAL BIAS: Z frequency is scaled up so tunnels prefer horizontal paths.
|
||||||
|
//
|
||||||
|
// NETWORK MASK: |N1|+|N2| < threshold carves tubes near the intersection of two noise
|
||||||
|
// zero-surfaces — but wherever the fields merely GRAZE the threshold it leaves pinhole
|
||||||
|
// pockets, and none of it is connected to anything (the far-field "confetti").
|
||||||
|
// WormNetworkRange masks the carve by distance to the room/tunnel network (CaveSDF is
|
||||||
|
// already computed by Step 4 — free): full strength at the network, smooth fade to zero
|
||||||
|
// at Range. Worms become braids/shortcuts hugging the cave system; no isolated speckle.
|
||||||
|
// Range = 0 → unmasked legacy behaviour. Bonus: fully-masked voxels skip both Perlins.
|
||||||
if (Params.WormStrength > 0.0f && Params.WormThreshold > 0.0f)
|
if (Params.WormStrength > 0.0f && Params.WormThreshold > 0.0f)
|
||||||
|
{
|
||||||
|
float NetworkMask = 1.0f;
|
||||||
|
if (Params.WormNetworkRange > 0.0f)
|
||||||
|
{
|
||||||
|
if (CaveSDF >= Params.WormNetworkRange) // also true when no network (FLT_MAX)
|
||||||
|
{
|
||||||
|
NetworkMask = 0.0f;
|
||||||
|
}
|
||||||
|
else if (CaveSDF > 0.0f)
|
||||||
|
{
|
||||||
|
NetworkMask = 1.0f - SmoothStep01(CaveSDF / Params.WormNetworkRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NetworkMask > 0.0f)
|
||||||
{
|
{
|
||||||
float WormZFreq = Params.WormFrequency * Params.WormHorizontalBias;
|
float WormZFreq = Params.WormFrequency * Params.WormHorizontalBias;
|
||||||
|
|
||||||
float N1 = FMath::Abs(FMath::PerlinNoise3D(FVector(
|
float N1 = FMath::Abs(VoxelNoise::Perlin3D(FVector(
|
||||||
WorldX * Params.WormFrequency + SeedF,
|
WorldX * Params.WormFrequency + SeedF,
|
||||||
WorldY * Params.WormFrequency + SeedF * 1.7f,
|
WorldY * Params.WormFrequency + SeedF * 1.7f,
|
||||||
EffectiveZ * WormZFreq + SeedF * 2.3f
|
EffectiveZ * WormZFreq + SeedF * 2.3f
|
||||||
)) * VOXEL_NOISE_SCALE);
|
)) * VOXEL_NOISE_SCALE);
|
||||||
|
|
||||||
float N2 = FMath::Abs(FMath::PerlinNoise3D(FVector(
|
float N2 = FMath::Abs(VoxelNoise::Perlin3D(FVector(
|
||||||
WorldX * Params.WormFrequency + SeedF + 137.0f,
|
WorldX * Params.WormFrequency + SeedF + 137.0f,
|
||||||
WorldY * Params.WormFrequency + SeedF * 1.7f + 259.0f,
|
WorldY * Params.WormFrequency + SeedF * 1.7f + 259.0f,
|
||||||
EffectiveZ * WormZFreq + SeedF * 2.3f + 431.0f
|
EffectiveZ * WormZFreq + SeedF * 2.3f + 431.0f
|
||||||
@@ -1479,7 +1597,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
|||||||
if (WormValue < Params.WormThreshold)
|
if (WormValue < Params.WormThreshold)
|
||||||
{
|
{
|
||||||
float t = 1.0f - (WormValue / Params.WormThreshold);
|
float t = 1.0f - (WormValue / Params.WormThreshold);
|
||||||
Density -= t * Params.WormStrength;
|
Density -= t * Params.WormStrength * NetworkMask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1792,21 +1911,37 @@ float UVoxelGenerator::GetMazeDensity(float WorldX, float WorldY, float WorldZ,
|
|||||||
// high solid "sky cap" ceiling, with a flattened beach band around the water line.
|
// high solid "sky cap" ceiling, with a flattened beach band around the water line.
|
||||||
// Open air fills the gap between ground and ceiling; water is a render-side overlay.
|
// Open air fills the gap between ground and ceiling; water is a render-side overlay.
|
||||||
|
|
||||||
float UVoxelGenerator::GetSurfaceDensity(float WorldX, float WorldY, float WorldZ,
|
float UVoxelGenerator::ComputeSurfaceTerrainZ(float WorldX, float WorldY,
|
||||||
const FSurfaceGenerationParams& Params) const
|
const FSurfaceGenerationParams& Params) const
|
||||||
{
|
{
|
||||||
const float H = Params.StrateTopWorldZ - Params.StrateBottomWorldZ;
|
const float H = Params.StrateTopWorldZ - Params.StrateBottomWorldZ;
|
||||||
if (H <= 0.0f) return 1.0f;
|
|
||||||
|
|
||||||
const float SeedF = (float)Seed;
|
const float SeedF = (float)Seed;
|
||||||
const float BottomZ = Params.StrateBottomWorldZ;
|
const float BottomZ = Params.StrateBottomWorldZ;
|
||||||
|
|
||||||
// --- Heightfield (a function of XY only — Z is a fixed seed slice) ---
|
// --- Heightfield (a function of XY only — Z is a fixed seed slice) ---
|
||||||
const float GroundBase = BottomZ + H * Params.BaseGroundRelative;
|
const float GroundBase = BottomZ + H * Params.BaseGroundRelative;
|
||||||
|
|
||||||
|
// Domain-warp the STRUCTURAL query coords (continents + mountains) so coastlines and
|
||||||
|
// ridgelines wind organically instead of looking like axis-aligned noise blobs. Detail
|
||||||
|
// noise stays on the real XY so fine bumps remain crisp and uncorrelated with the warp.
|
||||||
|
float QX = WorldX, QY = WorldY;
|
||||||
|
if (Params.HeightWarpStrength > 0.0f)
|
||||||
|
{
|
||||||
|
const float WF = Params.HeightWarpFrequency;
|
||||||
|
const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + SeedF * 0.31f, WorldY * WF + 4.2f, SeedF * 1.7f));
|
||||||
|
const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 8.6f, WorldY * WF + SeedF * 0.53f, SeedF * 2.9f));
|
||||||
|
QX += wx * VOXEL_NOISE_SCALE * Params.HeightWarpStrength;
|
||||||
|
QY += wy * VOXEL_NOISE_SCALE * Params.HeightWarpStrength;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Macro relief map [0,1]: where this XY sits on the plains <-> mountains spectrum.
|
||||||
|
// M is the "mountainous-ness"; ReliefStrength=0 → M=1 everywhere (uniform terrain).
|
||||||
|
const float Relief = SampleRelief(WorldX, WorldY, Params.ReliefFrequency, Params.ReliefContrast);
|
||||||
|
const float M = FMath::Lerp(1.0f, Relief, Params.ReliefStrength);
|
||||||
|
|
||||||
float Cont = FractalNoise3D(FVector(
|
float Cont = FractalNoise3D(FVector(
|
||||||
WorldX * Params.ContinentFrequency + SeedF * 3.1f,
|
QX * Params.ContinentFrequency + SeedF * 3.1f,
|
||||||
WorldY * Params.ContinentFrequency + SeedF * 5.7f,
|
QY * Params.ContinentFrequency + SeedF * 5.7f,
|
||||||
SeedF * 0.7f), 4); // [-1,1]
|
SeedF * 0.7f), 4); // [-1,1]
|
||||||
|
|
||||||
float Detail = FractalNoise3D(FVector(
|
float Detail = FractalNoise3D(FVector(
|
||||||
@@ -1818,19 +1953,31 @@ float UVoxelGenerator::GetSurfaceDensity(float WorldX, float WorldY, float World
|
|||||||
if (Params.MountainStrength > 0.0f)
|
if (Params.MountainStrength > 0.0f)
|
||||||
{
|
{
|
||||||
float Ridge = RidgedNoise3D(FVector(
|
float Ridge = RidgedNoise3D(FVector(
|
||||||
WorldX * Params.MountainFrequency + 99.0f,
|
QX * Params.MountainFrequency + 99.0f,
|
||||||
WorldY * Params.MountainFrequency + 77.0f,
|
QY * Params.MountainFrequency + 77.0f,
|
||||||
SeedF * 0.9f), 4); // [-1,1]
|
SeedF * 0.9f), 4); // [-1,1]
|
||||||
Ridge = Ridge * 0.5f + 0.5f; // [0,1] peaks
|
Ridge = Ridge * 0.5f + 0.5f; // [0,1] peaks
|
||||||
Mountain = Ridge * Params.MountainStrength;
|
Mountain = Ridge * Params.MountainStrength * M; // mountains rise only in high-relief regions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plains keep a fraction of the continental swell; highlands get the full range.
|
||||||
|
const float ContScale = FMath::Lerp(0.45f, 1.0f, M);
|
||||||
|
|
||||||
float Terrain = GroundBase
|
float Terrain = GroundBase
|
||||||
+ Cont * Params.ElevationRange * 0.5f
|
+ Cont * Params.ElevationRange * 0.5f * ContScale
|
||||||
+ Mountain * Params.ElevationRange
|
+ Mountain * Params.ElevationRange
|
||||||
+ Detail * Params.SurfaceRoughness;
|
+ Detail * Params.SurfaceRoughness;
|
||||||
|
|
||||||
// Beach: flatten terrain toward the water line within BeachWidth.
|
// Plateau/mesa terracing — quantize height into steps, but only in high-relief areas and
|
||||||
|
// only as strongly as TerraceStrength asks. Layered cliffs/mesas up top, smooth lowlands.
|
||||||
|
if (Params.TerraceStrength > 0.0f && Params.TerraceHeight > 0.0f)
|
||||||
|
{
|
||||||
|
const float Stepped = FMath::RoundToFloat(Terrain / Params.TerraceHeight) * Params.TerraceHeight;
|
||||||
|
Terrain = FMath::Lerp(Terrain, Stepped, Params.TerraceStrength * M);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beach: flatten terrain toward the water line within BeachWidth. (Water level is
|
||||||
|
// strate-global — forced from the strate — so the water plane stays continuous.)
|
||||||
const float WaterZ = BottomZ + H * Params.WaterLevelRelative;
|
const float WaterZ = BottomZ + H * Params.WaterLevelRelative;
|
||||||
if (Params.WaterLevelRelative > 0.0f && Params.BeachWidth > 0.0f)
|
if (Params.WaterLevelRelative > 0.0f && Params.BeachWidth > 0.0f)
|
||||||
{
|
{
|
||||||
@@ -1842,11 +1989,15 @@ float UVoxelGenerator::GetSurfaceDensity(float WorldX, float WorldY, float World
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Solid below the terrain surface (positive = solid).
|
return Terrain;
|
||||||
float Density = Terrain - WorldZ;
|
}
|
||||||
|
|
||||||
// Sky cap: solid ceiling near the top of the strate, bumpy downward.
|
float UVoxelGenerator::ComputeSurfaceCeiling(float WorldX, float WorldY,
|
||||||
const float CeilZ = BottomZ + H * Params.CeilingRelative;
|
const FSurfaceGenerationParams& Params) const
|
||||||
|
{
|
||||||
|
const float H = Params.StrateTopWorldZ - Params.StrateBottomWorldZ;
|
||||||
|
const float SeedF = (float)Seed;
|
||||||
|
const float CeilZ = Params.StrateBottomWorldZ + H * Params.CeilingRelative;
|
||||||
float CeilNoise = 0.0f;
|
float CeilNoise = 0.0f;
|
||||||
if (Params.CeilingRoughness > 0.0f)
|
if (Params.CeilingRoughness > 0.0f)
|
||||||
{
|
{
|
||||||
@@ -1854,26 +2005,323 @@ float UVoxelGenerator::GetSurfaceDensity(float WorldX, float WorldY, float World
|
|||||||
WorldX * 0.04f + 5.0f, WorldY * 0.04f + 6.0f, SeedF * 2.1f), 3))
|
WorldX * 0.04f + 5.0f, WorldY * 0.04f + 6.0f, SeedF * 2.1f), 3))
|
||||||
* VOXEL_NOISE_SCALE * Params.CeilingRoughness;
|
* VOXEL_NOISE_SCALE * Params.CeilingRoughness;
|
||||||
}
|
}
|
||||||
const float CeilSurface = CeilZ - CeilNoise;
|
return CeilZ - CeilNoise;
|
||||||
Density = FMath::Max(Density, WorldZ - CeilSurface); // add solid above the ceiling
|
}
|
||||||
|
|
||||||
|
float UVoxelGenerator::SurfaceDensityFromColumn(float WorldX, float WorldY, float WorldZ,
|
||||||
|
float TerrainZ, float CeilSurf,
|
||||||
|
const FSurfaceGenerationParams& S) const
|
||||||
|
{
|
||||||
|
// Solid below the terrain surface; solid above the sky-cap ceiling.
|
||||||
|
float Density = TerrainZ - WorldZ;
|
||||||
|
Density = FMath::Max(Density, WorldZ - CeilSurf);
|
||||||
|
|
||||||
|
// Structural fields (Z bounds, seal, base) are forced equal across biomes → S is safe.
|
||||||
ApplyOriginSpine(Density, WorldX, WorldY, WorldZ,
|
ApplyOriginSpine(Density, WorldX, WorldY, WorldZ,
|
||||||
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
|
S.StrateTopWorldZ, S.StrateBottomWorldZ,
|
||||||
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
|
S.BoundarySealThickness, S.BaseDensity, OriginSpineRadius);
|
||||||
|
|
||||||
ApplyBoundarySeal(Density, WorldZ,
|
ApplyBoundarySeal(Density, WorldZ,
|
||||||
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
|
S.StrateTopWorldZ, S.StrateBottomWorldZ,
|
||||||
Params.BoundarySealThickness, Params.BaseDensity);
|
S.BoundarySealThickness, S.BaseDensity);
|
||||||
|
|
||||||
if (StrateManager)
|
if (StrateManager)
|
||||||
{
|
{
|
||||||
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ);
|
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ);
|
||||||
ApplyPassageCarving(Density, ModSDF, Params.BaseDensity, Params.BoundarySealThickness);
|
ApplyPassageCarving(Density, ModSDF, S.BaseDensity, S.BoundarySealThickness);
|
||||||
}
|
}
|
||||||
|
|
||||||
return -Density;
|
return -Density;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float UVoxelGenerator::GetSurfaceDensity(float WorldX, float WorldY, float WorldZ,
|
||||||
|
const FSurfaceGenerationParams& ParamsD,
|
||||||
|
const FSurfaceGenerationParams& ParamsN,
|
||||||
|
float NeighborWeight) const
|
||||||
|
{
|
||||||
|
if (ParamsD.StrateTopWorldZ - ParamsD.StrateBottomWorldZ <= 0.0f) return 1.0f;
|
||||||
|
|
||||||
|
// Heightfield: dominant biome, OUTPUT-BLENDED toward the nearest neighbour in the
|
||||||
|
// border band. Blending heights (not params) keeps borders seamless across any param
|
||||||
|
// difference. ParamsD == ParamsN, weight 0 ⇒ single eval (bit-identical, no biomes).
|
||||||
|
float TerrainZ = ComputeSurfaceTerrainZ(WorldX, WorldY, ParamsD);
|
||||||
|
if (NeighborWeight > 0.0f)
|
||||||
|
{
|
||||||
|
TerrainZ = FMath::Lerp(TerrainZ, ComputeSurfaceTerrainZ(WorldX, WorldY, ParamsN), NeighborWeight);
|
||||||
|
}
|
||||||
|
const float CeilSurf = ComputeSurfaceCeiling(WorldX, WorldY, ParamsD);
|
||||||
|
|
||||||
|
return SurfaceDensityFromColumn(WorldX, WorldY, WorldZ, TerrainZ, CeilSurf, ParamsD);
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// CLIMATE & BIOME FIELDS
|
||||||
|
//=============================================================================
|
||||||
|
// Pure functions of world XY (+ seed). The relief field is shared with SurfaceWorld
|
||||||
|
// terrain so the biome map and the terrain it modulates stay in agreement. The biome
|
||||||
|
// field is a warped Voronoi whose cells are assigned a biome by climate — coherent
|
||||||
|
// geography (mountains cluster in high relief), window-invariant by construction.
|
||||||
|
|
||||||
|
float UVoxelGenerator::SampleRelief(float WorldX, float WorldY, float Frequency, float Contrast) const
|
||||||
|
{
|
||||||
|
const float SeedF = (float)Seed;
|
||||||
|
// Same offsets/octaves as the original SurfaceWorld relief so existing worlds are
|
||||||
|
// unchanged (this is the function that code path now calls).
|
||||||
|
float R = FractalNoise3D(FVector(
|
||||||
|
WorldX * Frequency + SeedF * 7.3f,
|
||||||
|
WorldY * Frequency + SeedF * 2.1f,
|
||||||
|
SeedF * 0.5f), 2) * 0.5f + 0.5f; // [0,1]
|
||||||
|
R = FMath::Clamp((R - 0.5f) * Contrast + 0.5f, 0.0f, 1.0f);
|
||||||
|
return SmoothStep01(R);
|
||||||
|
}
|
||||||
|
|
||||||
|
float UVoxelGenerator::SampleMoisture(float WorldX, float WorldY, float Frequency) const
|
||||||
|
{
|
||||||
|
const float SeedF = (float)Seed;
|
||||||
|
const float N = FractalNoise3D(FVector(
|
||||||
|
WorldX * Frequency + SeedF * 4.7f,
|
||||||
|
WorldY * Frequency + SeedF * 8.9f,
|
||||||
|
SeedF * 1.3f), 2) * 0.5f + 0.5f; // [0,1]
|
||||||
|
return FMath::Clamp(N, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UVoxelGenerator::ClassifyBiomeAtSite(float SiteX, float SiteY,
|
||||||
|
const FBiomeContext& Ctx, uint32 SiteHash) const
|
||||||
|
{
|
||||||
|
const float R = SampleRelief(SiteX, SiteY, Ctx.Map.ReliefFrequency, Ctx.Map.ReliefContrast);
|
||||||
|
const float Mo = SampleMoisture(SiteX, SiteY, Ctx.Map.MoistureFrequency);
|
||||||
|
|
||||||
|
int32 Best = 0;
|
||||||
|
float BestScore = FLT_MAX;
|
||||||
|
for (int32 i = 0; i < Ctx.Biomes.Num(); ++i)
|
||||||
|
{
|
||||||
|
const FBiomeResolved& B = Ctx.Biomes[i];
|
||||||
|
// Distance in climate space to the biome's box (0 when inside it).
|
||||||
|
const float dr = (R < B.ReliefMin) ? (B.ReliefMin - R)
|
||||||
|
: (R > B.ReliefMax) ? (R - B.ReliefMax) : 0.0f;
|
||||||
|
const float dm = (Mo < B.MoistureMin) ? (B.MoistureMin - Mo)
|
||||||
|
: (Mo > B.MoistureMax) ? (Mo - B.MoistureMax) : 0.0f;
|
||||||
|
float Score = dr * dr + dm * dm;
|
||||||
|
// Tiny deterministic jitter so overlapping boxes don't all collapse to biome 0.
|
||||||
|
Score += VoxelHash::ToFloat01(VoxelHash::Mix(SiteHash ^ (0x9e3779b9u * (uint32)(i + 1)))) * 1.0e-4f;
|
||||||
|
if (Score < BestScore) { BestScore = Score; Best = i; }
|
||||||
|
}
|
||||||
|
return Best;
|
||||||
|
}
|
||||||
|
|
||||||
|
FBiomeSample UVoxelGenerator::SampleBiomeAt(float WorldX, float WorldY, const FBiomeContext& Ctx) const
|
||||||
|
{
|
||||||
|
FBiomeSample Out;
|
||||||
|
if (Ctx.Biomes.Num() == 0) return Out; // biomes disabled
|
||||||
|
if (Ctx.Biomes.Num() == 1) { Out.DominantIndex = 0; return Out; }
|
||||||
|
|
||||||
|
const FBiomeMapParams& MP = Ctx.Map;
|
||||||
|
const float Cell = FMath::Max(MP.CellSize, 1.0f);
|
||||||
|
const uint32 S = (uint32)Seed ^ 0x42494f4du; // 'BIOM'
|
||||||
|
|
||||||
|
// Domain-warp the query so cell borders wind organically (not a hex grid).
|
||||||
|
float QX = WorldX, QY = WorldY;
|
||||||
|
if (MP.WarpStrength > 0.0f)
|
||||||
|
{
|
||||||
|
const float SeedF = (float)Seed;
|
||||||
|
const float WF = MP.WarpFrequency;
|
||||||
|
const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + SeedF * 0.27f, WorldY * WF + 3.1f, SeedF * 1.1f));
|
||||||
|
const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 7.7f, WorldY * WF + SeedF * 0.61f, SeedF * 2.3f));
|
||||||
|
QX += wx * VOXEL_NOISE_SCALE * MP.WarpStrength;
|
||||||
|
QY += wy * VOXEL_NOISE_SCALE * MP.WarpStrength;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 CX = FMath::FloorToInt(QX / Cell);
|
||||||
|
const int32 CY = FMath::FloorToInt(QY / Cell);
|
||||||
|
|
||||||
|
// Worley F1/F2 over the 3x3 neighbourhood of the (warped) cell. With jitter confined
|
||||||
|
// to [0,1) of a cell, the nearest site is always within this neighbourhood.
|
||||||
|
float BestD2 = FLT_MAX, SecondD2 = FLT_MAX;
|
||||||
|
int32 BestBiome = 0, SecondBiome = 0;
|
||||||
|
for (int32 dy = -1; dy <= 1; ++dy)
|
||||||
|
for (int32 dx = -1; dx <= 1; ++dx)
|
||||||
|
{
|
||||||
|
const int32 nx = CX + dx, ny = CY + dy;
|
||||||
|
const uint32 h = VoxelHash::Cell(nx, ny, S);
|
||||||
|
const float jx = VoxelHash::ToFloat01(h);
|
||||||
|
const float jy = VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0x68bc21ebu));
|
||||||
|
const float sx = (nx + jx) * Cell;
|
||||||
|
const float sy = (ny + jy) * Cell;
|
||||||
|
const float ddx = sx - QX, ddy = sy - QY;
|
||||||
|
const float d2 = ddx * ddx + ddy * ddy;
|
||||||
|
|
||||||
|
if (d2 < BestD2)
|
||||||
|
{
|
||||||
|
SecondD2 = BestD2; SecondBiome = BestBiome;
|
||||||
|
BestD2 = d2; BestBiome = ClassifyBiomeAtSite(sx, sy, Ctx, h);
|
||||||
|
}
|
||||||
|
else if (d2 < SecondD2)
|
||||||
|
{
|
||||||
|
SecondD2 = d2; SecondBiome = ClassifyBiomeAtSite(sx, sy, Ctx, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Out.DominantIndex = BestBiome;
|
||||||
|
Out.NeighborIndex = SecondBiome;
|
||||||
|
|
||||||
|
// Blend weight: 0.5 at the shared border (d1≈d2), fading to 0 a BorderBlend-wide
|
||||||
|
// band inside the dominant cell. Only blend when the neighbour is a DIFFERENT biome.
|
||||||
|
if (MP.BorderBlend > 0.0f && SecondD2 < FLT_MAX && BestBiome != SecondBiome)
|
||||||
|
{
|
||||||
|
const float d1 = FMath::Sqrt(BestD2);
|
||||||
|
const float d2 = FMath::Sqrt(SecondD2);
|
||||||
|
const float t = FMath::Clamp((d2 - d1) / FMath::Max(MP.BorderBlend, 1.0f), 0.0f, 1.0f);
|
||||||
|
Out.NeighborWeight = 0.5f * (1.0f - SmoothStep01(t));
|
||||||
|
}
|
||||||
|
return Out;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVoxelGenerator::RebuildBiomeGrid(int32 ChunkX, int32 ChunkY, int32 ChunkZ,
|
||||||
|
const FBiomeContext& Ctx, FChunkBiomeCache& Cache) const
|
||||||
|
{
|
||||||
|
Cache.ChunkZ = ChunkZ;
|
||||||
|
Cache.Seed = Seed;
|
||||||
|
Cache.bActive = Ctx.IsValid();
|
||||||
|
Cache.Ctx = Ctx;
|
||||||
|
Cache.CellBiome.Reset();
|
||||||
|
|
||||||
|
if (!Cache.bActive)
|
||||||
|
{
|
||||||
|
// Mark the whole chunk footprint valid so non-biome chunks don't rebuild per voxel.
|
||||||
|
Cache.ValidMinX = (float)ChunkX * CHUNK_SIZE - (float)CHUNK_SIZE;
|
||||||
|
Cache.ValidMaxX = (float)(ChunkX + 1) * CHUNK_SIZE + (float)CHUNK_SIZE;
|
||||||
|
Cache.ValidMinY = (float)ChunkY * CHUNK_SIZE - (float)CHUNK_SIZE;
|
||||||
|
Cache.ValidMaxY = (float)(ChunkY + 1) * CHUNK_SIZE + (float)CHUNK_SIZE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FBiomeMapParams& MP = Ctx.Map;
|
||||||
|
const float Cell = FMath::Max(MP.CellSize, 1.0f);
|
||||||
|
const uint32 S = (uint32)Seed ^ 0x42494f4du; // 'BIOM' (must match SampleBiomeAt)
|
||||||
|
|
||||||
|
// Validity halo: one chunk beyond the footprint, so the +X/+Y boundary corners and
|
||||||
|
// ±1 gradient-normal samples stay inside the valid box (no rebuild thrash, §8.10).
|
||||||
|
const float Halo = (float)CHUNK_SIZE;
|
||||||
|
Cache.ValidMinX = (float)ChunkX * CHUNK_SIZE - Halo;
|
||||||
|
Cache.ValidMaxX = (float)(ChunkX + 1) * CHUNK_SIZE + Halo;
|
||||||
|
Cache.ValidMinY = (float)ChunkY * CHUNK_SIZE - Halo;
|
||||||
|
Cache.ValidMaxY = (float)(ChunkY + 1) * CHUNK_SIZE + Halo;
|
||||||
|
|
||||||
|
// Cell-grid coverage: the valid box expanded by the max warp displacement + one cell,
|
||||||
|
// so the 3x3 search around any in-box query's warped point is fully present.
|
||||||
|
const float CellMargin = MP.WarpStrength * VOXEL_NOISE_SCALE + Cell + 1.0f;
|
||||||
|
Cache.BaseCellX = FMath::FloorToInt((Cache.ValidMinX - CellMargin) / Cell);
|
||||||
|
Cache.BaseCellY = FMath::FloorToInt((Cache.ValidMinY - CellMargin) / Cell);
|
||||||
|
const int32 MaxCellX = FMath::FloorToInt((Cache.ValidMaxX + CellMargin) / Cell);
|
||||||
|
const int32 MaxCellY = FMath::FloorToInt((Cache.ValidMaxY + CellMargin) / Cell);
|
||||||
|
Cache.CellsX = MaxCellX - Cache.BaseCellX + 1;
|
||||||
|
Cache.CellsY = MaxCellY - Cache.BaseCellY + 1;
|
||||||
|
Cache.CellBiome.SetNumUninitialized(Cache.CellsX * Cache.CellsY);
|
||||||
|
|
||||||
|
for (int32 cy = 0; cy < Cache.CellsY; ++cy)
|
||||||
|
for (int32 cx = 0; cx < Cache.CellsX; ++cx)
|
||||||
|
{
|
||||||
|
const int32 nx = Cache.BaseCellX + cx;
|
||||||
|
const int32 ny = Cache.BaseCellY + cy;
|
||||||
|
const uint32 h = VoxelHash::Cell(nx, ny, S);
|
||||||
|
const float jx = VoxelHash::ToFloat01(h);
|
||||||
|
const float jy = VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0x68bc21ebu));
|
||||||
|
const float sx = (nx + jx) * Cell;
|
||||||
|
const float sy = (ny + jy) * Cell;
|
||||||
|
Cache.CellBiome[cy * Cache.CellsX + cx] = ClassifyBiomeAtSite(sx, sy, Ctx, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FBiomeSample UVoxelGenerator::ResolveBiomeSampleAt(float WorldX, float WorldY, int32 ChunkZ,
|
||||||
|
const FBiomeContext& Ctx, FChunkBiomeCache& Cache) const
|
||||||
|
{
|
||||||
|
FBiomeSample Out;
|
||||||
|
|
||||||
|
// Box-validated rebuild (perf-only; result is the pure function of XY either way).
|
||||||
|
if (!Cache.Contains(WorldX, WorldY, ChunkZ, Seed))
|
||||||
|
{
|
||||||
|
RebuildBiomeGrid(FMath::FloorToInt(WorldX / CHUNK_SIZE),
|
||||||
|
FMath::FloorToInt(WorldY / CHUNK_SIZE), ChunkZ, Ctx, Cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Cache.bActive) return Out;
|
||||||
|
if (Cache.Ctx.Biomes.Num() == 1) { Out.DominantIndex = 0; return Out; }
|
||||||
|
|
||||||
|
const FBiomeMapParams& MP = Cache.Ctx.Map;
|
||||||
|
const float Cell = FMath::Max(MP.CellSize, 1.0f);
|
||||||
|
const uint32 S = (uint32)Seed ^ 0x42494f4du;
|
||||||
|
|
||||||
|
// Same warp + 3x3 Worley as SampleBiomeAt → identical assignment (matches the preview).
|
||||||
|
float QX = WorldX, QY = WorldY;
|
||||||
|
if (MP.WarpStrength > 0.0f)
|
||||||
|
{
|
||||||
|
const float SeedF = (float)Seed;
|
||||||
|
const float WF = MP.WarpFrequency;
|
||||||
|
const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + SeedF * 0.27f, WorldY * WF + 3.1f, SeedF * 1.1f));
|
||||||
|
const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 7.7f, WorldY * WF + SeedF * 0.61f, SeedF * 2.3f));
|
||||||
|
QX += wx * VOXEL_NOISE_SCALE * MP.WarpStrength;
|
||||||
|
QY += wy * VOXEL_NOISE_SCALE * MP.WarpStrength;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 CX = FMath::FloorToInt(QX / Cell);
|
||||||
|
const int32 CY = FMath::FloorToInt(QY / Cell);
|
||||||
|
|
||||||
|
float BestD2 = FLT_MAX, SecondD2 = FLT_MAX;
|
||||||
|
int32 BestBiome = 0, SecondBiome = 0;
|
||||||
|
for (int32 dy = -1; dy <= 1; ++dy)
|
||||||
|
for (int32 dx = -1; dx <= 1; ++dx)
|
||||||
|
{
|
||||||
|
const int32 nx = CX + dx, ny = CY + dy;
|
||||||
|
const uint32 h = VoxelHash::Cell(nx, ny, S);
|
||||||
|
const float jx = VoxelHash::ToFloat01(h);
|
||||||
|
const float jy = VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0x68bc21ebu));
|
||||||
|
const float sx = (nx + jx) * Cell;
|
||||||
|
const float sy = (ny + jy) * Cell;
|
||||||
|
const float ddx = sx - QX, ddy = sy - QY;
|
||||||
|
const float d2 = ddx * ddx + ddy * ddy;
|
||||||
|
|
||||||
|
// Cached biome index (fallback to classify on the rare margin miss → still correct).
|
||||||
|
const int32 gx = nx - Cache.BaseCellX;
|
||||||
|
const int32 gy = ny - Cache.BaseCellY;
|
||||||
|
const int32 BiomeIdx = (gx >= 0 && gx < Cache.CellsX && gy >= 0 && gy < Cache.CellsY)
|
||||||
|
? Cache.CellBiome[gy * Cache.CellsX + gx]
|
||||||
|
: ClassifyBiomeAtSite(sx, sy, Cache.Ctx, h);
|
||||||
|
|
||||||
|
if (d2 < BestD2) { SecondD2 = BestD2; SecondBiome = BestBiome; BestD2 = d2; BestBiome = BiomeIdx; }
|
||||||
|
else if (d2 < SecondD2) { SecondD2 = d2; SecondBiome = BiomeIdx; }
|
||||||
|
}
|
||||||
|
|
||||||
|
Out.DominantIndex = BestBiome;
|
||||||
|
Out.NeighborIndex = SecondBiome;
|
||||||
|
if (MP.BorderBlend > 0.0f && SecondD2 < FLT_MAX && BestBiome != SecondBiome)
|
||||||
|
{
|
||||||
|
const float d1 = FMath::Sqrt(BestD2);
|
||||||
|
const float d2 = FMath::Sqrt(SecondD2);
|
||||||
|
const float t = FMath::Clamp((d2 - d1) / FMath::Max(MP.BorderBlend, 1.0f), 0.0f, 1.0f);
|
||||||
|
Out.NeighborWeight = 0.5f * (1.0f - SmoothStep01(t));
|
||||||
|
}
|
||||||
|
return Out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UVoxelBiomeDefinition* UVoxelGenerator::GetDominantBiomeAt(float WorldX, float WorldY, int32 ChunkZ) const
|
||||||
|
{
|
||||||
|
if (!StrateManager) return nullptr;
|
||||||
|
|
||||||
|
const FIntVector Coord(FMath::FloorToInt(WorldX / CHUNK_SIZE),
|
||||||
|
FMath::FloorToInt(WorldY / CHUNK_SIZE), ChunkZ);
|
||||||
|
|
||||||
|
const FBiomeContext Ctx = StrateManager->GetBiomeContextForChunk(Coord);
|
||||||
|
if (!Ctx.IsValid()) return nullptr;
|
||||||
|
|
||||||
|
const FBiomeSample S = SampleBiomeAt(WorldX, WorldY, Ctx);
|
||||||
|
if (!Ctx.Biomes.IsValidIndex(S.DominantIndex)) return nullptr;
|
||||||
|
|
||||||
|
// Map the context position back to the strate's Biomes[] asset.
|
||||||
|
const int32 StrateBiomeIdx = Ctx.Biomes[S.DominantIndex].Index;
|
||||||
|
const UVoxelStrateDefinition* Def = StrateManager->GetStrateForChunk(Coord);
|
||||||
|
return (Def && Def->Biomes.IsValidIndex(StrateBiomeIdx)) ? Def->Biomes[StrateBiomeIdx] : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// VERTICAL-SHAFT GENERATOR (ECaveGeneratorType::VerticalShafts)
|
// VERTICAL-SHAFT GENERATOR (ECaveGeneratorType::VerticalShafts)
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
|
|||||||
@@ -89,7 +89,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(const FVoxelChunk& Chunk,
|
|||||||
// Les vertices partagés permettent des normales lisses et réduisent le count ~3x.
|
// Les vertices partagés permettent des normales lisses et réduisent le count ~3x.
|
||||||
TMap<FIntVector, int32> VertexMap;
|
TMap<FIntVector, int32> VertexMap;
|
||||||
|
|
||||||
auto GetOrCreateVertex = [&](const FVector& WorldPos) -> int32
|
// Normale fournie par l'appelant (gradient lu dans la grille de densité, T1.b) —
|
||||||
|
// plus d'échantillonnage de densité par vertex. RawNormal pointe solide→air ; on la
|
||||||
|
// normalise ici (fallback up si dégénérée).
|
||||||
|
auto GetOrCreateVertex = [&](const FVector& WorldPos, const FVector& RawNormal) -> int32
|
||||||
{
|
{
|
||||||
const FIntVector Key(
|
const FIntVector Key(
|
||||||
FMath::RoundToInt(WorldPos.X * 100.0f),
|
FMath::RoundToInt(WorldPos.X * 100.0f),
|
||||||
@@ -107,18 +110,12 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(const FVoxelChunk& Chunk,
|
|||||||
|
|
||||||
MeshData.Vertices.Add(WorldPos);
|
MeshData.Vertices.Add(WorldPos);
|
||||||
|
|
||||||
// Normale par gradient de densité (shading lisse).
|
FVector Normal = RawNormal;
|
||||||
if (Generator)
|
if (!Normal.Normalize())
|
||||||
{
|
{
|
||||||
const float VoxelX = WorldPos.X / VOXEL_SIZE;
|
Normal = FVector(0.0f, 0.0f, 1.0f); // dégénéré (zone plate)
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
MeshData.Normals.Add(Normal);
|
||||||
|
|
||||||
// UVs planaires — le triplanar mapping se fait dans le matériau.
|
// UVs planaires — le triplanar mapping se fait dans le matériau.
|
||||||
MeshData.UVs.Add(FVector2D(WorldPos.X / VOXEL_SIZE, WorldPos.Y / VOXEL_SIZE));
|
MeshData.UVs.Add(FVector2D(WorldPos.X / VOXEL_SIZE, WorldPos.Y / VOXEL_SIZE));
|
||||||
@@ -161,23 +158,44 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(const FVoxelChunk& Chunk,
|
|||||||
// CellsPerAxis = nombre de cellules par axe ; GridDim = points de grille (+1).
|
// 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.
|
// 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.
|
// Ordre de remplissage z→y→x : garde le cache SDF (search-box) bien chaud.
|
||||||
|
// On échantillonne avec un anneau de marge de 1 point de chaque côté (indices -1..GridDim)
|
||||||
|
// pour pouvoir calculer les normales par GRADIENT DE GRILLE (T1.b) — différences centrales
|
||||||
|
// sur la grille au lieu de 6 appels densité frais par vertex. La marge utilise les mêmes
|
||||||
|
// échantillons monde purs qu'un chunk voisin, donc les normales restent continues aux bords
|
||||||
|
// de chunk (pas de couture de shading). La géométrie est inchangée au bit près (mêmes
|
||||||
|
// positions d'arête) ; seules les normales changent.
|
||||||
const int32 CellsPerAxis = CHUNK_SIZE / Step;
|
const int32 CellsPerAxis = CHUNK_SIZE / Step;
|
||||||
const int32 GridDim = CellsPerAxis + 1;
|
const int32 GridDim = CellsPerAxis + 1;
|
||||||
|
const int32 MDim = GridDim + 2; // +1 marge de chaque côté
|
||||||
|
|
||||||
TArray<float> DensityGrid;
|
TArray<float> DensityGrid;
|
||||||
DensityGrid.SetNumUninitialized(GridDim * GridDim * GridDim);
|
DensityGrid.SetNumUninitialized(MDim * MDim * MDim);
|
||||||
for (int32 gz = 0; gz < GridDim; gz++)
|
for (int32 gz = -1; gz <= GridDim; gz++)
|
||||||
{
|
{
|
||||||
for (int32 gy = 0; gy < GridDim; gy++)
|
for (int32 gy = -1; gy <= GridDim; gy++)
|
||||||
{
|
{
|
||||||
for (int32 gx = 0; gx < GridDim; gx++)
|
for (int32 gx = -1; gx <= GridDim; gx++)
|
||||||
{
|
{
|
||||||
DensityGrid[(gz * GridDim + gy) * GridDim + gx] =
|
DensityGrid[((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1)] =
|
||||||
GetDensity(Chunk, gx * Step, gy * Step, gz * Step);
|
GetDensity(Chunk, gx * Step, gy * Step, gz * Step);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lecture grille (avec offset de marge) + gradient central depuis la grille.
|
||||||
|
auto SampleG = [&](int32 gx, int32 gy, int32 gz) -> float
|
||||||
|
{
|
||||||
|
return DensityGrid[((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1)];
|
||||||
|
};
|
||||||
|
auto GradAt = [&](int32 gx, int32 gy, int32 gz) -> FVector
|
||||||
|
{
|
||||||
|
// Densité négative=solide, positive=air → le gradient pointe vers l'air (sortant).
|
||||||
|
return FVector(
|
||||||
|
SampleG(gx + 1, gy, gz) - SampleG(gx - 1, gy, gz),
|
||||||
|
SampleG(gx, gy + 1, gz) - SampleG(gx, gy - 1, gz),
|
||||||
|
SampleG(gx, gy, gz + 1) - SampleG(gx, gy, gz - 1));
|
||||||
|
};
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// ITÉRATION SUR LES CELLULES
|
// ITÉRATION SUR LES CELLULES
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -190,9 +208,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(const FVoxelChunk& Chunk,
|
|||||||
{
|
{
|
||||||
for (int32 cx = 0; cx < CellsPerAxis; cx++)
|
for (int32 cx = 0; cx < CellsPerAxis; cx++)
|
||||||
{
|
{
|
||||||
// Densités + positions aux 8 coins (lues dans la grille pré-calculée)
|
// Densités + positions + gradients aux 8 coins (lus dans la grille).
|
||||||
float Densities[8];
|
float Densities[8];
|
||||||
FVector Positions[8];
|
FVector Positions[8];
|
||||||
|
FVector Gradients[8];
|
||||||
|
|
||||||
for (int32 i = 0; i < 8; i++)
|
for (int32 i = 0; i < 8; i++)
|
||||||
{
|
{
|
||||||
@@ -200,9 +219,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(const FVoxelChunk& Chunk,
|
|||||||
const int32 GY = cy + CornerOffsets[i].Y;
|
const int32 GY = cy + CornerOffsets[i].Y;
|
||||||
const int32 GZ = cz + CornerOffsets[i].Z;
|
const int32 GZ = cz + CornerOffsets[i].Z;
|
||||||
|
|
||||||
Densities[i] = DensityGrid[(GZ * GridDim + GY) * GridDim + GX];
|
Densities[i] = SampleG(GX, GY, GZ);
|
||||||
Positions[i] = ChunkWorldPos
|
Positions[i] = ChunkWorldPos
|
||||||
+ FVector(GX * Step, GY * Step, GZ * Step) * VOXEL_SIZE;
|
+ FVector(GX * Step, GY * Step, GZ * Step) * VOXEL_SIZE;
|
||||||
|
Gradients[i] = GradAt(GX, GY, GZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Index de cas MC (8 bits, un par coin)
|
// Index de cas MC (8 bits, un par coin)
|
||||||
@@ -217,18 +237,23 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(const FVoxelChunk& Chunk,
|
|||||||
|
|
||||||
if (MCEdgeTable[CaseIndex] == 0) continue; // Pas de surface ici
|
if (MCEdgeTable[CaseIndex] == 0) continue; // Pas de surface ici
|
||||||
|
|
||||||
// Interpolation des positions sur les arêtes traversées
|
// Interpolation des positions + normales sur les arêtes traversées. Le t est
|
||||||
|
// calculé exactement comme InterpolateEdge → positions bit-identiques (topologie
|
||||||
|
// inchangée) ; la normale interpole les gradients de coin par le même t.
|
||||||
FVector EdgeVertices[12];
|
FVector EdgeVertices[12];
|
||||||
|
FVector EdgeNormals[12];
|
||||||
for (int32 i = 0; i < 12; i++)
|
for (int32 i = 0; i < 12; i++)
|
||||||
{
|
{
|
||||||
if (MCEdgeTable[CaseIndex] & (1 << i))
|
if (MCEdgeTable[CaseIndex] & (1 << i))
|
||||||
{
|
{
|
||||||
const int32 A = EdgeCorners[i][0];
|
const int32 A = EdgeCorners[i][0];
|
||||||
const int32 B = EdgeCorners[i][1];
|
const int32 B = EdgeCorners[i][1];
|
||||||
EdgeVertices[i] = InterpolateEdge(
|
const float D1 = Densities[A], D2 = Densities[B];
|
||||||
Positions[A], Positions[B],
|
const float T = (FMath::Abs(D2 - D1) < KINDA_SMALL_NUMBER)
|
||||||
Densities[A], Densities[B]
|
? 0.5f
|
||||||
);
|
: FMath::Clamp((IsoLevel - D1) / (D2 - D1), 0.0f, 1.0f);
|
||||||
|
EdgeVertices[i] = Positions[A] + T * (Positions[B] - Positions[A]);
|
||||||
|
EdgeNormals[i] = Gradients[A] + T * (Gradients[B] - Gradients[A]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,9 +261,12 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(const FVoxelChunk& Chunk,
|
|||||||
// Ordre 0, 2, 1 (pas 0, 1, 2) pour le winding attendu par RealtimeMesh.
|
// Ordre 0, 2, 1 (pas 0, 1, 2) pour le winding attendu par RealtimeMesh.
|
||||||
for (int32 i = 0; MCTriTable[CaseIndex][i] != -1; i += 3)
|
for (int32 i = 0; MCTriTable[CaseIndex][i] != -1; i += 3)
|
||||||
{
|
{
|
||||||
const int32 Idx0 = GetOrCreateVertex(EdgeVertices[MCTriTable[CaseIndex][i]]);
|
const int32 E0 = MCTriTable[CaseIndex][i];
|
||||||
const int32 Idx1 = GetOrCreateVertex(EdgeVertices[MCTriTable[CaseIndex][i + 1]]);
|
const int32 E1 = MCTriTable[CaseIndex][i + 1];
|
||||||
const int32 Idx2 = GetOrCreateVertex(EdgeVertices[MCTriTable[CaseIndex][i + 2]]);
|
const int32 E2 = MCTriTable[CaseIndex][i + 2];
|
||||||
|
const int32 Idx0 = GetOrCreateVertex(EdgeVertices[E0], EdgeNormals[E0]);
|
||||||
|
const int32 Idx1 = GetOrCreateVertex(EdgeVertices[E1], EdgeNormals[E1]);
|
||||||
|
const int32 Idx2 = GetOrCreateVertex(EdgeVertices[E2], EdgeNormals[E2]);
|
||||||
|
|
||||||
MeshData.Triangles.Add(Idx0);
|
MeshData.Triangles.Add(Idx0);
|
||||||
MeshData.Triangles.Add(Idx2);
|
MeshData.Triangles.Add(Idx2);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord
|
#include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord
|
||||||
#include "VoxelCaveMorphology.h" // For VoxelSDF and VoxelHash
|
#include "VoxelCaveMorphology.h" // For VoxelSDF and VoxelHash
|
||||||
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
||||||
|
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
|
||||||
|
|
||||||
// Fractal Brownian Motion (layered Perlin) along a 1D parameter, ~[-1,1].
|
// Fractal Brownian Motion (layered Perlin) along a 1D parameter, ~[-1,1].
|
||||||
// Independent octaves at increasing frequency / decreasing amplitude give an organic,
|
// Independent octaves at increasing frequency / decreasing amplitude give an organic,
|
||||||
@@ -536,6 +537,33 @@ VF_ARCHETYPE_PARAMS_GETTER(GetFloatingIslandParamsForChunk, FFloatingIslandParam
|
|||||||
|
|
||||||
#undef VF_ARCHETYPE_PARAMS_GETTER
|
#undef VF_ARCHETYPE_PARAMS_GETTER
|
||||||
|
|
||||||
|
FBiomeContext UVoxelStrateManager::GetBiomeContextForChunk(const FIntVector& ChunkCoord) const
|
||||||
|
{
|
||||||
|
FBiomeContext Out;
|
||||||
|
|
||||||
|
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||||
|
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition) return Out;
|
||||||
|
|
||||||
|
const UVoxelStrateDefinition* Def = StrateLayout[SlotIdx].Definition;
|
||||||
|
if (Def->Biomes.Num() == 0) return Out; // biomes disabled for this strate
|
||||||
|
|
||||||
|
Out.Map = Def->BiomeMapParams;
|
||||||
|
Out.Biomes.Reserve(Def->Biomes.Num());
|
||||||
|
for (int32 i = 0; i < Def->Biomes.Num(); ++i)
|
||||||
|
{
|
||||||
|
const UVoxelBiomeDefinition* B = Def->Biomes[i];
|
||||||
|
if (!B) continue; // skip null entries (keep original index for content lookup)
|
||||||
|
|
||||||
|
FBiomeResolved R;
|
||||||
|
R.Index = i;
|
||||||
|
R.ReliefMin = B->ReliefMin; R.ReliefMax = B->ReliefMax;
|
||||||
|
R.MoistureMin = B->MoistureMin; R.MoistureMax = B->MoistureMax;
|
||||||
|
R.DebugColor = B->DebugColor.ToFColor(true);
|
||||||
|
Out.Biomes.Add(R);
|
||||||
|
}
|
||||||
|
return Out;
|
||||||
|
}
|
||||||
|
|
||||||
bool UVoxelStrateManager::GetStrateUnrealZRange(float WorldZ, float& OutTopZ, float& OutBottomZ) const
|
bool UVoxelStrateManager::GetStrateUnrealZRange(float WorldZ, float& OutTopZ, float& OutBottomZ) const
|
||||||
{
|
{
|
||||||
const int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / CHUNK_SIZE);
|
const int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / CHUNK_SIZE);
|
||||||
|
|||||||
@@ -7,10 +7,17 @@
|
|||||||
#include "RealtimeMeshSimple.h"
|
#include "RealtimeMeshSimple.h"
|
||||||
#include "VoxelMarchingCubesMesher.h"
|
#include "VoxelMarchingCubesMesher.h"
|
||||||
#include "VoxelStrateDefinition.h"
|
#include "VoxelStrateDefinition.h"
|
||||||
|
#include "VoxelBiomeDefinition.h"
|
||||||
#include "VoxelTerrainOpDefinition.h"
|
#include "VoxelTerrainOpDefinition.h"
|
||||||
#include "VoxelContentManager.h"
|
#include "VoxelContentManager.h"
|
||||||
#include "VoxelAtmosphereManager.h"
|
#include "VoxelAtmosphereManager.h"
|
||||||
#include "DrawDebugHelpers.h"
|
#include "DrawDebugHelpers.h"
|
||||||
|
#include "IImageWrapper.h"
|
||||||
|
#include "IImageWrapperModule.h"
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
#include "Misc/FileHelper.h"
|
||||||
|
#include "Misc/Paths.h"
|
||||||
|
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
|
||||||
|
|
||||||
AVoxelWorld::AVoxelWorld()
|
AVoxelWorld::AVoxelWorld()
|
||||||
{
|
{
|
||||||
@@ -241,13 +248,13 @@ void AVoxelWorld::BeginPlay()
|
|||||||
|
|
||||||
// Content manager — scatters decorations/actors + water per chunk as they stream in.
|
// Content manager — scatters decorations/actors + water per chunk as they stream in.
|
||||||
ContentManager = NewObject<UVoxelContentManager>(this);
|
ContentManager = NewObject<UVoxelContentManager>(this);
|
||||||
ContentManager->Initialize(this, StrateManager, Settings->Seed);
|
ContentManager->Initialize(this, StrateManager, Generator, Settings->Seed);
|
||||||
|
|
||||||
// Atmosphere manager — per-strate fog + ambient + persistent ceiling/floor layers.
|
// Atmosphere manager — per-strate fog + ambient + persistent ceiling/floor layers.
|
||||||
if (bManageAtmosphere && StrateManager)
|
if (bManageAtmosphere && StrateManager)
|
||||||
{
|
{
|
||||||
AtmosphereManager = NewObject<UVoxelAtmosphereManager>(this);
|
AtmosphereManager = NewObject<UVoxelAtmosphereManager>(this);
|
||||||
AtmosphereManager->Initialize(this, StrateManager);
|
AtmosphereManager->Initialize(this, StrateManager, Generator);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
@@ -270,6 +277,11 @@ void AVoxelWorld::Tick(float DeltaTime)
|
|||||||
{
|
{
|
||||||
AtmosphereManager->UpdateForPlayer(PlayerLastPos);
|
AtmosphereManager->UpdateForPlayer(PlayerLastPos);
|
||||||
}
|
}
|
||||||
|
if (ContentManager)
|
||||||
|
{
|
||||||
|
// Strate light culling — no-op unless the player changed strate.
|
||||||
|
ContentManager->SetActiveStrate(GetStrateAtPosition(PlayerLastPos));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ProcessPendingChunks();
|
ProcessPendingChunks();
|
||||||
|
|
||||||
@@ -595,7 +607,10 @@ void AVoxelWorld::LoadChunk(const FIntVector& ChunkCoord)
|
|||||||
Result.Chunk = Chunk;
|
Result.Chunk = Chunk;
|
||||||
Result.LODLevel = LODLevel;
|
Result.LODLevel = LODLevel;
|
||||||
Result.Epoch = TaskEpoch;
|
Result.Epoch = TaskEpoch;
|
||||||
|
{
|
||||||
|
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GenerateMesh);
|
||||||
Result.MeshData = Mesher->GenerateMesh(Chunk, Step);
|
Result.MeshData = Mesher->GenerateMesh(Chunk, Step);
|
||||||
|
}
|
||||||
|
|
||||||
if (!bShuttingDown.load(std::memory_order_relaxed))
|
if (!bShuttingDown.load(std::memory_order_relaxed))
|
||||||
{
|
{
|
||||||
@@ -622,6 +637,8 @@ void AVoxelWorld::UnloadChunk(const FIntVector& ChunkCoord)
|
|||||||
|
|
||||||
void AVoxelWorld::ApplyMeshToChunk(const FIntVector& ChunkCoord, const FVoxelMeshData& MeshData)
|
void AVoxelWorld::ApplyMeshToChunk(const FIntVector& ChunkCoord, const FVoxelMeshData& MeshData)
|
||||||
{
|
{
|
||||||
|
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ApplyMeshToChunk);
|
||||||
|
|
||||||
//==========================================================================
|
//==========================================================================
|
||||||
// STEP 1: EARLY EXIT IF NO MESH DATA
|
// STEP 1: EARLY EXIT IF NO MESH DATA
|
||||||
//==========================================================================
|
//==========================================================================
|
||||||
@@ -652,6 +669,17 @@ void AVoxelWorld::ApplyMeshToChunk(const FIntVector& ChunkCoord, const FVoxelMes
|
|||||||
// 'this' (AVoxelWorld) is the "outer" - it owns this component
|
// 'this' (AVoxelWorld) is the "outer" - it owns this component
|
||||||
MeshComp = NewObject<URealtimeMeshComponent>(this);
|
MeshComp = NewObject<URealtimeMeshComponent>(this);
|
||||||
|
|
||||||
|
// 40k+ chunk components hammer the GAME THREAD, not the GPU. Kill per-component
|
||||||
|
// bookkeeping the engine would otherwise do every frame for each of them:
|
||||||
|
// - overlap events: chunks never use overlap callbacks (gameplay uses raycasts
|
||||||
|
// against the LOD0 collision), and UpdateOverlaps over tens of thousands of
|
||||||
|
// components is a classic game-thread sink.
|
||||||
|
// - navigation: terrain isn't navmesh-driven here.
|
||||||
|
// (The real fix for component COUNT is the chunk-LOD clipmap; this trims the
|
||||||
|
// per-component cost meanwhile and survives that refactor.)
|
||||||
|
MeshComp->SetGenerateOverlapEvents(false);
|
||||||
|
MeshComp->SetCanEverAffectNavigation(false);
|
||||||
|
|
||||||
// RegisterComponent() tells Unreal "this component is ready to use"
|
// RegisterComponent() tells Unreal "this component is ready to use"
|
||||||
// Without this, the component won't tick, render, or do anything
|
// Without this, the component won't tick, render, or do anything
|
||||||
MeshComp->RegisterComponent();
|
MeshComp->RegisterComponent();
|
||||||
@@ -789,7 +817,21 @@ void AVoxelWorld::ApplyMeshToChunk(const FIntVector& ChunkCoord, const FVoxelMes
|
|||||||
}
|
}
|
||||||
|
|
||||||
RTMesh->SetupMaterialSlot(0, "Main", ChunkMaterial);
|
RTMesh->SetupMaterialSlot(0, "Main", ChunkMaterial);
|
||||||
RTMesh->UpdateSectionConfig(SectionKey, SectionConfig, true);
|
|
||||||
|
// Collision only at LOD0. Distant chunks are unreachable by definition (the LOD
|
||||||
|
// reconciliation loop §8.10 hot-swaps a chunk to LOD0 before the player can touch it),
|
||||||
|
// so cooking Chaos tri-mesh collision for LOD1/2 is pure waste — kills the cook cost +
|
||||||
|
// collision memory for the large majority of loaded chunks. (T1.c)
|
||||||
|
const int32 ChunkLOD = ChunkLODs.FindRef(ChunkCoord);
|
||||||
|
const bool bWantCollision = (ChunkLOD == 0);
|
||||||
|
RTMesh->UpdateSectionConfig(SectionKey, SectionConfig, bWantCollision);
|
||||||
|
|
||||||
|
// Shadow casting: the FARTHEST tier (LOD2) does NOT cast shadows. Each shadow-casting
|
||||||
|
// chunk adds a SECOND draw call in the shadow-depth pass (~doubles the draw cost of the
|
||||||
|
// terrain), and crisp shadows on distant blocky LOD2 geometry aren't worth it. LOD0/1
|
||||||
|
// keep shadows. Conservative first cut — widen to `<= 0` (LOD1 too) if still draw-bound.
|
||||||
|
// Re-applied every time because LOD hot-swaps reuse the same component.
|
||||||
|
MeshComp->SetCastShadow(ChunkLOD <= 1);
|
||||||
|
|
||||||
//==========================================================================
|
//==========================================================================
|
||||||
// STEP 11: POPULATE CONTENT (decorations + water)
|
// STEP 11: POPULATE CONTENT (decorations + water)
|
||||||
@@ -925,6 +967,118 @@ void AVoxelWorld::EditorFillSphere()
|
|||||||
FillAtPosition(EditorBrushCenter, EditorBrushRadius, EditorBrushStrength);
|
FillAtPosition(EditorBrushCenter, EditorBrushRadius, EditorBrushStrength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// BIOME MAP PREVIEW — bake the XY biome field to Saved/BiomePreview.png
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
void AVoxelWorld::BakeBiomePreview()
|
||||||
|
{
|
||||||
|
if (!BiomePreviewStrate)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("[VoxelWorld] BakeBiomePreview: assign BiomePreviewStrate first."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (BiomePreviewChannel == EBiomePreviewChannel::Biome && BiomePreviewStrate->Biomes.Num() == 0)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("[VoxelWorld] BakeBiomePreview: '%s' has no Biomes — bake Relief/Moisture instead, or add biomes."),
|
||||||
|
*BiomePreviewStrate->GetName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transient generator so this works in the editor without PIE.
|
||||||
|
UVoxelGenerator* Gen = NewObject<UVoxelGenerator>(this);
|
||||||
|
Gen->Seed = Settings ? Settings->Seed : 0;
|
||||||
|
|
||||||
|
// Flatten the strate's biomes into a context (mirrors StrateManager::GetBiomeContextForChunk).
|
||||||
|
FBiomeContext Ctx;
|
||||||
|
Ctx.Map = BiomePreviewStrate->BiomeMapParams;
|
||||||
|
for (int32 i = 0; i < BiomePreviewStrate->Biomes.Num(); ++i)
|
||||||
|
{
|
||||||
|
const UVoxelBiomeDefinition* B = BiomePreviewStrate->Biomes[i];
|
||||||
|
if (!B) continue;
|
||||||
|
FBiomeResolved R;
|
||||||
|
R.Index = i;
|
||||||
|
R.ReliefMin = B->ReliefMin; R.ReliefMax = B->ReliefMax;
|
||||||
|
R.MoistureMin = B->MoistureMin; R.MoistureMax = B->MoistureMax;
|
||||||
|
R.DebugColor = B->DebugColor.ToFColor(true);
|
||||||
|
Ctx.Biomes.Add(R);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 Res = FMath::Clamp(BiomePreviewResolution, 64, 2048);
|
||||||
|
const float Size = FMath::Max(BiomePreviewWorldSize, 1.0f);
|
||||||
|
const float Step = Size / (float)Res;
|
||||||
|
const float OriginX = BiomePreviewCenter.X - Size * 0.5f;
|
||||||
|
const float OriginY = BiomePreviewCenter.Y - Size * 0.5f;
|
||||||
|
|
||||||
|
TArray<FColor> Pixels;
|
||||||
|
Pixels.SetNumUninitialized(Res * Res);
|
||||||
|
|
||||||
|
for (int32 py = 0; py < Res; ++py)
|
||||||
|
for (int32 px = 0; px < Res; ++px)
|
||||||
|
{
|
||||||
|
const float wx = OriginX + (px + 0.5f) * Step;
|
||||||
|
const float wy = OriginY + (py + 0.5f) * Step;
|
||||||
|
|
||||||
|
FColor C = FColor::Black;
|
||||||
|
switch (BiomePreviewChannel)
|
||||||
|
{
|
||||||
|
case EBiomePreviewChannel::Relief:
|
||||||
|
{
|
||||||
|
const float r = Gen->SampleRelief(wx, wy, Ctx.Map.ReliefFrequency, Ctx.Map.ReliefContrast);
|
||||||
|
const uint8 v = (uint8)FMath::Clamp(r * 255.0f, 0.0f, 255.0f);
|
||||||
|
C = FColor(v, v, v);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EBiomePreviewChannel::Moisture:
|
||||||
|
{
|
||||||
|
const float m = Gen->SampleMoisture(wx, wy, Ctx.Map.MoistureFrequency);
|
||||||
|
const uint8 v = (uint8)FMath::Clamp(m * 255.0f, 0.0f, 255.0f);
|
||||||
|
C = FColor(0, v, (uint8)(255 - v)); // dry=blue → wet=green
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: // Biome
|
||||||
|
{
|
||||||
|
const FBiomeSample S = Gen->SampleBiomeAt(wx, wy, Ctx);
|
||||||
|
if (Ctx.Biomes.IsValidIndex(S.DominantIndex))
|
||||||
|
{
|
||||||
|
C = Ctx.Biomes[S.DominantIndex].DebugColor;
|
||||||
|
if (S.NeighborWeight > 0.0f && Ctx.Biomes.IsValidIndex(S.NeighborIndex))
|
||||||
|
{
|
||||||
|
const FLinearColor A(C);
|
||||||
|
const FLinearColor Bn(Ctx.Biomes[S.NeighborIndex].DebugColor);
|
||||||
|
C = FLinearColor::LerpUsingHSV(A, Bn, S.NeighborWeight).ToFColor(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
C.A = 255;
|
||||||
|
Pixels[py * Res + px] = C;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode PNG and write to Saved/.
|
||||||
|
IImageWrapperModule& Module = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
|
||||||
|
const TSharedPtr<IImageWrapper> Wrapper = Module.CreateImageWrapper(EImageFormat::PNG);
|
||||||
|
if (!Wrapper.IsValid() ||
|
||||||
|
!Wrapper->SetRaw(Pixels.GetData(), (int64)Pixels.Num() * sizeof(FColor), Res, Res, ERGBFormat::BGRA, 8))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] BakeBiomePreview: failed to encode image."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TArray64<uint8>& Png = Wrapper->GetCompressed(100);
|
||||||
|
const FString Path = FPaths::ProjectSavedDir() / TEXT("BiomePreview.png");
|
||||||
|
if (FFileHelper::SaveArrayToFile(Png, *Path))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("[VoxelWorld] Biome preview (%dx%d, %s) saved to %s"),
|
||||||
|
Res, Res, *UEnum::GetValueAsString(BiomePreviewChannel), *FPaths::ConvertRelativePathToFull(Path));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] BakeBiomePreview: failed to write %s"), *Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void AVoxelWorld::ClearAllModifications()
|
void AVoxelWorld::ClearAllModifications()
|
||||||
{
|
{
|
||||||
if (!DiffLayer) return;
|
if (!DiffLayer) return;
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
class UVoxelStrateManager;
|
class UVoxelStrateManager;
|
||||||
class UVoxelStrateDefinition;
|
class UVoxelStrateDefinition;
|
||||||
|
class UVoxelBiomeDefinition;
|
||||||
|
class UVoxelGenerator;
|
||||||
class UExponentialHeightFogComponent;
|
class UExponentialHeightFogComponent;
|
||||||
class USkyLightComponent;
|
class USkyLightComponent;
|
||||||
|
|
||||||
@@ -23,23 +25,31 @@ class VOXELFORGE_API UVoxelAtmosphereManager : public UObject
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/** Create the managed fog + skylight components on the owner actor. */
|
/** Create the managed fog + skylight components on the owner actor. Generator supplies
|
||||||
void Initialize(AActor* InOwner, UVoxelStrateManager* InStrateManager);
|
* the dominant-biome query so atmosphere can vary by biome within a strate. */
|
||||||
|
void Initialize(AActor* InOwner, UVoxelStrateManager* InStrateManager, UVoxelGenerator* InGenerator);
|
||||||
|
|
||||||
/** Call each frame with the player's world position. Cheap: only reacts on strate change. */
|
/** Call each frame with the player's world position. Cheap: only reacts on strate OR
|
||||||
|
* dominant-biome change. */
|
||||||
void UpdateForPlayer(const FVector& PlayerWorldPos);
|
void UpdateForPlayer(const FVector& PlayerWorldPos);
|
||||||
|
|
||||||
/** Tear down spawned layer actors + reset (season reset / shutdown). */
|
/** Tear down spawned layer actors + reset (season reset / shutdown). */
|
||||||
void Reset();
|
void Reset();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void ApplyStrate(const UVoxelStrateDefinition* Def);
|
// Full strate apply: layer actors + atmosphere BP + fog/sky. Biome retints fog/sky.
|
||||||
|
void ApplyStrate(const UVoxelStrateDefinition* Def, const UVoxelBiomeDefinition* Biome);
|
||||||
|
// Just the managed fog + skylight (biome override beats strate when set).
|
||||||
|
void ApplyFogSky(const UVoxelStrateDefinition* Def, const UVoxelBiomeDefinition* Biome);
|
||||||
|
|
||||||
TWeakObjectPtr<AActor> Owner;
|
TWeakObjectPtr<AActor> Owner;
|
||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
UVoxelStrateManager* StrateManager = nullptr;
|
UVoxelStrateManager* StrateManager = nullptr;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
UVoxelGenerator* Generator = nullptr;
|
||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
UExponentialHeightFogComponent* Fog = nullptr;
|
UExponentialHeightFogComponent* Fog = nullptr;
|
||||||
|
|
||||||
@@ -59,4 +69,7 @@ private:
|
|||||||
|
|
||||||
// Which strate's atmosphere is currently applied (INT32_MIN = none yet).
|
// Which strate's atmosphere is currently applied (INT32_MIN = none yet).
|
||||||
int32 CurrentStrateIndex = INT32_MIN;
|
int32 CurrentStrateIndex = INT32_MIN;
|
||||||
|
|
||||||
|
// Dominant biome currently driving fog/sky (identity token for change detection).
|
||||||
|
TWeakObjectPtr<const UVoxelBiomeDefinition> CurrentBiome;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,22 +6,39 @@
|
|||||||
// - PopulateChunk(coord, meshdata) after a chunk's mesh is applied
|
// - PopulateChunk(coord, meshdata) after a chunk's mesh is applied
|
||||||
// - ClearChunk(coord) when a chunk unloads / is re-meshed
|
// - ClearChunk(coord) when a chunk unloads / is re-meshed
|
||||||
// - ClearAll() on regenerate / season reset
|
// - ClearAll() on regenerate / season reset
|
||||||
|
// - SetActiveStrate(playerStrate) each Tick (no-op unless the strate changed)
|
||||||
//
|
//
|
||||||
// DETERMINISM: every placement decision is a pure hash of (chunk, surface index,
|
// DETERMINISM: every placement decision is a pure hash of (chunk, surface index,
|
||||||
// entry index, seed), so the same world re-populates identically. Spawning itself
|
// entry index, seed), so the same world re-populates identically. Spawning itself
|
||||||
// must run on the game thread (UWorld::SpawnActor), which it does — ApplyMeshToChunk
|
// must run on the game thread (UWorld::SpawnActor), which it does — ApplyMeshToChunk
|
||||||
// is game-thread.
|
// is game-thread.
|
||||||
|
//
|
||||||
|
// RENDERING PATHS per decoration entry (FStrateDecoration):
|
||||||
|
// - ActorClass → real actors (lights, logic, interaction). Keep MaxLODLevel=0.
|
||||||
|
// - InstancedMesh → HISM instances batched per chunk: no tick, no per-actor cost,
|
||||||
|
// engine-culled. Safe to allow at LOD 1-2 so visual props don't
|
||||||
|
// pop out with LOD0 (emissive materials still glow at distance).
|
||||||
|
//
|
||||||
|
// STRATE LIGHT CULLING: a light in another strate can never legitimately reach the
|
||||||
|
// player (seals + bedrock gap are solid), but a shadowless light BLEEDS through rock
|
||||||
|
// and a shadowed one pays full cost to render black. So light components on spawned
|
||||||
|
// decoration actors are only visible while the player is in the SAME strate — the
|
||||||
|
// analytical form of occlusion culling, computed once per strate change.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "VoxelTypes.h"
|
#include "VoxelTypes.h"
|
||||||
|
#include "VoxelStrateTypes.h" // FStrateDecoration (resolved per dominant biome)
|
||||||
#include "VoxelContentManager.generated.h"
|
#include "VoxelContentManager.generated.h"
|
||||||
|
|
||||||
class UVoxelStrateManager;
|
class UVoxelStrateManager;
|
||||||
class UVoxelStrateDefinition;
|
class UVoxelStrateDefinition;
|
||||||
|
class UVoxelGenerator;
|
||||||
class UStaticMesh;
|
class UStaticMesh;
|
||||||
class UStaticMeshComponent;
|
class UStaticMeshComponent;
|
||||||
|
class UHierarchicalInstancedStaticMeshComponent;
|
||||||
|
class UMaterialInterface;
|
||||||
|
|
||||||
UCLASS()
|
UCLASS()
|
||||||
class VOXELFORGE_API UVoxelContentManager : public UObject
|
class VOXELFORGE_API UVoxelContentManager : public UObject
|
||||||
@@ -29,34 +46,55 @@ class VOXELFORGE_API UVoxelContentManager : public UObject
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/** Wire up services. Owner is the AVoxelWorld actor that owns spawned content. */
|
/** Wire up services. Owner is the AVoxelWorld actor that owns spawned content.
|
||||||
void Initialize(AActor* InOwner, UVoxelStrateManager* InStrateManager, int32 InSeed);
|
* Generator supplies the dominant-biome query for per-biome content selection. */
|
||||||
|
void Initialize(AActor* InOwner, UVoxelStrateManager* InStrateManager,
|
||||||
|
UVoxelGenerator* InGenerator, int32 InSeed);
|
||||||
|
|
||||||
/** Update the seed used for placement hashing (season reset). */
|
/** Update the seed used for placement hashing (season reset). */
|
||||||
void SetSeed(int32 InSeed) { Seed = InSeed; }
|
void SetSeed(int32 InSeed) { Seed = InSeed; }
|
||||||
|
|
||||||
/** Populate decorations + water for a chunk. Clears any previous content first.
|
/** 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. */
|
* Each decoration entry spawns only while LOD <= its MaxLODLevel; water at any LOD. */
|
||||||
void PopulateChunk(const FIntVector& ChunkCoord, const FVoxelMeshData& MeshData, int32 LODLevel = 0);
|
void PopulateChunk(const FIntVector& ChunkCoord, const FVoxelMeshData& MeshData, int32 LODLevel = 0);
|
||||||
|
|
||||||
/** Destroy all spawned content (actors + water plane) for one chunk. */
|
/** Destroy all spawned content (actors + instances + water plane) for one chunk. */
|
||||||
void ClearChunk(const FIntVector& ChunkCoord);
|
void ClearChunk(const FIntVector& ChunkCoord);
|
||||||
|
|
||||||
/** Destroy all spawned content for every chunk. */
|
/** Destroy all spawned content for every chunk. */
|
||||||
void ClearAll();
|
void ClearAll();
|
||||||
|
|
||||||
|
/** Player strate changed → toggle decoration lights (strict: lights only live in the
|
||||||
|
* player's strate). Cheap no-op while the strate stays the same — call every Tick. */
|
||||||
|
void SetActiveStrate(int32 PlayerStrateIndex);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void SpawnDecorations(const FIntVector& ChunkCoord, const FVoxelMeshData& MeshData,
|
void SpawnDecorations(const FIntVector& ChunkCoord, const FVoxelMeshData& MeshData,
|
||||||
const UVoxelStrateDefinition* Def, TArray<TWeakObjectPtr<AActor>>& Out);
|
const TArray<FStrateDecoration>& Decorations, int32 LODLevel,
|
||||||
void SpawnWater(const FIntVector& ChunkCoord, const UVoxelStrateDefinition* Def);
|
TArray<TWeakObjectPtr<AActor>>& Out);
|
||||||
|
void SpawnWater(const FIntVector& ChunkCoord, const UVoxelStrateDefinition* Def,
|
||||||
|
UMaterialInterface* WaterMaterial);
|
||||||
|
|
||||||
|
/** Strate index a chunk belongs to (center-Z lookup). */
|
||||||
|
int32 GetChunkStrateIndex(const FIntVector& ChunkCoord) const;
|
||||||
|
|
||||||
|
/** Show/hide every light component on a decoration actor. */
|
||||||
|
static void SetActorLightsEnabled(AActor* Actor, bool bEnabled);
|
||||||
|
|
||||||
TWeakObjectPtr<AActor> Owner;
|
TWeakObjectPtr<AActor> Owner;
|
||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
UVoxelStrateManager* StrateManager = nullptr;
|
UVoxelStrateManager* StrateManager = nullptr;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
UVoxelGenerator* Generator = nullptr;
|
||||||
|
|
||||||
int32 Seed = 0;
|
int32 Seed = 0;
|
||||||
|
|
||||||
|
// Player's current strate (INT32_MIN until first SetActiveStrate). Lights on spawned
|
||||||
|
// actors are enabled iff their chunk's strate matches this.
|
||||||
|
int32 ActiveStrateIndex = INT32_MIN;
|
||||||
|
|
||||||
// Engine unit plane (/Engine/BasicShapes/Plane) reused for every water surface.
|
// Engine unit plane (/Engine/BasicShapes/Plane) reused for every water surface.
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
UStaticMesh* PlaneMesh = nullptr;
|
UStaticMesh* PlaneMesh = nullptr;
|
||||||
@@ -64,6 +102,9 @@ private:
|
|||||||
// Spawned decoration/ambient actors per chunk (weak — they live in the level).
|
// Spawned decoration/ambient actors per chunk (weak — they live in the level).
|
||||||
TMap<FIntVector, TArray<TWeakObjectPtr<AActor>>> SpawnedActors;
|
TMap<FIntVector, TArray<TWeakObjectPtr<AActor>>> SpawnedActors;
|
||||||
|
|
||||||
|
// HISM components per chunk (weak — registered components are owned by the actor).
|
||||||
|
TMap<FIntVector, TArray<TWeakObjectPtr<UHierarchicalInstancedStaticMeshComponent>>> ChunkInstances;
|
||||||
|
|
||||||
// Water surface component per chunk.
|
// Water surface component per chunk.
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TMap<FIntVector, UStaticMeshComponent*> WaterPlanes;
|
TMap<FIntVector, UStaticMeshComponent*> WaterPlanes;
|
||||||
|
|||||||
@@ -11,12 +11,14 @@
|
|||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "VoxelTypes.h"
|
#include "VoxelTypes.h"
|
||||||
#include "VoxelStrateTypes.h"
|
#include "VoxelStrateTypes.h"
|
||||||
|
#include "VoxelBiomeTypes.h"
|
||||||
#include "VoxelGenerator.generated.h"
|
#include "VoxelGenerator.generated.h"
|
||||||
|
|
||||||
// Forward decls (évite les includes transitifs)
|
// Forward decls (évite les includes transitifs)
|
||||||
class UVoxelSettings;
|
class UVoxelSettings;
|
||||||
class UVoxelStrateManager;
|
class UVoxelStrateManager;
|
||||||
class UVoxelDiffLayer;
|
class UVoxelDiffLayer;
|
||||||
|
class UVoxelBiomeDefinition;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UVoxelGenerator
|
* UVoxelGenerator
|
||||||
@@ -103,9 +105,18 @@ public:
|
|||||||
/**
|
/**
|
||||||
* Densité pour une strate SurfaceWorld — terrain à ciel ouvert (collines,
|
* Densité pour une strate SurfaceWorld — terrain à ciel ouvert (collines,
|
||||||
* montagnes, plages) sous un plafond solide, avec nappe d'eau optionnelle.
|
* montagnes, plages) sous un plafond solide, avec nappe d'eau optionnelle.
|
||||||
|
*
|
||||||
|
* Biome output-blend: the heightfield is evaluated with ParamsD (the voxel's dominant
|
||||||
|
* biome) and, when NeighborWeight > 0, also with ParamsN (its nearest neighbour); the
|
||||||
|
* two surface HEIGHTS are lerped. This stays seamless across ANY param difference
|
||||||
|
* (frequencies included). With no biomes, pass ParamsD == ParamsN and weight 0 →
|
||||||
|
* bit-identical to the single-param terrain. Structural fields (Z bounds, seal, base,
|
||||||
|
* water level) must be equal in both (forced from the strate).
|
||||||
*/
|
*/
|
||||||
float GetSurfaceDensity(float WorldX, float WorldY, float WorldZ,
|
float GetSurfaceDensity(float WorldX, float WorldY, float WorldZ,
|
||||||
const FSurfaceGenerationParams& Params) const;
|
const FSurfaceGenerationParams& ParamsD,
|
||||||
|
const FSurfaceGenerationParams& ParamsN,
|
||||||
|
float NeighborWeight) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Densité pour une strate VerticalShafts — puits verticaux pleine hauteur,
|
* Densité pour une strate VerticalShafts — puits verticaux pleine hauteur,
|
||||||
@@ -120,4 +131,67 @@ public:
|
|||||||
*/
|
*/
|
||||||
float GetFloatingIslandDensity(float WorldX, float WorldY, float WorldZ,
|
float GetFloatingIslandDensity(float WorldX, float WorldY, float WorldZ,
|
||||||
const FFloatingIslandParams& Params) const;
|
const FFloatingIslandParams& Params) const;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// CLIMATE & BIOME FIELDS (pure XY, deterministic, window-invariant)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relief ("elevation") field at a world XY → [0,1], contrast-shaped & smoothed.
|
||||||
|
* The single source of truth for the relief map M: SurfaceWorld terrain and the
|
||||||
|
* biome climate map both call this, so they agree when given the same freq/contrast.
|
||||||
|
*/
|
||||||
|
float SampleRelief(float WorldX, float WorldY, float Frequency, float Contrast) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moisture field at a world XY → [0,1]. The second climate axis for biome placement.
|
||||||
|
*/
|
||||||
|
float SampleMoisture(float WorldX, float WorldY, float Frequency) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the biome at a world XY: dominant cell + nearest neighbour + blend weight.
|
||||||
|
* Warped Voronoi over a jittered grid; each cell's biome is chosen by its site's
|
||||||
|
* (relief, moisture) against the context's climate boxes. PURE function of (XY, seed,
|
||||||
|
* Ctx) — no chunk-window dependence (CODEMAP §8.4). Returns an empty sample when the
|
||||||
|
* context has no biomes. Used by the 2D preview bake and (next) the density path.
|
||||||
|
*/
|
||||||
|
FBiomeSample SampleBiomeAt(float WorldX, float WorldY, const FBiomeContext& Ctx) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hot-path biome resolve: the dominant + neighbour biome and blend weight at a world
|
||||||
|
* XY for the given strate slice. Uses (and lazily rebuilds) Cache — a box-validated
|
||||||
|
* per-chunk grid — so the noise-heavy cell classification happens once per chunk, not
|
||||||
|
* per voxel. Bit-identical to SampleBiomeAt, so the baked preview matches the terrain.
|
||||||
|
*/
|
||||||
|
FBiomeSample ResolveBiomeSampleAt(float WorldX, float WorldY, int32 ChunkZ,
|
||||||
|
const FBiomeContext& Ctx, FChunkBiomeCache& Cache) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dominant biome ASSET at a world XY for the given strate slice (chunk Z), or nullptr
|
||||||
|
* when the strate has no biomes / the point is outside the strate range. Game-thread
|
||||||
|
* helper for content + atmosphere selection — uses the same field as the density path.
|
||||||
|
*/
|
||||||
|
const UVoxelBiomeDefinition* GetDominantBiomeAt(float WorldX, float WorldY, int32 ChunkZ) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
/** Pick the biome (index into Ctx.Biomes) for a Voronoi site, by its climate. */
|
||||||
|
int32 ClassifyBiomeAtSite(float SiteX, float SiteY, const FBiomeContext& Ctx, uint32 SiteHash) const;
|
||||||
|
|
||||||
|
/** The SurfaceWorld heightfield: world XY → terrain surface Z (voxel coords). Pure
|
||||||
|
* per-XY; the part that's evaluated per biome and blended in GetSurfaceDensity. */
|
||||||
|
float ComputeSurfaceTerrainZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const;
|
||||||
|
|
||||||
|
/** The SurfaceWorld sky-cap ceiling surface Z at a world XY (also pure per-XY). */
|
||||||
|
float ComputeSurfaceCeiling(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const;
|
||||||
|
|
||||||
|
/** Final SurfaceWorld density from a column's precomputed terrain Z + ceiling: the
|
||||||
|
* cheap per-voxel Z-combine + origin spine + boundary seal + passage carving. The
|
||||||
|
* XY-only work (terrain/ceiling) is done once per column and cached (T1.a). */
|
||||||
|
float SurfaceDensityFromColumn(float WorldX, float WorldY, float WorldZ,
|
||||||
|
float TerrainZ, float CeilSurf,
|
||||||
|
const FSurfaceGenerationParams& Structural) const;
|
||||||
|
|
||||||
|
/** (Re)build the per-chunk biome cell grid covering chunk (X,Y) footprint + margin. */
|
||||||
|
void RebuildBiomeGrid(int32 ChunkX, int32 ChunkY, int32 ChunkZ,
|
||||||
|
const FBiomeContext& Ctx, FChunkBiomeCache& Cache) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public:
|
|||||||
// STREAMING (distance de vue)
|
// STREAMING (distance de vue)
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
|
|
||||||
|
// En CHUNKS (CHUNK_SIZE=32). Couverture linéaire = ViewDistanceXY × 32 × 25 cm.
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
|
||||||
int32 ViewDistanceXY = 16;
|
int32 ViewDistanceXY = 16;
|
||||||
|
|
||||||
@@ -35,7 +36,10 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
|
||||||
int32 MaxConcurrentTasks = 16;
|
int32 MaxConcurrentTasks = 16;
|
||||||
|
|
||||||
// Nombre max de meshes appliqués par frame (évite les stutters d'upload GPU).
|
// Nombre max de meshes appliqués (upload GPU) par frame. Seuls les vrais applies
|
||||||
|
// comptent (chunks vides/périmés se vident gratuitement). À 32³ chaque apply est léger
|
||||||
|
// (~0.1 ms) — tunable en live sur l'asset : montez (8-16) si le remplissage traîne,
|
||||||
|
// baissez si l'apparition des chunks fait des à-coups.
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
|
||||||
int32 MaxMeshAppliesPerFrame = 4;
|
int32 MaxMeshAppliesPerFrame = 4;
|
||||||
|
|
||||||
@@ -47,8 +51,10 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|LOD")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|LOD")
|
||||||
int32 LOD0Distance = 4;
|
int32 LOD0Distance = 4;
|
||||||
|
|
||||||
// Distance en chunks pour LOD1 (demi-résolution, step=2).
|
// Distance en chunks pour LOD1 (demi-résolution, step=2). Au-delà → LOD2 (quart-rés,
|
||||||
// Au-delà → LOD2 (quart-résolution, step=4).
|
// step=4). LOD2 = le plus lointain ; ces chunks ne projettent PLUS d'ombre (cf.
|
||||||
|
// ApplyMeshToChunk) → rapprocher LOD0/LOD1 pousse plus de chunks dans la bande
|
||||||
|
// LOD2 sans-ombre = moins de draws (levier fps gratuit, à doser visuellement).
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|LOD")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|LOD")
|
||||||
int32 LOD1Distance = 8;
|
int32 LOD1Distance = 8;
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,11 @@
|
|||||||
#include "Engine/DataAsset.h"
|
#include "Engine/DataAsset.h"
|
||||||
#include "GameplayTagContainer.h"
|
#include "GameplayTagContainer.h"
|
||||||
#include "VoxelStrateTypes.h"
|
#include "VoxelStrateTypes.h"
|
||||||
|
#include "VoxelBiomeTypes.h"
|
||||||
#include "VoxelStrateDefinition.generated.h"
|
#include "VoxelStrateDefinition.generated.h"
|
||||||
|
|
||||||
|
class UVoxelBiomeDefinition;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UVoxelStrateDefinition — The content bag for a strate type.
|
* UVoxelStrateDefinition — The content bag for a strate type.
|
||||||
*
|
*
|
||||||
@@ -147,6 +150,24 @@ public:
|
|||||||
meta = (EditCondition = "GeneratorType == ECaveGeneratorType::FloatingIslands"))
|
meta = (EditCondition = "GeneratorType == ECaveGeneratorType::FloatingIslands"))
|
||||||
FFloatingIslandParams FloatingIslandParams;
|
FFloatingIslandParams FloatingIslandParams;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// BIOMES (vary terrain & content WITHIN this strate — any archetype)
|
||||||
|
//=========================================================================
|
||||||
|
// Optional. When empty, this strate generates exactly as before (no biome field,
|
||||||
|
// bit-identical output). When populated, a deterministic world-XY biome field
|
||||||
|
// (warped Voronoi + climate, see FBiomeMapParams) assigns regions; each biome can
|
||||||
|
// supply its OWN archetype params (a mini-strate-variant, output-blended for surface)
|
||||||
|
// plus its own decorations / atmosphere / water. Surface terrain wired today;
|
||||||
|
// content/atmosphere work for any archetype.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Biomes")
|
||||||
|
TArray<UVoxelBiomeDefinition*> Biomes;
|
||||||
|
|
||||||
|
// World-XY biome field tuning (cell size, border warp/blend, climate fields).
|
||||||
|
// Only relevant when Biomes is non-empty. Bake AVoxelWorld::BakeBiomePreview to
|
||||||
|
// see the resulting map before regenerating the world.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Biomes")
|
||||||
|
FBiomeMapParams BiomeMapParams;
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// DISTURBANCES (the "wow" layer — applies on top of ANY archetype)
|
// DISTURBANCES (the "wow" layer — applies on top of ANY archetype)
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
|
|||||||
@@ -227,6 +227,14 @@ public:
|
|||||||
FVerticalShaftParams GetVerticalShaftParamsForChunk(const FIntVector& ChunkCoord) const;
|
FVerticalShaftParams GetVerticalShaftParamsForChunk(const FIntVector& ChunkCoord) const;
|
||||||
FFloatingIslandParams GetFloatingIslandParamsForChunk(const FIntVector& ChunkCoord) const;
|
FFloatingIslandParams GetFloatingIslandParamsForChunk(const FIntVector& ChunkCoord) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flatten the strate's Biomes[] + BiomeMapParams into a POD FBiomeContext for the
|
||||||
|
* biome field. Returns an empty (invalid) context when the strate has no biomes —
|
||||||
|
* callers treat that as "biomes disabled" and fall back to base params. The result
|
||||||
|
* is window-invariant (depends only on the strate at this Z, not the chunk window).
|
||||||
|
*/
|
||||||
|
FBiomeContext GetBiomeContextForChunk(const FIntVector& ChunkCoord) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* World-space Z (voxel coords) of this strate's water surface, or -FLT_MAX if the
|
* 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
|
* strate has no water. Derived from the active archetype's WaterLevelRelative and
|
||||||
|
|||||||
@@ -304,6 +304,16 @@ struct VOXELFORGE_API FStrateGenerationParams
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Worm Tunnels")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Worm Tunnels")
|
||||||
float WormStrength = 10.0f;
|
float WormStrength = 10.0f;
|
||||||
|
|
||||||
|
// Worms only carve within this distance (voxels) of the room/tunnel network, fading
|
||||||
|
// smoothly to zero at the edge. Keeps worms as organic braids and shortcuts that HUG
|
||||||
|
// the cave system instead of spraying disconnected noise pockets through the whole
|
||||||
|
// strate (the far-field "confetti"). 0 = unlimited (legacy unmasked behaviour).
|
||||||
|
// 16 → tight braiding right along rooms/tunnels
|
||||||
|
// 24 → braids + short noodle shortcuts (good default)
|
||||||
|
// 48+ → loose, wandering side-passages
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Worm Tunnels", meta = (ClampMin = "0.0"))
|
||||||
|
float WormNetworkRange = 24.0f;
|
||||||
|
|
||||||
// ===== CAVE MORPHOLOGY (room-and-corridor) =====
|
// ===== CAVE MORPHOLOGY (room-and-corridor) =====
|
||||||
//
|
//
|
||||||
// Cave shape is defined by SDF (Signed Distance Field) primitives:
|
// Cave shape is defined by SDF (Signed Distance Field) primitives:
|
||||||
@@ -489,6 +499,16 @@ struct VOXELFORGE_API FStrateGenerationParams
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
float TunnelHorizontalBias = 0.5f;
|
float TunnelHorizontalBias = 0.5f;
|
||||||
|
|
||||||
|
// Topology of the guaranteed tunnel network.
|
||||||
|
// true → each room links to its best candidate among rooms CLOSER to (0,0): the whole
|
||||||
|
// network becomes a tree rooted at the origin room — every cave is reachable
|
||||||
|
// from the spine hub, tunnels flow inward like tributaries (intentional descent
|
||||||
|
// structure). TunnelDensity still adds loops on top.
|
||||||
|
// false → legacy nearest-neighbor pairing: organic scattered clusters, but connectivity
|
||||||
|
// between clusters is NOT guaranteed (isolated pockets are common).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels")
|
||||||
|
bool bTunnelsFlowTowardOrigin = true;
|
||||||
|
|
||||||
// How much tunnel endpoints shift up/down within rooms (0-1).
|
// How much tunnel endpoints shift up/down within rooms (0-1).
|
||||||
// Fraction of the room's vertical radius. Each tunnel endpoint gets
|
// Fraction of the room's vertical radius. Each tunnel endpoint gets
|
||||||
// a hash-derived Z offset, so tunnels enter rooms at different heights.
|
// a hash-derived Z offset, so tunnels enter rooms at different heights.
|
||||||
@@ -884,6 +904,7 @@ struct VOXELFORGE_API FStrateGenerationParams
|
|||||||
Result.WormHorizontalBias = FMath::Lerp(A.WormHorizontalBias, B.WormHorizontalBias, Alpha);
|
Result.WormHorizontalBias = FMath::Lerp(A.WormHorizontalBias, B.WormHorizontalBias, Alpha);
|
||||||
Result.WormThreshold = FMath::Lerp(A.WormThreshold, B.WormThreshold, Alpha);
|
Result.WormThreshold = FMath::Lerp(A.WormThreshold, B.WormThreshold, Alpha);
|
||||||
Result.WormStrength = FMath::Lerp(A.WormStrength, B.WormStrength, Alpha);
|
Result.WormStrength = FMath::Lerp(A.WormStrength, B.WormStrength, Alpha);
|
||||||
|
Result.WormNetworkRange = FMath::Lerp(A.WormNetworkRange, B.WormNetworkRange, Alpha);
|
||||||
// Cave morphology
|
// Cave morphology
|
||||||
Result.RoomSpacing = FMath::Lerp(A.RoomSpacing, B.RoomSpacing, Alpha);
|
Result.RoomSpacing = FMath::Lerp(A.RoomSpacing, B.RoomSpacing, Alpha);
|
||||||
Result.RoomDensity = FMath::Lerp(A.RoomDensity, B.RoomDensity, Alpha);
|
Result.RoomDensity = FMath::Lerp(A.RoomDensity, B.RoomDensity, Alpha);
|
||||||
@@ -903,6 +924,7 @@ struct VOXELFORGE_API FStrateGenerationParams
|
|||||||
Result.MaxTunnelLength = FMath::Lerp(A.MaxTunnelLength, B.MaxTunnelLength, Alpha);
|
Result.MaxTunnelLength = FMath::Lerp(A.MaxTunnelLength, B.MaxTunnelLength, Alpha);
|
||||||
Result.TunnelWarpStrength = FMath::Lerp(A.TunnelWarpStrength, B.TunnelWarpStrength, Alpha);
|
Result.TunnelWarpStrength = FMath::Lerp(A.TunnelWarpStrength, B.TunnelWarpStrength, Alpha);
|
||||||
Result.TunnelHorizontalBias = FMath::Lerp(A.TunnelHorizontalBias, B.TunnelHorizontalBias, Alpha);
|
Result.TunnelHorizontalBias = FMath::Lerp(A.TunnelHorizontalBias, B.TunnelHorizontalBias, Alpha);
|
||||||
|
Result.bTunnelsFlowTowardOrigin = (Alpha < 0.5f) ? A.bTunnelsFlowTowardOrigin : B.bTunnelsFlowTowardOrigin;
|
||||||
Result.TunnelEndpointZOffset = FMath::Lerp(A.TunnelEndpointZOffset, B.TunnelEndpointZOffset, Alpha);
|
Result.TunnelEndpointZOffset = FMath::Lerp(A.TunnelEndpointZOffset, B.TunnelEndpointZOffset, Alpha);
|
||||||
Result.SDFBlendRadius = FMath::Lerp(A.SDFBlendRadius, B.SDFBlendRadius, Alpha);
|
Result.SDFBlendRadius = FMath::Lerp(A.SDFBlendRadius, B.SDFBlendRadius, Alpha);
|
||||||
Result.WaterLevelRelative = FMath::Lerp(A.WaterLevelRelative, B.WaterLevelRelative, Alpha);
|
Result.WaterLevelRelative = FMath::Lerp(A.WaterLevelRelative, B.WaterLevelRelative, Alpha);
|
||||||
@@ -976,6 +998,7 @@ struct VOXELFORGE_API FStrateGenerationParams
|
|||||||
// Forward declaration — the actual data asset lives in VoxelTerrainOpDefinition.h.
|
// Forward declaration — the actual data asset lives in VoxelTerrainOpDefinition.h.
|
||||||
// We only need a soft pointer here, so no #include needed.
|
// We only need a soft pointer here, so no #include needed.
|
||||||
class UVoxelTerrainOpDefinition;
|
class UVoxelTerrainOpDefinition;
|
||||||
|
class UStaticMesh;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FStrateTerrainOpEntry — A reference to a terrain operation with a weight.
|
* FStrateTerrainOpEntry — A reference to a terrain operation with a weight.
|
||||||
@@ -1276,6 +1299,43 @@ struct VOXELFORGE_API FSurfaceGenerationParams
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Shape", meta = (ClampMin = "0.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Shape", meta = (ClampMin = "0.0"))
|
||||||
float SurfaceRoughness = 3.0f;
|
float SurfaceRoughness = 3.0f;
|
||||||
|
|
||||||
|
// ----- Macro relief & landforms -----
|
||||||
|
|
||||||
|
// Domain-warp the heightfield query by this many voxels of XY displacement before
|
||||||
|
// sampling continents/mountains. Bends straight coastlines and ridgelines into winding,
|
||||||
|
// organic landforms. 0 = no warp (axis-aligned blobby noise, the old look).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro", meta = (ClampMin = "0.0"))
|
||||||
|
float HeightWarpStrength = 35.0f;
|
||||||
|
|
||||||
|
// Frequency of the domain-warp noise. Lower = broader, sweeping bends.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro")
|
||||||
|
float HeightWarpFrequency = 0.008f;
|
||||||
|
|
||||||
|
// Macro "relief" map frequency: a very-low-frequency field that makes some regions flat
|
||||||
|
// plains and others mountainous highlands — distinct terrain depending on where you
|
||||||
|
// stand. (The cheap, continuous precursor to a full biome system.) Lower = larger regions.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro")
|
||||||
|
float ReliefFrequency = 0.0015f;
|
||||||
|
|
||||||
|
// How strongly the relief map modulates terrain (0-1). 0 = uniform everywhere (old
|
||||||
|
// behaviour); 1 = full plains <-> mountains variation across the world.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float ReliefStrength = 0.7f;
|
||||||
|
|
||||||
|
// Contrast of the relief map. >1 sharpens the plains/highland boundary (more distinct
|
||||||
|
// regions); ~1 keeps it a gradual blend.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro", meta = (ClampMin = "0.25", ClampMax = "4.0"))
|
||||||
|
float ReliefContrast = 1.6f;
|
||||||
|
|
||||||
|
// Plateau/mesa terracing strength (0-1), applied in high-relief regions only. 0 = off
|
||||||
|
// (smooth slopes). Crank up for stepped mesas and layered cliffs in the mountainous areas.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float TerraceStrength = 0.0f;
|
||||||
|
|
||||||
|
// Height of each terrace step in voxels (when TerraceStrength > 0).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro", meta = (ClampMin = "1.0"))
|
||||||
|
float TerraceHeight = 12.0f;
|
||||||
|
|
||||||
// ----- Water -----
|
// ----- Water -----
|
||||||
|
|
||||||
// Water table height as a fraction of strate height (0 = no water). Valleys below
|
// Water table height as a fraction of strate height (0 = no water). Valleys below
|
||||||
@@ -1600,10 +1660,26 @@ struct VOXELFORGE_API FStrateDecoration
|
|||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
// The actor class to spawn (e.g., BP_Stalactite, BP_CrystalCluster)
|
// The actor class to spawn (e.g., BP_Stalactite, BP_CrystalCluster).
|
||||||
|
// Real actors: lights, logic, interaction. They cost game-thread time per instance —
|
||||||
|
// keep MaxLODLevel at 0 for these, and prefer InstancedMesh for pure visual props.
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration")
|
||||||
TSubclassOf<AActor> ActorClass;
|
TSubclassOf<AActor> ActorClass;
|
||||||
|
|
||||||
|
// INSTANCED path: if set, this entry renders as batched HISM instances instead of
|
||||||
|
// spawning ActorClass (which is then ignored). No tick, no per-actor overhead,
|
||||||
|
// engine-culled — orders of magnitude cheaper. Use for everything that doesn't need
|
||||||
|
// logic/lights/interaction; an emissive material still glows at distance without a light.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration")
|
||||||
|
UStaticMesh* InstancedMesh = nullptr;
|
||||||
|
|
||||||
|
// Spawn while the chunk's LOD <= this (0 = LOD0 only, the old behaviour).
|
||||||
|
// Lets instanced visual props persist on LOD1-2 chunks instead of popping out with
|
||||||
|
// LOD0. NOTE: placement samples the LOD's mesh vertices, so instances re-scatter
|
||||||
|
// slightly on LOD transitions (masked by the terrain's own LOD pop).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration", meta = (ClampMin = "0", ClampMax = "2"))
|
||||||
|
int32 MaxLODLevel = 0;
|
||||||
|
|
||||||
// Which surface type this decoration can be placed on
|
// Which surface type this decoration can be placed on
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration")
|
||||||
ESurfaceType SurfacePlacement = ESurfaceType::Any;
|
ESurfaceType SurfacePlacement = ESurfaceType::Any;
|
||||||
|
|||||||
@@ -14,7 +14,13 @@
|
|||||||
//
|
//
|
||||||
// Taille de chunk: 32^3 = 32 768 voxels.
|
// Taille de chunk: 32^3 = 32 768 voxels.
|
||||||
// Pourquoi 32 ? Puissance de 2 → astuces bit à bit + bon alignement GPU.
|
// Pourquoi 32 ? Puissance de 2 → astuces bit à bit + bon alignement GPU.
|
||||||
// VOXEL_SIZE = 25 cm/voxel (Unreal travaille en centimètres).
|
// VOXEL_SIZE = 25 cm/voxel (Unreal travaille en centimètres). 1 chunk = 8 m.
|
||||||
|
//
|
||||||
|
// NOTE: 64³ a été essayé ("B", 2026-06-16) pour couper les draw calls (8× moins de
|
||||||
|
// chunks) mais le streaming devenait trop saccadé (briques 8× plus lourdes → applies
|
||||||
|
// + spawns de contenu en gros à-coups sur le game thread). Reverté à 32³ : le fps se
|
||||||
|
// règle côté RENDU (ombres off sur LOD lointain, voir ApplyMeshToChunk), pas via la
|
||||||
|
// taille de chunk. La taille de chunk reste le levier streaming-vs-draws si besoin.
|
||||||
|
|
||||||
constexpr int32 CHUNK_SIZE = 32;
|
constexpr int32 CHUNK_SIZE = 32;
|
||||||
constexpr int32 CHUNK_SIZE_SQUARED = CHUNK_SIZE * CHUNK_SIZE; // 1024
|
constexpr int32 CHUNK_SIZE_SQUARED = CHUNK_SIZE * CHUNK_SIZE; // 1024
|
||||||
@@ -142,8 +148,8 @@ inline float SmoothStep01(float x)
|
|||||||
return x * x * (3.0f - 2.0f * x);
|
return x * x * (3.0f - 2.0f * x);
|
||||||
}
|
}
|
||||||
|
|
||||||
// UE's PerlinNoise3D renvoie ~[-0.8, 0.8] — ce facteur remet à ~[-1, 1]
|
// Le coeur de bruit (VoxelNoise::Perlin3D, T2.a) renvoie ~[-0.8, 0.8] comme l'ancien
|
||||||
// pour correspondre aux attentes des formules de densité.
|
// FMath::PerlinNoise3D — ce facteur remet à ~[-1, 1] pour les formules de densité.
|
||||||
constexpr float VOXEL_NOISE_SCALE = 1.25f;
|
constexpr float VOXEL_NOISE_SCALE = 1.25f;
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
|
|||||||
@@ -273,6 +273,37 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Debug")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Debug")
|
||||||
bool bDebugDrawPassages = false;
|
bool bDebugDrawPassages = false;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// BIOME MAP PREVIEW (bake the XY biome field to a PNG — works without PIE)
|
||||||
|
//=========================================================================
|
||||||
|
// Tune biome layout (cell size, warp, climate boxes) without flying around: set
|
||||||
|
// the strate + window, click Bake, open Saved/BiomePreview.png. Uses a transient
|
||||||
|
// generator seeded from VoxelSettings, so it works in the editor with no PIE.
|
||||||
|
|
||||||
|
/** The strate whose Biomes[] + BiomeMapParams to preview. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome Preview")
|
||||||
|
UVoxelStrateDefinition* BiomePreviewStrate = nullptr;
|
||||||
|
|
||||||
|
/** Width/height of the sampled window in VOXELS (centred on BiomePreviewCenter). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome Preview", meta = (ClampMin = "1.0"))
|
||||||
|
float BiomePreviewWorldSize = 16000.0f;
|
||||||
|
|
||||||
|
/** Centre of the preview window in voxel coords (X,Y). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome Preview")
|
||||||
|
FVector2D BiomePreviewCenter = FVector2D::ZeroVector;
|
||||||
|
|
||||||
|
/** Output image resolution (pixels per side). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome Preview", meta = (ClampMin = "64", ClampMax = "2048"))
|
||||||
|
int32 BiomePreviewResolution = 512;
|
||||||
|
|
||||||
|
/** Which field to visualise: biome debug colours, relief, or moisture. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome Preview")
|
||||||
|
EBiomePreviewChannel BiomePreviewChannel = EBiomePreviewChannel::Biome;
|
||||||
|
|
||||||
|
/** Bake the selected channel to Saved/BiomePreview.png. */
|
||||||
|
UFUNCTION(CallInEditor, BlueprintCallable, Category = "Biome Preview")
|
||||||
|
void BakeBiomePreview();
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
|
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class VoxelForge : ModuleRules
|
|||||||
// Private dependencies - only used in our .cpp files, not exposed in headers
|
// Private dependencies - only used in our .cpp files, not exposed in headers
|
||||||
PrivateDependencyModuleNames.AddRange(new string[]
|
PrivateDependencyModuleNames.AddRange(new string[]
|
||||||
{
|
{
|
||||||
// None for now - we'll add more as needed
|
"ImageWrapper", // PNG encode for the biome-map preview bake (BakeBiomePreview)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user