tmp
This commit is contained in:
+58
-20
@@ -101,7 +101,7 @@ redesign; tile identity lives in `FVoxelTileKey` (VoxelWorld.h).
|
|||||||
| Group | Fields (line) |
|
| Group | Fields (line) |
|
||||||
|-------|---------------|
|
|-------|---------------|
|
||||||
| Streaming | `ViewDistanceXY=16`, `ViewDistanceUp/Down=5`, `MaxConcurrentTasks=16`, `MaxMeshAppliesPerFrame=4` (defaults — actual values live on the data asset) |
|
| Streaming | `ViewDistanceXY=16`, `ViewDistanceUp/Down=5`, `MaxConcurrentTasks=16`, `MaxMeshAppliesPerFrame=4` (defaults — actual values live on the data asset) |
|
||||||
| Clipmap | `ClipRadius`, `MaxClipLevel`, `FullResClipLevels`, `CoarseTileCells`, skirts (the old `LOD0/1Distance` + `ContentMaxLevel` were dead → removed) |
|
| Clipmap | `ClipRadius`, `MaxClipLevel`, `FullResClipLevels`, `CoarseTileCells`, `RenderDistanceChunks` (custom horizontal reach: the outermost shell keeps generating until it covers this many chunks; 0 = off), `bFarSheetRing` + `FarSheetSpanLevels` (F18 — the render-distance ring streams per-surface SHEETS: level MaxClipLevel+span heightfield tiles instead of MC, see `GenerateSheetMesh`), skirts, `LODOctaveDrop` (T2.b octave drop on coarse tiles — 0 = off/byte-identical) (the old `LOD0/1Distance` + `ContentMaxLevel` were dead → removed) |
|
||||||
| Lighting | `bEnableDensityVolume` + DensityVolume* tunables (§3.11 density clipmap / mini-sun shadows) |
|
| Lighting | `bEnableDensityVolume` + DensityVolume* tunables (§3.11 density clipmap / mini-sun shadows) |
|
||||||
| 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) |
|
||||||
@@ -120,27 +120,38 @@ redesign; tile identity lives in `FVoxelTileKey` (VoxelWorld.h).
|
|||||||
|--------|-----------|------|
|
|--------|-----------|------|
|
||||||
| `AVoxelWorld()` ctor | 12 | Enables Tick. |
|
| `AVoxelWorld()` ctor | 12 | Enables Tick. |
|
||||||
| `RegenerateAllChunks()` | 21 | Bumps epoch, unloads all → Tick reloads. CallInEditor button. |
|
| `RegenerateAllChunks()` | 21 | Bumps epoch, unloads all → Tick reloads. CallInEditor button. |
|
||||||
|
| `ValidateDeterminism()` | — | **F2 CallInEditor button (PIE)**: re-samples boundary points under left- vs right-chunk cache warm-ups + a same-alignment repeat; any non-zero delta = window-invariance regression (§8.4). Run after every "bit-identical" hot-path refactor. |
|
||||||
|
| `GetMaxConcurrentTasks()` | — | T2.d — asset `MaxConcurrentTasks` capped to logical cores − 2 (all three budget checks use it). |
|
||||||
| `PostEditChangeProperty` | 45 | Editor live-edit hook. |
|
| `PostEditChangeProperty` | 45 | Editor live-edit hook. |
|
||||||
| `OnObjectModifiedInEditor` | 58 | Regenerates when a strate asset is edited (if `bLiveEditStrates`). |
|
| `OnObjectModifiedInEditor` | 58 | Regenerates when a strate asset is edited (if `bLiveEditStrates`). |
|
||||||
| `EndPlay` | 140 | Sets `bShuttingDown`, **waits for `ActiveTaskCount`→0**, unbinds delegate. |
|
| `EndPlay` | 140 | Sets `bShuttingDown`, **waits for `ActiveTaskCount`→0**, unbinds delegate. |
|
||||||
| `BeginPlay` | 177 | Constructs Generator/Mesher/StrateManager/DiffLayer, wires services, seeds. |
|
| `BeginPlay` | 177 | Constructs Generator/Mesher/StrateManager/DiffLayer, wires services, seeds. |
|
||||||
| `Tick` | 220 | `UpdateChunksAroundPosition(player)` + `ProcessPendingChunks()`. |
|
| `Tick` | 220 | `UpdateChunksAroundPosition(player)` + `ProcessPendingChunks()`. |
|
||||||
| `GetPlayerPosition` | 231 | Pawn position or zero. (`GetLODForChunk`/`LODToStep`/`IsChunkInRange` removed — dead since the clipmap.) |
|
| `GetPlayerPosition` | 231 | Pawn position or zero. (`GetLODForChunk`/`LODToStep`/`IsChunkInRange` removed — dead since the clipmap.) |
|
||||||
| `ProcessPendingChunks` | 301 | Drains ProcessQueue under per-frame budget; **discards stale epochs**; applies meshes. |
|
| `ProcessPendingChunks` | 301 | Drains ProcessQueue under per-frame budget; applies each via `ApplyTileResult`. |
|
||||||
| `UpdateChunksAroundPosition` | 362 | Builds desired set, sorts by distance, loads/unloads, handles LOD changes. |
|
| `ApplyTileResult` | — | **Shared game-thread apply** for one `FChunkResult` (async drain + sync carve): discards stale epochs, marks loaded, ingests capture, EMPTY releases the tile's existing component (a re-gen can flip content→empty on band change — old geometry must not linger), else `ApplyMeshToTile`. Returns true iff a visible mesh uploaded (counts the budget). Doesn't touch `PendingTiles` (caller's). |
|
||||||
| `LoadChunk` | 445 | Budget check → `UE::Tasks::Launch` background gen+mesh; RAII task guard. |
|
| `GenerateTileResult` | — | **Shared worker-side gen** for one tile (async `LoadTile` task + sync `SyncRemeshTile`): ClassifyTile (T1.d) → `GenerateMesh`/`GenerateSheetMesh` → `BuildTileStreamSet`. Reads Generator/Mesher only → safe on a worker or the game thread; fills `FChunkResult`, no enqueue. |
|
||||||
| `UnloadChunk` | 493 | Destroys mesh component + map entries. |
|
| `SyncRemeshTile` | — | **INSTANT DIG**: level-0 same-frame re-mesh on the game thread (`GenerateTileResult` + `ApplyTileResult` inline, Cells=CHUNK_SIZE/Step=1 + strate band, no capture). Used for the tile under the brush centre so a carve is visible THIS frame; one full-res gen on the game thread. |
|
||||||
| `ApplyMeshToChunk` | — | Upload geometry. **LOD0 → own component (`ChunkMeshes`, collision); LOD1/2 → batched into one component per region (`ChunkRegions`), each chunk a SectionGroup, no collision.** Handles LOD promote/demote between the two. §8.10. |
|
| `UpdateChunksAroundPosition` | 362 | Builds desired set, sorts by distance, loads/unloads, handles LOD changes. **Delta cull**: `BuildDesiredTiles` returns the LEAVERS (stamped `DesiredStamped` map, one sweep) — only those + `TransitionHold` are considered per crossing, not every loaded tile. `BuildDesiredTiles` also applies `RenderDistanceChunks`: the outermost shell widens to cover the distance — as level-MaxClipLevel MC tiles, or (F18 `bFarSheetRing`) as a SHEET ring at level MaxClipLevel+span (covered-check vs the MaxClipLevel box; `VF_OuterShell` shared with `IsTileInClipRange` so the cull sees the same horizon), dz pre-clamped to the vertical band. **§9.3 anchors:** also prunes dead `StreamingAnchors` + detects their chunk crossings → rebuilds the desired set when an anchor moves/(un)registers (`bAnchorsMoved`/`bForceDesiredRebuild`), same cadence as player movement. §8.10. |
|
||||||
| `ChunkToRegion` / `ChunkSectionGroupName` / `RemoveChunkFromRegion` / `DestroyIndividualChunkComponent` | — | Plumbing for the batched far-chunk scheme. |
|
| `BuildDesiredTiles` | ~722 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells + F18 sheet ring) then `AddAnchorDesiredTiles()` before the leaver sweep. |
|
||||||
|
| `AddAnchorDesiredTiles` | — | **§9.3 multi-anchor:** folds each `FVoxelStreamingAnchor`'s thin level-0 box (`XYRadiusChunks`/`ZBelowChunks`/`ZAboveChunks`) into the SAME desired set (deduped by stamp) so AI/remote players keep collision loaded around them; delta cull releases them on move/unregister. **§9.4:** a tile only a CollisionOnly anchor wants (clipmap didn't stamp it) → `CollisionOnlyTiles` → hidden at apply. Zero cost when no anchors. |
|
||||||
|
| `ReconcileAnchorTileVisibility` | — | **§9.4:** after each rebuild, toggle `SetVisibility` on ALREADY-LOADED tiles that flipped render↔collision-only (diff `CollisionOnlyTiles` vs prev — bounded, no O(loaded) scan; unhide only if still desired). No-op without CollisionOnly anchors. |
|
||||||
|
| `RegisterStreamingAnchor` / `UnregisterStreamingAnchor` | — | **BlueprintCallable §9.3:** add/remove an actor as a streaming anchor (`EVoxelAnchorPolicy` CollisionOnly/FullVisual + XY/ZBelow/ZAbove box). Idempotent; forces a rebuild next Tick. §9.4: CollisionOnly tiles cook collision but are hidden (no draw/VSM) unless the player clipmap wants them too. |
|
||||||
|
| `LoadChunk` | 445 | Budget check → `UE::Tasks::Launch` background gen+mesh; RAII task guard. Worker runs `Generator->ClassifyTile` first (T1.d): AllSolid/AllAir ⇒ skip `GenerateMesh`, tile stays empty (capture tiles always generate). STRATE CONTENT CUT: tiles ≥ `StrateContentCutMinLevel` pass the player-strate band (`MeshBandChunkLo/Hi` → voxels) to `GenerateMesh` + stamp it on `FChunkResult::BandChunkLo/Hi`; band change re-queues via `BandRemeshQueue` (see `UpdateChunksAroundPosition`). TOO-COARSE SKIP: if one cell is taller than the band (`Step > band height` — level ≥7 territory) the tile is enqueued EMPTY without launching a task (cell-granular cut could only render garbage). F18: `Tile.Level > MaxClipLevel` = SHEET tile → routed to `GenerateSheetMesh` (band mid-chunk = strate ref; band unarmed ⇒ empty; MC-ring sampling density, cells capped 128/axis; carries the XY hole `SheetHole*Vox` — hole moves ⇒ overlapping sheets re-queue via `BandRemeshQueue`, see `UpdateChunksAroundPosition`). §8.10. |
|
||||||
|
| `UnloadTile` | — | Clears tile state; the component is PARKED in the pool (T2.c), not destroyed. |
|
||||||
|
| `ApplyMeshToTile` | — | Upload geometry. One component per tile (clipmap keeps count low; supersedes the old region batching); worker-built streams (T1.f) → `CreateSectionGroup(MoveTemp)`. Reuses the component's existing `URealtimeMesh` (no per-apply mesh alloc). **F17: two polygroups** (0 ground / 1 sky-cap, per-triangle class from the mesher) → RMC auto-section per non-empty group; slot 0 = override/default material, slot 1 = `CeilingMaterial` (fallback ground); config gated by `FChunkResult::bHasGroundTris/bHasCeilingTris`. Takes `FChunkResult&`; strate lookups clamp Z into `Result.BandChunkLo/Hi` (strate content cut) — ground at clamped bottom chunk, cap at clamped top (mid = gap fallback). Collision level-0 only (T1.c) both groups; shadow per SECTION: ground casts at level≤1, cap never. **§9.4:** `SetVisibility(false)` when the tile is in `CollisionOnlyTiles` (anchor-only, hidden — collision still cooks). §8.10 + ARCHITECTURE SurfaceWorld row. |
|
||||||
|
| `AcquireTileComponent` / `ReleaseTileComponent` | — | **T2.c component pool** (`TileComponentPool`, bounded): park on unload (geometry+collision stripped, hidden, stays registered), pop on apply — no `NewObject`/`RegisterComponent`/GC churn during travel & regen. §8.10. |
|
||||||
| `GetStrateAtPosition` | 965 | Gameplay query → strate index. |
|
| `GetStrateAtPosition` | 965 | Gameplay query → strate index. |
|
||||||
| `GetBiomeAtWorldLocation` | — | **BlueprintCallable** biome probe at a world point (undoes actor xf → voxel → `Generator::QueryBiomeAt`). Returns `FVoxelBiomeQuery` for BP debug ("what biome / how many decos under the cursor?"). |
|
| `GetBiomeAtWorldLocation` | — | **BlueprintCallable** biome probe at a world point (undoes actor xf → voxel → `Generator::QueryBiomeAt`). Returns `FVoxelBiomeQuery` for BP debug ("what biome / how many decos under the cursor?"). |
|
||||||
| `CarveAtPosition` / `FillAtPosition` | 691 / 709 | Build `FVoxelModification` → DiffLayer → RemeshDirtyChunks. |
|
| `GetVoxelSurfaceHeightAt` | ~1534 | **BlueprintCallable** ground finder (F7 bridge): world XY → terrain + sky-cap world-Z via `Generator::GetSurfaceHeightAt`, NO trace/collision, deterministic, available before the area meshes → self-arranging prefab/ruin BPs snap their parts to the real ground. False (outs=input Z) on non-SurfaceWorld strates; ignores passage/spine carving. |
|
||||||
|
| `CarveAtPosition` / `FillAtPosition` | 691 / 709 | Build `FVoxelModification` → `ApplyModification`. |
|
||||||
|
| `ApplyModification` | ~1581 | Single funnel for all brushes: DiffLayer → **sync-remesh the brush-centre level-0 tile** (`SyncRemeshTile`, instant hole; skipped if that tile is mid-gen — would race a stale in-flight result) → `RemeshDirtyChunks(..., excludeCenter)` for the neighbours → `RemoveDecorationsInSphere`. |
|
||||||
| `ClearAllModifications` | 726 | Clears diff layer, regenerates. |
|
| `ClearAllModifications` | 726 | Clears diff layer, regenerates. |
|
||||||
| `ChangeSeed` | 740 | **Season reset**: new seed everywhere, clear diffs, bump season, reload. |
|
| `ChangeSeed` | 740 | **Season reset**: new seed everywhere, clear diffs, bump season, reload. |
|
||||||
| `GetCurrentSeed` / `GetCurrentSeason` | 784 / 789 | Accessors. |
|
| `GetCurrentSeed` / `GetCurrentSeason` | 784 / 789 | Accessors. |
|
||||||
| `RemeshDirtyChunks` | 798 | Re-queue loaded chunks for async re-mesh (no visual pop). |
|
| `RemeshDirtyChunks` | 798 | Queue loaded level-0 dirty tiles onto `DirtyRemeshQueue` (async re-mesh, no pop) + `MarkDirtyVoxelBox` the volume. Optional `ExcludeTile` = the sync'd centre. Drained FIRST in the submit loop at **BackgroundHigh** (ahead of streaming/band) so a dig never waits behind streaming; in-flight tiles stay QUEUED (not dropped) so a stale pre-carve result is corrected once it lands — fixes "hole shows up a beat late / not until I move". |
|
||||||
|
|
||||||
> **Game-thread profiling (Perf):** `AVoxelWorld::Tick` and its sub-steps are wrapped in `TRACE_CPUPROFILER_EVENT_SCOPE` — `VoxelForge_Tick / UpdateChunks / BuildDesiredTiles / CullTiles / SubmitTiles / ProcessPending / ProcessUnload / UpdateDecorations / UpdateWater`. Capture a `Count/Incl/Excl` Insights timer export and read the `Excl` column to see which step owns the per-frame cost (the actor tick shows as `BP_VoxelWorld_C` if subclassed in BP). `VoxelForge_GenerateMesh` + `VoxelForge_BuildStreams` are worker-side (off the frame): the RMC `FRealtimeMeshStreamSet` is now built on the gen worker (`BuildTileStreamSet`) and carried on `FChunkResult::Streams` (TSharedPtr), so `ApplyMeshToTile` is game-thread-cheap — just material/ceiling resolve + `CreateSectionGroup(MoveTemp)`. See ARCHITECTURE §8.10 "Worker-built StreamSet (T1.f)".
|
> **Game-thread profiling (Perf):** `AVoxelWorld::Tick` and its sub-steps are wrapped in `TRACE_CPUPROFILER_EVENT_SCOPE` — `VoxelForge_Tick / UpdateChunks / BuildDesiredTiles / CullTiles / SubmitTiles / ProcessPending / ProcessUnload / UpdateDecorations / UpdateWater`. Capture a `Count/Incl/Excl` Insights timer export and read the `Excl` column to see which step owns the per-frame cost (the actor tick shows as `BP_VoxelWorld_C` if subclassed in BP). `VoxelForge_ClassifyTile` (T1.d) / `VoxelForge_GenerateMesh` + `VoxelForge_BuildStreams` are worker-side (off the frame): the RMC `FRealtimeMeshStreamSet` is now built on the gen worker (`BuildTileStreamSet`) and carried on `FChunkResult::Streams` (TSharedPtr), so `ApplyMeshToTile` is game-thread-cheap — just material/ceiling resolve + `CreateSectionGroup(MoveTemp)`. See ARCHITECTURE §8.10 "Worker-built StreamSet (T1.f)".
|
||||||
|
|
||||||
### 3.6 Density generator — `Public/VoxelGenerator.h` + `Private/VoxelGenerator.cpp`
|
### 3.6 Density generator — `Public/VoxelGenerator.h` + `Private/VoxelGenerator.cpp`
|
||||||
`UVoxelGenerator : UObject` — lightweight; holds `Seed`, and injected services
|
`UVoxelGenerator : UObject` — lightweight; holds `Seed`, and injected services
|
||||||
@@ -157,12 +168,26 @@ redesign; tile identity lives in `FVoxelTileKey` (VoxelWorld.h).
|
|||||||
| **`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. |
|
| `SampleSurfaceStructuralZ` | — | **F20:** the RAW SurfaceWorld heightfield (continents+mountains+detail), BEFORE any terrain op; returns terrain Z + relief M. Cliff re-samples it at an XY offset for a cheap analytic slope. |
|
||||||
|
| `ComputeSurfaceTerrainZ` / `GetSurfaceDensity` | — | SurfaceWorld heightfield → terrain Z, then density; biome **output-blend** lerps dominant/neighbour heights (`ParamsD`/`ParamsN`/weight). **F20 surface ops** (`FSurfaceGenerationParams`, biome-selected + slope/relief-conditioned, all default off): Cliff (slope-gated STEEPENING — push height from local mean where steep ⇒ sheer walls; 4 structural resamples only when on), Terrace (relief-gated + `TerraceHardness`), LayerLines (sedimentary shelves) — pure per-column height REMAPS applied here so the single height oracle stays consistent (MC/sheets/ClassifyTile/deco/BP bridge). **Phase 2 OVERHANG** (volumetric — real jutting shelves): in `SurfaceDensityFromColumn`, for AIR voxels in a window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height (tiny low ⇒ air over the void, full high ⇒ borrows the far cliff rock) and unioned in ⇒ a shelf attached to the cliff, tapering out over the void with air beneath (the sketch). Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill `(DirX,DirY)` resolved once in `ComputeSurfaceColumn` (gradient sampled at the REACH scale so a spot over the void can see the cliff), cached on `FSurfaceColumn`. Genuine 3D (per-voxel structural re-eval, gated to steep overhang columns). Off ⇒ byte-identical. §8.14. |
|
||||||
|
| `ClassifyTile` | — | **T1.d trivial-tile reject** (worker, called by `LoadTile` before `GenerateMesh`): proves a tile AllSolid/AllAir on the mesher's exact lattice (gap chunks + SurfaceWorld columns via the SHARED `GSurfColCache`; seal bands; guards: diff mods, passages, spine, disturbances, **F20 overhang** — a column point in `(TerrainZ, TerrainZ+OverhangMargin]` (margin = max `OverhangHeight`) is unprovable ⇒ Mixed, UPWARD only since the shelf union only ADDS rock above ground, so an overhang shelf never holes a trivially-skipped tile) → skip gen. Mixed = generate normally. §8.10. |
|
||||||
| `SampleRelief` / `SampleMoisture` | — | Climate fields (pure XY, [0,1]). Relief = shared source of truth for the relief map M. §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. |
|
| `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. |
|
| `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. |
|
| `GetDominantBiomeAt` | — | Game-thread query → dominant biome ASSET (content/atmosphere). §8.14. |
|
||||||
| `QueryBiomeAt` | — | Rich game-thread biome probe → `FVoxelBiomeQuery` (dominant/neighbour asset, relief/moisture, blend weight, dominant deco count). Diagnostic behind `AVoxelWorld::GetBiomeAtWorldLocation`. §8.14. |
|
| `QueryBiomeAt` | — | Rich game-thread biome probe → `FVoxelBiomeQuery` (dominant/neighbour asset, relief/moisture, blend weight, dominant deco count). Diagnostic behind `AVoxelWorld::GetBiomeAtWorldLocation`. §8.14. |
|
||||||
|
| `EvaluateTerrainConditions` | ~2548 | **F7 aware placement:** AND-evaluate an entry's `FTerrainCondition[]` (relief/moisture/biome-border) at a candidate voxel XY. Empty = true (zero cost). Pure query (SampleRelief/SampleMoisture/SampleBiomeAt) → worker-safe + game-thread; caller passes the strate's `FBiomeContext` (freq/contrast + Voronoi map). Consumed by deco `BuildCellSpawns` + `SpawnLandmarkInstance`; shared core of the future quest FindFeature locator. |
|
||||||
|
|
||||||
|
> **Per-voxel hot-path memos (perf pass 2, all bit-identical — same hashes/math, hoisted per
|
||||||
|
> chunk/cell):** `GetDensityAt` uses the DiffLayer snapshot cache (§3.9); `GetDensityWithParams`
|
||||||
|
> memoizes the strate index per (chunkZ, `GetLayoutVersion()`); `thread_local` per-cell lattice
|
||||||
|
> bakes cover slab columns (`GetSlabDensity` step 4), maze open edges, vertical shafts +
|
||||||
|
> cross-connectors, floating-island constants, and the disturbance chasms/bridges/ridges; worm
|
||||||
|
> tunnels short-circuit the 2nd Perlin when N1 ≥ WormThreshold (N2 ≥ 0 ⇒ can't carve). Room
|
||||||
|
> shapes are pre-baked in `FCachedRoom` (§3.7). **T2.b:** per-voxel fractal call sites take their
|
||||||
|
> octave count through `VoxelGenLOD::Eff(N)` (VoxelGenerator.h) — a `thread_local` bias set per
|
||||||
|
> tile by the mesher drops tail octaves on coarse tiles (opt-in `LODOctaveDrop`, default 0 = off);
|
||||||
|
> XY-field noise (heightfield/ceiling/relief/moisture) deliberately stays un-biased. §8.10.
|
||||||
|
|
||||||
### 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.
|
||||||
@@ -176,8 +201,8 @@ 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, 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. |
|
| `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 via the shared `BakeRoomFeature` hash-placement skeleton (one gate/XY/radius pattern + per-type Emit lambda), 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` | 757 | **Phase 2** (per voxel): SmoothMin over cached rooms/tunnels; returns nearest room idx for terrain-op lookup. **Signature changed (perf pass 2): `RoomShapeVariety` param REMOVED** — the shape roll + capsule trig are pre-baked into `FCachedRoom` (`ShapeType/ShapeA/ShapeB/ShapeR`) by `BuildChunkCache`, bit-identical. |
|
||||||
| `EvaluateSDF` | 738 | Convenience wrapper (builds temp cache) for one-off queries. |
|
| `EvaluateSDF` | 738 | Convenience wrapper (builds temp cache) for one-off queries. |
|
||||||
|
|
||||||
Performance note (h:209-220): caching rooms/tunnels once per chunk instead of per
|
Performance note (h:209-220): caching rooms/tunnels once per chunk instead of per
|
||||||
@@ -192,10 +217,13 @@ Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
|||||||
| `EVoxelNoiseType` | 99 | FBM/Ridged/Mixed/Cellular. |
|
| `EVoxelNoiseType` | 99 | FBM/Ridged/Mixed/Cellular. |
|
||||||
| `ECaveGeneratorType` | 146 | TunnelNetwork / FlatPlain / CrystalChamber. |
|
| `ECaveGeneratorType` | 146 | TunnelNetwork / FlatPlain / CrystalChamber. |
|
||||||
| `EVoxelStrateTransition` | 183 | Gradient / Hard / Interleaved boundary blends. |
|
| `EVoxelStrateTransition` | 183 | Gradient / Hard / Interleaved boundary blends. |
|
||||||
| **`FStrateGenerationParams`** | 213 | The giant TunnelNetwork param bag (rock, worms, rooms, tunnels, warp, roughness, all terrain-op transport fields, boundary seal). `Lerp()` static at 844 blends two sets at boundaries. |
|
| **`FStrateGenerationParams`** | ~350 | The giant TunnelNetwork param bag (rock, worms, rooms, tunnels, warp, roughness, all terrain-op transport fields, boundary seal). `Lerp()` static blends two sets at boundaries — it expands the **`VF_STRATE_PARAM_FIELDS` X-macro** (defined just above the struct): **adding a field to the struct? add it to that list** or blends silently reset it to default. |
|
||||||
| `FStrateTerrainOpEntry` | 965 | Soft-ptr to a terrain op + Weight + Probability. |
|
| `FStrateTerrainOpEntry` | 965 | Soft-ptr to a terrain op + Weight + Probability. |
|
||||||
| **`FSlabGenerationParams`** | 1019 | Floor/ceiling heights, roughness, columns, seal — for slab generators. |
|
| **`FSlabGenerationParams`** | 1019 | Floor/ceiling heights, roughness, columns, seal — for slab generators. |
|
||||||
| `FStrateDecoration` / `FStrateAmbientActor` / `FStrateCreature` | 1160 / 1192 / 1212 | Content spawn entries (consumed by future systems). |
|
| **`FPlacementProfile`** | ~1747 | **Shared placement vocabulary** for every scatter primitive (`FStrateDecoration`, `FStrateLandmark`, coming `FStrateSetPiece`): spawn (ActorClass/InstancedMesh), Filter gates (surface/slope/overhang/water/RequiredBiome + **F7 awareness `Conditions[]`** — `FTerrainCondition` relief/moisture/biome-border predicates, AND-ed, evaluated by `Generator::EvaluateTerrainConditions`), Transform (align/offsets/RotationOffset+RandomRotation/scale), Render (cull/shadow). Each primitive embeds it as `Profile` + keeps only its own DISTRIBUTION fields. Per-primitive defaults set in each struct's ctor (deco: scale 0.8-1.2 + RandomRotation.Yaw=360; landmark: Ceiling + no align). |
|
||||||
|
| `FStrateDecoration` / `FStrateLandmark` | ~1830 / ~1900 | `Profile` + distribution: deco = StreamTier/SpawnDensity/MaxPerChunk; landmark = SpacingChunks/JitterFraction/SpawnProbability/StreamRadiusChunks + Light-Orb block. |
|
||||||
|
| `ELandmarkAnchor` (on `FStrateLandmark`) | ~1940 | F7: `AnchorMode` = HashLattice / PassageMouth + passage toggles + exclusion (`ExclusionRadiusChunks`/`Priority`) folded into `FStrateLandmark` (set-pieces merged in — one primitive, one `Landmarks` list, `UpdateLandmarks`). |
|
||||||
|
| `FStrateAmbientActor` / `FStrateCreature` | ~2090 / ~2110 | Content spawn entries (consumed by future systems). |
|
||||||
|
|
||||||
**`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),
|
||||||
@@ -213,8 +241,10 @@ Maps depth→strate at runtime; owns passages.
|
|||||||
| `Initialize` | 10 | Builds the stacked layout from settings+seed (fixed slots + shuffled pool), then `GeneratePassages`. |
|
| `Initialize` | 10 | Builds the stacked layout from settings+seed (fixed slots + shuffled pool), then `GeneratePassages`. |
|
||||||
| `GeneratePassages` | 146 | Deterministic passages between consecutive strates (per-type control points). |
|
| `GeneratePassages` | 146 | Deterministic passages between consecutive strates (per-type control points). |
|
||||||
| `EvaluateModifierSDF` | 357 | SDF of passages at a point (for carving). Per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`. §8.10. |
|
| `EvaluateModifierSDF` | 357 | SDF of passages at a point (for carving). Per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`. §8.10. |
|
||||||
|
| `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. |
|
||||||
| `FindSlotIndexForChunkZ` | 427 | Z → layout index. |
|
| `FindSlotIndexForChunkZ` | 427 | Z → layout index. |
|
||||||
| `GetStrateAt` / `GetStrateIndex` | 443 / 455 | World-Z queries. |
|
| `GetStrateAt` / `GetStrateIndex` | 443 / 455 | World-Z queries. |
|
||||||
|
| `GetLayoutVersion` | h:161 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize`). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so editor rebuilds never serve stale data. |
|
||||||
| `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). |
|
||||||
@@ -251,6 +281,10 @@ atmosphere override, `WaterMaterial`, `MaterialPaletteIndex` (F6 — baked to ve
|
|||||||
| `ApplyModification` | 63 | Enforces budget, stores in all overlapped chunks, returns dirty coords. |
|
| `ApplyModification` | 63 | Enforces budget, stores in all overlapped chunks, returns dirty coords. |
|
||||||
| `GetDensityOffset` | 131 | Per-voxel combined diff (smoothstep falloff, additive). |
|
| `GetDensityOffset` | 131 | Per-voxel combined diff (smoothstep falloff, additive). |
|
||||||
| `HasModifications` | 160 | Fast reject for hot path. |
|
| `HasModifications` | 160 | Fast reject for hot path. |
|
||||||
|
| `HasAnyMods` / `GetModsVersion` | h:238 / h:241 (inline) | Lock-free atomics: any-mod-exists flag + monotonic mod-state version (bumped by `ApplyModification`/`Clear`). |
|
||||||
|
| `GetChunkModsSnapshot` | — | Copy one chunk's mod list under ONE read lock. Workers snapshot per (chunk, version) instead of locking per voxel — `GetDensityAt` keys a `thread_local` 64-slot direct-mapped cache on it (~27 lock ops per tile task instead of ~86k once any carve exists). |
|
||||||
|
| `HasAnyModInChunkRange` | — | Any modified chunk key in an inclusive chunk box? One key walk under a read lock — ClassifyTile's diff guard (per tile, conservative by construction: mods are stored in every chunk their radius overlaps). |
|
||||||
|
| `EvaluateMods` (static) | — | Lock-free pure evaluation of a mod list at a voxel — shared core of `GetDensityOffset` and the generator's snapshot path. |
|
||||||
| `Clear` | 170 | Wipe all (season reset). |
|
| `Clear` | 170 | Wipe all (season reset). |
|
||||||
| `GetTotalModificationCount` / `GetModifiedChunkCount` | 182 / 192 | Stats. |
|
| `GetTotalModificationCount` / `GetModifiedChunkCount` | 182 / 192 | Stats. |
|
||||||
|
|
||||||
@@ -260,7 +294,8 @@ atmosphere override, `WaterMaterial`, `MaterialPaletteIndex` (F6 — baked to ve
|
|||||||
removed — since T1.b the pre-sampled grid supplies positions AND gradients inline.)
|
removed — since T1.b the pre-sampled grid supplies positions AND gradients inline.)
|
||||||
| Method | .cpp line | Role |
|
| Method | .cpp line | Role |
|
||||||
|--------|-----------|------|
|
|--------|-----------|------|
|
||||||
| **`GenerateMesh`** | ~15 | The MC loop over cells; `Step` controls LOD sampling. Edge `t` + grid-gradient normals computed inline (`SampleG`/`GradAt`). Optional `OutCaptureGrid` (4th arg) = CAPTURE-DURING-MESHING: when non-null + full-res (`CellsPerAxis==CHUNK_SIZE`), copies the already-sampled `CHUNK_SIZE³` density grid (quantized via `VF_QuantizeDensity`, VoxelTypes.h) so the density clipmap reuses it instead of re-sampling `GetDensityAt`. Pure read of the grid — §8.10 untouched. |
|
| **`GenerateSheetMesh`** | ~455 | **F18 far-field SHEET** (render-distance ring, `Tile.Level > MaxClipLevel`): two displaced heightfield grids per tile — ground (polygroup 0) + sky-cap (polygroup 1) from `GetSurfaceHeightAt` columns (StrateChunkZ = band mid identifies the strate; non-SurfaceWorld ⇒ empty). Classes true BY CONSTRUCTION (no vote/probes). Same conventions as `GenerateMesh` (world-cm positions, planar UVs, F6 colour masks, −N double-faced perimeter skirts per bucket, ground‖cap + `NumCeilingTriangles`). Margin ring keeps normals continuous between sheets. XY HOLE params: cells fully inside the MC-covered box around the player (`AVoxelWorld::SheetHole*Vox`, shrunk 1 tile for seam overlap) are skipped — a partially-covered sheet must not overlay near terrain; hole-edge cells get no skirt. Carved features (passages/spine/chasms) + diff layer NOT represented — accepted at sheet distance. |
|
||||||
|
| **`GenerateMesh`** | ~15 | The MC loop over cells; `Step` controls LOD sampling. Sets the T2.b octave bias for the tile (`TGuardValue` on `VoxelGenLOD::OctaveBias`, from `LODOctaveDrop` × log2(Step); 0 at LOD0/off). Edge `t` + grid-gradient normals computed inline (`SampleG`/`GradAt`). Optional `OutCaptureGrid` (4th arg) = CAPTURE-DURING-MESHING: when non-null + full-res (`CellsPerAxis==CHUNK_SIZE`), copies the already-sampled `CHUNK_SIZE³` density grid (quantized via `VF_QuantizeDensity`, VoxelTypes.h) so the density clipmap reuses it instead of re-sampling `GetDensityAt`. Pure read of the grid — §8.10 untouched. **F17 surface class**: each unique vertex is classified sol/sky-cap in `GetOrCreateVertex` (down-facing only → memoized `GetSurfaceHeightAt`, nearer `CeilSurf` = cap); triangles bucket by majority into `GroundTris`/`CapTris` (thread_local), skirts emit per bucket, then `Triangles = ground‖cap` + `FVoxelMeshData::NumCeilingTriangles` (→ polygroups in `BuildTileStreamSet`). Same tris, only index ORDER changes. STRATE CONTENT CUT: optional `BandZMin/MaxVox` params restrict the cz cell loop (+ gz sampling rows) to the player-strate band — coarse straddling tiles mesh ONE strate (kills far-LOD inter-strate aliasing holes); meshed cells bit-identical. |
|
||||||
|
|
||||||
**`Public/MarchingCubesTables.h`** — `EdgeTable` + `TriTable` reference data (Paul
|
**`Public/MarchingCubesTables.h`** — `EdgeTable` + `TriTable` reference data (Paul
|
||||||
Bourke). Cube corner/edge layout documented at top (lines 7-37). Rarely needs editing.
|
Bourke). Cube corner/edge layout documented at top (lines 7-37). Rarely needs editing.
|
||||||
@@ -268,7 +303,7 @@ Bourke). Cube corner/edge layout documented at top (lines 7-37). Rarely needs ed
|
|||||||
### 3.11 Per-chunk content & per-strate atmosphere (2026 redesign — see §8)
|
### 3.11 Per-chunk content & per-strate atmosphere (2026 redesign — see §8)
|
||||||
| File | Role |
|
| File | Role |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `Public/Private/VoxelContentManager.h/.cpp` | `UVoxelContentManager` — distance-based world-grid decoration scatter (no LOD pop, surface-snapped via `GetDensityAt`) + level-0 water planes. **TWO streaming grids** (`FDecoGrid` Near/Far, picked per entry via `FStrateDecoration::StreamTier`): NearGrid = short radius + fine column grid (groundcover); FarGrid = full radius + coarse grid (cheap rare/large props). **Plus `UpdateLandmarks`** — rare far-visible objects (the "mini-suns") on a coarse HASH LATTICE (`FStrateLandmark`, cell = SpacingChunks chunks → cheap at any radius, no per-chunk freeze); synchronous, deterministic, strate-wide. Owned by `AVoxelWorld`. §8.5. |
|
| `Public/Private/VoxelContentManager.h/.cpp` | `UVoxelContentManager` — distance-based world-grid decoration scatter (no LOD pop, surface-snapped via `GetDensityAt`) + level-0 water planes. **F7 companions:** each `FStrateDecoration` may list `FDecoCompanion` satellites (rocks/mushrooms around a tree) — emitted per placed parent in `PlaceAtCrossing`, deterministic (pure fn of the parent hash), inherit the parent's surface point; `FDecoSpawn::CompanionIdx` routes each back to its `Companions[ci].Profile` at apply. **TWO streaming grids** (`FDecoGrid` Near/Far, picked per entry via `FStrateDecoration::StreamTier`): NearGrid = short radius + fine column grid (groundcover); FarGrid = full radius + coarse grid (cheap rare/large props). **Plus `UpdateLandmarks`** — rare deliberately-placed objects: mini-suns, ruins, shrines, monuments (`FStrateLandmark`; set-pieces folded in 2026-07-06). Per entry an `AnchorMode`: **HashLattice** (coarse hash lattice, cell = SpacingChunks chunks → cheap at any radius, no per-chunk freeze) or **PassageMouth** (`GetPassages()` endpoints landing in this strate). Gated by `Profile` + `Profile.Conditions`; optional deterministic **exclusion radius** (Priority+hash rank, `ExclusionRadiusChunks`; 0 = off → pure scatter is unchanged, skips the O(n²) resolve). Synchronous, deterministic, strate-wide; spawns via `SpawnFromProfile` (+ orb wrapper). Exclusion is POP-FREE (gathers a `MaxExcl` ring of suppress-only candidates so a piece's fate is player-position-independent). `bSuppressDecorationsUnder`/`SuppressRadiusChunks` clear grass under a footprint (on spawn + re-cleared in `ApplyRegion`). **`RemoveDecorationsInSphere`** (world sphere → both grids' HISMs via `GetInstancesOverlappingSphere`+`RemoveInstances`) is the shared primitive, also called by `AVoxelWorld::ApplyModification` so player digging removes floating grass instantly. Owned by `AVoxelWorld`. §8.5. |
|
||||||
| `Public/Private/VoxelAtmosphereManager.h/.cpp` | `UVoxelAtmosphereManager` — per-strate fog/skylight + persistent ceiling/floor layer actors + full `AtmosphereActor` override. Owned by `AVoxelWorld`. §8.6. |
|
| `Public/Private/VoxelAtmosphereManager.h/.cpp` | `UVoxelAtmosphereManager` — per-strate fog/skylight + persistent ceiling/floor layer actors + full `AtmosphereActor` override. Owned by `AVoxelWorld`. §8.6. |
|
||||||
| `Public/Private/VoxelDensityVolume.h/.cpp` | `UVoxelDensityVolume` — player-centred DENSITY CLIPMAP (N toroidal R8 levels, fine near / coarse far) streamed to GPU `UVolumeTexture`s for the mini-sun raymarched shadow march. Fills run on ONE dedicated thread (`FVoxelDensityFillRunnable`, off the task pool); level 0 is mostly fed by CAPTURE-DURING-MESHING (mesher grid reuse, gated by `IsTileCaptureUseful` so only tiles near the shadow window pay the capture). Carve → `MarkDirtyVoxelBox` refills locally. `VolumeEpoch` drops stale fills. Owned by `AVoxelWorld` (`bEnableDensityVolume`); shader params pushed via shared per-base-material MIDs (`AVoxelWorld::UpdateTerrainMaterialParams`, change-detected). |
|
| `Public/Private/VoxelDensityVolume.h/.cpp` | `UVoxelDensityVolume` — player-centred DENSITY CLIPMAP (N toroidal R8 levels, fine near / coarse far) streamed to GPU `UVolumeTexture`s for the mini-sun raymarched shadow march. Fills run on ONE dedicated thread (`FVoxelDensityFillRunnable`, off the task pool); level 0 is mostly fed by CAPTURE-DURING-MESHING (mesher grid reuse, gated by `IsTileCaptureUseful` so only tiles near the shadow window pay the capture). Carve → `MarkDirtyVoxelBox` refills locally. `VolumeEpoch` drops stale fills. Owned by `AVoxelWorld` (`bEnableDensityVolume`); shader params pushed via shared per-base-material MIDs (`AVoxelWorld::UpdateTerrainMaterialParams`, change-detected). |
|
||||||
|
|
||||||
@@ -325,10 +360,10 @@ Stage order (negative=solid throughout). Each stage's anchor:
|
|||||||
| How chunks stream in/out | `UpdateChunksAroundPosition` VoxelWorld.cpp:362. |
|
| How chunks stream in/out | `UpdateChunksAroundPosition` VoxelWorld.cpp:362. |
|
||||||
| Async threading / stale-result handling | `LoadChunk` :445, `ProcessPendingChunks` :301, Epoch logic. |
|
| Async threading / stale-result handling | `LoadChunk` :445, `ProcessPendingChunks` :301, Epoch logic. |
|
||||||
| Add a new cave feature / terrain op | Add enum in `VoxelTerrainOpDefinition.h:36`, params there, `ApplyTo` (.cpp:6), transport fields in `FStrateGenerationParams`, consume it in a new Step inside `GetDensityWithParams`. |
|
| Add a new cave feature / terrain op | Add enum in `VoxelTerrainOpDefinition.h:36`, params there, `ApplyTo` (.cpp:6), transport fields in `FStrateGenerationParams`, consume it in a new Step inside `GetDensityWithParams`. |
|
||||||
| Tweak room/tunnel shapes | `VoxelCaveMorphology.cpp` `BuildChunkCache` :47 / `EvaluateSDFCached` :589. |
|
| Tweak room/tunnel shapes | `VoxelCaveMorphology.cpp` `BuildChunkCache` :47 / `EvaluateSDFCached` :757. |
|
||||||
| Worm tunnel behavior | `GetDensityWithParams` Step 5, VoxelGenerator.cpp:1241. |
|
| Worm tunnel behavior | `GetDensityWithParams` Step 5, VoxelGenerator.cpp:1241. |
|
||||||
| Strate stacking / which strate where | `UVoxelStrateManager::Initialize` :10. |
|
| Strate stacking / which strate where | `UVoxelStrateManager::Initialize` :10. |
|
||||||
| Boundary blend between strates | `GetGenerationParams` :515 + `FStrateGenerationParams::Lerp` (StrateTypes.h:844). |
|
| Boundary blend between strates | `GetGenerationParams` :515 + `FStrateGenerationParams::Lerp` (expands `VF_STRATE_PARAM_FIELDS`, StrateTypes.h — new fields go in that list). |
|
||||||
| Passages between strates | `GeneratePassages` :146 + `EvaluateModifierSDF` :371 + `ApplyPassageCarving` (Generator.cpp:197). |
|
| Passages between strates | `GeneratePassages` :146 + `EvaluateModifierSDF` :371 + `ApplyPassageCarving` (Generator.cpp:197). |
|
||||||
| 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 | Grid-gradient in `GenerateMesh` (`GradAt` lambda), `IsoLevel` (h). |
|
| Mesh smoothness / normals | Grid-gradient in `GenerateMesh` (`GradAt` lambda), `IsoLevel` (h). |
|
||||||
@@ -371,4 +406,7 @@ Stage order (negative=solid throughout). Each stage's anchor:
|
|||||||
Moved out of the codemap to keep this file a fast navigation index. The archetypes, (0,0)
|
Moved out of the codemap to keep this file a fast navigation index. The archetypes, (0,0)
|
||||||
spine, disturbances, content/atmosphere, biomes, and the **performance invariants**
|
spine, disturbances, content/atmosphere, biomes, and the **performance invariants**
|
||||||
(`§8.10` — read before optimizing the hot path) now live in **[ARCHITECTURE.md](ARCHITECTURE.md)**.
|
(`§8.10` — read before optimizing the hot path) now live in **[ARCHITECTURE.md](ARCHITECTURE.md)**.
|
||||||
All `§8.x` cross-references throughout this file point there.
|
All `§8.x` cross-references throughout this file point there. **`§9` = the MULTIPLAYER model**
|
||||||
|
(listen-server-first, design-only) — read before touching streaming / carve / AI: terrain is never
|
||||||
|
replicated (determinism = replicate seed+layout+diff events only), streaming goes multi-anchor
|
||||||
|
(collision-only vs full-visual policy per anchor), carves are server-authoritative.
|
||||||
|
|||||||
@@ -49,6 +49,44 @@ struct FBuildRoom
|
|||||||
// (collected for connectivity decisions either way).
|
// (collected for connectivity decisions either way).
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// INTERNAL: Shared hash-placement skeleton for the per-room baked features
|
||||||
|
// (pits / chimneys / columns). Squelette commun de placement par hash — les
|
||||||
|
// trois boucles de bake étaient des copies quasi identiques de ce motif.
|
||||||
|
//
|
||||||
|
// Rolls EXACTLY the hash chain the hand-written loops used (bit-identical):
|
||||||
|
// H = Mix(RoomHash ^ (SaltBase + i * SaltStep)) → density gate
|
||||||
|
// H2 = Mix(H ^ Salt2) → XY offset (X: H2, Y: Mix(H2))
|
||||||
|
// H3 = Mix(H2 ^ Salt3) → radius lerp [MinRadius, MaxRadius]
|
||||||
|
// Type-specific work (Z anchor, flare, bounds, struct fill) lives in the Emit
|
||||||
|
// lambda; it receives H3 so pits/chimneys can chain their 4th hash from it.
|
||||||
|
//=============================================================================
|
||||||
|
template <typename FEmit>
|
||||||
|
static void BakeRoomFeature(
|
||||||
|
const FCachedRoom& CR,
|
||||||
|
int32 MaxCount, float Density,
|
||||||
|
uint32 SaltBase, uint32 SaltStep, uint32 Salt2, uint32 Salt3,
|
||||||
|
float XYScale, float MinRadius, float MaxRadius,
|
||||||
|
FEmit&& Emit) // Emit(X, Y, Radius, H3)
|
||||||
|
{
|
||||||
|
if (Density <= 0.0f) return;
|
||||||
|
|
||||||
|
for (int32 i = 0; i < MaxCount; i++)
|
||||||
|
{
|
||||||
|
const uint32 H = VoxelHash::Mix(CR.Hash ^ (SaltBase + (uint32)i * SaltStep));
|
||||||
|
if (VoxelHash::ToFloat01(H) > Density) continue;
|
||||||
|
|
||||||
|
const uint32 H2 = VoxelHash::Mix(H ^ Salt2);
|
||||||
|
const uint32 H3 = VoxelHash::Mix(H2 ^ Salt3);
|
||||||
|
|
||||||
|
const float X = CR.Center.X + VoxelHash::ToFloatSigned(H2) * CR.RadiusXY * XYScale;
|
||||||
|
const float Y = CR.Center.Y + VoxelHash::ToFloatSigned(VoxelHash::Mix(H2)) * CR.RadiusXY * XYScale;
|
||||||
|
const float R = FMath::Lerp(MinRadius, MaxRadius, VoxelHash::ToFloat01(H3));
|
||||||
|
|
||||||
|
Emit(X, Y, R, H3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// PHASE 1: BUILD CHUNK CACHE
|
// PHASE 1: BUILD CHUNK CACHE
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -541,6 +579,41 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
float MaxExtent = FMath::Max(BR.RadiusXY * 1.5f, BR.RadiusZ) + BlendK * 3.0f;
|
float MaxExtent = FMath::Max(BR.RadiusXY * 1.5f, BR.RadiusZ) + BlendK * 3.0f;
|
||||||
CR.CullRadiusSq = MaxExtent * MaxExtent;
|
CR.CullRadiusSq = MaxExtent * MaxExtent;
|
||||||
|
|
||||||
|
// --- PRE-BAKED SHAPE ---
|
||||||
|
// Same hash roll + thresholds + capsule trig the evaluator used to redo PER VOXEL;
|
||||||
|
// done once here → EvaluateSDFCached just switches on ShapeType. Bit-identical output.
|
||||||
|
{
|
||||||
|
const uint32 ShapeHash = VoxelHash::Mix(CR.Hash ^ 0xDEADBEEFu);
|
||||||
|
const float ShapeRoll = CR.bIsOrigin ? 0.0f : VoxelHash::ToFloat01(ShapeHash);
|
||||||
|
const float BoxThreshold = 1.0f - Params.RoomShapeVariety * 0.5f;
|
||||||
|
const float CapsuleThreshold = 1.0f - Params.RoomShapeVariety * 0.2f;
|
||||||
|
|
||||||
|
if (ShapeRoll >= BoxThreshold && ShapeRoll < CapsuleThreshold)
|
||||||
|
{
|
||||||
|
// ROUNDED BOX: angular chamber with smooth corners
|
||||||
|
CR.ShapeType = 1;
|
||||||
|
CR.ShapeA = FVector(CR.RadiusXY * 0.8f, CR.RadiusXY * 0.8f, CR.RadiusZ * 0.8f);
|
||||||
|
CR.ShapeR = CR.RadiusXY * 0.25f;
|
||||||
|
}
|
||||||
|
else if (ShapeRoll >= CapsuleThreshold)
|
||||||
|
{
|
||||||
|
// ELONGATED CAPSULE: stretched hall/corridor-room
|
||||||
|
CR.ShapeType = 2;
|
||||||
|
const float DirAngle = VoxelHash::ToFloat01(VoxelHash::Mix(CR.Hash ^ 0xCAFEBABEu)) * 2.0f * PI;
|
||||||
|
const float StretchDist = CR.RadiusXY * 0.7f;
|
||||||
|
const FVector Dir(FMath::Cos(DirAngle), FMath::Sin(DirAngle), 0.0f);
|
||||||
|
CR.ShapeA = CR.Center + Dir * StretchDist;
|
||||||
|
CR.ShapeB = CR.Center - Dir * StretchDist;
|
||||||
|
CR.ShapeR = FMath::Min(CR.RadiusXY * 0.6f, CR.RadiusZ);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// ELLIPSOID (default): smooth oval chamber
|
||||||
|
CR.ShapeType = 0;
|
||||||
|
CR.ShapeA = FVector(CR.RadiusXY, CR.RadiusXY, CR.RadiusZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Flat floor cut: soft floor plane per room, hash-rolled from [Min, Max].
|
// Flat floor cut: soft floor plane per room, hash-rolled from [Min, Max].
|
||||||
// SmoothMax applied in EvaluateSDFCached so tunnels/pits don't create hard seams.
|
// SmoothMax applied in EvaluateSDFCached so tunnels/pits don't create hard seams.
|
||||||
// Sentinel -FLT_MAX means "no cut" so the per-voxel check is a single compare.
|
// Sentinel -FLT_MAX means "no cut" so the per-voxel check is a single compare.
|
||||||
@@ -604,110 +677,67 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
OpParams = FStrateGenerationParams{};
|
OpParams = FStrateGenerationParams{};
|
||||||
CR.RoomOp->ApplyTo(OpParams, CR.RoomOpWeight);
|
CR.RoomOp->ApplyTo(OpParams, CR.RoomOpWeight);
|
||||||
|
|
||||||
// PITS
|
// PITS — downward shafts anchored in the room's lower half.
|
||||||
if (OpParams.PitDensity > 0.0f)
|
BakeRoomFeature(CR, /*Max*/2, OpParams.PitDensity,
|
||||||
{
|
0xDE1A7Eu, 6271u, 0xABCDu, 0x5EEDu,
|
||||||
const int32 MaxPits = 2;
|
/*XYScale*/0.6f, OpParams.PitMinRadius, OpParams.PitMaxRadius,
|
||||||
for (int32 i = 0; i < MaxPits; i++)
|
[&](float PX, float PY, float PitRadius, uint32 PH3)
|
||||||
{
|
{
|
||||||
uint32 PH = VoxelHash::Mix(BR.Hash ^ (0xDE1A7Eu + (uint32)i * 6271u));
|
const uint32 PH4 = VoxelHash::Mix(PH3 ^ 0xF00Du);
|
||||||
if (VoxelHash::ToFloat01(PH) > OpParams.PitDensity) continue;
|
|
||||||
|
|
||||||
uint32 PH2 = VoxelHash::Mix(PH ^ 0xABCDu);
|
|
||||||
uint32 PH3 = VoxelHash::Mix(PH2 ^ 0x5EEDu);
|
|
||||||
uint32 PH4 = VoxelHash::Mix(PH3 ^ 0xF00Du);
|
|
||||||
|
|
||||||
float PX = CR.Center.X + VoxelHash::ToFloatSigned(PH2) * CR.RadiusXY * 0.6f;
|
|
||||||
float PY = CR.Center.Y + VoxelHash::ToFloatSigned(VoxelHash::Mix(PH2)) * CR.RadiusXY * 0.6f;
|
|
||||||
|
|
||||||
float PitRadius = FMath::Lerp(OpParams.PitMinRadius, OpParams.PitMaxRadius,
|
|
||||||
VoxelHash::ToFloat01(PH3));
|
|
||||||
|
|
||||||
float PitTopZ = CR.Center.Z - CR.RadiusZ * 0.5f
|
|
||||||
+ VoxelHash::ToFloat01(PH4) * CR.RadiusZ * 0.2f;
|
|
||||||
|
|
||||||
FCachedPit Pit;
|
FCachedPit Pit;
|
||||||
Pit.CenterX = PX;
|
Pit.CenterX = PX;
|
||||||
Pit.CenterY = PY;
|
Pit.CenterY = PY;
|
||||||
Pit.TopZ = PitTopZ;
|
Pit.TopZ = CR.Center.Z - CR.RadiusZ * 0.5f
|
||||||
|
+ VoxelHash::ToFloat01(PH4) * CR.RadiusZ * 0.2f;
|
||||||
Pit.Radius = PitRadius;
|
Pit.Radius = PitRadius;
|
||||||
Pit.Depth = OpParams.PitDepth;
|
Pit.Depth = OpParams.PitDepth;
|
||||||
Pit.FlareDist = PitRadius * 2.0f;
|
Pit.FlareDist = PitRadius * 2.0f;
|
||||||
Pit.FlareExtra = PitRadius * 1.0f;
|
Pit.FlareExtra = PitRadius * 1.0f;
|
||||||
Pit.BaseDensity = Params.BaseDensity;
|
Pit.BaseDensity = Params.BaseDensity;
|
||||||
Pit.BlendK = Params.SDFBlendRadius;
|
Pit.BlendK = Params.SDFBlendRadius;
|
||||||
float MaxXYR = PitRadius + PitRadius + Params.SDFBlendRadius + 4.0f;
|
const float MaxXYR = PitRadius + PitRadius + Params.SDFBlendRadius + 4.0f;
|
||||||
Pit.BoundXYRadiusSq = MaxXYR * MaxXYR;
|
Pit.BoundXYRadiusSq = MaxXYR * MaxXYR;
|
||||||
OutCache.Pits.Add(Pit);
|
OutCache.Pits.Add(Pit);
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// CHIMNEYS
|
// CHIMNEYS — mirror of pits: upward tubes anchored in the room's upper half.
|
||||||
if (OpParams.ChimneyDensity > 0.0f)
|
BakeRoomFeature(CR, /*Max*/2, OpParams.ChimneyDensity,
|
||||||
{
|
0xC4F007u, 7919u, 0x1337u, 0xCAFEu,
|
||||||
const int32 MaxChimneys = 2;
|
/*XYScale*/0.6f, OpParams.ChimneyMinRadius, OpParams.ChimneyMaxRadius,
|
||||||
for (int32 i = 0; i < MaxChimneys; i++)
|
[&](float CX, float CY, float ChmRadius, uint32 CH3)
|
||||||
{
|
{
|
||||||
uint32 CH = VoxelHash::Mix(BR.Hash ^ (0xC4F007u + (uint32)i * 7919u));
|
const uint32 CH4 = VoxelHash::Mix(CH3 ^ 0xD00Du);
|
||||||
if (VoxelHash::ToFloat01(CH) > OpParams.ChimneyDensity) continue;
|
|
||||||
|
|
||||||
uint32 CH2 = VoxelHash::Mix(CH ^ 0x1337u);
|
|
||||||
uint32 CH3 = VoxelHash::Mix(CH2 ^ 0xCAFEu);
|
|
||||||
uint32 CH4 = VoxelHash::Mix(CH3 ^ 0xD00Du);
|
|
||||||
|
|
||||||
float CX = CR.Center.X + VoxelHash::ToFloatSigned(CH2) * CR.RadiusXY * 0.6f;
|
|
||||||
float CY = CR.Center.Y + VoxelHash::ToFloatSigned(VoxelHash::Mix(CH2)) * CR.RadiusXY * 0.6f;
|
|
||||||
|
|
||||||
float ChmRadius = FMath::Lerp(OpParams.ChimneyMinRadius, OpParams.ChimneyMaxRadius,
|
|
||||||
VoxelHash::ToFloat01(CH3));
|
|
||||||
|
|
||||||
float ChmBottomZ = CR.Center.Z + CR.RadiusZ * 0.5f
|
|
||||||
- VoxelHash::ToFloat01(CH4) * CR.RadiusZ * 0.2f;
|
|
||||||
|
|
||||||
FCachedChimney Chim;
|
FCachedChimney Chim;
|
||||||
Chim.CenterX = CX;
|
Chim.CenterX = CX;
|
||||||
Chim.CenterY = CY;
|
Chim.CenterY = CY;
|
||||||
Chim.BottomZ = ChmBottomZ;
|
Chim.BottomZ = CR.Center.Z + CR.RadiusZ * 0.5f
|
||||||
|
- VoxelHash::ToFloat01(CH4) * CR.RadiusZ * 0.2f;
|
||||||
Chim.Radius = ChmRadius;
|
Chim.Radius = ChmRadius;
|
||||||
Chim.Height = OpParams.ChimneyHeight;
|
Chim.Height = OpParams.ChimneyHeight;
|
||||||
Chim.FlareDist = ChmRadius * 2.0f;
|
Chim.FlareDist = ChmRadius * 2.0f;
|
||||||
Chim.FlareExtra = ChmRadius * 1.0f;
|
Chim.FlareExtra = ChmRadius * 1.0f;
|
||||||
Chim.BaseDensity = Params.BaseDensity;
|
Chim.BaseDensity = Params.BaseDensity;
|
||||||
Chim.BlendK = Params.SDFBlendRadius;
|
Chim.BlendK = Params.SDFBlendRadius;
|
||||||
float MaxXYR = ChmRadius + ChmRadius + Params.SDFBlendRadius + 4.0f;
|
const float MaxXYR = ChmRadius + ChmRadius + Params.SDFBlendRadius + 4.0f;
|
||||||
Chim.BoundXYRadiusSq = MaxXYR * MaxXYR;
|
Chim.BoundXYRadiusSq = MaxXYR * MaxXYR;
|
||||||
OutCache.Chimneys.Add(Chim);
|
OutCache.Chimneys.Add(Chim);
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// COLUMNS
|
// COLUMNS — full-height solid cylinders (no Z anchor, no flare).
|
||||||
if (OpParams.ColumnDensity > 0.0f)
|
BakeRoomFeature(CR, /*Max*/4, OpParams.ColumnDensity,
|
||||||
{
|
0xC01C01u, 3571u, 0x1A2B3Cu, 0xBEEFu,
|
||||||
const int32 MaxCols = 4;
|
/*XYScale*/0.75f, OpParams.ColumnMinRadius, OpParams.ColumnMaxRadius,
|
||||||
for (int32 i = 0; i < MaxCols; i++)
|
[&](float ColX, float ColY, float ColR, uint32 /*H3*/)
|
||||||
{
|
{
|
||||||
uint32 H = VoxelHash::Mix(BR.Hash ^ (0xC01C01u + (uint32)i * 3571u));
|
|
||||||
if (VoxelHash::ToFloat01(H) > OpParams.ColumnDensity) continue;
|
|
||||||
|
|
||||||
uint32 H2 = VoxelHash::Mix(H ^ 0x1A2B3Cu);
|
|
||||||
|
|
||||||
float ColX = CR.Center.X + VoxelHash::ToFloatSigned(H2) * CR.RadiusXY * 0.75f;
|
|
||||||
float ColY = CR.Center.Y + VoxelHash::ToFloatSigned(VoxelHash::Mix(H2)) * CR.RadiusXY * 0.75f;
|
|
||||||
|
|
||||||
uint32 H3 = VoxelHash::Mix(H2 ^ 0xBEEFu);
|
|
||||||
float ColR = FMath::Lerp(OpParams.ColumnMinRadius, OpParams.ColumnMaxRadius,
|
|
||||||
VoxelHash::ToFloat01(H3));
|
|
||||||
|
|
||||||
FCachedColumn Col;
|
FCachedColumn Col;
|
||||||
Col.CenterX = ColX;
|
Col.CenterX = ColX;
|
||||||
Col.CenterY = ColY;
|
Col.CenterY = ColY;
|
||||||
Col.Radius = ColR;
|
Col.Radius = ColR;
|
||||||
Col.BaseDensity = Params.BaseDensity;
|
Col.BaseDensity = Params.BaseDensity;
|
||||||
float MaxXYR = ColR + 6.0f;
|
const float MaxXYR = ColR + 6.0f;
|
||||||
Col.BoundXYRadiusSq = MaxXYR * MaxXYR;
|
Col.BoundXYRadiusSq = MaxXYR * MaxXYR;
|
||||||
OutCache.Columns.Add(Col);
|
OutCache.Columns.Add(Col);
|
||||||
}
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -723,7 +753,6 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
|||||||
float WorldX, float WorldY, float WorldZ,
|
float WorldX, float WorldY, float WorldZ,
|
||||||
const FChunkSDFCache& Cache,
|
const FChunkSDFCache& Cache,
|
||||||
float SDFBlendRadius,
|
float SDFBlendRadius,
|
||||||
float RoomShapeVariety,
|
|
||||||
int32* OutNearestRoomIdx)
|
int32* OutNearestRoomIdx)
|
||||||
{
|
{
|
||||||
float MinSDF = FLT_MAX;
|
float MinSDF = FLT_MAX;
|
||||||
@@ -748,42 +777,13 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
|||||||
const float DistSq = FVector::DistSquared(Pos, Room.Center);
|
const float DistSq = FVector::DistSquared(Pos, Room.Center);
|
||||||
if (DistSq > Room.CullRadiusSq) continue;
|
if (DistSq > Room.CullRadiusSq) continue;
|
||||||
|
|
||||||
// --- SHAPE SELECTION ---
|
// --- SHAPE (pre-baked in BuildChunkCache — no per-voxel hash roll / trig) ---
|
||||||
float RoomSDF;
|
float RoomSDF;
|
||||||
const uint32 ShapeHash = VoxelHash::Mix(Room.Hash ^ 0xDEADBEEFu);
|
switch (Room.ShapeType)
|
||||||
const float ShapeRoll = Room.bIsOrigin ? 0.0f : VoxelHash::ToFloat01(ShapeHash);
|
|
||||||
|
|
||||||
// Thresholds: Variety=0 → all ellipsoid. Variety=1 → 50/30/20 split.
|
|
||||||
const float BoxThreshold = 1.0f - RoomShapeVariety * 0.5f;
|
|
||||||
const float CapsuleThreshold = 1.0f - RoomShapeVariety * 0.2f;
|
|
||||||
|
|
||||||
if (ShapeRoll >= BoxThreshold && ShapeRoll < CapsuleThreshold)
|
|
||||||
{
|
{
|
||||||
// ROUNDED BOX: angular chamber with smooth corners
|
case 1: RoomSDF = VoxelSDF::RoundedBox(Pos, Room.Center, Room.ShapeA, Room.ShapeR); break;
|
||||||
FVector HalfExtent(
|
case 2: RoomSDF = VoxelSDF::Capsule(Pos, Room.ShapeA, Room.ShapeB, Room.ShapeR); break;
|
||||||
Room.RadiusXY * 0.8f,
|
default: RoomSDF = VoxelSDF::Ellipsoid(Pos, Room.Center, Room.ShapeA); break;
|
||||||
Room.RadiusXY * 0.8f,
|
|
||||||
Room.RadiusZ * 0.8f
|
|
||||||
);
|
|
||||||
float Rounding = Room.RadiusXY * 0.25f;
|
|
||||||
RoomSDF = VoxelSDF::RoundedBox(Pos, Room.Center, HalfExtent, Rounding);
|
|
||||||
}
|
|
||||||
else if (ShapeRoll >= CapsuleThreshold)
|
|
||||||
{
|
|
||||||
// ELONGATED CAPSULE: stretched hall/corridor-room
|
|
||||||
float DirAngle = VoxelHash::ToFloat01(VoxelHash::Mix(Room.Hash ^ 0xCAFEBABEu)) * 2.0f * PI;
|
|
||||||
float StretchDist = Room.RadiusXY * 0.7f;
|
|
||||||
FVector Dir(FMath::Cos(DirAngle), FMath::Sin(DirAngle), 0.0f);
|
|
||||||
FVector EndA = Room.Center + Dir * StretchDist;
|
|
||||||
FVector EndB = Room.Center - Dir * StretchDist;
|
|
||||||
float CapsuleR = FMath::Min(Room.RadiusXY * 0.6f, Room.RadiusZ);
|
|
||||||
RoomSDF = VoxelSDF::Capsule(Pos, EndA, EndB, CapsuleR);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// ELLIPSOID (default): smooth oval chamber
|
|
||||||
const FVector Radii(Room.RadiusXY, Room.RadiusXY, Room.RadiusZ);
|
|
||||||
RoomSDF = VoxelSDF::Ellipsoid(Pos, Room.Center, Radii);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Soft floor: SmoothMax of the room SDF and the floor half-space.
|
// Soft floor: SmoothMax of the room SDF and the floor half-space.
|
||||||
@@ -883,6 +883,6 @@ float VoxelCaveMorphology::EvaluateSDF(
|
|||||||
|
|
||||||
return EvaluateSDFCached(
|
return EvaluateSDFCached(
|
||||||
WorldX, WorldY, WorldZ,
|
WorldX, WorldY, WorldZ,
|
||||||
TempCache, Params.SDFBlendRadius, Params.RoomShapeVariety
|
TempCache, Params.SDFBlendRadius
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
// HISM instances are exempt — they're batched render data, capped per entry by MaxPerChunk.
|
// HISM instances are exempt — they're batched render data, capped per entry by MaxPerChunk.
|
||||||
static constexpr int32 GMaxDecorationActorsPerCell = 400;
|
static constexpr int32 GMaxDecorationActorsPerCell = 400;
|
||||||
|
|
||||||
|
// Hard cap on total companion satellites (level 1 + level 2) spawned per placed PARENT — the safety net that
|
||||||
|
// makes 2-level nesting impossible to blow up regardless of authored counts.
|
||||||
|
static constexpr int32 GMaxCompanionsPerParent = 256;
|
||||||
|
|
||||||
// One cell = one chunk XY footprint (so DecorationRadiusChunks reads as a radius in chunks, and a
|
// One cell = one chunk XY footprint (so DecorationRadiusChunks reads as a radius in chunks, and a
|
||||||
// decoration's per-cell MaxPerChunk keeps its "per chunk" meaning).
|
// decoration's per-cell MaxPerChunk keeps its "per chunk" meaning).
|
||||||
static constexpr int32 DECO_CELL_VOXELS = CHUNK_SIZE;
|
static constexpr int32 DECO_CELL_VOXELS = CHUNK_SIZE;
|
||||||
@@ -429,10 +433,10 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
|||||||
CosMinSlope.SetNumUninitialized(Entries.Num());
|
CosMinSlope.SetNumUninitialized(Entries.Num());
|
||||||
for (int32 e = 0; e < Entries.Num(); ++e)
|
for (int32 e = 0; e < Entries.Num(); ++e)
|
||||||
{
|
{
|
||||||
CosMaxSlope[e] = (Entries[e].MaxSlopeAngle < 89.99f)
|
CosMaxSlope[e] = (Entries[e].Profile.MaxSlopeAngle < 89.99f)
|
||||||
? FMath::Cos(FMath::DegreesToRadians(Entries[e].MaxSlopeAngle)) : -1.0f;
|
? FMath::Cos(FMath::DegreesToRadians(Entries[e].Profile.MaxSlopeAngle)) : -1.0f;
|
||||||
CosMinSlope[e] = (Entries[e].MinSlopeAngle > 0.01f)
|
CosMinSlope[e] = (Entries[e].Profile.MinSlopeAngle > 0.01f)
|
||||||
? FMath::Cos(FMath::DegreesToRadians(Entries[e].MinSlopeAngle)) : -1.0f;
|
? FMath::Cos(FMath::DegreesToRadians(Entries[e].Profile.MinSlopeAngle)) : -1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-COLUMN biome cache: ResolveBiomeSampleAt's noise-heavy cell classification is box-validated
|
// Per-COLUMN biome cache: ResolveBiomeSampleAt's noise-heavy cell classification is box-validated
|
||||||
@@ -443,6 +447,26 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
|||||||
|
|
||||||
auto D = [&](float VX, float VY, float VZ) { return Gen->GetDensityAt(VX, VY, VZ); };
|
auto D = [&](float VX, float VY, float VZ) { return Gen->GetDensityAt(VX, VY, VZ); };
|
||||||
|
|
||||||
|
// Build a companion satellite's transform from its profile + per-instance hash at a snapped surface point
|
||||||
|
// (voxel XY/Z + outward normal). Shared by level-1 and level-2 companions.
|
||||||
|
auto MakeCompanionXf = [&](const FPlacementProfile& P, uint32 IHash,
|
||||||
|
float sVX, float sVY, float sZ, const FVector& sN) -> FTransform
|
||||||
|
{
|
||||||
|
const FVector Local(sVX * VOXEL_SIZE, sVY * VOXEL_SIZE, sZ * VOXEL_SIZE);
|
||||||
|
const FVector Pos = OwnerXf.TransformPosition(Local) + sN * P.SurfaceOffset + P.LocationOffset;
|
||||||
|
FQuat Q = P.bAlignToSurface ? FRotationMatrix::MakeFromZ(sN).ToQuat() : FQuat::Identity;
|
||||||
|
Q = Q * P.RotationOffset.Quaternion();
|
||||||
|
if (!P.RandomRotation.IsNearlyZero())
|
||||||
|
{
|
||||||
|
const float rp = (VoxelHash::ToFloat01(VoxelHash::Mix(IHash ^ 0x1111A1u)) - 0.5f) * P.RandomRotation.Pitch;
|
||||||
|
const float ry = (VoxelHash::ToFloat01(VoxelHash::Mix(IHash ^ 0x2222B2u)) - 0.5f) * P.RandomRotation.Yaw;
|
||||||
|
const float rr = (VoxelHash::ToFloat01(VoxelHash::Mix(IHash ^ 0x3333C3u)) - 0.5f) * P.RandomRotation.Roll;
|
||||||
|
Q = Q * FRotator(rp, ry, rr).Quaternion();
|
||||||
|
}
|
||||||
|
const float Sc = FMath::Lerp(P.MinScale, P.MaxScale, VoxelHash::ToFloat01(VoxelHash::Mix(IHash ^ 0x5CA1E000u)));
|
||||||
|
return FTransform(Q, Pos, FVector(Sc));
|
||||||
|
};
|
||||||
|
|
||||||
// Shared: roll every decoration entry at one surface point (voxel XY, voxel Z, outward world normal)
|
// Shared: roll every decoration entry at one surface point (voxel XY, voxel Z, outward world normal)
|
||||||
// and append the passing ones to OutSpawns. CrossingIdx salts the hash so stacked surfaces differ.
|
// and append the passing ones to OutSpawns. CrossingIdx salts the hash so stacked surfaces differ.
|
||||||
// ColBiome = the column's dominant context-biome index (-1 when biomes are off); an entry is rolled
|
// ColBiome = the column's dominant context-biome index (-1 when biomes are off); an entry is rolled
|
||||||
@@ -465,14 +489,14 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
|||||||
if (EntryBiome[EntryIdx] != ColBiome) continue;
|
if (EntryBiome[EntryIdx] != ColBiome) continue;
|
||||||
|
|
||||||
const FStrateDecoration& Deco = Entries[EntryIdx];
|
const FStrateDecoration& Deco = Entries[EntryIdx];
|
||||||
const bool bInstanced = (Deco.InstancedMesh != nullptr);
|
const bool bInstanced = (Deco.Profile.InstancedMesh != nullptr);
|
||||||
if (!bInstanced && !Deco.ActorClass) continue;
|
if (!bInstanced && !Deco.Profile.ActorClass) continue;
|
||||||
if (Deco.SpawnDensity <= 0.0f) continue;
|
if (Deco.SpawnDensity <= 0.0f) continue;
|
||||||
if (EntryCount[EntryIdx] >= Deco.MaxPerChunk) continue;
|
if (EntryCount[EntryIdx] >= Deco.MaxPerChunk) continue;
|
||||||
if (!bInstanced && TotalActors >= GMaxDecorationActorsPerCell) continue;
|
if (!bInstanced && TotalActors >= GMaxDecorationActorsPerCell) continue;
|
||||||
|
|
||||||
bool bMatches = true;
|
bool bMatches = true;
|
||||||
switch (Deco.SurfacePlacement)
|
switch (Deco.Profile.SurfacePlacement)
|
||||||
{
|
{
|
||||||
case ESurfaceType::Floor: bMatches = bFloor; break;
|
case ESurfaceType::Floor: bMatches = bFloor; break;
|
||||||
case ESurfaceType::Wall: bMatches = bWall; break;
|
case ESurfaceType::Wall: bMatches = bWall; break;
|
||||||
@@ -484,7 +508,7 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
|||||||
// Strict-wall overhang gate: a "wall" point also covers surfaces that lean slightly downward
|
// Strict-wall overhang gate: a "wall" point also covers surfaces that lean slightly downward
|
||||||
// (N.Z in [-0.5, 0)). For props flagged wall-only-upright, drop those so overhangs don't take
|
// (N.Z in [-0.5, 0)). For props flagged wall-only-upright, drop those so overhangs don't take
|
||||||
// wall decals. Applies whenever the point IS a wall (independent of Floor/Wall/Any setting).
|
// wall decals. Applies whenever the point IS a wall (independent of Floor/Wall/Any setting).
|
||||||
if (bWall && Deco.bWallExcludeOverhangs && NormalWorld.Z < 0.0f) continue;
|
if (bWall && Deco.Profile.bWallExcludeOverhangs && NormalWorld.Z < 0.0f) continue;
|
||||||
|
|
||||||
// Surface-tilt gates: tilt = acos(|N.Z|) (0 = flat, 90 = vertical). |N.Z| < cos(MaxSlope) ⇔
|
// Surface-tilt gates: tilt = acos(|N.Z|) (0 = flat, 90 = vertical). |N.Z| < cos(MaxSlope) ⇔
|
||||||
// tilt > MaxSlope (skip steeper); |N.Z| > cos(MinSlope) ⇔ tilt < MinSlope (skip flatter).
|
// tilt > MaxSlope (skip steeper); |N.Z| > cos(MinSlope) ⇔ tilt < MinSlope (skip flatter).
|
||||||
@@ -495,26 +519,34 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
|||||||
const uint32 H = DecoHash(Cell.X, Cell.Y, gx, gy, CrossingIdx, EntryIdx, InSeed, 0xDEC0u);
|
const uint32 H = DecoHash(Cell.X, Cell.Y, gx, gy, CrossingIdx, EntryIdx, InSeed, 0xDEC0u);
|
||||||
if (VoxelHash::ToFloat01(H) > Deco.SpawnDensity) continue;
|
if (VoxelHash::ToFloat01(H) > Deco.SpawnDensity) continue;
|
||||||
|
|
||||||
if (Deco.bRequireWaterRelative && Ctx.bHasWater)
|
if (Deco.Profile.bRequireWaterRelative && Ctx.bHasWater)
|
||||||
{
|
{
|
||||||
if (bBelowWater != Deco.bPlaceBelowWater) continue;
|
if (bBelowWater != Deco.Profile.bPlaceBelowWater) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FVector SpawnPos = PosWorld + NormalWorld * Deco.SurfaceOffset;
|
// F7 aware placement: relational conditions (relief/moisture/biome-border). Opt-in per entry —
|
||||||
FQuat BaseQ = Deco.bAlignToSurface
|
// skipped entirely when the list is empty. Worker-safe pure query; uses the cell's biome context.
|
||||||
|
if (Deco.Profile.Conditions.Num() > 0 &&
|
||||||
|
!Gen->EvaluateTerrainConditions(Deco.Profile.Conditions, VX, VY, Ctx.BiomeCtx)) continue;
|
||||||
|
|
||||||
|
const FVector SpawnPos = PosWorld + NormalWorld * Deco.Profile.SurfaceOffset + Deco.Profile.LocationOffset;
|
||||||
|
// Rotation: optional surface-align → fixed offset → per-axis hash random (same model as landmarks).
|
||||||
|
// RandomRotation.Yaw defaults to 360 for decoration (see FStrateDecoration ctor) = full random
|
||||||
|
// heading, reproducing the legacy random-yaw look; the exact per-instance yaw values reshuffle
|
||||||
|
// once (different hash mix) but the distribution is identical.
|
||||||
|
FQuat BaseQ = Deco.Profile.bAlignToSurface
|
||||||
? FRotationMatrix::MakeFromZ(NormalWorld).ToQuat()
|
? FRotationMatrix::MakeFromZ(NormalWorld).ToQuat()
|
||||||
: FQuat::Identity;
|
: FQuat::Identity;
|
||||||
if (Deco.bRandomYaw)
|
BaseQ = BaseQ * Deco.Profile.RotationOffset.Quaternion();
|
||||||
|
if (!Deco.Profile.RandomRotation.IsNearlyZero())
|
||||||
{
|
{
|
||||||
// Roll within [MinYaw, MaxYaw]; the default 0..360 reproduces the legacy full-turn roll
|
const float rp = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x1111A1u)) - 0.5f) * Deco.Profile.RandomRotation.Pitch;
|
||||||
// bit-for-bit (Lerp(0,360,t)° == t·2π rad), so existing assets are unchanged.
|
const float ry = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x2222B2u)) - 0.5f) * Deco.Profile.RandomRotation.Yaw;
|
||||||
const float YawT = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x59415721u));
|
const float rr = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x3333C3u)) - 0.5f) * Deco.Profile.RandomRotation.Roll;
|
||||||
const float Yaw = FMath::DegreesToRadians(FMath::Lerp(Deco.MinYaw, Deco.MaxYaw, YawT));
|
BaseQ = BaseQ * FRotator(rp, ry, rr).Quaternion();
|
||||||
const FVector Axis = Deco.bAlignToSurface ? NormalWorld : FVector::UpVector;
|
|
||||||
BaseQ = FQuat(Axis, Yaw) * BaseQ;
|
|
||||||
}
|
}
|
||||||
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.Profile.MinScale, Deco.Profile.MaxScale, ScaleT);
|
||||||
|
|
||||||
FDecoSpawn& Out = OutSpawns.AddDefaulted_GetRef();
|
FDecoSpawn& Out = OutSpawns.AddDefaulted_GetRef();
|
||||||
Out.EntryIdx = EntryIdx;
|
Out.EntryIdx = EntryIdx;
|
||||||
@@ -522,6 +554,96 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
|||||||
Out.Xf = FTransform(BaseQ, SpawnPos, FVector(Scale));
|
Out.Xf = FTransform(BaseQ, SpawnPos, FVector(Scale));
|
||||||
++EntryCount[EntryIdx];
|
++EntryCount[EntryIdx];
|
||||||
if (!bInstanced) ++TotalActors;
|
if (!bInstanced) ++TotalActors;
|
||||||
|
|
||||||
|
// ---- Companions (F7 relational placement): deterministic cluster satellites around this parent.
|
||||||
|
// Level 1 re-snaps to the real surface at its own XY; level-2 SubCompanions inherit their L1
|
||||||
|
// satellite's point (no re-snap → cheap). A per-parent budget caps the total so nesting can't blow
|
||||||
|
// up. Pure function of the parent hash H → no "did a tree spawn here?" search.
|
||||||
|
int32 CompBudget = GMaxCompanionsPerParent;
|
||||||
|
for (int32 ci = 0; ci < Deco.Companions.Num() && CompBudget > 0; ++ci)
|
||||||
|
{
|
||||||
|
const FDecoCompanion& Comp = Deco.Companions[ci];
|
||||||
|
const bool bCompInst = (Comp.Profile.InstancedMesh != nullptr);
|
||||||
|
if (!bCompInst && !Comp.Profile.ActorClass) continue;
|
||||||
|
|
||||||
|
const uint32 CH = VoxelHash::Mix(H ^ (0x00C0FFEEu + (uint32)ci * 0x9E3779B1u));
|
||||||
|
if (VoxelHash::ToFloat01(CH) > Comp.Probability) continue;
|
||||||
|
|
||||||
|
const int32 Span = FMath::Max(0, Comp.CountMax - Comp.CountMin);
|
||||||
|
const int32 Count = Comp.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(CH ^ 0x1234u)) * (float)(Span + 1));
|
||||||
|
for (int32 ii = 0; ii < Count; ++ii)
|
||||||
|
{
|
||||||
|
if (CompBudget <= 0) break;
|
||||||
|
if (!bCompInst && TotalActors >= GMaxDecorationActorsPerCell) break; // actor budget
|
||||||
|
|
||||||
|
const uint32 IH = VoxelHash::Mix(CH ^ ((uint32)ii * 0x85EBCA77u + 0x2545F491u));
|
||||||
|
const float Ang = VoxelHash::ToFloat01(IH) * 2.0f * PI;
|
||||||
|
const float RadVox = FMath::Lerp(Comp.RadiusMinVox, Comp.RadiusMaxVox,
|
||||||
|
VoxelHash::ToFloat01(VoxelHash::Mix(IH ^ 0x77u)));
|
||||||
|
const float SatVX = VX + FMath::Cos(Ang) * RadVox; // voxel, actor-local
|
||||||
|
const float SatVY = VY + FMath::Sin(Ang) * RadVox;
|
||||||
|
|
||||||
|
// Optional per-satellite gating (relief/moisture/biome-border at ITS own XY).
|
||||||
|
if (Comp.Profile.Conditions.Num() > 0 &&
|
||||||
|
!Gen->EvaluateTerrainConditions(Comp.Profile.Conditions, SatVX, SatVY, Ctx.BiomeCtx)) continue;
|
||||||
|
|
||||||
|
// Surface: SNAP to the real ground/ceiling at the satellite XY (kills floaters); or inherit.
|
||||||
|
float SatZ; FVector SatN;
|
||||||
|
if (Comp.bSnapToSurface)
|
||||||
|
{
|
||||||
|
if (!FindLandmarkColumn(Gen, OwnerXf, Ctx, SatVX, SatVY, Comp.Profile.SurfacePlacement,
|
||||||
|
Step, ColumnDepth, SatZ, SatN)) continue; // no surface → no floater
|
||||||
|
}
|
||||||
|
else { SatZ = ZC; SatN = NormalWorld; }
|
||||||
|
|
||||||
|
FDecoSpawn& CO = OutSpawns.AddDefaulted_GetRef();
|
||||||
|
CO.EntryIdx = EntryIdx;
|
||||||
|
CO.CompanionIdx = ci;
|
||||||
|
CO.bInstanced = bCompInst;
|
||||||
|
CO.Xf = MakeCompanionXf(Comp.Profile, IH, SatVX, SatVY, SatZ, SatN);
|
||||||
|
if (!bCompInst) ++TotalActors;
|
||||||
|
--CompBudget;
|
||||||
|
|
||||||
|
// ---- Level 2: SubCompanions ON this satellite (moss on a rock). Inherit the L1 satellite's
|
||||||
|
// snapped point (SatZ, SatN) — no re-snap, so nesting stays cheap. Small disk around it.
|
||||||
|
for (int32 si = 0; si < Comp.SubCompanions.Num() && CompBudget > 0; ++si)
|
||||||
|
{
|
||||||
|
const FDecoSubCompanion& Sub = Comp.SubCompanions[si];
|
||||||
|
const bool bSubInst = (Sub.Profile.InstancedMesh != nullptr);
|
||||||
|
if (!bSubInst && !Sub.Profile.ActorClass) continue;
|
||||||
|
|
||||||
|
const uint32 SH = VoxelHash::Mix(IH ^ (0x0000544Bu + (uint32)si * 0x27D4EB2Fu));
|
||||||
|
if (VoxelHash::ToFloat01(SH) > Sub.Probability) continue;
|
||||||
|
|
||||||
|
const int32 SubSpan = FMath::Max(0, Sub.CountMax - Sub.CountMin);
|
||||||
|
const int32 SubCount = Sub.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(SH ^ 0x1234u)) * (float)(SubSpan + 1));
|
||||||
|
for (int32 sj = 0; sj < SubCount; ++sj)
|
||||||
|
{
|
||||||
|
if (CompBudget <= 0) break;
|
||||||
|
if (!bSubInst && TotalActors >= GMaxDecorationActorsPerCell) break;
|
||||||
|
|
||||||
|
const uint32 JH = VoxelHash::Mix(SH ^ ((uint32)sj * 0x85EBCA77u + 0x165667B1u));
|
||||||
|
const float SAng = VoxelHash::ToFloat01(JH) * 2.0f * PI;
|
||||||
|
const float SRadVox = FMath::Lerp(Sub.RadiusMinVox, Sub.RadiusMaxVox,
|
||||||
|
VoxelHash::ToFloat01(VoxelHash::Mix(JH ^ 0x77u)));
|
||||||
|
const float SubVX = SatVX + FMath::Cos(SAng) * SRadVox;
|
||||||
|
const float SubVY = SatVY + FMath::Sin(SAng) * SRadVox;
|
||||||
|
|
||||||
|
if (Sub.Profile.Conditions.Num() > 0 &&
|
||||||
|
!Gen->EvaluateTerrainConditions(Sub.Profile.Conditions, SubVX, SubVY, Ctx.BiomeCtx)) continue;
|
||||||
|
|
||||||
|
FDecoSpawn& SO = OutSpawns.AddDefaulted_GetRef();
|
||||||
|
SO.EntryIdx = EntryIdx;
|
||||||
|
SO.CompanionIdx = ci;
|
||||||
|
SO.SubIdx = si;
|
||||||
|
SO.bInstanced = bSubInst;
|
||||||
|
SO.Xf = MakeCompanionXf(Sub.Profile, JH, SubVX, SubVY, SatZ, SatN);
|
||||||
|
if (!bSubInst) ++TotalActors;
|
||||||
|
--CompBudget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -703,20 +825,30 @@ void UVoxelContentManager::MergeCellResult(FDecoGrid& G, const FDecoCellResult&
|
|||||||
{
|
{
|
||||||
if (!Result.Entries.IsValidIndex(S.EntryIdx)) continue;
|
if (!Result.Entries.IsValidIndex(S.EntryIdx)) continue;
|
||||||
const FStrateDecoration& Deco = Result.Entries[S.EntryIdx];
|
const FStrateDecoration& Deco = Result.Entries[S.EntryIdx];
|
||||||
|
// Resolve which profile owns this spawn: the entry itself (CompanionIdx<0), a level-1 companion, or a
|
||||||
|
// level-2 sub-companion (F7). Each carries its own mesh/actor + render tuning.
|
||||||
|
const FPlacementProfile* ProfPtr = &Deco.Profile;
|
||||||
|
if (S.CompanionIdx >= 0 && Deco.Companions.IsValidIndex(S.CompanionIdx))
|
||||||
|
{
|
||||||
|
const FDecoCompanion& Comp = Deco.Companions[S.CompanionIdx];
|
||||||
|
ProfPtr = (S.SubIdx >= 0 && Comp.SubCompanions.IsValidIndex(S.SubIdx))
|
||||||
|
? &Comp.SubCompanions[S.SubIdx].Profile : &Comp.Profile;
|
||||||
|
}
|
||||||
|
const FPlacementProfile& Prof = *ProfPtr;
|
||||||
|
|
||||||
if (S.bInstanced)
|
if (S.bInstanced)
|
||||||
{
|
{
|
||||||
if (!Deco.InstancedMesh) continue;
|
if (!Prof.InstancedMesh) continue;
|
||||||
// Bucket by MESH so cells (and biomes) sharing a mesh collapse into one region HISM. The first
|
// Bucket by MESH so cells (and biomes) sharing a mesh collapse into one region HISM. The first
|
||||||
// contributor sets the render tuning (cull/shadow/scale) for the whole region's instances.
|
// contributor sets the render tuning (cull/shadow/scale) for the whole region's instances.
|
||||||
FRegionMeshBucket& Bucket = Build->MeshBuckets.FindOrAdd(Deco.InstancedMesh);
|
FRegionMeshBucket& Bucket = Build->MeshBuckets.FindOrAdd(Prof.InstancedMesh);
|
||||||
if (Bucket.Xforms.Num() == 0) { Bucket.Deco = Deco; }
|
if (Bucket.Xforms.Num() == 0) { Bucket.Profile = Prof; }
|
||||||
Bucket.Xforms.Add(S.Xf);
|
Bucket.Xforms.Add(S.Xf);
|
||||||
}
|
}
|
||||||
else if (Deco.ActorClass)
|
else if (Prof.ActorClass)
|
||||||
{
|
{
|
||||||
FRegionActorSpawn& A = Build->ActorSpawns.AddDefaulted_GetRef();
|
FRegionActorSpawn& A = Build->ActorSpawns.AddDefaulted_GetRef();
|
||||||
A.ActorClass = Deco.ActorClass;
|
A.ActorClass = Prof.ActorClass;
|
||||||
A.Xf = S.Xf;
|
A.Xf = S.Xf;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -776,7 +908,7 @@ void UVoxelContentManager::ApplyRegion(FDecoGrid& G, const FIntPoint& Region, FD
|
|||||||
FRegionMeshBucket& Bucket = Pair.Value;
|
FRegionMeshBucket& Bucket = Pair.Value;
|
||||||
UStaticMesh* Mesh = Pair.Key.Get();
|
UStaticMesh* Mesh = Pair.Key.Get();
|
||||||
if (!Mesh || Bucket.Xforms.Num() == 0) continue;
|
if (!Mesh || Bucket.Xforms.Num() == 0) continue;
|
||||||
const FStrateDecoration& Deco = Bucket.Deco;
|
const FPlacementProfile& Prof = Bucket.Profile;
|
||||||
|
|
||||||
UHierarchicalInstancedStaticMeshComponent* HISM =
|
UHierarchicalInstancedStaticMeshComponent* HISM =
|
||||||
NewObject<UHierarchicalInstancedStaticMeshComponent>(OwnerActor);
|
NewObject<UHierarchicalInstancedStaticMeshComponent>(OwnerActor);
|
||||||
@@ -792,11 +924,11 @@ void UVoxelContentManager::ApplyRegion(FDecoGrid& G, const FIntPoint& Region, FD
|
|||||||
// so the render proxy is created once with the final state (no rebuild):
|
// so the render proxy is created once with the final state (no rebuild):
|
||||||
// • CullDistance bounds GPU cost — grass is drawn only near the player even when placed thickly.
|
// • CullDistance bounds GPU cost — grass is drawn only near the player even when placed thickly.
|
||||||
// • bCastShadow off removes the dominant cost of dense instanced foliage.
|
// • bCastShadow off removes the dominant cost of dense instanced foliage.
|
||||||
HISM->SetCastShadow(Deco.bCastShadow);
|
HISM->SetCastShadow(Prof.bCastShadow);
|
||||||
if (Deco.CullDistance > 0.0f)
|
if (Prof.CullDistance > 0.0f)
|
||||||
{
|
{
|
||||||
const int32 End = FMath::Max(1, (int32)Deco.CullDistance);
|
const int32 End = FMath::Max(1, (int32)Prof.CullDistance);
|
||||||
const int32 Start = FMath::Max(1, (int32)(Deco.CullDistance * 0.8f));
|
const int32 Start = FMath::Max(1, (int32)(Prof.CullDistance * 0.8f));
|
||||||
HISM->SetCullDistances(Start, End); // fade band 0.8x→1.0x, then gone
|
HISM->SetCullDistances(Start, End); // fade band 0.8x→1.0x, then gone
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -815,6 +947,21 @@ void UVoxelContentManager::ApplyRegion(FDecoGrid& G, const FIntPoint& Region, FD
|
|||||||
}
|
}
|
||||||
Content.Instances.Add(HISM);
|
Content.Instances.Add(HISM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Landmark footprints (F7): a suppressing landmark cleared whatever decorations existed when it spawned,
|
||||||
|
// but this region just streamed in FRESH → re-clear under any loaded suppressing landmark overlapping it
|
||||||
|
// (the per-HISM spatial query early-outs for the non-overlapping majority). Keeps temple floors clear as
|
||||||
|
// you leave and return.
|
||||||
|
if (Content.Instances.Num() > 0)
|
||||||
|
{
|
||||||
|
for (const TPair<FIntVector, FLandmarkInstance>& LP : LandmarkInstances)
|
||||||
|
{
|
||||||
|
if (LP.Value.SuppressRadiusWorld > 0.0f)
|
||||||
|
{
|
||||||
|
RemoveInstancesInContent(Content, LP.Value.SuppressCenter, LP.Value.SuppressRadiusWorld);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UVoxelContentManager::ClearDecorationRegion(FDecoGrid& G, const FIntPoint& Region)
|
void UVoxelContentManager::ClearDecorationRegion(FDecoGrid& G, const FIntPoint& Region)
|
||||||
@@ -945,93 +1092,111 @@ bool UVoxelContentManager::FindLandmarkColumn(const UVoxelGenerator* Gen, const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UVoxelContentManager::SpawnFromProfile(const FPlacementProfile& P, uint32 H, const FDecoContext& Ctx,
|
||||||
|
const FTransform& OwnerXf, AActor* OwnerActor,
|
||||||
|
float LocalX, float LocalY, float Step, float ColDepth,
|
||||||
|
FTransform& OutXf, FLandmarkInstance& Out)
|
||||||
|
{
|
||||||
|
if (!Generator) return false;
|
||||||
|
const float VX = LocalX / VOXEL_SIZE;
|
||||||
|
const float VY = LocalY / VOXEL_SIZE;
|
||||||
|
|
||||||
|
// Biome filter (resolved at the candidate XY, same field the density/deco paths use).
|
||||||
|
if (P.RequiredBiome)
|
||||||
|
{
|
||||||
|
const UVoxelBiomeDefinition* Bio = Generator->GetDominantBiomeAt(VX, VY, Ctx.RepChunkZ);
|
||||||
|
if (Bio != P.RequiredBiome) return false; // leaves Out empty → evaluated, nothing placed
|
||||||
|
}
|
||||||
|
|
||||||
|
float ZC; FVector N;
|
||||||
|
if (!FindLandmarkColumn(Generator, OwnerXf, Ctx, VX, VY, P.SurfacePlacement, Step, ColDepth, ZC, N))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Surface-tilt gates (acos(|N.Z|); guarded so defaults cost no trig).
|
||||||
|
if (P.MaxSlopeAngle < 89.99f &&
|
||||||
|
FMath::Abs(N.Z) < FMath::Cos(FMath::DegreesToRadians(P.MaxSlopeAngle))) return false;
|
||||||
|
if (P.MinSlopeAngle > 0.01f &&
|
||||||
|
FMath::Abs(N.Z) > FMath::Cos(FMath::DegreesToRadians(P.MinSlopeAngle))) return false;
|
||||||
|
|
||||||
|
const FVector LocalPos(LocalX, LocalY, ZC * VOXEL_SIZE);
|
||||||
|
if (P.bRequireWaterRelative && Ctx.bHasWater)
|
||||||
|
{
|
||||||
|
const bool bBelowWater = (LocalPos.Z < Ctx.WaterLocalZ);
|
||||||
|
if (bBelowWater != P.bPlaceBelowWater) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// F7 aware placement: relational conditions (relief/moisture/biome-border), evaluated at the candidate
|
||||||
|
// XY. Opt-in — empty list is free. "A monument only on high mesas / near a biome edge" lives here.
|
||||||
|
if (P.Conditions.Num() > 0 &&
|
||||||
|
!Generator->EvaluateTerrainConditions(P.Conditions, VX, VY, Ctx.BiomeCtx)) return false;
|
||||||
|
|
||||||
|
// Rotation: optional surface-align → fixed offset → per-axis hash random.
|
||||||
|
FQuat Q = P.bAlignToSurface ? FRotationMatrix::MakeFromZ(N).ToQuat() : FQuat::Identity;
|
||||||
|
Q = Q * P.RotationOffset.Quaternion();
|
||||||
|
if (!P.RandomRotation.IsNearlyZero())
|
||||||
|
{
|
||||||
|
const float rp = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x1111A1u)) - 0.5f) * P.RandomRotation.Pitch;
|
||||||
|
const float ry = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x2222B2u)) - 0.5f) * P.RandomRotation.Yaw;
|
||||||
|
const float rr = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x3333C3u)) - 0.5f) * P.RandomRotation.Roll;
|
||||||
|
Q = Q * FRotator(rp, ry, rr).Quaternion();
|
||||||
|
}
|
||||||
|
|
||||||
|
const float ScaleT = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x5CA1E777u));
|
||||||
|
const float Scale = FMath::Lerp(P.MinScale, P.MaxScale, ScaleT);
|
||||||
|
|
||||||
|
// World-space position + XYZ offset (e.g. +Z lifts a sun off the sky-cap into the cavern).
|
||||||
|
const FVector WorldPos = OwnerXf.TransformPosition(LocalPos) + P.LocationOffset;
|
||||||
|
OutXf = FTransform(Q, WorldPos, FVector(Scale));
|
||||||
|
|
||||||
|
if (P.ActorClass)
|
||||||
|
{
|
||||||
|
UWorld* World = OwnerActor->GetWorld();
|
||||||
|
if (!World) return false;
|
||||||
|
FActorSpawnParameters SP;
|
||||||
|
SP.Owner = OwnerActor;
|
||||||
|
SP.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
|
if (AActor* A = World->SpawnActor<AActor>(P.ActorClass, OutXf, SP)) { Out.Actor = A; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (P.InstancedMesh)
|
||||||
|
{
|
||||||
|
UStaticMeshComponent* C = NewObject<UStaticMeshComponent>(OwnerActor);
|
||||||
|
C->SetStaticMesh(P.InstancedMesh);
|
||||||
|
C->SetMobility(EComponentMobility::Static); // placed once, never moves → cached draw + VSM shadow
|
||||||
|
C->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||||
|
C->SetCastShadow(P.bCastShadow);
|
||||||
|
if (P.CullDistance > 0.0f) { C->SetCullDistance(P.CullDistance); } // 0 = never cull (far sun)
|
||||||
|
C->SetWorldTransform(OutXf);
|
||||||
|
C->RegisterComponent();
|
||||||
|
C->AttachToComponent(OwnerActor->GetRootComponent(), FAttachmentTransformRules::KeepWorldTransform);
|
||||||
|
Out.Component = C;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void UVoxelContentManager::SpawnLandmarkInstance(const FStrateLandmark& L, uint32 H, const FDecoContext& Ctx,
|
void UVoxelContentManager::SpawnLandmarkInstance(const FStrateLandmark& L, uint32 H, const FDecoContext& Ctx,
|
||||||
const FTransform& OwnerXf, AActor* OwnerActor,
|
const FTransform& OwnerXf, AActor* OwnerActor,
|
||||||
float LocalX, float LocalY, float Step, float ColDepth,
|
float LocalX, float LocalY, float Step, float ColDepth,
|
||||||
FLandmarkInstance& Out)
|
FLandmarkInstance& Out)
|
||||||
{
|
{
|
||||||
if (!Generator) return;
|
FTransform Xf;
|
||||||
const float VX = LocalX / VOXEL_SIZE;
|
if (!SpawnFromProfile(L.Profile, H, Ctx, OwnerXf, OwnerActor, LocalX, LocalY, Step, ColDepth, Xf, Out))
|
||||||
const float VY = LocalY / VOXEL_SIZE;
|
return; // Out stays empty → evaluated, nothing placed
|
||||||
|
|
||||||
// Biome filter (resolved at the candidate XY, same field the density/deco paths use).
|
// Mini-sun light orb (landmark-only): record world-space data for the terrain material's raymarched
|
||||||
if (L.RequiredBiome)
|
// shadows. Distances convert voxels→cm (×VOXEL_SIZE); the emitter radius scales with the instance too.
|
||||||
{
|
|
||||||
const UVoxelBiomeDefinition* Bio = Generator->GetDominantBiomeAt(VX, VY, Ctx.RepChunkZ);
|
|
||||||
if (Bio != L.RequiredBiome) return; // leaves Out empty → evaluated, nothing placed
|
|
||||||
}
|
|
||||||
|
|
||||||
float ZC; FVector N;
|
|
||||||
if (!FindLandmarkColumn(Generator, OwnerXf, Ctx, VX, VY, L.SurfacePlacement, Step, ColDepth, ZC, N))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Surface-tilt gates (acos(|N.Z|); guarded so defaults cost no trig).
|
|
||||||
if (L.MaxSlopeAngle < 89.99f &&
|
|
||||||
FMath::Abs(N.Z) < FMath::Cos(FMath::DegreesToRadians(L.MaxSlopeAngle))) return;
|
|
||||||
if (L.MinSlopeAngle > 0.01f &&
|
|
||||||
FMath::Abs(N.Z) > FMath::Cos(FMath::DegreesToRadians(L.MinSlopeAngle))) return;
|
|
||||||
|
|
||||||
const FVector LocalPos(LocalX, LocalY, ZC * VOXEL_SIZE);
|
|
||||||
if (L.bRequireWaterRelative && Ctx.bHasWater)
|
|
||||||
{
|
|
||||||
const bool bBelowWater = (LocalPos.Z < Ctx.WaterLocalZ);
|
|
||||||
if (bBelowWater != L.bPlaceBelowWater) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rotation: optional surface-align → fixed offset → per-axis hash random.
|
|
||||||
FQuat Q = L.bAlignToSurface ? FRotationMatrix::MakeFromZ(N).ToQuat() : FQuat::Identity;
|
|
||||||
Q = Q * L.RotationOffset.Quaternion();
|
|
||||||
if (!L.RandomRotation.IsNearlyZero())
|
|
||||||
{
|
|
||||||
const float rp = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x1111A1u)) - 0.5f) * L.RandomRotation.Pitch;
|
|
||||||
const float ry = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x2222B2u)) - 0.5f) * L.RandomRotation.Yaw;
|
|
||||||
const float rr = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x3333C3u)) - 0.5f) * L.RandomRotation.Roll;
|
|
||||||
Q = Q * FRotator(rp, ry, rr).Quaternion();
|
|
||||||
}
|
|
||||||
|
|
||||||
const float ScaleT = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x5CA1E777u));
|
|
||||||
const float Scale = FMath::Lerp(L.MinScale, L.MaxScale, ScaleT);
|
|
||||||
|
|
||||||
// World-space position + XYZ offset (e.g. +Z lifts a sun off the sky-cap into the cavern).
|
|
||||||
const FVector WorldPos = OwnerXf.TransformPosition(LocalPos) + L.LocationOffset;
|
|
||||||
const FTransform Xf(Q, WorldPos, FVector(Scale));
|
|
||||||
|
|
||||||
// Mini-sun light orb: record world-space data for the terrain material's raymarched shadows. Distances
|
|
||||||
// convert voxels→cm (×VOXEL_SIZE); the emitter radius scales with the instance scale too.
|
|
||||||
if (L.bIsLightOrb)
|
if (L.bIsLightOrb)
|
||||||
{
|
{
|
||||||
|
const float Scale = Xf.GetScale3D().X;
|
||||||
Out.bIsOrb = true;
|
Out.bIsOrb = true;
|
||||||
Out.Orb.WorldPos = WorldPos;
|
Out.Orb.WorldPos = Xf.GetLocation();
|
||||||
Out.Orb.Color = L.OrbColor;
|
Out.Orb.Color = L.OrbColor;
|
||||||
Out.Orb.Intensity = L.OrbIntensity;
|
Out.Orb.Intensity = L.OrbIntensity;
|
||||||
Out.Orb.RadiusWorld = L.OrbRadiusVoxels * VOXEL_SIZE * Scale;
|
Out.Orb.RadiusWorld = L.OrbRadiusVoxels * VOXEL_SIZE * Scale;
|
||||||
Out.Orb.FalloffWorld = L.OrbFalloffVoxels * VOXEL_SIZE;
|
Out.Orb.FalloffWorld = L.OrbFalloffVoxels * VOXEL_SIZE;
|
||||||
Out.Orb.MaxShadowDistWorld = L.OrbMaxShadowDistanceVoxels * VOXEL_SIZE;
|
Out.Orb.MaxShadowDistWorld = L.OrbMaxShadowDistanceVoxels * VOXEL_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (L.ActorClass)
|
|
||||||
{
|
|
||||||
UWorld* World = OwnerActor->GetWorld();
|
|
||||||
if (!World) return;
|
|
||||||
FActorSpawnParameters SP;
|
|
||||||
SP.Owner = OwnerActor;
|
|
||||||
SP.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
|
||||||
if (AActor* A = World->SpawnActor<AActor>(L.ActorClass, Xf, SP)) { Out.Actor = A; }
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (L.InstancedMesh)
|
|
||||||
{
|
|
||||||
UStaticMeshComponent* C = NewObject<UStaticMeshComponent>(OwnerActor);
|
|
||||||
C->SetStaticMesh(L.InstancedMesh);
|
|
||||||
C->SetMobility(EComponentMobility::Static); // placed once, never moves → cached draw + VSM shadow
|
|
||||||
C->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
|
||||||
C->SetCastShadow(L.bCastShadow);
|
|
||||||
if (L.CullDistance > 0.0f) { C->SetCullDistance(L.CullDistance); } // 0 = never cull (far sun)
|
|
||||||
C->SetWorldTransform(Xf);
|
|
||||||
C->RegisterComponent();
|
|
||||||
C->AttachToComponent(OwnerActor->GetRootComponent(), FAttachmentTransformRules::KeepWorldTransform);
|
|
||||||
Out.Component = C;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UVoxelContentManager::GetActiveOrbs(TArray<FVoxelActiveOrb>& OutOrbs) const
|
void UVoxelContentManager::GetActiveOrbs(TArray<FVoxelActiveOrb>& OutOrbs) const
|
||||||
@@ -1057,6 +1222,38 @@ void UVoxelContentManager::ClearAllLandmarks()
|
|||||||
LandmarkInstances.Reset();
|
LandmarkInstances.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32 UVoxelContentManager::RemoveInstancesInContent(FDecoRegionContent& Content, const FVector& Center, float Radius)
|
||||||
|
{
|
||||||
|
int32 Removed = 0;
|
||||||
|
for (TWeakObjectPtr<UHierarchicalInstancedStaticMeshComponent>& WP : Content.Instances)
|
||||||
|
{
|
||||||
|
UHierarchicalInstancedStaticMeshComponent* HISM = WP.Get();
|
||||||
|
if (!HISM || HISM->GetInstanceCount() == 0) continue;
|
||||||
|
// Fast bounds-tested spatial query → the vast majority of region HISMs return empty immediately.
|
||||||
|
TArray<int32> Hits = HISM->GetInstancesOverlappingSphere(Center, Radius, /*bSphereInWorldSpace*/ true);
|
||||||
|
if (Hits.Num() > 0)
|
||||||
|
{
|
||||||
|
HISM->RemoveInstances(Hits); // handles index shifting; marks render state dirty
|
||||||
|
Removed += Hits.Num();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UVoxelContentManager::RemoveDecorationsInSphere(const FVector& WorldCenter, float WorldRadius)
|
||||||
|
{
|
||||||
|
if (WorldRadius <= 0.0f) return 0;
|
||||||
|
int32 Removed = 0;
|
||||||
|
for (FDecoGrid* G : { &NearGrid, &FarGrid })
|
||||||
|
{
|
||||||
|
for (TPair<FIntPoint, FDecoRegionContent>& RP : G->Regions)
|
||||||
|
{
|
||||||
|
Removed += RemoveInstancesInContent(RP.Value, WorldCenter, WorldRadius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Removed;
|
||||||
|
}
|
||||||
|
|
||||||
void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
|
void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
|
||||||
{
|
{
|
||||||
if (!StrateManager || !Generator || !Settings) return;
|
if (!StrateManager || !Generator || !Settings) return;
|
||||||
@@ -1109,53 +1306,141 @@ void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
|
|||||||
const float ColDepth = (float)FMath::Max(8, Settings->DecorationColumnDepthVoxels);
|
const float ColDepth = (float)FMath::Max(8, Settings->DecorationColumnDepthVoxels);
|
||||||
const uint32 LocalSeed = (uint32)Seed;
|
const uint32 LocalSeed = (uint32)Seed;
|
||||||
|
|
||||||
// Walk each entry's lattice within its radius (a tiny box), spawn newly-entered cells, drop exited ones.
|
// ---- Gather candidates (a small set) across every entry + anchor mode. Local XY (actor-space cm) is
|
||||||
TSet<FIntVector> Desired;
|
// enough for the exclusion test — the surface-find only moves Z. ----
|
||||||
|
// bSpawnable = within the entry's real stream radius. POP-FREE EXCLUSION: we gather each entry in
|
||||||
|
// (radius + MaxExcl) so every conflictor of an in-range candidate is present regardless of player
|
||||||
|
// position; the extra "ring" candidates only SUPPRESS (never spawn), so a candidate's fate is a pure
|
||||||
|
// function of (seed, layout) → no edge-of-radius flicker.
|
||||||
|
struct FCand { FIntVector Key; int32 EntryIdx; uint32 H; float LocalX, LocalY; float ExclWorld; int32 Priority; bool bSpawnable; };
|
||||||
|
TArray<FCand> Cands;
|
||||||
|
bool bAnyExclusion = false; // pure scatter (all radii 0) skips the O(n²) resolve → same cost as before
|
||||||
|
|
||||||
|
float MaxExclChunks = 0.0f;
|
||||||
|
for (const FStrateLandmark& LE : Def->Landmarks) { MaxExclChunks = FMath::Max(MaxExclChunks, FMath::Max(0.0f, LE.ExclusionRadiusChunks)); }
|
||||||
|
const float MaxExclWorld = MaxExclChunks * ChunkWorld;
|
||||||
|
|
||||||
for (int32 EntryIdx = 0; EntryIdx < Def->Landmarks.Num(); ++EntryIdx)
|
for (int32 EntryIdx = 0; EntryIdx < Def->Landmarks.Num(); ++EntryIdx)
|
||||||
{
|
{
|
||||||
const FStrateLandmark& L = Def->Landmarks[EntryIdx];
|
const FStrateLandmark& L = Def->Landmarks[EntryIdx];
|
||||||
if (!L.ActorClass && !L.InstancedMesh) continue;
|
if (!L.Profile.ActorClass && !L.Profile.InstancedMesh) continue;
|
||||||
|
|
||||||
const float SpacingChunks = FMath::Max(1.0f, L.SpacingChunks);
|
const int32 RadiusChunks = FMath::Max(1, L.StreamRadiusChunks);
|
||||||
const int32 RadiusChunks = FMath::Max(1, L.StreamRadiusChunks);
|
const float RadiusWorld = (float)RadiusChunks * ChunkWorld;
|
||||||
const float CellWorld = SpacingChunks * ChunkWorld; // lattice cell size in cm
|
const float GatherWorld = RadiusWorld + MaxExclWorld; // widened so all conflictors are gathered
|
||||||
const float RadiusWorld = (float)RadiusChunks * ChunkWorld;
|
const float ExclWorld = FMath::Max(0.0f, L.ExclusionRadiusChunks) * ChunkWorld;
|
||||||
const float JitterRange = FMath::Clamp(L.JitterFraction, 0.0f, 1.0f);
|
if (ExclWorld > 0.0f) bAnyExclusion = true;
|
||||||
|
|
||||||
const FIntPoint PlayerLCell(FMath::FloorToInt(LocalPlayer.X / CellWorld),
|
if (L.AnchorMode == ELandmarkAnchor::HashLattice)
|
||||||
FMath::FloorToInt(LocalPlayer.Y / CellWorld));
|
|
||||||
const int32 CellRange = FMath::CeilToInt((float)RadiusChunks / SpacingChunks);
|
|
||||||
|
|
||||||
for (int32 dy = -CellRange; dy <= CellRange; ++dy)
|
|
||||||
for (int32 dx = -CellRange; dx <= CellRange; ++dx)
|
|
||||||
{
|
{
|
||||||
const FIntPoint LCell(PlayerLCell.X + dx, PlayerLCell.Y + dy);
|
const float SpacingChunks = FMath::Max(1.0f, L.SpacingChunks);
|
||||||
|
const float CellWorld = SpacingChunks * ChunkWorld; // lattice cell size in cm
|
||||||
|
const float JitterRange = FMath::Clamp(L.JitterFraction, 0.0f, 1.0f);
|
||||||
|
const FIntPoint PlayerLCell(FMath::FloorToInt(LocalPlayer.X / CellWorld),
|
||||||
|
FMath::FloorToInt(LocalPlayer.Y / CellWorld));
|
||||||
|
const int32 CellRange = FMath::CeilToInt(((float)RadiusChunks + MaxExclChunks) / SpacingChunks);
|
||||||
|
|
||||||
// Existence roll for this lattice cell + entry.
|
for (int32 dy = -CellRange; dy <= CellRange; ++dy)
|
||||||
const uint32 H = DecoHash(LCell.X, LCell.Y, 0, 0, 0, EntryIdx, LocalSeed, 0x1A2D5u);
|
for (int32 dx = -CellRange; dx <= CellRange; ++dx)
|
||||||
if (VoxelHash::ToFloat01(H) > L.SpawnProbability) continue;
|
{
|
||||||
|
const FIntPoint LCell(PlayerLCell.X + dx, PlayerLCell.Y + dy);
|
||||||
|
|
||||||
// Jittered position inside the cell (centred so two neighbours stay ≥ Spacing·(1-Jitter) apart).
|
// Existence roll (salt 0x1A2D5u kept from the original landmark path → positions unchanged).
|
||||||
const float jx = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x51A3F1u)) - 0.5f) * JitterRange;
|
const uint32 H = DecoHash(LCell.X, LCell.Y, 0, 0, 0, EntryIdx, LocalSeed, 0x1A2D5u);
|
||||||
const float jy = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x7C2B93u)) - 0.5f) * JitterRange;
|
if (VoxelHash::ToFloat01(H) > L.SpawnProbability) continue;
|
||||||
const float LocalX = ((float)LCell.X + 0.5f + jx) * CellWorld;
|
|
||||||
const float LocalY = ((float)LCell.Y + 0.5f + jy) * CellWorld;
|
|
||||||
|
|
||||||
// Radius is a true disk (the lattice box corners would otherwise overshoot it).
|
const float jx = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x51A3F1u)) - 0.5f) * JitterRange;
|
||||||
const float ddx = LocalX - LocalPlayer.X, ddy = LocalY - LocalPlayer.Y;
|
const float jy = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x7C2B93u)) - 0.5f) * JitterRange;
|
||||||
if (ddx * ddx + ddy * ddy > RadiusWorld * RadiusWorld) continue;
|
const float LocalX = ((float)LCell.X + 0.5f + jx) * CellWorld;
|
||||||
|
const float LocalY = ((float)LCell.Y + 0.5f + jy) * CellWorld;
|
||||||
|
|
||||||
const FIntVector Key(LCell.X, LCell.Y, EntryIdx);
|
const float ddx = LocalX - LocalPlayer.X, ddy = LocalY - LocalPlayer.Y;
|
||||||
Desired.Add(Key);
|
const float DistSq = ddx * ddx + ddy * ddy;
|
||||||
if (LandmarkInstances.Contains(Key)) continue; // already evaluated (spawned OR empty)
|
if (DistSq > GatherWorld * GatherWorld) continue;
|
||||||
|
|
||||||
FLandmarkInstance Inst;
|
Cands.Add({ FIntVector(LCell.X, LCell.Y, EntryIdx), EntryIdx, H, LocalX, LocalY, ExclWorld, L.Priority,
|
||||||
SpawnLandmarkInstance(L, H, Ctx, OwnerXf, OwnerActor, LocalX, LocalY, Step, ColDepth, Inst);
|
DistSq <= RadiusWorld * RadiusWorld });
|
||||||
LandmarkInstances.Add(Key, Inst); // stored even if empty → never re-evaluated while in range
|
}
|
||||||
|
}
|
||||||
|
else // PassageMouth — enumerate the finite passage list, keep endpoints that land in THIS strate.
|
||||||
|
{
|
||||||
|
const TArray<FVoxelPassage>& Passages = StrateManager->GetPassages();
|
||||||
|
for (int32 pi = 0; pi < Passages.Num(); ++pi)
|
||||||
|
{
|
||||||
|
const FVoxelPassage& Pg = Passages[pi];
|
||||||
|
for (int32 side = 0; side < 2; ++side)
|
||||||
|
{
|
||||||
|
const bool bDescent = (side == 0); // 0 = this strate is the passage's UPPER (hole DOWN)
|
||||||
|
if (bDescent && !(L.bAtDescentMouths && Pg.UpperStrateIndex == StrateIndex)) continue;
|
||||||
|
if (!bDescent && !(L.bAtArrivalMouths && Pg.LowerStrateIndex == StrateIndex)) continue;
|
||||||
|
|
||||||
|
// Passage endpoints are GLOBAL VOXEL coords (same space as the SDF path) → local cm.
|
||||||
|
const FVector MouthVox = bDescent ? Pg.UpperPoint : Pg.LowerPoint;
|
||||||
|
const float LocalX = MouthVox.X * VOXEL_SIZE;
|
||||||
|
const float LocalY = MouthVox.Y * VOXEL_SIZE;
|
||||||
|
|
||||||
|
const float ddx = LocalX - LocalPlayer.X, ddy = LocalY - LocalPlayer.Y;
|
||||||
|
const float DistSq = ddx * ddx + ddy * ddy;
|
||||||
|
if (DistSq > GatherWorld * GatherWorld) continue;
|
||||||
|
|
||||||
|
const uint32 H = DecoHash(pi, side, 0, 0, 0, EntryIdx, LocalSeed, 0x5E7C9u);
|
||||||
|
if (VoxelHash::ToFloat01(H) > L.MouthProbability) continue;
|
||||||
|
|
||||||
|
Cands.Add({ FIntVector(pi, side, EntryIdx), EntryIdx, H, LocalX, LocalY, ExclWorld, L.Priority,
|
||||||
|
DistSq <= RadiusWorld * RadiusWorld });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop instances no longer desired (player moved away, strate's list shrank, etc.).
|
// ---- Resolve exclusion (only if any entry opts in): a candidate is suppressed when a HIGHER-RANKED one's
|
||||||
|
// disk covers it. Rank = (Priority, then hash) → deterministic. v1 resolves within the in-range set. ----
|
||||||
|
TSet<FIntVector> Desired;
|
||||||
|
Desired.Reserve(Cands.Num());
|
||||||
|
for (int32 i = 0; i < Cands.Num(); ++i)
|
||||||
|
{
|
||||||
|
const FCand& C = Cands[i];
|
||||||
|
if (!C.bSpawnable) continue; // ring-only conflictor (gathered for pop-free resolve, never spawned)
|
||||||
|
if (bAnyExclusion)
|
||||||
|
{
|
||||||
|
bool bSuppressed = false;
|
||||||
|
for (int32 j = 0; j < Cands.Num(); ++j)
|
||||||
|
{
|
||||||
|
if (j == i) continue;
|
||||||
|
const FCand& D = Cands[j];
|
||||||
|
if (D.ExclWorld <= 0.0f) continue;
|
||||||
|
const bool bDOutranks = (D.Priority != C.Priority) ? (D.Priority > C.Priority) : (D.H > C.H);
|
||||||
|
if (!bDOutranks) continue;
|
||||||
|
const float dxl = D.LocalX - C.LocalX, dyl = D.LocalY - C.LocalY;
|
||||||
|
if (dxl * dxl + dyl * dyl < D.ExclWorld * D.ExclWorld) { bSuppressed = true; break; }
|
||||||
|
}
|
||||||
|
if (bSuppressed) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Desired.Add(C.Key);
|
||||||
|
if (LandmarkInstances.Contains(C.Key)) continue; // already evaluated (spawned OR empty)
|
||||||
|
|
||||||
|
FLandmarkInstance Inst;
|
||||||
|
SpawnLandmarkInstance(Def->Landmarks[C.EntryIdx], C.H, Ctx, OwnerXf, OwnerActor,
|
||||||
|
C.LocalX, C.LocalY, Step, ColDepth, Inst);
|
||||||
|
|
||||||
|
// Decoration footprint (F7): clear groundcover under a placed suppressing landmark now, and remember
|
||||||
|
// the footprint so a deco region streaming in near it re-clears too (see ApplyRegion).
|
||||||
|
const FStrateLandmark& LE = Def->Landmarks[C.EntryIdx];
|
||||||
|
if (LE.bSuppressDecorationsUnder && LE.SuppressRadiusChunks > 0.0f
|
||||||
|
&& (Inst.Actor.IsValid() || Inst.Component.IsValid()))
|
||||||
|
{
|
||||||
|
const FVector WP = Inst.Actor.IsValid() ? Inst.Actor.Get()->GetActorLocation()
|
||||||
|
: Inst.Component.Get()->GetComponentLocation();
|
||||||
|
Inst.SuppressCenter = WP;
|
||||||
|
Inst.SuppressRadiusWorld = LE.SuppressRadiusChunks * ChunkWorld;
|
||||||
|
RemoveDecorationsInSphere(WP, Inst.SuppressRadiusWorld);
|
||||||
|
}
|
||||||
|
|
||||||
|
LandmarkInstances.Add(C.Key, Inst); // stored even if empty → never re-evaluated while in range
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop instances no longer desired (player moved away, list shrank, lost an exclusion conflict).
|
||||||
for (auto It = LandmarkInstances.CreateIterator(); It; ++It)
|
for (auto It = LandmarkInstances.CreateIterator(); It; ++It)
|
||||||
{
|
{
|
||||||
if (Desired.Contains(It.Key())) continue;
|
if (Desired.Contains(It.Key())) continue;
|
||||||
|
|||||||
@@ -196,7 +196,12 @@ void UVoxelDensityVolume::EnsureTextures()
|
|||||||
T->Filter = TF_Trilinear; // smooth iso crossing (sub-voxel crisp edge)
|
T->Filter = TF_Trilinear; // smooth iso crossing (sub-voxel crisp edge)
|
||||||
T->CompressionSettings = TC_Grayscale; // single-channel
|
T->CompressionSettings = TC_Grayscale; // single-channel
|
||||||
T->NeverStream = true;
|
T->NeverStream = true;
|
||||||
T->MipGenSettings = TMGS_NoMipmaps; // 1b-i: base mip only; solidity mips come with the march
|
#if WITH_EDITORONLY_DATA
|
||||||
|
// MipGenSettings n'existe que dans les builds éditeur (WITH_EDITORONLY_DATA) — c'est un hint
|
||||||
|
// pour le mip-builder du cooker. En packagé le PlatformData construit ici n'a qu'UN mip de
|
||||||
|
// toute façon (1b-i: base mip only; solidity mips come with the march).
|
||||||
|
T->MipGenSettings = TMGS_NoMipmaps;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Runtime platform data: one R8 (PF_G8) mip, zero-initialised. NOTE (UE5.7 API surface — flag if
|
// Runtime platform data: one R8 (PF_G8) mip, zero-initialised. NOTE (UE5.7 API surface — flag if
|
||||||
// the build rejects any of these): FTexturePlatformData / SetNumSlices / SetPlatformData /
|
// the build rejects any of these): FTexturePlatformData / SetNumSlices / SetPlatformData /
|
||||||
|
|||||||
@@ -115,8 +115,10 @@ TArray<FIntVector> UVoxelDiffLayer::ApplyModification(const FVoxelModification&
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Publish: subsequent readers must now take the lock instead of fast-rejecting.
|
// Publish: subsequent readers must now take the lock instead of fast-rejecting, and
|
||||||
|
// worker-side snapshots keyed on the version re-copy their chunk's list.
|
||||||
bHasAnyMods.store(true, std::memory_order_release);
|
bHasAnyMods.store(true, std::memory_order_release);
|
||||||
|
ModsVersion.fetch_add(1, std::memory_order_release);
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log,
|
UE_LOG(LogTemp, Log,
|
||||||
@@ -142,13 +144,53 @@ float UVoxelDiffLayer::GetDensityOffset(const FIntVector& ChunkCoord,
|
|||||||
if (!bHasAnyMods.load(std::memory_order_acquire)) return 0.0f;
|
if (!bHasAnyMods.load(std::memory_order_acquire)) return 0.0f;
|
||||||
|
|
||||||
// Hold the read lock for the whole body: Mods points INTO the map and is dereferenced through the
|
// Hold the read lock for the whole body: Mods points INTO the map and is dereferenced through the
|
||||||
// falloff loop below, so the map must not be rehashed by a concurrent writer meanwhile.
|
// falloff loop, so the map must not be rehashed by a concurrent writer meanwhile. NOTE: hot-path
|
||||||
|
// callers (the generator) should prefer GetChunkModsSnapshot + EvaluateMods — one lock per chunk
|
||||||
|
// instead of one per voxel.
|
||||||
FReadScopeLock Lock(ModsLock);
|
FReadScopeLock Lock(ModsLock);
|
||||||
|
|
||||||
// Fast path: if this chunk has no modifications, return 0
|
|
||||||
const TArray<FVoxelModification>* Mods = ChunkMods.Find(ChunkCoord);
|
const TArray<FVoxelModification>* Mods = ChunkMods.Find(ChunkCoord);
|
||||||
if (!Mods || Mods->Num() == 0) return 0.0f;
|
if (!Mods || Mods->Num() == 0) return 0.0f;
|
||||||
|
|
||||||
|
return EvaluateMods(*Mods, WorldX, WorldY, WorldZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVoxelDiffLayer::GetChunkModsSnapshot(const FIntVector& ChunkCoord, TArray<FVoxelModification>& Out) const
|
||||||
|
{
|
||||||
|
Out.Reset();
|
||||||
|
if (!bHasAnyMods.load(std::memory_order_acquire)) return;
|
||||||
|
|
||||||
|
FReadScopeLock Lock(ModsLock);
|
||||||
|
if (const TArray<FVoxelModification>* Mods = ChunkMods.Find(ChunkCoord))
|
||||||
|
{
|
||||||
|
Out = *Mods;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UVoxelDiffLayer::HasAnyModInChunkRange(const FIntVector& MinChunk, const FIntVector& MaxChunk) const
|
||||||
|
{
|
||||||
|
if (!bHasAnyMods.load(std::memory_order_acquire)) return false;
|
||||||
|
|
||||||
|
// Walk the modified-chunk KEYS under one read lock. Conservative: a mod is registered in every
|
||||||
|
// chunk its radius overlaps (ApplyModification), so key-in-range ⟺ the mod can reach the range.
|
||||||
|
FReadScopeLock Lock(ModsLock);
|
||||||
|
for (const auto& Pair : ChunkMods)
|
||||||
|
{
|
||||||
|
const FIntVector& C = Pair.Key;
|
||||||
|
if (C.X >= MinChunk.X && C.X <= MaxChunk.X &&
|
||||||
|
C.Y >= MinChunk.Y && C.Y <= MaxChunk.Y &&
|
||||||
|
C.Z >= MinChunk.Z && C.Z <= MaxChunk.Z &&
|
||||||
|
Pair.Value.Num() > 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float UVoxelDiffLayer::EvaluateMods(const TArray<FVoxelModification>& Mods,
|
||||||
|
float WorldX, float WorldY, float WorldZ)
|
||||||
|
{
|
||||||
float TotalOffset = 0.0f;
|
float TotalOffset = 0.0f;
|
||||||
const FVector Pos(WorldX, WorldY, WorldZ);
|
const FVector Pos(WorldX, WorldY, WorldZ);
|
||||||
|
|
||||||
@@ -159,7 +201,7 @@ float UVoxelDiffLayer::GetDensityOffset(const FIntVector& ChunkCoord,
|
|||||||
return 1.0f - T * T * (3.0f - 2.0f * T);
|
return 1.0f - T * T * (3.0f - 2.0f * T);
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const FVoxelModification& Mod : *Mods)
|
for (const FVoxelModification& Mod : Mods)
|
||||||
{
|
{
|
||||||
float Falloff = 0.0f;
|
float Falloff = 0.0f;
|
||||||
|
|
||||||
@@ -229,6 +271,7 @@ void UVoxelDiffLayer::Clear()
|
|||||||
FWriteScopeLock Lock(ModsLock);
|
FWriteScopeLock Lock(ModsLock);
|
||||||
bHasAnyMods.store(false, std::memory_order_release);
|
bHasAnyMods.store(false, std::memory_order_release);
|
||||||
ChunkMods.Empty();
|
ChunkMods.Empty();
|
||||||
|
ModsVersion.fetch_add(1, std::memory_order_release); // invalidate worker snapshots
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset budget counters — player gets a fresh budget after clear/season reset
|
// Reset budget counters — player gets a fresh budget after clear/season reset
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,8 @@
|
|||||||
// mort depuis T1.b — la grille pré-échantillonnée fournit positions ET gradients.)
|
// mort depuis T1.b — la grille pré-échantillonnée fournit positions ET gradients.)
|
||||||
|
|
||||||
FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, int32 Step, int32 InCellsPerAxis,
|
FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, int32 Step, int32 InCellsPerAxis,
|
||||||
TArray<uint8>* OutCaptureGrid)
|
TArray<uint8>* OutCaptureGrid,
|
||||||
|
int32 BandZMinVox, int32 BandZMaxVox)
|
||||||
{
|
{
|
||||||
FVoxelMeshData MeshData;
|
FVoxelMeshData MeshData;
|
||||||
if (OutCaptureGrid) { OutCaptureGrid->Reset(); }
|
if (OutCaptureGrid) { OutCaptureGrid->Reset(); }
|
||||||
@@ -21,6 +22,14 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
// grows). Coarse tiles also use FEWER cells (InCellsPerAxis) for cheaper gen.
|
// grows). Coarse tiles also use FEWER cells (InCellsPerAxis) for cheaper gen.
|
||||||
Step = FMath::Max(1, Step);
|
Step = FMath::Max(1, Step);
|
||||||
|
|
||||||
|
// T2.b — octave bias for THIS tile: drop LODOctaveDrop octaves per Step doubling from
|
||||||
|
// the generator's per-voxel volumetric noise (sub-cell octaves can't shape a coarse
|
||||||
|
// isosurface). TGuardValue restores 0 on every exit path, so nothing outside this tile
|
||||||
|
// (deco snapping, density-volume fill, the next task on this pooled thread) sees a bias.
|
||||||
|
const int32 OctaveBias = (LODOctaveDrop > 0 && Step > 1)
|
||||||
|
? LODOctaveDrop * (int32)FMath::FloorLog2((uint32)Step) : 0;
|
||||||
|
TGuardValue<int32> OctaveBiasGuard(VoxelGenLOD::OctaveBias, OctaveBias);
|
||||||
|
|
||||||
// World-cm origin of the tile's min corner (positions are built relative to this).
|
// World-cm origin of the tile's min corner (positions are built relative to this).
|
||||||
const FVector ChunkWorldPos = FVector(OriginVoxels) * VOXEL_SIZE;
|
const FVector ChunkWorldPos = FVector(OriginVoxels) * VOXEL_SIZE;
|
||||||
|
|
||||||
@@ -35,6 +44,22 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
static thread_local TMap<FIntVector, int32> VertexMap;
|
static thread_local TMap<FIntVector, int32> VertexMap;
|
||||||
VertexMap.Reset();
|
VertexMap.Reset();
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// F17 — CLASSE DE SURFACE (sol vs plafond sky-cap), par vertex → par triangle
|
||||||
|
//=========================================================================
|
||||||
|
// Le discriminant est SÉMANTIQUE, pas géométrique (fable-idea F17) : un vertex orienté vers
|
||||||
|
// le bas est un sky-cap seulement s'il est proche de CeilSurf ; proche de TerrainZ c'est un
|
||||||
|
// surplomb de terrain (reste "sol" — l'ancien vote par tuile mettait le matériau ciel sous
|
||||||
|
// les surplombs des tuiles majoritairement plafond). Un futur toit de grotte (aussi down-
|
||||||
|
// facing, mais SOUS TerrainZ) tombera correctement côté "sol/roche" par la même règle.
|
||||||
|
// Coût : GetSurfaceHeightAt (pile XY complète) UNIQUEMENT pour les vertex down-facing,
|
||||||
|
// mémoïsé par colonne quantifiée au pas de la grille → ≤ (colonnes touchées) appels ;
|
||||||
|
// ~zéro sur une tuile de sol pur, borné par lattice² sur une tuile de cap.
|
||||||
|
static thread_local TArray<uint8> VertexClasses; // 0 = sol, 1 = sky-cap
|
||||||
|
static thread_local TMap<FIntVector, FVector2f> SurfColMemo; // (Xq,Yq,chunkZ) → (TerrainZ, CeilSurf) ; X=FLT_MAX ⇒ pas SurfaceWorld
|
||||||
|
VertexClasses.Reset();
|
||||||
|
SurfColMemo.Reset();
|
||||||
|
|
||||||
// Normale fournie par l'appelant (gradient lu dans la grille de densité, T1.b) —
|
// 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
|
// plus d'échantillonnage de densité par vertex. RawNormal pointe solide→air ; on la
|
||||||
// normalise ici (fallback up si dégénérée).
|
// normalise ici (fallback up si dégénérée).
|
||||||
@@ -84,6 +109,45 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
const uint8 Bc = (uint8)FMath::Clamp(FMath::RoundToInt(BlendW * 255.0f), 0, 255);
|
const uint8 Bc = (uint8)FMath::Clamp(FMath::RoundToInt(BlendW * 255.0f), 0, 255);
|
||||||
MeshData.Colors.Add(FColor(R, G, Bc, A));
|
MeshData.Colors.Add(FColor(R, G, Bc, A));
|
||||||
|
|
||||||
|
// F17 — classe de surface (voir le bloc en tête de fonction). Down-facing seulement :
|
||||||
|
// les vertex de sol/falaise (N.Z ≥ -0.1) sont "sol" sans payer la requête colonne.
|
||||||
|
uint8 SurfClass = 0;
|
||||||
|
if (Normal.Z < -0.1f)
|
||||||
|
{
|
||||||
|
const float Zv = WorldPos.Z / VOXEL_SIZE;
|
||||||
|
const float StepF = (float)Step;
|
||||||
|
const FIntVector MemoKey(
|
||||||
|
FMath::RoundToInt(WorldPos.X / (VOXEL_SIZE * StepF)),
|
||||||
|
FMath::RoundToInt(WorldPos.Y / (VOXEL_SIZE * StepF)),
|
||||||
|
FMath::FloorToInt(Zv / (float)CHUNK_SIZE));
|
||||||
|
FVector2f* Col = SurfColMemo.Find(MemoKey);
|
||||||
|
if (!Col)
|
||||||
|
{
|
||||||
|
float TerrainZ = 0.0f, CeilSurf = 0.0f;
|
||||||
|
// Un vertex du cap vit à la FRONTIÈRE HAUTE de la strate : arrondi/interpolation
|
||||||
|
// peuvent le faire flotter dans le chunk gap/seal juste AU-DESSUS (hors
|
||||||
|
// SurfaceWorld ⇒ sonde ratée ⇒ faux "sol"). On retente un chunk plus bas.
|
||||||
|
const bool bSurf = Generator->GetSurfaceHeightAt(
|
||||||
|
(float)(MemoKey.X * Step), (float)(MemoKey.Y * Step), MemoKey.Z,
|
||||||
|
TerrainZ, CeilSurf)
|
||||||
|
|| Generator->GetSurfaceHeightAt(
|
||||||
|
(float)(MemoKey.X * Step), (float)(MemoKey.Y * Step), MemoKey.Z - 1,
|
||||||
|
TerrainZ, CeilSurf);
|
||||||
|
Col = &SurfColMemo.Add(MemoKey, bSurf ? FVector2f(TerrainZ, CeilSurf)
|
||||||
|
: FVector2f(FLT_MAX, -FLT_MAX));
|
||||||
|
}
|
||||||
|
// Plus proche du plafond que du terrain ⇒ sky-cap. (Hors SurfaceWorld : sol.)
|
||||||
|
// + garde : un vrai vertex de cap est AU niveau du cap (± slop d'interpolation) ;
|
||||||
|
// loin AU-DESSUS = plancher/toit de la strate d'à côté atteint via le retry chunkZ-1
|
||||||
|
// (strates contiguës) → reste sol.
|
||||||
|
if (Col->X != FLT_MAX && FMath::Abs(Zv - Col->Y) < FMath::Abs(Zv - Col->X)
|
||||||
|
&& Zv <= Col->Y + 2.0f * StepF)
|
||||||
|
{
|
||||||
|
SurfClass = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VertexClasses.Add(SurfClass);
|
||||||
|
|
||||||
return NewIndex;
|
return NewIndex;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -132,11 +196,34 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
const int32 GridDim = CellsPerAxis + 1;
|
const int32 GridDim = CellsPerAxis + 1;
|
||||||
const int32 MDim = GridDim + 2; // +1 marge de chaque côté
|
const int32 MDim = GridDim + 2; // +1 marge de chaque côté
|
||||||
|
|
||||||
|
// COUPE DE CONTENU PAR STRATE — restreint le maillage (et l'échantillonnage) aux cellules
|
||||||
|
// dont l'intervalle Z chevauche la bande [BandZMinVox, BandZMaxVox] (voxels inclusifs).
|
||||||
|
// Les tuiles à capture ne sont jamais bandées (niveau 0 — garde-fou ci-dessous).
|
||||||
|
if (OutCaptureGrid) { BandZMinVox = INT32_MIN; BandZMaxVox = INT32_MAX; }
|
||||||
|
int32 CzLo = 0, CzHi = CellsPerAxis - 1;
|
||||||
|
if (BandZMinVox > INT32_MIN || BandZMaxVox < INT32_MAX)
|
||||||
|
{
|
||||||
|
auto FloorDivI = [](int64 A, int64 B) -> int32
|
||||||
|
{
|
||||||
|
const int64 Q = A / B;
|
||||||
|
return (int32)(Q - (((A % B) != 0 && ((A < 0) != (B < 0))) ? 1 : 0));
|
||||||
|
};
|
||||||
|
// Cellule c couvre [O.Z + c*Step, O.Z + (c+1)*Step] : première/dernière cellule contenant
|
||||||
|
// la borne (un contact purement tangent est exclu — sa traversée serait hors bande).
|
||||||
|
CzLo = FMath::Max(CzLo, FloorDivI((int64)BandZMinVox - OriginVoxels.Z, Step));
|
||||||
|
CzHi = FMath::Min(CzHi, FloorDivI((int64)BandZMaxVox - OriginVoxels.Z, Step));
|
||||||
|
if (CzHi < CzLo) { return MeshData; } // tuile entièrement hors bande → vide
|
||||||
|
}
|
||||||
|
const int32 GzLo = CzLo - 1; // coins CzLo..CzHi+1, gradients ±1
|
||||||
|
const int32 GzHi = CzHi + 2; // (== -1..GridDim sans bande)
|
||||||
|
|
||||||
// Réutilise le tampon entre tuiles (thread_local) : SetNumUninitialized garde la
|
// Réutilise le tampon entre tuiles (thread_local) : SetNumUninitialized garde la
|
||||||
// capacité, donc plus de malloc/free de ~170 Ko (35³ floats) par tuile.
|
// capacité, donc plus de malloc/free de ~170 Ko (35³ floats) par tuile.
|
||||||
static thread_local TArray<float> DensityGrid;
|
static thread_local TArray<float> DensityGrid;
|
||||||
DensityGrid.SetNumUninitialized(MDim * MDim * MDim);
|
DensityGrid.SetNumUninitialized(MDim * MDim * MDim);
|
||||||
for (int32 gz = -1; gz <= GridDim; gz++)
|
// Bande de strate : seules les rangées Z réellement lues (cellules CzLo..CzHi + marges de
|
||||||
|
// gradient) sont échantillonnées — le reste du tampon reste non initialisé et non lu.
|
||||||
|
for (int32 gz = GzLo; gz <= GzHi; gz++)
|
||||||
{
|
{
|
||||||
for (int32 gy = -1; gy <= GridDim; gy++)
|
for (int32 gy = -1; gy <= GridDim; gy++)
|
||||||
{
|
{
|
||||||
@@ -193,7 +280,14 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
// On itère sur les CELLULES (indices de grille), pas sur les voxels monde.
|
// On itère sur les CELLULES (indices de grille), pas sur les voxels monde.
|
||||||
// Coin i de la cellule = point de grille (cx+ox, cy+oy, cz+oz) ; coord voxel
|
// Coin i de la cellule = point de grille (cx+ox, cy+oy, cz+oz) ; coord voxel
|
||||||
// monde = ce point × Step. LOD0 Step=1 → full res ; LOD1 Step=2 → ~4× moins.
|
// monde = ce point × Step. LOD0 Step=1 → full res ; LOD1 Step=2 → ~4× moins.
|
||||||
for (int32 cz = 0; cz < CellsPerAxis; cz++)
|
// F17 — les triangles sont émis dans DEUX seaux (sol / sky-cap, vote majoritaire des
|
||||||
|
// classes de vertex) puis concaténés sol-puis-cap : RMC exige un run d'indices contigu
|
||||||
|
// par polygroup. Géométrie inchangée au bit près — seul l'ORDRE des triangles bouge.
|
||||||
|
static thread_local TArray<int32> GroundTris;
|
||||||
|
static thread_local TArray<int32> CapTris;
|
||||||
|
GroundTris.Reset();
|
||||||
|
CapTris.Reset();
|
||||||
|
for (int32 cz = CzLo; cz <= CzHi; cz++) // bande de strate : cf. CzLo/CzHi plus haut
|
||||||
{
|
{
|
||||||
for (int32 cy = 0; cy < CellsPerAxis; cy++)
|
for (int32 cy = 0; cy < CellsPerAxis; cy++)
|
||||||
{
|
{
|
||||||
@@ -261,9 +355,14 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
const int32 Idx1 = GetOrCreateVertex(EdgeVertices[E1], EdgeNormals[E1]);
|
const int32 Idx1 = GetOrCreateVertex(EdgeVertices[E1], EdgeNormals[E1]);
|
||||||
const int32 Idx2 = GetOrCreateVertex(EdgeVertices[E2], EdgeNormals[E2]);
|
const int32 Idx2 = GetOrCreateVertex(EdgeVertices[E2], EdgeNormals[E2]);
|
||||||
|
|
||||||
MeshData.Triangles.Add(Idx0);
|
// F17 — vote majoritaire (≥ 2 vertex sky-cap ⇒ triangle sky-cap).
|
||||||
MeshData.Triangles.Add(Idx2);
|
const int32 CapVotes = (int32)VertexClasses[Idx0]
|
||||||
MeshData.Triangles.Add(Idx1);
|
+ (int32)VertexClasses[Idx1]
|
||||||
|
+ (int32)VertexClasses[Idx2];
|
||||||
|
TArray<int32>& Dst = (CapVotes >= 2) ? CapTris : GroundTris;
|
||||||
|
Dst.Add(Idx0);
|
||||||
|
Dst.Add(Idx2);
|
||||||
|
Dst.Add(Idx1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,7 +380,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
// Émis en DOUBLE FACE (deux orientations) pour s'afficher quel que soit le côté caméra / le
|
// Émis en DOUBLE FACE (deux orientations) pour s'afficher quel que soit le côté caméra / le
|
||||||
// matériau. Une arête de surface posée sur une face de frontière a sa coordonnée d'axe EXACTE
|
// matériau. Une arête de surface posée sur une face de frontière a sa coordonnée d'axe EXACTE
|
||||||
// (l'interpolation MC garde fixe l'axe de la face) → comparaison flottante exacte fiable.
|
// (l'interpolation MC garde fixe l'axe de la face) → comparaison flottante exacte fiable.
|
||||||
if (bGenerateSkirts && MeshData.Triangles.Num() > 0)
|
if (bGenerateSkirts && (GroundTris.Num() + CapTris.Num()) > 0)
|
||||||
{
|
{
|
||||||
const float ExtentCm = (float)(CellsPerAxis * Step) * VOXEL_SIZE;
|
const float ExtentCm = (float)(CellsPerAxis * Step) * VOXEL_SIZE;
|
||||||
const float MinX = ChunkWorldPos.X, MinY = ChunkWorldPos.Y, MinZ = ChunkWorldPos.Z;
|
const float MinX = ChunkWorldPos.X, MinY = ChunkWorldPos.Y, MinZ = ChunkWorldPos.Z;
|
||||||
@@ -305,34 +404,261 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
return Idx;
|
return Idx;
|
||||||
};
|
};
|
||||||
|
|
||||||
// On ajoute en itérant : on fige le nombre de triangles de surface et on n'ajoute qu'au-delà.
|
// F17 — jupes émises PAR SEAU : chaque jupe hérite la classe (donc le polygroup /
|
||||||
const int32 BaseTriNum = MeshData.Triangles.Num();
|
// matériau) de son triangle source ; ajouter les jupes après coup casserait les runs
|
||||||
for (int32 t = 0; t + 2 < BaseTriNum; t += 3)
|
// d'indices contigus par groupe qu'exige RMC.
|
||||||
|
auto EmitSkirts = [&](TArray<int32>& Tris)
|
||||||
{
|
{
|
||||||
const int32 Tri[3] = { MeshData.Triangles[t], MeshData.Triangles[t + 1], MeshData.Triangles[t + 2] };
|
// On ajoute en itérant : on fige le nombre de triangles de surface et on n'ajoute qu'au-delà.
|
||||||
for (int32 e = 0; e < 3; ++e)
|
const int32 BaseTriNum = Tris.Num();
|
||||||
|
for (int32 t = 0; t + 2 < BaseTriNum; t += 3)
|
||||||
{
|
{
|
||||||
const int32 iA = Tri[e], iB = Tri[(e + 1) % 3];
|
const int32 Tri[3] = { Tris[t], Tris[t + 1], Tris[t + 2] };
|
||||||
// COPIES par valeur — AddSkirtVert réalloue Vertices/Normals (invaliderait des refs).
|
for (int32 e = 0; e < 3; ++e)
|
||||||
const FVector PA = MeshData.Vertices[iA];
|
{
|
||||||
const FVector PB = MeshData.Vertices[iB];
|
const int32 iA = Tri[e], iB = Tri[(e + 1) % 3];
|
||||||
if (!OnBoundaryPlane(PA, PB)) continue;
|
// COPIES par valeur — AddSkirtVert réalloue Vertices/Normals (invaliderait des refs).
|
||||||
|
const FVector PA = MeshData.Vertices[iA];
|
||||||
|
const FVector PB = MeshData.Vertices[iB];
|
||||||
|
if (!OnBoundaryPlane(PA, PB)) continue;
|
||||||
|
|
||||||
const FVector NA = MeshData.Normals[iA];
|
const FVector NA = MeshData.Normals[iA];
|
||||||
const FVector NB = MeshData.Normals[iB];
|
const FVector NB = MeshData.Normals[iB];
|
||||||
const FColor CA = MeshData.Colors[iA];
|
const FColor CA = MeshData.Colors[iA];
|
||||||
const FColor CB = MeshData.Colors[iB];
|
const FColor CB = MeshData.Colors[iB];
|
||||||
const int32 iA2 = AddSkirtVert(PA - NA * SkirtDepth, NA, CA);
|
const int32 iA2 = AddSkirtVert(PA - NA * SkirtDepth, NA, CA);
|
||||||
const int32 iB2 = AddSkirtVert(PB - NB * SkirtDepth, NB, CB);
|
const int32 iB2 = AddSkirtVert(PB - NB * SkirtDepth, NB, CB);
|
||||||
|
|
||||||
// Quad (iA, iB, iB2, iA2) → 2 triangles, émis dans LES DEUX orientations.
|
// Quad (iA, iB, iB2, iA2) → 2 triangles, émis dans LES DEUX orientations.
|
||||||
MeshData.Triangles.Add(iA); MeshData.Triangles.Add(iB); MeshData.Triangles.Add(iB2);
|
Tris.Add(iA); Tris.Add(iB); Tris.Add(iB2);
|
||||||
MeshData.Triangles.Add(iA); MeshData.Triangles.Add(iB2); MeshData.Triangles.Add(iA2);
|
Tris.Add(iA); Tris.Add(iB2); Tris.Add(iA2);
|
||||||
MeshData.Triangles.Add(iA); MeshData.Triangles.Add(iB2); MeshData.Triangles.Add(iB);
|
Tris.Add(iA); Tris.Add(iB2); Tris.Add(iB);
|
||||||
MeshData.Triangles.Add(iA); MeshData.Triangles.Add(iA2); MeshData.Triangles.Add(iB2);
|
Tris.Add(iA); Tris.Add(iA2); Tris.Add(iB2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
EmitSkirts(GroundTris);
|
||||||
|
EmitSkirts(CapTris);
|
||||||
|
}
|
||||||
|
|
||||||
|
// F17 — concatène sol PUIS sky-cap : un run d'indices contigu par polygroup (voir
|
||||||
|
// FVoxelMeshData::NumCeilingTriangles). Mêmes triangles qu'avant, seul l'ordre change.
|
||||||
|
MeshData.Triangles.Reserve(GroundTris.Num() + CapTris.Num());
|
||||||
|
MeshData.Triangles.Append(GroundTris);
|
||||||
|
MeshData.Triangles.Append(CapTris);
|
||||||
|
MeshData.NumCeilingTriangles = CapTris.Num() / 3;
|
||||||
|
|
||||||
|
return MeshData;
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// F18 — FEUILLE DE CHAMP LOINTAIN (voir le doc de la déclaration dans le .h)
|
||||||
|
//=============================================================================
|
||||||
|
// Deux grilles déplacées (sol + cap) au lieu d'un marching cubes 3D. Conventions IDENTIQUES à
|
||||||
|
// GenerateMesh : positions monde cm, UVs planaires (voxels), masques F6 par GetBiomeMaterialAt,
|
||||||
|
// jupes double-face extrudées le long de −N par seau, indices sol‖cap + NumCeilingTriangles.
|
||||||
|
// Winding : le chemin MC émet (0,2,1) des tables Bourke ⇒ une surface orientée +Z s'écrit
|
||||||
|
// (x,y) → (x,y+1) → (x+1,y) ; le cap (orienté −Z) inverse.
|
||||||
|
FVoxelMeshData UVoxelMarchingCubesMesher::GenerateSheetMesh(FIntVector OriginVoxels, int32 StepXY,
|
||||||
|
int32 CellsXY, int32 StrateChunkZ,
|
||||||
|
int32 HoleMinXVox, int32 HoleMinYVox,
|
||||||
|
int32 HoleMaxXVox, int32 HoleMaxYVox)
|
||||||
|
{
|
||||||
|
FVoxelMeshData MeshData;
|
||||||
|
if (!Generator) return MeshData;
|
||||||
|
StepXY = FMath::Max(1, StepXY);
|
||||||
|
CellsXY = FMath::Clamp(CellsXY, 2, 512);
|
||||||
|
|
||||||
|
// TROU XY — cellule [x0, x0+Step]×[y0, y0+Step] entièrement dans le rectangle (Max exclusif)
|
||||||
|
// ⇒ sautée : la zone est couverte par les coquilles MC (voir VoxelWorld.h SheetHole*).
|
||||||
|
auto CellInHole = [&](int32 cx, int32 cy) -> bool
|
||||||
|
{
|
||||||
|
const int32 X0 = OriginVoxels.X + cx * StepXY;
|
||||||
|
const int32 Y0 = OriginVoxels.Y + cy * StepXY;
|
||||||
|
return X0 >= HoleMinXVox && X0 + StepXY <= HoleMaxXVox
|
||||||
|
&& Y0 >= HoleMinYVox && Y0 + StepXY <= HoleMaxYVox;
|
||||||
|
};
|
||||||
|
|
||||||
|
const int32 GridDim = CellsXY + 1; // points de grille par axe
|
||||||
|
const int32 MDim = GridDim + 2; // + anneau de marge ±1 (normales continues entre feuilles)
|
||||||
|
|
||||||
|
// Early-out : la validité SurfaceWorld ne dépend que de ChunkZ (les strates sont des couches
|
||||||
|
// horizontales) — une sonde suffit. La sentinelle par colonne plus bas reste par prudence.
|
||||||
|
{
|
||||||
|
float T0 = 0.0f, C0 = 0.0f;
|
||||||
|
if (!Generator->GetSurfaceHeightAt((float)OriginVoxels.X, (float)OriginVoxels.Y,
|
||||||
|
StrateChunkZ, T0, C0))
|
||||||
|
{
|
||||||
|
return MeshData; // pas une strate SurfaceWorld → feuille vide
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colonnes (TerrainZ, CeilSurf) en Z-voxel monde absolu, marge incluse. X==FLT_MAX ⇒ invalide.
|
||||||
|
static thread_local TArray<FVector2f> SheetCols;
|
||||||
|
SheetCols.SetNumUninitialized(MDim * MDim);
|
||||||
|
for (int32 gy = -1; gy <= GridDim; ++gy)
|
||||||
|
{
|
||||||
|
for (int32 gx = -1; gx <= GridDim; ++gx)
|
||||||
|
{
|
||||||
|
float Tz = 0.0f, Cz = 0.0f;
|
||||||
|
const bool bOk = Generator->GetSurfaceHeightAt(
|
||||||
|
(float)(OriginVoxels.X + gx * StepXY), (float)(OriginVoxels.Y + gy * StepXY),
|
||||||
|
StrateChunkZ, Tz, Cz);
|
||||||
|
SheetCols[(gy + 1) * MDim + (gx + 1)] = bOk ? FVector2f(Tz, Cz)
|
||||||
|
: FVector2f(FLT_MAX, -FLT_MAX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto Col = [&](int32 gx, int32 gy) -> const FVector2f&
|
||||||
|
{
|
||||||
|
return SheetCols[(gy + 1) * MDim + (gx + 1)];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Index de vertex par point de grille (−1 = pas créé), sol et cap séparés — le partage de
|
||||||
|
// vertex est structurel (grille régulière), pas besoin de map de déduplication.
|
||||||
|
static thread_local TArray<int32> GroundIdx;
|
||||||
|
static thread_local TArray<int32> CapIdx;
|
||||||
|
GroundIdx.Init(-1, GridDim * GridDim);
|
||||||
|
CapIdx.Init(-1, GridDim * GridDim);
|
||||||
|
|
||||||
|
auto MakeVert = [&](int32 gx, int32 gy, bool bCap) -> int32
|
||||||
|
{
|
||||||
|
int32& Slot = (bCap ? CapIdx : GroundIdx)[gy * GridDim + gx];
|
||||||
|
if (Slot >= 0) return Slot;
|
||||||
|
|
||||||
|
const FVector2f C = Col(gx, gy);
|
||||||
|
const float H = bCap ? C.Y : C.X;
|
||||||
|
// Pente par différences centrales sur la grille de hauteurs (voxels/voxels — l'échelle cm
|
||||||
|
// se simplifie). Voisin invalide ⇒ composante plate (ne devrait pas arriver : cf. early-out).
|
||||||
|
auto HAt = [&](int32 x, int32 y) -> float
|
||||||
|
{
|
||||||
|
const FVector2f& N = Col(x, y);
|
||||||
|
return (N.X == FLT_MAX) ? H : (bCap ? N.Y : N.X);
|
||||||
|
};
|
||||||
|
const float Sx = (HAt(gx + 1, gy) - HAt(gx - 1, gy)) / (2.0f * (float)StepXY);
|
||||||
|
const float Sy = (HAt(gx, gy + 1) - HAt(gx, gy - 1)) / (2.0f * (float)StepXY);
|
||||||
|
// Sol : air au-dessus ⇒ normale vers le haut. Cap : l'air (l'intérieur de la strate) est
|
||||||
|
// EN-DESSOUS ⇒ normale vers le bas (−∇(z − C) = (Cx, Cy, −1)).
|
||||||
|
FVector Normal = bCap ? FVector(Sx, Sy, -1.0f) : FVector(-Sx, -Sy, 1.0f);
|
||||||
|
if (!Normal.Normalize())
|
||||||
|
{
|
||||||
|
Normal = FVector(0.0f, 0.0f, bCap ? -1.0f : 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float Xv = (float)(OriginVoxels.X + gx * StepXY);
|
||||||
|
const float Yv = (float)(OriginVoxels.Y + gy * StepXY);
|
||||||
|
const FVector WorldPos(Xv * VOXEL_SIZE, Yv * VOXEL_SIZE, H * VOXEL_SIZE);
|
||||||
|
|
||||||
|
Slot = MeshData.Vertices.Num();
|
||||||
|
MeshData.Vertices.Add(WorldPos);
|
||||||
|
MeshData.Normals.Add(Normal);
|
||||||
|
MeshData.UVs.Add(FVector2D(Xv, Yv)); // == WorldPos.XY / VOXEL_SIZE (convention MC)
|
||||||
|
|
||||||
|
// Mêmes masques F6 que GenerateMesh (palette biome dominant/voisin, pente, fondu).
|
||||||
|
int32 PalD = 0, PalN = 0; float BlendW = 0.0f;
|
||||||
|
Generator->GetBiomeMaterialAt(Xv, Yv, H, PalD, PalN, BlendW);
|
||||||
|
const uint8 Rr = (uint8)FMath::Clamp(PalD, 0, 255);
|
||||||
|
const uint8 Aa = (uint8)FMath::Clamp(PalN, 0, 255);
|
||||||
|
const uint8 Gg = (uint8)FMath::Clamp(FMath::RoundToInt((1.0f - FMath::Abs((float)Normal.Z)) * 255.0f), 0, 255);
|
||||||
|
const uint8 Bb = (uint8)FMath::Clamp(FMath::RoundToInt(BlendW * 255.0f), 0, 255);
|
||||||
|
MeshData.Colors.Add(FColor(Rr, Gg, Bb, Aa));
|
||||||
|
|
||||||
|
return Slot;
|
||||||
|
};
|
||||||
|
|
||||||
|
static thread_local TArray<int32> SheetGroundTris;
|
||||||
|
static thread_local TArray<int32> SheetCapTris;
|
||||||
|
SheetGroundTris.Reset();
|
||||||
|
SheetCapTris.Reset();
|
||||||
|
|
||||||
|
for (int32 cy = 0; cy < CellsXY; ++cy)
|
||||||
|
{
|
||||||
|
for (int32 cx = 0; cx < CellsXY; ++cx)
|
||||||
|
{
|
||||||
|
if (CellInHole(cx, cy)) continue; // couverte par les coquilles MC proches
|
||||||
|
if (Col(cx, cy).X == FLT_MAX || Col(cx + 1, cy).X == FLT_MAX ||
|
||||||
|
Col(cx, cy + 1).X == FLT_MAX || Col(cx + 1, cy + 1).X == FLT_MAX)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SOL (polygroup 0) — orienté +Z.
|
||||||
|
const int32 g00 = MakeVert(cx, cy, false);
|
||||||
|
const int32 g10 = MakeVert(cx + 1, cy, false);
|
||||||
|
const int32 g01 = MakeVert(cx, cy + 1, false);
|
||||||
|
const int32 g11 = MakeVert(cx + 1, cy + 1, false);
|
||||||
|
SheetGroundTris.Add(g00); SheetGroundTris.Add(g01); SheetGroundTris.Add(g10);
|
||||||
|
SheetGroundTris.Add(g10); SheetGroundTris.Add(g01); SheetGroundTris.Add(g11);
|
||||||
|
|
||||||
|
// CAP (polygroup 1) — orienté −Z ⇒ winding inversé.
|
||||||
|
const int32 c00 = MakeVert(cx, cy, true);
|
||||||
|
const int32 c10 = MakeVert(cx + 1, cy, true);
|
||||||
|
const int32 c01 = MakeVert(cx, cy + 1, true);
|
||||||
|
const int32 c11 = MakeVert(cx + 1, cy + 1, true);
|
||||||
|
SheetCapTris.Add(c00); SheetCapTris.Add(c10); SheetCapTris.Add(c01);
|
||||||
|
SheetCapTris.Add(c10); SheetCapTris.Add(c11); SheetCapTris.Add(c01);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JUPES périmètre — mêmes règles que le chemin MC : extrusion le long de −N (vers le solide :
|
||||||
|
// bas pour le sol, HAUT pour le cap), double face, émises PAR SEAU (héritent le polygroup).
|
||||||
|
// Entre feuilles voisines les coins coïncident (mêmes échantillons monde) → pas de fissure ;
|
||||||
|
// la jupe couvre surtout la couture feuille ↔ anneau MC et les décalages de niveau.
|
||||||
|
if (bGenerateSkirts && (SheetGroundTris.Num() + SheetCapTris.Num()) > 0)
|
||||||
|
{
|
||||||
|
const float SkirtDepth = FMath::Max(1.0f, SkirtCells) * (float)StepXY * VOXEL_SIZE;
|
||||||
|
|
||||||
|
auto EmitEdgeSkirt = [&](int32 ax, int32 ay, int32 bx, int32 by, bool bCap, TArray<int32>& Tris)
|
||||||
|
{
|
||||||
|
if (Col(ax, ay).X == FLT_MAX || Col(bx, by).X == FLT_MAX) return;
|
||||||
|
const int32 iA = MakeVert(ax, ay, bCap);
|
||||||
|
const int32 iB = MakeVert(bx, by, bCap);
|
||||||
|
// COPIES par valeur — les Add ci-dessous réallouent Vertices/Normals/Colors.
|
||||||
|
const FVector PA = MeshData.Vertices[iA], PB = MeshData.Vertices[iB];
|
||||||
|
const FVector NA = MeshData.Normals[iA], NB = MeshData.Normals[iB];
|
||||||
|
const FColor CA = MeshData.Colors[iA], CB = MeshData.Colors[iB];
|
||||||
|
|
||||||
|
const int32 iA2 = MeshData.Vertices.Num();
|
||||||
|
MeshData.Vertices.Add(PA - NA * SkirtDepth);
|
||||||
|
MeshData.Normals.Add(NA);
|
||||||
|
MeshData.UVs.Add(FVector2D(PA.X / VOXEL_SIZE, PA.Y / VOXEL_SIZE));
|
||||||
|
MeshData.Colors.Add(CA);
|
||||||
|
const int32 iB2 = MeshData.Vertices.Num();
|
||||||
|
MeshData.Vertices.Add(PB - NB * SkirtDepth);
|
||||||
|
MeshData.Normals.Add(NB);
|
||||||
|
MeshData.UVs.Add(FVector2D(PB.X / VOXEL_SIZE, PB.Y / VOXEL_SIZE));
|
||||||
|
MeshData.Colors.Add(CB);
|
||||||
|
|
||||||
|
Tris.Add(iA); Tris.Add(iB); Tris.Add(iB2);
|
||||||
|
Tris.Add(iA); Tris.Add(iB2); Tris.Add(iA2);
|
||||||
|
Tris.Add(iA); Tris.Add(iB2); Tris.Add(iB);
|
||||||
|
Tris.Add(iA); Tris.Add(iA2); Tris.Add(iB2);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Une jupe n'est émise que si la cellule de bord adjacente a réellement été maillée
|
||||||
|
// (pas dans le trou XY) — sinon mur flottant sans surface.
|
||||||
|
for (int32 c = 0; c < CellsXY; ++c)
|
||||||
|
{
|
||||||
|
for (int32 Pass = 0; Pass < 2; ++Pass)
|
||||||
|
{
|
||||||
|
const bool bCap = (Pass == 1);
|
||||||
|
TArray<int32>& Tris = bCap ? SheetCapTris : SheetGroundTris;
|
||||||
|
if (!CellInHole(c, 0))
|
||||||
|
EmitEdgeSkirt(c, 0, c + 1, 0, bCap, Tris); // bord Y-min
|
||||||
|
if (!CellInHole(c, CellsXY - 1))
|
||||||
|
EmitEdgeSkirt(c, CellsXY, c + 1, CellsXY, bCap, Tris); // bord Y-max
|
||||||
|
if (!CellInHole(0, c))
|
||||||
|
EmitEdgeSkirt(0, c, 0, c + 1, bCap, Tris); // bord X-min
|
||||||
|
if (!CellInHole(CellsXY - 1, c))
|
||||||
|
EmitEdgeSkirt(CellsXY, c, CellsXY, c + 1, bCap, Tris); // bord X-max
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Concatène sol PUIS cap : un run d'indices contigu par polygroup (contrat F17/BuildTileStreamSet).
|
||||||
|
MeshData.Triangles.Reserve(SheetGroundTris.Num() + SheetCapTris.Num());
|
||||||
|
MeshData.Triangles.Append(SheetGroundTris);
|
||||||
|
MeshData.Triangles.Append(SheetCapTris);
|
||||||
|
MeshData.NumCeilingTriangles = SheetCapTris.Num() / 3;
|
||||||
|
|
||||||
return MeshData;
|
return MeshData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -452,6 +452,27 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
|
|||||||
return MinSDF;
|
return MinSDF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UVoxelStrateManager::AnyPassageNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const
|
||||||
|
{
|
||||||
|
// Le carve d'un passage atteint ModSDF < PASSAGE_BLEND_RADIUS (4, VoxelGenerator.cpp) au-delà de
|
||||||
|
// sa surface ; BoundRadius inclut déjà rayon + blend, on re-pad par sécurité (conservatif).
|
||||||
|
constexpr float CarvePad = 4.0f;
|
||||||
|
for (const FVoxelPassage& P : Passages)
|
||||||
|
{
|
||||||
|
// Point de la boîte le plus proche du centre de la sphère → test sphère/AABB.
|
||||||
|
const FVector C(
|
||||||
|
FMath::Clamp(P.BoundCenter.X, MinVoxel.X, MaxVoxel.X),
|
||||||
|
FMath::Clamp(P.BoundCenter.Y, MinVoxel.Y, MaxVoxel.Y),
|
||||||
|
FMath::Clamp(P.BoundCenter.Z, MinVoxel.Z, MaxVoxel.Z));
|
||||||
|
const float Reach = P.BoundRadius + CarvePad;
|
||||||
|
if (FVector::DistSquared(C, P.BoundCenter) <= Reach * Reach)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// QUERIES
|
// QUERIES
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -251,6 +251,18 @@ struct FCachedRoom
|
|||||||
// Intensity scale for this room's op (from FStrateTerrainOpEntry::Weight).
|
// Intensity scale for this room's op (from FStrateTerrainOpEntry::Weight).
|
||||||
// 1.0 = use op as configured, 0.5 = half intensity, 2.0 = double.
|
// 1.0 = use op as configured, 0.5 = half intensity, 2.0 = double.
|
||||||
float RoomOpWeight = 1.0f;
|
float RoomOpWeight = 1.0f;
|
||||||
|
|
||||||
|
// PRE-BAKED SHAPE (BuildChunkCache). The shape roll, variety thresholds and the capsule's
|
||||||
|
// Cos/Sin direction used to be re-derived PER VOXEL per room inside EvaluateSDFCached — hash
|
||||||
|
// mixes + trig in the hottest loop of the plugin for values that are constants of the room.
|
||||||
|
// Bit-identical to the old per-voxel roll (same hashes, same math, done once per chunk).
|
||||||
|
// 0 = ellipsoid → ShapeA = radii (x=y=RadiusXY, z=RadiusZ)
|
||||||
|
// 1 = rounded box → ShapeA = half-extents, ShapeR = corner rounding
|
||||||
|
// 2 = capsule → ShapeA/ShapeB = world endpoints, ShapeR = tube radius
|
||||||
|
uint8 ShapeType = 0;
|
||||||
|
FVector ShapeA = FVector::ZeroVector;
|
||||||
|
FVector ShapeB = FVector::ZeroVector;
|
||||||
|
float ShapeR = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
// A pre-computed tunnel segment — all connection decisions and hash-derived
|
// A pre-computed tunnel segment — all connection decisions and hash-derived
|
||||||
@@ -370,17 +382,16 @@ namespace VoxelCaveMorphology
|
|||||||
// @param WorldX, WorldY, WorldZ — position in voxel coordinates (may be warped)
|
// @param WorldX, WorldY, WorldZ — position in voxel coordinates (may be warped)
|
||||||
// @param Cache — pre-built cache from BuildChunkCache
|
// @param Cache — pre-built cache from BuildChunkCache
|
||||||
// @param SDFBlendRadius — SmoothMin blend radius (from Params.SDFBlendRadius)
|
// @param SDFBlendRadius — SmoothMin blend radius (from Params.SDFBlendRadius)
|
||||||
// @param RoomShapeVariety — shape variety factor (from Params.RoomShapeVariety)
|
|
||||||
// @param OutNearestRoomIdx — optional out: index of the room with minimum SDF
|
// @param OutNearestRoomIdx — optional out: index of the room with minimum SDF
|
||||||
// contribution. -1 if no room passed the cull test.
|
// contribution. -1 if no room passed the cull test.
|
||||||
// Used by the terrain ops system to look up the
|
// Used by the terrain ops system to look up the
|
||||||
// per-room terrain op assigned to this voxel's room.
|
// per-room terrain op assigned to this voxel's room.
|
||||||
|
// (Room shape variety is baked into FCachedRoom by BuildChunkCache — no per-voxel roll.)
|
||||||
// @return negative = inside cave, positive = solid rock
|
// @return negative = inside cave, positive = solid rock
|
||||||
float EvaluateSDFCached(
|
float EvaluateSDFCached(
|
||||||
float WorldX, float WorldY, float WorldZ,
|
float WorldX, float WorldY, float WorldZ,
|
||||||
const FChunkSDFCache& Cache,
|
const FChunkSDFCache& Cache,
|
||||||
float SDFBlendRadius,
|
float SDFBlendRadius,
|
||||||
float RoomShapeVariety,
|
|
||||||
int32* OutNearestRoomIdx = nullptr
|
int32* OutNearestRoomIdx = nullptr
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,11 @@ public:
|
|||||||
* raymarched shadows. */
|
* raymarched shadows. */
|
||||||
void GetActiveOrbs(TArray<FVoxelActiveOrb>& OutOrbs) const;
|
void GetActiveOrbs(TArray<FVoxelActiveOrb>& OutOrbs) const;
|
||||||
|
|
||||||
|
/** Remove decoration instances inside a world-space sphere (both grids). Used by player digging (grass
|
||||||
|
* shouldn't float over a hole) and landmark footprints. The placer already skips carved columns on any
|
||||||
|
* future rebuild — this patches the LIVE instances. Returns how many were removed. Game-thread. */
|
||||||
|
int32 RemoveDecorationsInSphere(const FVector& WorldCenter, float WorldRadius);
|
||||||
|
|
||||||
/** Destroy all spawned content (decorations + water). Regenerate / season reset. Bumps the deco
|
/** Destroy all spawned content (decorations + water). Regenerate / season reset. Bumps the deco
|
||||||
* epoch so any in-flight march tasks' results are discarded. */
|
* epoch so any in-flight march tasks' results are discarded. */
|
||||||
void ClearAll();
|
void ClearAll();
|
||||||
@@ -136,6 +141,8 @@ public:
|
|||||||
struct FDecoSpawn
|
struct FDecoSpawn
|
||||||
{
|
{
|
||||||
int32 EntryIdx = 0;
|
int32 EntryIdx = 0;
|
||||||
|
int32 CompanionIdx = -1; // -1 = the entry itself; else index into Entries[EntryIdx].Companions (F7)
|
||||||
|
int32 SubIdx = -1; // when CompanionIdx>=0: -1 = the companion; else its SubCompanions[SubIdx]
|
||||||
bool bInstanced = false;
|
bool bInstanced = false;
|
||||||
FTransform Xf = FTransform::Identity;
|
FTransform Xf = FTransform::Identity;
|
||||||
};
|
};
|
||||||
@@ -163,7 +170,8 @@ private:
|
|||||||
// Instanced transforms for one mesh, accumulated across all of a region's cells → one batched HISM.
|
// Instanced transforms for one mesh, accumulated across all of a region's cells → one batched HISM.
|
||||||
struct FRegionMeshBucket
|
struct FRegionMeshBucket
|
||||||
{
|
{
|
||||||
FStrateDecoration Deco; // representative entry (mesh + HISM render tuning: cull/shadow/scale)
|
FPlacementProfile Profile; // representative profile (mesh + HISM render tuning: cull/shadow) —
|
||||||
|
// may be a decoration entry's OR a companion's (F7).
|
||||||
TArray<FTransform> Xforms;
|
TArray<FTransform> Xforms;
|
||||||
};
|
};
|
||||||
// A non-instanced actor placement, spawned when the region is applied.
|
// A non-instanced actor placement, spawned when the region is applied.
|
||||||
@@ -240,6 +248,11 @@ private:
|
|||||||
// terrain material consumes the nearest active orb for raymarched shadows (see GetActiveOrbs).
|
// terrain material consumes the nearest active orb for raymarched shadows (see GetActiveOrbs).
|
||||||
bool bIsOrb = false;
|
bool bIsOrb = false;
|
||||||
FVoxelActiveOrb Orb;
|
FVoxelActiveOrb Orb;
|
||||||
|
|
||||||
|
// Decoration footprint (F7): >0 = clear decorations within this world sphere. Set when the landmark
|
||||||
|
// has bSuppressDecorationsUnder, so a newly-applied deco region can re-clear under it too.
|
||||||
|
FVector SuppressCenter = FVector::ZeroVector;
|
||||||
|
float SuppressRadiusWorld = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** WORKER-THREAD surface find → fills OutSpawns for one cell. SurfaceWorld uses the height oracle
|
/** WORKER-THREAD surface find → fills OutSpawns for one cell. SurfaceWorld uses the height oracle
|
||||||
@@ -278,8 +291,18 @@ private:
|
|||||||
void SpawnLandmarkInstance(const FStrateLandmark& L, uint32 H, const FDecoContext& Ctx,
|
void SpawnLandmarkInstance(const FStrateLandmark& L, uint32 H, const FDecoContext& Ctx,
|
||||||
const FTransform& OwnerXf, AActor* OwnerActor,
|
const FTransform& OwnerXf, AActor* OwnerActor,
|
||||||
float LocalX, float LocalY, float Step, float ColDepth, FLandmarkInstance& Out);
|
float LocalX, float LocalY, float Step, float ColDepth, FLandmarkInstance& Out);
|
||||||
|
// Shared spawn core for landmarks AND set-pieces: surface-find + all FPlacementProfile gates (biome/
|
||||||
|
// slope/water/Conditions) → transform → spawn actor OR one static mesh into Out. Returns true (OutXf
|
||||||
|
// = final transform) when placed; false (Out untouched) on any gate fail = "evaluated, nothing placed".
|
||||||
|
bool SpawnFromProfile(const FPlacementProfile& P, uint32 H, const FDecoContext& Ctx,
|
||||||
|
const FTransform& OwnerXf, AActor* OwnerActor,
|
||||||
|
float LocalX, float LocalY, float Step, float ColDepth,
|
||||||
|
FTransform& OutXf, FLandmarkInstance& Out);
|
||||||
void DestroyLandmarkInstance(FLandmarkInstance& Inst);
|
void DestroyLandmarkInstance(FLandmarkInstance& Inst);
|
||||||
void ClearAllLandmarks();
|
void ClearAllLandmarks();
|
||||||
|
// Remove instances within a world sphere from ONE region's HISMs (the per-region core of
|
||||||
|
// RemoveDecorationsInSphere; also used by ApplyRegion to clear under a landmark footprint). Returns count.
|
||||||
|
static int32 RemoveInstancesInContent(FDecoRegionContent& Content, const FVector& Center, float Radius);
|
||||||
// Single-column surface find for a landmark (voxel XY): SurfaceWorld → height oracle, else ray-march the
|
// Single-column surface find for a landmark (voxel XY): SurfaceWorld → height oracle, else ray-march the
|
||||||
// strate band for the first crossing whose orientation matches Surf. Fills Z (voxel) + outward world normal.
|
// strate band for the first crossing whose orientation matches Surf. Fills Z (voxel) + outward world normal.
|
||||||
static bool FindLandmarkColumn(const UVoxelGenerator* Gen, const FTransform& OwnerXf,
|
static bool FindLandmarkColumn(const UVoxelGenerator* Gen, const FTransform& OwnerXf,
|
||||||
@@ -318,6 +341,7 @@ private:
|
|||||||
// Spawned landmarks, keyed by FIntVector(latticeCellX, latticeCellY, entryIndex) — FIntVector already
|
// Spawned landmarks, keyed by FIntVector(latticeCellX, latticeCellY, entryIndex) — FIntVector already
|
||||||
// hashes, so no custom key type is needed. An entry with both ptrs null = "evaluated, nothing placed"
|
// hashes, so no custom key type is needed. An entry with both ptrs null = "evaluated, nothing placed"
|
||||||
// (kept until the cell leaves range so the surface-find isn't repeated). Strate-bounded.
|
// (kept until the cell leaves range so the surface-find isn't repeated). Strate-bounded.
|
||||||
|
// Keyed by FIntVector(cellX|passageIdx, cellY|side, entryIndex) — lattice cell OR passage endpoint.
|
||||||
TMap<FIntVector, FLandmarkInstance> LandmarkInstances;
|
TMap<FIntVector, FLandmarkInstance> LandmarkInstances;
|
||||||
int32 LastLandmarkStrate = INT32_MIN; // strate change → wipe + rebuild landmarks
|
int32 LastLandmarkStrate = INT32_MIN; // strate change → wipe + rebuild landmarks
|
||||||
|
|
||||||
|
|||||||
@@ -225,6 +225,36 @@ public:
|
|||||||
*/
|
*/
|
||||||
bool HasModifications(const FIntVector& ChunkCoord) const;
|
bool HasModifications(const FIntVector& ChunkCoord) const;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// HOT-PATH SNAPSHOT API (per-chunk, lock-amortised)
|
||||||
|
//=========================================================================
|
||||||
|
// Once ANY carve exists, calling HasModifications + GetDensityOffset per voxel costs two
|
||||||
|
// ModsLock acquisitions + two TMap finds per density sample (~86k lock ops per tile task —
|
||||||
|
// during carve gameplay, exactly when re-mesh latency matters). Instead, a worker snapshots a
|
||||||
|
// chunk's mod list ONCE per (chunk, version) and evaluates it lock-free via EvaluateMods; the
|
||||||
|
// version bump on ApplyModification/Clear invalidates worker-side caches.
|
||||||
|
|
||||||
|
/** Lock-free: true once any modification exists anywhere (false = the common streaming case). */
|
||||||
|
bool HasAnyMods() const { return bHasAnyMods.load(std::memory_order_acquire); }
|
||||||
|
|
||||||
|
/** Monotonic mod-state version — bumped by ApplyModification and Clear. */
|
||||||
|
uint32 GetModsVersion() const { return ModsVersion.load(std::memory_order_acquire); }
|
||||||
|
|
||||||
|
/** Copy this chunk's modification list under ONE read lock (Out emptied if none). */
|
||||||
|
void GetChunkModsSnapshot(const FIntVector& ChunkCoord, TArray<FVoxelModification>& Out) const;
|
||||||
|
|
||||||
|
/** True si un chunk modifié intersecte [MinChunk, MaxChunk] (inclusif). Conservatif par
|
||||||
|
* construction : ApplyModification enregistre le mod dans TOUS les chunks que son rayon
|
||||||
|
* touche, donc le test par clé de chunk suffit. Une passe de lecture sur les clés (les
|
||||||
|
* mondes édités ont peu de chunks modifiés) — utilisé par ClassifyTile, PAS par voxel. */
|
||||||
|
bool HasAnyModInChunkRange(const FIntVector& MinChunk, const FIntVector& MaxChunk) const;
|
||||||
|
|
||||||
|
/** Evaluate a mod list at a voxel — the lock-free core shared by GetDensityOffset and the
|
||||||
|
* generator's snapshot path. Pure function (deterministic). Returns the combined offset
|
||||||
|
* (negative = carve, positive = fill). */
|
||||||
|
static float EvaluateMods(const TArray<FVoxelModification>& Mods,
|
||||||
|
float WorldX, float WorldY, float WorldZ);
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// MANAGEMENT
|
// MANAGEMENT
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -256,6 +286,7 @@ private:
|
|||||||
TMap<FIntVector, TArray<FVoxelModification>> ChunkMods;
|
TMap<FIntVector, TArray<FVoxelModification>> ChunkMods;
|
||||||
mutable FRWLock ModsLock;
|
mutable FRWLock ModsLock;
|
||||||
std::atomic<bool> bHasAnyMods{ false };
|
std::atomic<bool> bHasAnyMods{ false };
|
||||||
|
std::atomic<uint32> ModsVersion{ 1 }; // see the snapshot API above
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// BUDGET TRACKING
|
// BUDGET TRACKING
|
||||||
|
|||||||
@@ -20,6 +20,54 @@ class UVoxelStrateManager;
|
|||||||
class UVoxelDiffLayer;
|
class UVoxelDiffLayer;
|
||||||
class UVoxelBiomeDefinition;
|
class UVoxelBiomeDefinition;
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// LOD-AWARE OCTAVE REDUCTION (T2.b)
|
||||||
|
//=============================================================================
|
||||||
|
// At Step=2/4, noise octaves whose wavelength is smaller than the sampling cell
|
||||||
|
// are pure aliasing cost — they can't shape the coarse isosurface, only shift it
|
||||||
|
// by sub-cell noise. Réduction d'octaves sur les tuiles lointaines (LOD).
|
||||||
|
//
|
||||||
|
// OctaveBias = octaves dropped from PER-VOXEL volumetric noise for the tile
|
||||||
|
// currently being meshed on THIS thread. Default 0 = full quality; set (via
|
||||||
|
// TGuardValue) by UVoxelMarchingCubesMesher::GenerateMesh from its Step and the
|
||||||
|
// opt-in UVoxelSettings::LODOctaveDrop, restored when the tile finishes — so
|
||||||
|
// game-thread queries, deco snapping and the density-volume fill thread always
|
||||||
|
// see 0. fBM/Ridged add octaves low→high frequency, so dropping the TAIL keeps
|
||||||
|
// the coarse shape identical; only sub-cell detail (already unrepresentable at
|
||||||
|
// that Step) disappears. LOD0 (Step=1) is byte-identical regardless.
|
||||||
|
//
|
||||||
|
// Deliberately NOT applied to XY-field noise (heightfield, ceiling, relief,
|
||||||
|
// moisture): those feed box-validated caches that can outlive a tile task on
|
||||||
|
// the same thread, and biome/climate must stay LOD-independent.
|
||||||
|
namespace VoxelGenLOD
|
||||||
|
{
|
||||||
|
// NOT VOXELFORGE_API: MSVC forbids dll-interface on thread_local (C2492). Both users
|
||||||
|
// (generator + mesher) are inside this module, so no export is needed anyway.
|
||||||
|
extern thread_local int32 OctaveBias;
|
||||||
|
|
||||||
|
// Effective octave count for a per-voxel noise call site.
|
||||||
|
// At least 1 octave always survives (the coarse base shape).
|
||||||
|
FORCEINLINE int32 Eff(int32 Octaves) { return FMath::Max(1, Octaves - OctaveBias); }
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// TRIVIAL-TILE CLASSIFICATION (T1.d)
|
||||||
|
//=============================================================================
|
||||||
|
// Verdict de ClassifyTile pour une tuile AVANT le pré-échantillonnage 33³+ :
|
||||||
|
// AllSolid / AllAir garantissent que CHAQUE point du treillis du mesher (marge
|
||||||
|
// ±1 incluse) est du même côté de l'iso ⇒ maillage vide, GenerateMesh est
|
||||||
|
// sautée. Mixed = "je ne peux pas le prouver" ⇒ génération normale. Un faux
|
||||||
|
// Mixed coûte juste du CPU ; un faux AllSolid/AllAir ferait un TROU — les
|
||||||
|
// verdicts ne sont donc émis que sur des bornes exactes (colonnes surface
|
||||||
|
// échantillonnées au MÊME treillis que le mesher) + gardes conservatives sur
|
||||||
|
// tout ce qui peut creuser/remplir (spine, passages, disturbances, diff layer).
|
||||||
|
enum class EVoxelTileClass : uint8
|
||||||
|
{
|
||||||
|
Mixed, // peut contenir une surface → mesher normalement
|
||||||
|
AllSolid, // chaque échantillon prouvé solide → maillage vide
|
||||||
|
AllAir, // chaque échantillon prouvé air → maillage vide
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UVoxelGenerator
|
* UVoxelGenerator
|
||||||
*
|
*
|
||||||
@@ -194,6 +242,16 @@ public:
|
|||||||
int32& OutDominantPalette, int32& OutNeighborPalette,
|
int32& OutDominantPalette, int32& OutNeighborPalette,
|
||||||
float& OutBlendWeight) const;
|
float& OutBlendWeight) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* F7 AWARE PLACEMENT: evaluate an entry's relational placement conditions at a candidate voxel XY.
|
||||||
|
* True = ALL conditions pass (AND); empty list = true (zero cost). Pure query of the analytic fields
|
||||||
|
* (relief/moisture/biome-border) — deterministic + worker-safe. The caller passes the strate's
|
||||||
|
* already-resolved biome context (freq/contrast + Voronoi map), so no re-resolve. Also the shared core
|
||||||
|
* of the future quest FindFeature locator (same predicate, run as an outward search).
|
||||||
|
*/
|
||||||
|
bool EvaluateTerrainConditions(const TArray<FTerrainCondition>& Conditions,
|
||||||
|
float WorldX, float WorldY, const FBiomeContext& BiomeCtx) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SurfaceWorld HEIGHT ORACLE: the terrain surface Z + sky-cap ceiling Z (voxel coords) at a world
|
* SurfaceWorld HEIGHT ORACLE: the terrain surface Z + sky-cap ceiling Z (voxel coords) at a world
|
||||||
* XY for the given strate slice (ChunkZ), WITHOUT ray-marching the density column. Returns false
|
* XY for the given strate slice (ChunkZ), WITHOUT ray-marching the density column. Returns false
|
||||||
@@ -205,6 +263,20 @@ public:
|
|||||||
bool GetSurfaceHeightAt(float WorldX, float WorldY, int32 ChunkZ,
|
bool GetSurfaceHeightAt(float WorldX, float WorldY, int32 ChunkZ,
|
||||||
float& OutTerrainZ, float& OutCeilSurf) const;
|
float& OutTerrainZ, float& OutCeilSurf) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* T1.d — classification conservative d'une tuile AVANT le pré-échantillonnage du mesher.
|
||||||
|
* (OriginVoxels, Step, CellsPerAxis) = les MÊMES arguments que GenerateMesh ; le verdict
|
||||||
|
* porte sur le treillis exact que le mesher échantillonnerait (marge ±1 incluse).
|
||||||
|
*
|
||||||
|
* v1 : ne prouve que les chunks GAP (bedrock) et les strates SurfaceWorld — colonnes
|
||||||
|
* terrain/plafond évaluées par le MÊME ComputeSurfaceColumn que le chemin densité (donc
|
||||||
|
* bit-identiques), bandes de seal solides, gardes spine/passages/disturbances/diff.
|
||||||
|
* Tout autre archétype (intérieur de caves) ⇒ Mixed. Worker-safe (lecture seule +
|
||||||
|
* caches thread_local partagés avec GetDensityAt — un verdict Mixed laisse les colonnes
|
||||||
|
* chaudes pour la génération qui suit).
|
||||||
|
*/
|
||||||
|
EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/** Pick the biome (index into Ctx.Biomes) for a Voronoi site, by its climate. */
|
/** 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;
|
int32 ClassifyBiomeAtSite(float SiteX, float SiteY, const FBiomeContext& Ctx, uint32 SiteHash) const;
|
||||||
@@ -213,6 +285,13 @@ private:
|
|||||||
* per-XY; the part that's evaluated per biome and blended in GetSurfaceDensity. */
|
* per-XY; the part that's evaluated per biome and blended in GetSurfaceDensity. */
|
||||||
float ComputeSurfaceTerrainZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const;
|
float ComputeSurfaceTerrainZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const;
|
||||||
|
|
||||||
|
/** F20 — the RAW structural heightfield (continents + mountains + detail), BEFORE any
|
||||||
|
* terrain op (cliff/terrace/layer-lines/beach). Ops in ComputeSurfaceTerrainZ build on
|
||||||
|
* this; Cliff re-samples it at an XY offset for a cheap analytic slope. OutM = the relief
|
||||||
|
* "mountainous-ness" [0,1], reused to relief-condition the ops. */
|
||||||
|
float SampleSurfaceStructuralZ(float WorldX, float WorldY,
|
||||||
|
const FSurfaceGenerationParams& Params, float& OutM) const;
|
||||||
|
|
||||||
/** The SurfaceWorld sky-cap ceiling surface Z at a world XY (also pure per-XY). */
|
/** 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;
|
float ComputeSurfaceCeiling(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const;
|
||||||
|
|
||||||
@@ -224,17 +303,20 @@ private:
|
|||||||
TArray<FSurfaceGenerationParams>& OutBiomeParams) const;
|
TArray<FSurfaceGenerationParams>& OutBiomeParams) const;
|
||||||
|
|
||||||
/** Biome-blended terrain Z + sky-cap ceiling Z for one column (the XY-only surface field). Shared
|
/** Biome-blended terrain Z + sky-cap ceiling Z for one column (the XY-only surface field). Shared
|
||||||
* by the density column cache (T1.a) and the oracle. */
|
* by the density column cache (T1.a) and the oracle. F20 phase 2 overhang, resolved per column:
|
||||||
|
* `OutOverhangAmp` = strength·slope-gate (0 = off), `(OutDirX,OutDirY)` = unit UPHILL gradient dir. */
|
||||||
void ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ,
|
void ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ,
|
||||||
const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx,
|
const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx,
|
||||||
const TArray<FSurfaceGenerationParams>& BiomeParams, FChunkBiomeCache& BiomeCache,
|
const TArray<FSurfaceGenerationParams>& BiomeParams, FChunkBiomeCache& BiomeCache,
|
||||||
float& OutTerrainZ, float& OutCeilSurf) const;
|
float& OutTerrainZ, float& OutCeilSurf,
|
||||||
|
float& OutOverhangAmp, float& OutDirX, float& OutDirY) const;
|
||||||
|
|
||||||
/** Final SurfaceWorld density from a column's precomputed terrain Z + ceiling: the
|
/** Final SurfaceWorld density from a column's precomputed terrain Z + ceiling: the cheap per-voxel
|
||||||
* cheap per-voxel Z-combine + origin spine + boundary seal + passage carving. The
|
* Z-combine + F20 overhang shelf (warped-terrain union, uphill dir) + origin spine + seal + passages.
|
||||||
* XY-only work (terrain/ceiling) is done once per column and cached (T1.a). */
|
* The XY-only work (terrain/ceiling/overhang amp+dir) is done once per column and cached (T1.a). */
|
||||||
float SurfaceDensityFromColumn(float WorldX, float WorldY, float WorldZ,
|
float SurfaceDensityFromColumn(float WorldX, float WorldY, float WorldZ,
|
||||||
float TerrainZ, float CeilSurf,
|
float TerrainZ, float CeilSurf,
|
||||||
|
float OverhangAmp, float DirX, float DirY,
|
||||||
const FSurfaceGenerationParams& Structural) const;
|
const FSurfaceGenerationParams& Structural) const;
|
||||||
|
|
||||||
/** (Re)build the per-chunk biome cell grid covering chunk (X,Y) footprint + margin. */
|
/** (Re)build the per-chunk biome cell grid covering chunk (X,Y) footprint + margin. */
|
||||||
|
|||||||
@@ -36,9 +36,47 @@ public:
|
|||||||
* densité déjà échantillonnée, quantifiés via VF_QuantizeDensity. Cela évite à
|
* densité déjà échantillonnée, quantifiés via VF_QuantizeDensity. Cela évite à
|
||||||
* UVoxelDensityVolume de re-sampler GetDensityAt pour ces cellules (le mesher
|
* UVoxelDensityVolume de re-sampler GetDensityAt pour ces cellules (le mesher
|
||||||
* les a déjà calculées). Vidé puis rempli ; reste vide si non éligible.
|
* les a déjà calculées). Vidé puis rempli ; reste vide si non éligible.
|
||||||
|
* @param BandZMinVox / BandZMaxVox - COUPE DE CONTENU PAR STRATE (optionnel, voxels Z INCLUSIFS,
|
||||||
|
* cf. UVoxelSettings::StrateContentCutMinLevel) : seules les cellules dont
|
||||||
|
* l'intervalle Z chevauche la bande sont maillées. Une tuile grossière qui
|
||||||
|
* chevauche une frontière de strate ne maille que la strate du joueur —
|
||||||
|
* supprime les trous d'aliasing (le bouchon seal/gap plus fin que Step
|
||||||
|
* tombait entre deux points du treillis) et le mélange de matériaux entre
|
||||||
|
* strates. Les cellules maillées restent identiques au bit près (mêmes
|
||||||
|
* échantillons monde purs). Jamais combiné avec OutCaptureGrid.
|
||||||
*/
|
*/
|
||||||
FVoxelMeshData GenerateMesh(FIntVector OriginVoxels, int32 Step = 1, int32 CellsPerAxis = CHUNK_SIZE,
|
FVoxelMeshData GenerateMesh(FIntVector OriginVoxels, int32 Step = 1, int32 CellsPerAxis = CHUNK_SIZE,
|
||||||
TArray<uint8>* OutCaptureGrid = nullptr);
|
TArray<uint8>* OutCaptureGrid = nullptr,
|
||||||
|
int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* F18 — FEUILLE de champ lointain (anneau render-distance, cf. UVoxelSettings::bFarSheetRing).
|
||||||
|
* Dans une strate ouverte (SurfaceWorld) le champ lointain est exactement DEUX heightfields —
|
||||||
|
* TerrainZ (sol) et CeilSurf (plafond sky-cap), déjà calculés par colonne par l'oracle
|
||||||
|
* GetSurfaceHeightAt. On construit donc deux grilles déplacées régulières au lieu d'un marching
|
||||||
|
* cubes 3D : sol = polygroup 0, cap = polygroup 1 (classes vraies PAR CONSTRUCTION — pas de vote,
|
||||||
|
* pas de sonde de classification), mêmes conventions que GenerateMesh (positions monde cm, UVs
|
||||||
|
* planaires, masques couleur F6 biome/pente/fondu, normales du gradient de hauteur, jupes
|
||||||
|
* périmètre par seau, run d'indices sol‖cap + NumCeilingTriangles).
|
||||||
|
*
|
||||||
|
* @param OriginVoxels - Coin min de la tuile (voxels). Seul XY est utilisé (les hauteurs sont absolues).
|
||||||
|
* @param StepXY - Pas d'échantillonnage XY en voxels (aligné sur l'anneau MC pour la continuité).
|
||||||
|
* @param CellsXY - Cellules par axe XY (extent = CellsXY × StepXY).
|
||||||
|
* @param StrateChunkZ - Chunk Z DANS la strate de référence (le cœur de la bande) — identifie la
|
||||||
|
* strate dont on maille sol+cap. Hors SurfaceWorld ⇒ mesh vide.
|
||||||
|
* @param HoleMin/MaxX/YVox - TROU XY (voxels, Max EXCLUSIF ; sentinelles MAX/MIN = pas de trou) :
|
||||||
|
* les cellules ENTIÈREMENT dans ce rectangle (la zone couverte par les
|
||||||
|
* coquilles MC autour du joueur) sont sautées — sinon la feuille recouvre
|
||||||
|
* le terrain proche avec son échantillonnage grossier. Les cellules à
|
||||||
|
* cheval restent (anneau de recouvrement au raccord) ; pas de jupe sur
|
||||||
|
* les bords du trou (le terrain MC remplit derrière).
|
||||||
|
* Non couvert (accepté, cf. fable-idea F18) : passages/spine/chasms creusés (le heightfield pur ne
|
||||||
|
* les contient pas), diff layer — invisibles à distance de feuille, l'anneau MC proche les garde.
|
||||||
|
*/
|
||||||
|
FVoxelMeshData GenerateSheetMesh(FIntVector OriginVoxels, int32 StepXY, int32 CellsXY,
|
||||||
|
int32 StrateChunkZ,
|
||||||
|
int32 HoleMinXVox = INT32_MAX, int32 HoleMinYVox = INT32_MAX,
|
||||||
|
int32 HoleMaxXVox = INT32_MIN, int32 HoleMaxYVox = INT32_MIN);
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// SERVICES (injectés par AVoxelWorld)
|
// SERVICES (injectés par AVoxelWorld)
|
||||||
@@ -66,4 +104,11 @@ public:
|
|||||||
// Profondeur de la jupe, en CELLULES de la tuile (× Step × VOXEL_SIZE). ~2 cellules couvrent
|
// Profondeur de la jupe, en CELLULES de la tuile (× Step × VOXEL_SIZE). ~2 cellules couvrent
|
||||||
// l'écart vers un voisin un niveau plus grossier (cellule 2×). Monter si des fissures persistent.
|
// l'écart vers un voisin un niveau plus grossier (cellule 2×). Monter si des fissures persistent.
|
||||||
float SkirtCells = 2.0f;
|
float SkirtCells = 2.0f;
|
||||||
|
|
||||||
|
// T2.b — LOD-aware octave reduction (opt-in, copied from UVoxelSettings::LODOctaveDrop).
|
||||||
|
// Octaves dropped from per-voxel volumetric noise PER Step doubling: a tile at Step=S
|
||||||
|
// drops LODOctaveDrop * log2(S) octaves (see VoxelGenLOD in VoxelGenerator.h).
|
||||||
|
// 0 (default) = off — every LOD samples full octaves, byte-identical to before.
|
||||||
|
// Réduction d'octaves sur les tuiles grossières ; 0 = désactivé.
|
||||||
|
int32 LODOctaveDrop = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -65,6 +65,20 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0"))
|
||||||
int32 StrateViewMarginChunks = 3;
|
int32 StrateViewMarginChunks = 3;
|
||||||
|
|
||||||
|
// STRATE CONTENT CUT (F17 separation) — a level-L tile is 2^L chunks TALL and can straddle a
|
||||||
|
// strate boundary: at coarse Steps the thin seal/gap solid between two strates' airs falls
|
||||||
|
// between lattice points (⇒ holes into the neighbour strate at far LOD) and one tile mixes
|
||||||
|
// both strates' materials. From this clip level UP, the mesher only meshes cells inside the
|
||||||
|
// PLAYER's strate Z-band (the other strates are sealed/enclosed ⇒ invisible from here anyway);
|
||||||
|
// loaded coarse tiles re-queue automatically when the band changes (strate transition).
|
||||||
|
// Default 0 = cut at EVERY level (tested verdict 2026-07-05: level 0/1 straddler tiles were
|
||||||
|
// the visible mixers — a higher floor left them mixing and looked like "no improvement").
|
||||||
|
// Raise only if the descent/passage transition needs full tiles near the player. 9 = off.
|
||||||
|
// At ultra-coarse levels where one CELL is taller than the band itself, the tile is skipped
|
||||||
|
// entirely (see LoadTile) — cell-granular cutting there could only render garbage.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0", ClampMax = "9"))
|
||||||
|
int32 StrateContentCutMinLevel = 0;
|
||||||
|
|
||||||
// Open-world SKY reach: the sky-cap ceiling of an open strate (SurfaceWorld / FloatingIslands)
|
// Open-world SKY reach: the sky-cap ceiling of an open strate (SurfaceWorld / FloatingIslands)
|
||||||
// is FAR, so the ceiling BAND is streamed across a wider horizontal radius = ViewDistanceXY ×
|
// is FAR, so the ceiling BAND is streamed across a wider horizontal radius = ViewDistanceXY ×
|
||||||
// this, so the sky reaches toward the horizon instead of being a patch over the player's head.
|
// this, so the sky reaches toward the horizon instead of being a patch over the player's head.
|
||||||
@@ -113,6 +127,35 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "4", ClampMax = "32"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "4", ClampMax = "32"))
|
||||||
int32 CoarseTileCells = 16;
|
int32 CoarseTileCells = 16;
|
||||||
|
|
||||||
|
// RENDER DISTANCE — custom horizontal reach, in CHUNKS (1 chunk = 8 m; 128 ≈ 1 km, 768 ≈ 6 km).
|
||||||
|
// When > 0 and farther than the natural clipmap reach (ClipRadius × 2^MaxClipLevel chunks), the
|
||||||
|
// OUTERMOST shell keeps generating level-MaxClipLevel tiles outward until it covers this
|
||||||
|
// distance. Pick MaxClipLevel = the coarsest level that still renders strates correctly (one
|
||||||
|
// cell must fit inside a strate band — level ≥7 blanks via the too-coarse skip) and buy the
|
||||||
|
// remaining horizon here. Cost: the extra ring is all same-level tiles — tile/draw/gen count
|
||||||
|
// grows with (distance / 2^MaxClipLevel)², so each MaxClipLevel step down quadruples the ring.
|
||||||
|
// 0 = off (natural reach, byte-identical streaming).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0"))
|
||||||
|
int32 RenderDistanceChunks = 0;
|
||||||
|
|
||||||
|
// F18 — the render-distance ring streams per-surface SHEETS instead of MC tiles: in an open
|
||||||
|
// strate the far field is exactly two heightfields (TerrainZ + sky-cap CeilSurf, both already
|
||||||
|
// computed per column), so each far tile becomes two displaced grids (ground polygroup 0 /
|
||||||
|
// cap polygroup 1 — same materials), ~3-6× cheaper to generate and far fewer components (one
|
||||||
|
// sheet spans 2^FarSheetSpanLevels MC-tile footprints per axis). Non-open strates produce
|
||||||
|
// empty sheets (their far ring was enclosed rock anyway). Carved features (passages, chasms,
|
||||||
|
// spine) don't show at sheet distance. Needs the strate band armed (StrateContentCutMinLevel
|
||||||
|
// active); in the inter-strate gap the sheet ring blanks until you land. Off = the ring stays
|
||||||
|
// MC tiles at level MaxClipLevel (pre-F18 behaviour). Only matters when RenderDistanceChunks > 0.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap")
|
||||||
|
bool bFarSheetRing = true;
|
||||||
|
|
||||||
|
// Sheet tile size = MaxClipLevel + this many levels (2 → one sheet covers 4×4 MC-tile
|
||||||
|
// footprints → ~16× fewer far components). Sampling density stays that of the MaxClipLevel
|
||||||
|
// MC ring (cell count grows instead), capped at 128 cells/axis (beyond, cells coarsen).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "1", ClampMax = "4", EditCondition = "bFarSheetRing"))
|
||||||
|
int32 FarSheetSpanLevels = 2;
|
||||||
|
|
||||||
// SKIRTS — seal the thin cracks where neighbouring clipmap shells (different resolutions) meet.
|
// SKIRTS — seal the thin cracks where neighbouring clipmap shells (different resolutions) meet.
|
||||||
// A short wall is extruded into the solid from each surface edge on the tile's outer faces.
|
// A short wall is extruded into the solid from each surface edge on the tile's outer faces.
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap")
|
||||||
@@ -123,6 +166,16 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0.5", ClampMax = "8.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0.5", ClampMax = "8.0"))
|
||||||
float SkirtCells = 2.0f;
|
float SkirtCells = 2.0f;
|
||||||
|
|
||||||
|
// T2.b — LOD-aware octave reduction. Octaves DROPPED from per-voxel volumetric noise
|
||||||
|
// (roughness, worms displacement, slab/maze/shaft/island detail) per Step doubling on
|
||||||
|
// coarse tiles: a Step=4 tile drops 2×this. Sub-cell octaves can't shape a coarse
|
||||||
|
// isosurface — they only cost CPU — so 1 shaves 30-50% off far-tile gen for a sub-cell
|
||||||
|
// isosurface shift (skirts already stitch bigger LOD seams). LOD0 is NEVER affected.
|
||||||
|
// 0 = off (every LOD samples full octaves — byte-identical to before this setting).
|
||||||
|
// Réduction d'octaves sur les tuiles lointaines ; 0 = désactivé, LOD0 jamais touché.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0", ClampMax = "3"))
|
||||||
|
int32 LODOctaveDrop = 0;
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// CONTENT — distance-based decoration grid (no LOD pop)
|
// CONTENT — distance-based decoration grid (no LOD pop)
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
|
|||||||
@@ -326,6 +326,8 @@ public:
|
|||||||
// Landmarks: RARE, large, far-visible objects placed on a coarse hash lattice (the underground
|
// Landmarks: RARE, large, far-visible objects placed on a coarse hash lattice (the underground
|
||||||
// "mini-suns" etc.). Strate-wide; each entry has its own spacing/biome/placement/transform settings.
|
// "mini-suns" etc.). Strate-wide; each entry has its own spacing/biome/placement/transform settings.
|
||||||
// Cheap at any radius — see FStrateLandmark / §8.5. (NOT part of the per-chunk decoration grid.)
|
// Cheap at any radius — see FStrateLandmark / §8.5. (NOT part of the per-chunk decoration grid.)
|
||||||
|
// Landmarks now cover set-pieces too (AnchorMode HashLattice/PassageMouth + exclusion + orb). See
|
||||||
|
// FStrateLandmark / §8.5 (F7).
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Content")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Content")
|
||||||
TArray<FStrateLandmark> Landmarks;
|
TArray<FStrateLandmark> Landmarks;
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,11 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Strate")
|
UFUNCTION(BlueprintCallable, Category = "Strate")
|
||||||
int32 GetStrateIndex(float WorldZ) const;
|
int32 GetStrateIndex(float WorldZ) const;
|
||||||
|
|
||||||
|
/** Layout/passage generation counter (bumped by every Initialize → GeneratePassages).
|
||||||
|
* Hot-path callers key thread_local memos on this so an editor rebuild (RebuildStrates /
|
||||||
|
* live edit) can never serve a stale strate index or passage shortlist. */
|
||||||
|
uint32 GetLayoutVersion() const { return PassagesVersion; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the strate definition for a specific chunk coordinate.
|
* Get the strate definition for a specific chunk coordinate.
|
||||||
*
|
*
|
||||||
@@ -279,6 +284,13 @@ public:
|
|||||||
*/
|
*/
|
||||||
float EvaluateModifierSDF(float WorldX, float WorldY, float WorldZ) const;
|
float EvaluateModifierSDF(float WorldX, float WorldY, float WorldZ) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True si la sphère englobante d'un passage (élargie du rayon de blend de carve) touche la
|
||||||
|
* boîte VOXEL [MinVoxel, MaxVoxel]. Test conservatif O(Passages) — utilisé par ClassifyTile
|
||||||
|
* (rejet des tuiles trivialement pleines) une fois PAR TUILE, jamais par voxel.
|
||||||
|
*/
|
||||||
|
bool AnyPassageNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
|
||||||
|
|
||||||
/** Get all generated passages (for debug display). */
|
/** Get all generated passages (for debug display). */
|
||||||
const TArray<FVoxelPassage>& GetPassages() const { return Passages; }
|
const TArray<FVoxelPassage>& GetPassages() const { return Passages; }
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
#include "GameplayTagContainer.h"
|
#include "GameplayTagContainer.h"
|
||||||
#include "VoxelStrateTypes.generated.h"
|
#include "VoxelStrateTypes.generated.h"
|
||||||
|
|
||||||
class UVoxelBiomeDefinition; // FStrateLandmark::RequiredBiome (optional per-landmark biome filter)
|
class UVoxelBiomeDefinition; // FPlacementProfile::RequiredBiome (optional per-entry biome filter)
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// ENUMS
|
// ENUMS
|
||||||
@@ -243,6 +243,113 @@ enum class EVoxelStrateTransition : uint8
|
|||||||
// GENERATION PARAMS
|
// GENERATION PARAMS
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
|
|
||||||
|
// X-macro field list for FStrateGenerationParams — the ONE place that enumerates
|
||||||
|
// every param participating in strate-boundary blending (Lerp expands it below).
|
||||||
|
// ADDING A FIELD TO THE STRUCT? ADD IT HERE TOO — with the old hand-written Lerp,
|
||||||
|
// a forgotten field silently reset to its default value inside blend zones.
|
||||||
|
// Liste X-macro des champs blendés aux frontières de strates (une seule source).
|
||||||
|
// LERPF(Name) — continuous value, FMath::Lerp between the two strates
|
||||||
|
// SNAPF(Name) — discrete value (bool / int / enum), snaps at Alpha = 0.5
|
||||||
|
#define VF_STRATE_PARAM_FIELDS(LERPF, SNAPF) \
|
||||||
|
/* Rock */ \
|
||||||
|
LERPF(BaseDensity) \
|
||||||
|
LERPF(VerticalScale) \
|
||||||
|
/* Worm tunnels */ \
|
||||||
|
LERPF(WormFrequency) \
|
||||||
|
LERPF(WormHorizontalBias) \
|
||||||
|
LERPF(WormThreshold) \
|
||||||
|
LERPF(WormStrength) \
|
||||||
|
LERPF(WormNetworkRange) \
|
||||||
|
/* Cave morphology — rooms */ \
|
||||||
|
LERPF(RoomSpacing) \
|
||||||
|
LERPF(RoomDensity) \
|
||||||
|
LERPF(MinRoomRadius) \
|
||||||
|
LERPF(MaxRoomRadius) \
|
||||||
|
LERPF(RoomHeightRatio) \
|
||||||
|
LERPF(RoomShapeVariety) \
|
||||||
|
LERPF(RoomFloorCutMin) \
|
||||||
|
LERPF(RoomFloorCutMax) \
|
||||||
|
LERPF(FloorReliefStrength) \
|
||||||
|
LERPF(FloorReliefFrequency) \
|
||||||
|
LERPF(OriginRoomRadius) \
|
||||||
|
SNAPF(OriginRoomMaxConnections) \
|
||||||
|
/* Cave morphology — tunnels */ \
|
||||||
|
LERPF(TunnelMinRadius) \
|
||||||
|
LERPF(TunnelMaxRadius) \
|
||||||
|
LERPF(TunnelDensity) \
|
||||||
|
LERPF(MaxTunnelLength) \
|
||||||
|
LERPF(TunnelWarpStrength) \
|
||||||
|
LERPF(TunnelHorizontalBias) \
|
||||||
|
SNAPF(bTunnelsFlowTowardOrigin) \
|
||||||
|
LERPF(TunnelEndpointZOffset) \
|
||||||
|
LERPF(SDFBlendRadius) \
|
||||||
|
LERPF(WaterLevelRelative) \
|
||||||
|
/* Cave warp */ \
|
||||||
|
LERPF(CaveWarpStrength) \
|
||||||
|
LERPF(CaveWarpFrequency) \
|
||||||
|
/* Roughness */ \
|
||||||
|
LERPF(SurfaceRoughness) \
|
||||||
|
LERPF(RoughnessFrequency) \
|
||||||
|
/* Boundary seal + runtime Z range */ \
|
||||||
|
LERPF(BoundarySealThickness) \
|
||||||
|
LERPF(StrateTopWorldZ) \
|
||||||
|
LERPF(StrateBottomWorldZ) \
|
||||||
|
/* Noise profile */ \
|
||||||
|
SNAPF(RoughnessNoiseType) \
|
||||||
|
LERPF(DomainWarpStrength) \
|
||||||
|
LERPF(DomainWarpFrequency) \
|
||||||
|
LERPF(FloorBias) \
|
||||||
|
/* Terrain ops — terracing / layer lines / overhangs */ \
|
||||||
|
LERPF(TerraceStepHeight) \
|
||||||
|
LERPF(TerraceHardness) \
|
||||||
|
LERPF(TerraceNoiseDisplacement) \
|
||||||
|
LERPF(LayerLineSpacing) \
|
||||||
|
LERPF(LayerLineDepth) \
|
||||||
|
LERPF(OverhangStrength) \
|
||||||
|
LERPF(OverhangDepth) \
|
||||||
|
LERPF(OverhangFrequency) \
|
||||||
|
/* Ribbing */ \
|
||||||
|
LERPF(RibbingSpacing) \
|
||||||
|
LERPF(RibbingDepth) \
|
||||||
|
/* Cliff */ \
|
||||||
|
LERPF(CliffStrength) \
|
||||||
|
/* Scallop */ \
|
||||||
|
LERPF(ScallopStrength) \
|
||||||
|
LERPF(ScallopFrequency) \
|
||||||
|
/* Arch */ \
|
||||||
|
LERPF(ArchDensity) \
|
||||||
|
LERPF(ArchMinRadius) \
|
||||||
|
LERPF(ArchMaxRadius) \
|
||||||
|
/* Columns */ \
|
||||||
|
LERPF(ColumnDensity) \
|
||||||
|
LERPF(ColumnMinRadius) \
|
||||||
|
LERPF(ColumnMaxRadius) \
|
||||||
|
/* Pits */ \
|
||||||
|
LERPF(PitDensity) \
|
||||||
|
LERPF(PitMinRadius) \
|
||||||
|
LERPF(PitMaxRadius) \
|
||||||
|
LERPF(PitDepth) \
|
||||||
|
/* Chimneys */ \
|
||||||
|
LERPF(ChimneyDensity) \
|
||||||
|
LERPF(ChimneyMinRadius) \
|
||||||
|
LERPF(ChimneyMaxRadius) \
|
||||||
|
LERPF(ChimneyHeight) \
|
||||||
|
/* Domes */ \
|
||||||
|
LERPF(DomeDensity) \
|
||||||
|
LERPF(DomeMinRadius) \
|
||||||
|
LERPF(DomeMaxRadius) \
|
||||||
|
LERPF(DomeHeightRatio) \
|
||||||
|
/* Pinch */ \
|
||||||
|
LERPF(PinchDensity) \
|
||||||
|
LERPF(PinchStrength) \
|
||||||
|
LERPF(PinchLength)
|
||||||
|
|
||||||
|
// Per-field expansions used by FStrateGenerationParams::Lerp. They reference the
|
||||||
|
// locals A / B / Alpha / Result of that function (lexical expansion). Defined at
|
||||||
|
// file scope so no preprocessor directive sits inside the USTRUCT body (UHT-safe).
|
||||||
|
#define VF_PARAM_LERP(Name) Result.Name = FMath::Lerp(A.Name, B.Name, Alpha);
|
||||||
|
#define VF_PARAM_SNAP(Name) Result.Name = (Alpha < 0.5f) ? A.Name : B.Name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FStrateGenerationParams — Cave generation parameters for one strate.
|
* FStrateGenerationParams — Cave generation parameters for one strate.
|
||||||
*
|
*
|
||||||
@@ -924,97 +1031,10 @@ struct VOXELFORGE_API FStrateGenerationParams
|
|||||||
float Alpha)
|
float Alpha)
|
||||||
{
|
{
|
||||||
FStrateGenerationParams Result;
|
FStrateGenerationParams Result;
|
||||||
// Rock
|
// One assignment per field, expanded from VF_STRATE_PARAM_FIELDS (defined
|
||||||
Result.BaseDensity = FMath::Lerp(A.BaseDensity, B.BaseDensity, Alpha);
|
// above the struct). Bit-identical to the old hand-written list — same
|
||||||
Result.VerticalScale = FMath::Lerp(A.VerticalScale, B.VerticalScale, Alpha);
|
// FMath::Lerp calls, same Alpha-0.5 snap for discrete fields.
|
||||||
// Worm tunnels
|
VF_STRATE_PARAM_FIELDS(VF_PARAM_LERP, VF_PARAM_SNAP)
|
||||||
Result.WormFrequency = FMath::Lerp(A.WormFrequency, B.WormFrequency, Alpha);
|
|
||||||
Result.WormHorizontalBias = FMath::Lerp(A.WormHorizontalBias, B.WormHorizontalBias, Alpha);
|
|
||||||
Result.WormThreshold = FMath::Lerp(A.WormThreshold, B.WormThreshold, Alpha);
|
|
||||||
Result.WormStrength = FMath::Lerp(A.WormStrength, B.WormStrength, Alpha);
|
|
||||||
Result.WormNetworkRange = FMath::Lerp(A.WormNetworkRange, B.WormNetworkRange, Alpha);
|
|
||||||
// Cave morphology
|
|
||||||
Result.RoomSpacing = FMath::Lerp(A.RoomSpacing, B.RoomSpacing, Alpha);
|
|
||||||
Result.RoomDensity = FMath::Lerp(A.RoomDensity, B.RoomDensity, Alpha);
|
|
||||||
Result.MinRoomRadius = FMath::Lerp(A.MinRoomRadius, B.MinRoomRadius, Alpha);
|
|
||||||
Result.MaxRoomRadius = FMath::Lerp(A.MaxRoomRadius, B.MaxRoomRadius, Alpha);
|
|
||||||
Result.RoomHeightRatio = FMath::Lerp(A.RoomHeightRatio, B.RoomHeightRatio, Alpha);
|
|
||||||
Result.RoomShapeVariety = FMath::Lerp(A.RoomShapeVariety, B.RoomShapeVariety, Alpha);
|
|
||||||
Result.RoomFloorCutMin = FMath::Lerp(A.RoomFloorCutMin, B.RoomFloorCutMin, Alpha);
|
|
||||||
Result.RoomFloorCutMax = FMath::Lerp(A.RoomFloorCutMax, B.RoomFloorCutMax, Alpha);
|
|
||||||
Result.FloorReliefStrength = FMath::Lerp(A.FloorReliefStrength, B.FloorReliefStrength, Alpha);
|
|
||||||
Result.FloorReliefFrequency = FMath::Lerp(A.FloorReliefFrequency, B.FloorReliefFrequency, Alpha);
|
|
||||||
Result.OriginRoomRadius = FMath::Lerp(A.OriginRoomRadius, B.OriginRoomRadius, Alpha);
|
|
||||||
Result.OriginRoomMaxConnections = (Alpha < 0.5f) ? A.OriginRoomMaxConnections : B.OriginRoomMaxConnections;
|
|
||||||
Result.TunnelMinRadius = FMath::Lerp(A.TunnelMinRadius, B.TunnelMinRadius, Alpha);
|
|
||||||
Result.TunnelMaxRadius = FMath::Lerp(A.TunnelMaxRadius, B.TunnelMaxRadius, Alpha);
|
|
||||||
Result.TunnelDensity = FMath::Lerp(A.TunnelDensity, B.TunnelDensity, Alpha);
|
|
||||||
Result.MaxTunnelLength = FMath::Lerp(A.MaxTunnelLength, B.MaxTunnelLength, Alpha);
|
|
||||||
Result.TunnelWarpStrength = FMath::Lerp(A.TunnelWarpStrength, B.TunnelWarpStrength, 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.SDFBlendRadius = FMath::Lerp(A.SDFBlendRadius, B.SDFBlendRadius, Alpha);
|
|
||||||
Result.WaterLevelRelative = FMath::Lerp(A.WaterLevelRelative, B.WaterLevelRelative, Alpha);
|
|
||||||
// Cave warp
|
|
||||||
Result.CaveWarpStrength = FMath::Lerp(A.CaveWarpStrength, B.CaveWarpStrength, Alpha);
|
|
||||||
Result.CaveWarpFrequency = FMath::Lerp(A.CaveWarpFrequency, B.CaveWarpFrequency, Alpha);
|
|
||||||
// Roughness
|
|
||||||
Result.SurfaceRoughness = FMath::Lerp(A.SurfaceRoughness, B.SurfaceRoughness, Alpha);
|
|
||||||
Result.RoughnessFrequency = FMath::Lerp(A.RoughnessFrequency, B.RoughnessFrequency, Alpha);
|
|
||||||
// Boundary seal
|
|
||||||
Result.BoundarySealThickness = FMath::Lerp(A.BoundarySealThickness, B.BoundarySealThickness, Alpha);
|
|
||||||
Result.StrateTopWorldZ = FMath::Lerp(A.StrateTopWorldZ, B.StrateTopWorldZ, Alpha);
|
|
||||||
Result.StrateBottomWorldZ = FMath::Lerp(A.StrateBottomWorldZ, B.StrateBottomWorldZ, Alpha);
|
|
||||||
// Noise profile
|
|
||||||
Result.RoughnessNoiseType = (Alpha < 0.5f) ? A.RoughnessNoiseType : B.RoughnessNoiseType;
|
|
||||||
Result.DomainWarpStrength = FMath::Lerp(A.DomainWarpStrength, B.DomainWarpStrength, Alpha);
|
|
||||||
Result.DomainWarpFrequency = FMath::Lerp(A.DomainWarpFrequency, B.DomainWarpFrequency, Alpha);
|
|
||||||
Result.FloorBias = FMath::Lerp(A.FloorBias, B.FloorBias, Alpha);
|
|
||||||
// Terrain ops
|
|
||||||
Result.TerraceStepHeight = FMath::Lerp(A.TerraceStepHeight, B.TerraceStepHeight, Alpha);
|
|
||||||
Result.TerraceHardness = FMath::Lerp(A.TerraceHardness, B.TerraceHardness, Alpha);
|
|
||||||
Result.TerraceNoiseDisplacement = FMath::Lerp(A.TerraceNoiseDisplacement, B.TerraceNoiseDisplacement, Alpha);
|
|
||||||
Result.LayerLineSpacing = FMath::Lerp(A.LayerLineSpacing, B.LayerLineSpacing, Alpha);
|
|
||||||
Result.LayerLineDepth = FMath::Lerp(A.LayerLineDepth, B.LayerLineDepth, Alpha);
|
|
||||||
Result.OverhangStrength = FMath::Lerp(A.OverhangStrength, B.OverhangStrength, Alpha);
|
|
||||||
Result.OverhangDepth = FMath::Lerp(A.OverhangDepth, B.OverhangDepth, Alpha);
|
|
||||||
Result.OverhangFrequency = FMath::Lerp(A.OverhangFrequency, B.OverhangFrequency, Alpha);
|
|
||||||
// Ribbing
|
|
||||||
Result.RibbingSpacing = FMath::Lerp(A.RibbingSpacing, B.RibbingSpacing, Alpha);
|
|
||||||
Result.RibbingDepth = FMath::Lerp(A.RibbingDepth, B.RibbingDepth, Alpha);
|
|
||||||
// Cliff
|
|
||||||
Result.CliffStrength = FMath::Lerp(A.CliffStrength, B.CliffStrength, Alpha);
|
|
||||||
// Scallop
|
|
||||||
Result.ScallopStrength = FMath::Lerp(A.ScallopStrength, B.ScallopStrength, Alpha);
|
|
||||||
Result.ScallopFrequency = FMath::Lerp(A.ScallopFrequency, B.ScallopFrequency, Alpha);
|
|
||||||
// Arch
|
|
||||||
Result.ArchDensity = FMath::Lerp(A.ArchDensity, B.ArchDensity, Alpha);
|
|
||||||
Result.ArchMinRadius = FMath::Lerp(A.ArchMinRadius, B.ArchMinRadius, Alpha);
|
|
||||||
Result.ArchMaxRadius = FMath::Lerp(A.ArchMaxRadius, B.ArchMaxRadius, Alpha);
|
|
||||||
// Columns
|
|
||||||
Result.ColumnDensity = FMath::Lerp(A.ColumnDensity, B.ColumnDensity, Alpha);
|
|
||||||
Result.ColumnMinRadius = FMath::Lerp(A.ColumnMinRadius, B.ColumnMinRadius, Alpha);
|
|
||||||
Result.ColumnMaxRadius = FMath::Lerp(A.ColumnMaxRadius, B.ColumnMaxRadius, Alpha);
|
|
||||||
// Pits
|
|
||||||
Result.PitDensity = FMath::Lerp(A.PitDensity, B.PitDensity, Alpha);
|
|
||||||
Result.PitMinRadius = FMath::Lerp(A.PitMinRadius, B.PitMinRadius, Alpha);
|
|
||||||
Result.PitMaxRadius = FMath::Lerp(A.PitMaxRadius, B.PitMaxRadius, Alpha);
|
|
||||||
Result.PitDepth = FMath::Lerp(A.PitDepth, B.PitDepth, Alpha);
|
|
||||||
// Chimneys
|
|
||||||
Result.ChimneyDensity = FMath::Lerp(A.ChimneyDensity, B.ChimneyDensity, Alpha);
|
|
||||||
Result.ChimneyMinRadius = FMath::Lerp(A.ChimneyMinRadius, B.ChimneyMinRadius, Alpha);
|
|
||||||
Result.ChimneyMaxRadius = FMath::Lerp(A.ChimneyMaxRadius, B.ChimneyMaxRadius, Alpha);
|
|
||||||
Result.ChimneyHeight = FMath::Lerp(A.ChimneyHeight, B.ChimneyHeight, Alpha);
|
|
||||||
// Domes
|
|
||||||
Result.DomeDensity = FMath::Lerp(A.DomeDensity, B.DomeDensity, Alpha);
|
|
||||||
Result.DomeMinRadius = FMath::Lerp(A.DomeMinRadius, B.DomeMinRadius, Alpha);
|
|
||||||
Result.DomeMaxRadius = FMath::Lerp(A.DomeMaxRadius, B.DomeMaxRadius, Alpha);
|
|
||||||
Result.DomeHeightRatio = FMath::Lerp(A.DomeHeightRatio, B.DomeHeightRatio, Alpha);
|
|
||||||
// Pinch
|
|
||||||
Result.PinchDensity = FMath::Lerp(A.PinchDensity, B.PinchDensity, Alpha);
|
|
||||||
Result.PinchStrength = FMath::Lerp(A.PinchStrength, B.PinchStrength, Alpha);
|
|
||||||
Result.PinchLength = FMath::Lerp(A.PinchLength, B.PinchLength, Alpha);
|
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1364,6 +1384,93 @@ struct VOXELFORGE_API FSurfaceGenerationParams
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro", meta = (ClampMin = "1.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro", meta = (ClampMin = "1.0"))
|
||||||
float TerraceHeight = 12.0f;
|
float TerraceHeight = 12.0f;
|
||||||
|
|
||||||
|
// Terrace edge sharpness (0-1). 0 = soft rounded steps; 1 = crisp flat mesas with near-
|
||||||
|
// vertical risers. Only matters when TerraceStrength > 0. (F20 — the plateau tops flatten
|
||||||
|
// and the risers steepen as this rises.)
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float TerraceHardness = 0.5f;
|
||||||
|
|
||||||
|
// ----- F20 surface terrain ops (heightfield tier — biome-selected, slope/relief aware) -----
|
||||||
|
// These reshape the ground HEIGHT as a function of XY. All default OFF (0) so a strate/biome
|
||||||
|
// that doesn't set them is byte-identical to before. Cheap: pure per-column height remaps
|
||||||
|
// (no extra 3D density sampling). Each biome carries its own set; the surface blend lerps the
|
||||||
|
// final heights between the dominant and neighbour biome for free.
|
||||||
|
|
||||||
|
// Sedimentary "layer lines": fine repeating shelves cut into slopes (exposed rock strata).
|
||||||
|
// Depth = voxels the surface is nudged toward each band plane; 0 = off. Reads on slopes,
|
||||||
|
// invisible on flats (a flat area shifts uniformly). Pair with a small Spacing for dense
|
||||||
|
// banding. Un-gated by relief so the geology reads everywhere.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.0"))
|
||||||
|
float LayerLineDepth = 0.0f;
|
||||||
|
|
||||||
|
// Vertical spacing between layer lines in voxels (band period).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "1.0"))
|
||||||
|
float LayerLineSpacing = 4.0f;
|
||||||
|
|
||||||
|
// Cliff STEEPENING (0-1 master): where the surface is already STEEP (slope > threshold),
|
||||||
|
// push the height away from the local mean so gentle slopes become sheer walls / canyon
|
||||||
|
// faces, while gentle ground stays untouched. 0 = off. This is the slope-CONDITIONED op —
|
||||||
|
// it hugs real steep terrain instead of scattering cliffs at random. Costs 4 extra structural
|
||||||
|
// samples per column ONLY when > 0 (the priciest phase-1 op, still per-column-cheap).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float CliffStrength = 0.0f;
|
||||||
|
|
||||||
|
// Slope (rise in voxels per voxel of XY) at which cliffs begin. Below this the ground is
|
||||||
|
// untouched; the effect ramps in above it. ~0.3 = 17°, ~0.5 = 27° (default), ~1.0 = 45°.
|
||||||
|
// Lower = more of the terrain qualifies as "cliff".
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.05"))
|
||||||
|
float CliffSlopeThreshold = 0.5f;
|
||||||
|
|
||||||
|
// Extra steepness multiplier at full effect: how far the height is pushed from the local mean.
|
||||||
|
// 1 = up to ~2× the local relief on the steepest gated slopes; 3 = dramatic vertical walls.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.0"))
|
||||||
|
float CliffSharpness = 2.0f;
|
||||||
|
|
||||||
|
// XY distance (voxels) used to measure the slope / local mean for Cliff. Larger = smoother,
|
||||||
|
// broader cliff faces; smaller = reacts to finer bumps. Keep a few voxels.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.5"))
|
||||||
|
float CliffSampleDist = 2.0f;
|
||||||
|
|
||||||
|
// ----- F20 phase 2: OVERHANG (volumetric — the first true-3D surface op) -----
|
||||||
|
// Real jutting rock shelves: for air voxels just above a steep slope, the heightfield is re-sampled
|
||||||
|
// UPHILL (toward the cliff) by a height-varying amount and unioned in — so cliff rock extends OUT
|
||||||
|
// over the void below, self-capping at the cliff's height. This is genuine 3D (per-voxel re-eval on
|
||||||
|
// steep overhang columns only), costlier than the heightfield ops. 0 = off ⇒ byte-identical.
|
||||||
|
// Biome-selected: each biome's strength blends across borders.
|
||||||
|
|
||||||
|
// Master overhang strength (0-1). 0 = off. Also the biome selector — a biome with 0 has no overhangs.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float OverhangStrength = 0.0f;
|
||||||
|
|
||||||
|
// Horizontal REACH (voxels): how far the shelf juts out over the void from the cliff, and the scale
|
||||||
|
// at which the terrain gradient is measured (so a spot over the void can "see" the cliff). Bigger =
|
||||||
|
// deeper overhangs reaching further out (and a bit more cost). ~8-20.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.0"))
|
||||||
|
float OverhangReach = 12.0f;
|
||||||
|
|
||||||
|
// Vertical HEIGHT (voxels) of the overhang zone above the local ground — where the shelf sits above
|
||||||
|
// the ground/void directly under it, AND the band ClassifyTile treats as ambiguous (so it never holes
|
||||||
|
// a trivially-skipped tile). Larger = taller/higher shelves but more woken air tiles near cliffs.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.0"))
|
||||||
|
float OverhangHeight = 20.0f;
|
||||||
|
|
||||||
|
// Horizontal frequency of the shelf-shape noise (breaks the reach up so shelves are ragged, not a
|
||||||
|
// uniform lip). Lower = broader, smoother shelves.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.0"))
|
||||||
|
float OverhangFrequency = 0.02f;
|
||||||
|
|
||||||
|
// Vertical frequency RATIO of the shelf noise (× the horizontal frequency). Higher = the shelf folds/
|
||||||
|
// curls more as it rises (more dramatic undercuts); near 0 = a flatter lip. ~0.4-0.8.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.0"))
|
||||||
|
float OverhangZScale = 0.5f;
|
||||||
|
|
||||||
|
// Slope (rise per voxel of XY, measured over ~Reach) at which overhangs begin. Below this, none.
|
||||||
|
// Lowered default so it triggers on merely-steep ground, not only near-vertical walls. ~0.2 = 11°,
|
||||||
|
// ~0.3 = 17° (default), ~0.6 = 31°. NOTE: a DRAMATIC jutting shelf still needs a near-vertical cliff
|
||||||
|
// (slope » 1) next to a drop — smooth hills can only get subtle folds; use Cliff to MAKE walls first.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.05"))
|
||||||
|
float OverhangSlopeThreshold = 0.3f;
|
||||||
|
|
||||||
// ----- 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
|
||||||
@@ -1715,142 +1822,319 @@ struct VOXELFORGE_API FStratePassageConfig
|
|||||||
// CONTENT ENTRY STRUCTS
|
// CONTENT ENTRY STRUCTS
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ETerrainConditionType — which analytic terrain field an FTerrainCondition tests.
|
||||||
|
* All are DERIVED PREDICATES (pure functions of XY + seed + strate), evaluated on demand — NOT stored
|
||||||
|
* terrain annotations (see the F7 design: "conditions, not annotations"). More types (water-edge,
|
||||||
|
* relief-peak local-max) land in later passes.
|
||||||
|
*/
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class ETerrainConditionType : uint8
|
||||||
|
{
|
||||||
|
Relief UMETA(DisplayName = "Relief (elevation 0-1)"), // SampleRelief — peaks/mesas/lowlands
|
||||||
|
Moisture UMETA(DisplayName = "Moisture (0-1)"), // SampleMoisture — wet/dry
|
||||||
|
BiomeBorder UMETA(DisplayName = "Biome border (0=deep, ~0.5=edge)"), // near a biome boundary
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FTerrainCondition — one relational "aware placement" predicate (F7). The candidate point's derived
|
||||||
|
* field (Relief/Moisture/biome-border weight) must fall in [Min,Max] (or OUTSIDE it when bInvert).
|
||||||
|
* Multiple conditions on one entry are AND-ed. Empty list = no test (zero cost). Deterministic +
|
||||||
|
* worker-safe (pure query of the analytic fields, no stored state). Also drives the future quest
|
||||||
|
* FindFeature locator (same predicate, run as a search).
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct VOXELFORGE_API FTerrainCondition
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Condition")
|
||||||
|
ETerrainConditionType Type = ETerrainConditionType::Relief;
|
||||||
|
|
||||||
|
// Inclusive lower/upper bound of the accepted band. All fields read 0..1; BiomeBorder is ~0 deep in a
|
||||||
|
// biome cell and approaches ~0.5 exactly on a border, so "near a border" ≈ Min 0.35.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Condition", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float Min = 0.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Condition", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float Max = 1.0f;
|
||||||
|
|
||||||
|
// Accept OUTSIDE [Min,Max] instead of inside (avoid peaks, keep off borders, …).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Condition")
|
||||||
|
bool bInvert = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FPlacementProfile — shared placement settings for every scatter primitive
|
||||||
|
* (FStrateDecoration and FStrateLandmark — the latter also covers set-pieces: ruins/shrines/monuments).
|
||||||
|
*
|
||||||
|
* Each primitive keeps only its own DISTRIBUTION fields (HOW candidates are enumerated —
|
||||||
|
* a per-column grid, a hash lattice, an anchor mode). Everything about "can it go here",
|
||||||
|
* "what spawns", and "how it looks" lives HERE, once, so all three primitives are authored
|
||||||
|
* with one identical vocabulary. The awareness layer (FTerrainCondition Conditions[]) lands
|
||||||
|
* in the Filter section in a later pass.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct VOXELFORGE_API FPlacementProfile
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
// ----- Spawn (one of these; ActorClass wins if both set) -----
|
||||||
|
|
||||||
|
// Real actor — lights, logic, interaction. Costs game-thread time per instance: prefer InstancedMesh
|
||||||
|
// for pure visual props (dense decoration especially — an actor per groundcover instance is ruinous).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement")
|
||||||
|
TSubclassOf<AActor> ActorClass;
|
||||||
|
|
||||||
|
// INSTANCED path: if set, renders as batched instances (decoration) or one StaticMeshComponent
|
||||||
|
// (landmark/set-piece) instead of spawning ActorClass (which is then ignored). No tick, no per-actor
|
||||||
|
// overhead, engine-culled. An emissive material still glows at distance without a light.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement")
|
||||||
|
UStaticMesh* InstancedMesh = nullptr;
|
||||||
|
|
||||||
|
// ----- Filter (can it go here) -----
|
||||||
|
|
||||||
|
// Which surface type this entry snaps to. (Decoration defaults Any; landmarks default Ceiling — set in
|
||||||
|
// each primitive's constructor.)
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Filter")
|
||||||
|
ESurfaceType SurfacePlacement = ESurfaceType::Any;
|
||||||
|
|
||||||
|
// Surface-tilt band (degrees from flat = acos(|normal.Z|); 0 = flat, 90 = vertical). MaxSlopeAngle
|
||||||
|
// rejects surfaces STEEPER than it (90 = no filter); MinSlopeAngle rejects surfaces FLATTER than it
|
||||||
|
// (0 = no filter). Pair them to band a prop onto a tilt range (e.g. 30..70 = slopes only).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Filter", meta = (ClampMin = "0.0", ClampMax = "90.0"))
|
||||||
|
float MaxSlopeAngle = 90.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Filter", meta = (ClampMin = "0.0", ClampMax = "90.0"))
|
||||||
|
float MinSlopeAngle = 0.0f;
|
||||||
|
|
||||||
|
// Wall entries only: exclude downward-facing OVERHANGS. A "wall" (|normal.Z| <= 0.5) still includes
|
||||||
|
// surfaces leaning slightly DOWNWARD; set this so only normals with Z >= 0 (upright walls) qualify.
|
||||||
|
// Ignored unless the point resolves as a wall.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Filter")
|
||||||
|
bool bWallExcludeOverhangs = false;
|
||||||
|
|
||||||
|
// Water-relative gate (ignored unless the strate has a water table): place only below (true) / above
|
||||||
|
// (false) the water line when bRequireWaterRelative is set.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Filter")
|
||||||
|
bool bRequireWaterRelative = false;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Filter",
|
||||||
|
meta = (EditCondition = "bRequireWaterRelative"))
|
||||||
|
bool bPlaceBelowWater = false;
|
||||||
|
|
||||||
|
// Optional: only place inside this biome (resolved at the candidate XY). Null = any biome in the
|
||||||
|
// strate. (Decoration entries are already scoped to a biome by being listed under it, so this is
|
||||||
|
// mostly for the strate-wide landmark / set-piece primitives.)
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Filter")
|
||||||
|
UVoxelBiomeDefinition* RequiredBiome = nullptr;
|
||||||
|
|
||||||
|
// Relational "aware placement" (F7): derived predicates the candidate must satisfy (relief/moisture/
|
||||||
|
// biome-border), all AND-ed and evaluated by pure query — temples on peaks, oasis in wet lowlands,
|
||||||
|
// markers on biome borders. Empty = no test (zero cost). Deterministic + worker-safe.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Filter")
|
||||||
|
TArray<FTerrainCondition> Conditions;
|
||||||
|
|
||||||
|
// ----- Transform -----
|
||||||
|
|
||||||
|
// Rotate the object so its up-axis follows the surface normal (plants stand up on floors, stalactites
|
||||||
|
// point down on ceilings). If false, keeps world-up. (Decoration defaults true; landmarks default
|
||||||
|
// false — set per primitive.)
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Transform")
|
||||||
|
bool bAlignToSurface = true;
|
||||||
|
|
||||||
|
// Offset along the surface normal (cm). Positive = lift off the surface, negative = sink in.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Transform")
|
||||||
|
float SurfaceOffset = 0.0f;
|
||||||
|
|
||||||
|
// WORLD-space position offset (cm) added after the surface snap (e.g. +Z lifts a sun off the sky-cap).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Transform")
|
||||||
|
FVector LocationOffset = FVector::ZeroVector;
|
||||||
|
|
||||||
|
// Fixed rotation applied on top of the (optional) surface alignment. Use Yaw here to face a prop
|
||||||
|
// roughly one way (pair with RandomRotation.Yaw for banded variation — replaces the old MinYaw/MaxYaw).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Transform")
|
||||||
|
FRotator RotationOffset = FRotator::ZeroRotator;
|
||||||
|
|
||||||
|
// Per-axis RANDOM rotation range (degrees) — each instance gets a hash-deterministic ±value/2 on each
|
||||||
|
// axis. Yaw alone = spin variety (360 = full random heading, the decoration default); all three =
|
||||||
|
// tumbled-debris look. 0 on an axis = no randomisation there.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Transform")
|
||||||
|
FRotator RandomRotation = FRotator::ZeroRotator;
|
||||||
|
|
||||||
|
// Uniform scale range (hash-random per instance).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Transform")
|
||||||
|
float MinScale = 1.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Transform")
|
||||||
|
float MaxScale = 1.0f;
|
||||||
|
|
||||||
|
// ----- Render (InstancedMesh / StaticMeshComponent path) -----
|
||||||
|
|
||||||
|
// Distance (cm) past which the mesh stops drawing. 0 = never cull (correct for trees / far-visible
|
||||||
|
// suns). THE lever that makes dense groundcover affordable — grass drawn only near the player.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Render", meta = (ClampMin = "0.0"))
|
||||||
|
float CullDistance = 0.0f;
|
||||||
|
|
||||||
|
// Whether the instances cast dynamic shadows. Dense instanced shadows are the single biggest cost of
|
||||||
|
// heavy foliage — turn OFF for grass / small clutter, leave ON for trees and large props.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement|Render")
|
||||||
|
bool bCastShadow = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FDecoSubCompanion — a LEVEL-2 satellite: clutter that spawns ON a level-1 companion (moss on a rock).
|
||||||
|
* Distinct type (not a self-recursive FDecoCompanion, which UHT can't reflect) so nesting caps at 2 levels.
|
||||||
|
* Always INHERITS its level-1 parent's snapped surface point (no per-satellite re-solve → nesting stays cheap);
|
||||||
|
* may still gate on its own Profile.Conditions. Small radii — it sits on its parent.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct VOXELFORGE_API FDecoSubCompanion
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
FDecoSubCompanion()
|
||||||
|
{
|
||||||
|
Profile.MinScale = 0.8f;
|
||||||
|
Profile.MaxScale = 1.2f;
|
||||||
|
Profile.RandomRotation.Yaw = 360.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SubCompanion", meta = (ShowOnlyInnerProperties))
|
||||||
|
FPlacementProfile Profile;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SubCompanion", meta = (ClampMin = "0.0"))
|
||||||
|
float RadiusMinVox = 1.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SubCompanion", meta = (ClampMin = "0.0"))
|
||||||
|
float RadiusMaxVox = 3.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SubCompanion", meta = (ClampMin = "0"))
|
||||||
|
int32 CountMin = 1;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SubCompanion", meta = (ClampMin = "0"))
|
||||||
|
int32 CountMax = 2;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SubCompanion", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float Probability = 1.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FDecoCompanion — a level-1 cluster satellite that spawns near each placed instance of its parent decoration
|
||||||
|
* (F7 relational placement). Deterministic (pure function of the parent's hash → no search), re-snaps to the
|
||||||
|
* real surface at its own XY (bSnapToSurface), may gate on its own Conditions, and may itself carry LEVEL-2
|
||||||
|
* `SubCompanions` (moss on a rock). E.g. a tree lists rocks + mushrooms; a rock lists moss. Two levels max
|
||||||
|
* (per-parent budget caps the total); deeper nesting is a later concern.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct VOXELFORGE_API FDecoCompanion
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
FDecoCompanion()
|
||||||
|
{
|
||||||
|
// Same clutter defaults as a decoration entry (variety scale + full random yaw).
|
||||||
|
Profile.MinScale = 0.8f;
|
||||||
|
Profile.MaxScale = 1.2f;
|
||||||
|
Profile.RandomRotation.Yaw = 360.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// What to spawn + how it looks (mesh/actor, transform, render). Same vocabulary as the parent.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Companion", meta = (ShowOnlyInnerProperties))
|
||||||
|
FPlacementProfile Profile;
|
||||||
|
|
||||||
|
// Disk (in VOXELS) around the parent that satellites scatter into.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Companion", meta = (ClampMin = "0.0"))
|
||||||
|
float RadiusMinVox = 2.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Companion", meta = (ClampMin = "0.0"))
|
||||||
|
float RadiusMaxVox = 6.0f;
|
||||||
|
|
||||||
|
// How many satellites per parent (inclusive range, deterministic per parent).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Companion", meta = (ClampMin = "0"))
|
||||||
|
int32 CountMin = 1;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Companion", meta = (ClampMin = "0"))
|
||||||
|
int32 CountMax = 3;
|
||||||
|
|
||||||
|
// Chance this companion type fires at all, per parent (0-1).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Companion", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float Probability = 1.0f;
|
||||||
|
|
||||||
|
// Snap each satellite to the REAL surface at its own XY (fixes floaters on uneven ground; respects the
|
||||||
|
// companion's own SurfacePlacement). Cheap on SurfaceWorld (height oracle), a short ray-march in caves.
|
||||||
|
// OFF = inherit the parent's exact height + normal (cheapest — fine only on flat ground). A satellite
|
||||||
|
// that finds no surface at its spot is simply skipped (no floater).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Companion")
|
||||||
|
bool bSnapToSurface = true;
|
||||||
|
|
||||||
|
// LEVEL-2 clutter that spawns ON each of THIS companion's satellites (e.g. this = rock, sub = moss).
|
||||||
|
// Inherits the satellite's surface point (no extra surface find). Capped by the per-parent budget.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Companion")
|
||||||
|
TArray<FDecoSubCompanion> SubCompanions;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FStrateDecoration — One decoration type that can spawn on surfaces.
|
* FStrateDecoration — One decoration type that can spawn on surfaces.
|
||||||
*
|
*
|
||||||
* Decorations are actors placed ON the cave surface (stalactites on ceilings,
|
* Placed ON the cave surface (stalactites on ceilings, mushrooms on floors, crystals on walls).
|
||||||
* mushrooms on floors, crystals on walls, etc.).
|
* All the placement/transform/render settings live on the shared `Profile`; this struct adds only
|
||||||
* The decoration placer (future system) reads these entries from the active
|
* decoration's own DISTRIBUTION fields (per-column dense grid, spawn density, per-chunk cap).
|
||||||
* strate definition and spawns actors accordingly.
|
|
||||||
*/
|
*/
|
||||||
USTRUCT(BlueprintType)
|
USTRUCT(BlueprintType)
|
||||||
struct VOXELFORGE_API FStrateDecoration
|
struct VOXELFORGE_API FStrateDecoration
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
// The actor class to spawn (e.g., BP_Stalactite, BP_CrystalCluster).
|
FStrateDecoration()
|
||||||
// Real actors: lights, logic, interaction. They cost game-thread time per instance —
|
{
|
||||||
// prefer InstancedMesh for pure visual props, and consider the Far tier so the coarse grid keeps
|
// Decoration defaults that differ from FPlacementProfile's neutral defaults: variety scale range
|
||||||
// their spawn count down.
|
// and a full random yaw (reproduces the legacy bRandomYaw = true, MinYaw/MaxYaw = 0..360 look).
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration")
|
Profile.MinScale = 0.8f;
|
||||||
TSubclassOf<AActor> ActorClass;
|
Profile.MaxScale = 1.2f;
|
||||||
|
Profile.RandomRotation.Yaw = 360.0f;
|
||||||
|
}
|
||||||
|
|
||||||
// INSTANCED path: if set, this entry renders as batched HISM instances instead of
|
// Shared placement/transform/render settings.
|
||||||
// spawning ActorClass (which is then ignored). No tick, no per-actor overhead,
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration", meta = (ShowOnlyInnerProperties))
|
||||||
// engine-culled — orders of magnitude cheaper. Use for everything that doesn't need
|
FPlacementProfile Profile;
|
||||||
// logic/lights/interaction; an emissive material still glows at distance without a light.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration")
|
|
||||||
UStaticMesh* InstancedMesh = nullptr;
|
|
||||||
|
|
||||||
// Which of the two decoration streaming grids this entry uses (§8.5). Far (default) = full radius +
|
// Which of the two decoration streaming grids this entry uses (§8.5). Far (default) = full radius +
|
||||||
// coarse column grid (cheap for rare/large props visible everywhere); Near = short radius + fine
|
// coarse column grid (cheap for rare/large props visible everywhere); Near = short radius + fine
|
||||||
// column grid (dense groundcover near the player only). The radius/spacing presets live on
|
// column grid (dense groundcover near the player only). The radius/spacing presets live on
|
||||||
// VoxelSettings; this only PICKS a grid. Defaults reproduce the legacy single-radius fine grid until
|
// VoxelSettings; this only PICKS a grid.
|
||||||
// you opt into a coarser far spacing or move an entry to Near. (Replaces the old vestigial MaxLODLevel.)
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration")
|
||||||
EDecoStreamTier StreamTier = EDecoStreamTier::Far;
|
EDecoStreamTier StreamTier = EDecoStreamTier::Far;
|
||||||
|
|
||||||
// Which surface type this decoration can be placed on
|
// Chance per valid surface point to spawn this decoration (0-1).
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration")
|
// 0.01 = rare, 0.1 = common, 0.5 = very dense.
|
||||||
ESurfaceType SurfacePlacement = ESurfaceType::Any;
|
|
||||||
|
|
||||||
// Maximum surface tilt (degrees from flat) this decoration tolerates. The surface tilt is
|
|
||||||
// acos(|normal.Z|): 0 = perfectly flat floor/ceiling, 90 = vertical wall. Grass on gentle
|
|
||||||
// ground → ~30-40; rock/lichen that clings to slopes → 90 (no filter, the default).
|
|
||||||
// 90 → place anywhere (default, no filter) · 35 → grass that avoids cliffs · 15 → flats only
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration", meta = (ClampMin = "0.0", ClampMax = "90.0"))
|
|
||||||
float MaxSlopeAngle = 90.0f;
|
|
||||||
|
|
||||||
// Minimum surface tilt (degrees from flat) — the LOWER companion to MaxSlopeAngle. Rejects surfaces
|
|
||||||
// FLATTER than this, so a prop can be kept OFF flat ground and restricted to slopes / walls. Same
|
|
||||||
// metric as MaxSlopeAngle: acos(|normal.Z|), 0 = flat, 90 = vertical. Pair the two to band a prop
|
|
||||||
// onto a tilt range (e.g. 30..70 = slopes only, never flats or sheer walls).
|
|
||||||
// 0 → no filter (default) · 45 → slopes & walls only · 70 → near-vertical only
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration", meta = (ClampMin = "0.0", ClampMax = "90.0"))
|
|
||||||
float MinSlopeAngle = 0.0f;
|
|
||||||
|
|
||||||
// Chance per valid surface point to spawn this decoration (0-1)
|
|
||||||
// 0.01 = rare, 0.1 = common, 0.5 = very dense
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
float SpawnDensity = 0.05f;
|
float SpawnDensity = 0.05f;
|
||||||
|
|
||||||
// Random scale range for variety
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration")
|
|
||||||
float MinScale = 0.8f;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration")
|
|
||||||
float MaxScale = 1.2f;
|
|
||||||
|
|
||||||
// ----- Placement rules -----
|
|
||||||
|
|
||||||
// Rotate the actor so its up-axis follows the surface normal (stalactites point
|
|
||||||
// down on ceilings, plants stand up on floors). If false, keeps world-up.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration|Placement")
|
|
||||||
bool bAlignToSurface = true;
|
|
||||||
|
|
||||||
// Apply a deterministic random yaw so instances don't all face the same way.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration|Placement")
|
|
||||||
bool bRandomYaw = true;
|
|
||||||
|
|
||||||
// When bRandomYaw is set, constrain the random yaw to [MinYaw, MaxYaw] degrees instead of a full
|
|
||||||
// turn. Lets a prop face roughly one way with a little variation (wind-bent grass: 80..100). The
|
|
||||||
// default 0..360 is a full unrestricted turn — byte-identical to the legacy behaviour. Ignored when
|
|
||||||
// bRandomYaw is false.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration|Placement",
|
|
||||||
meta = (EditCondition = "bRandomYaw", ClampMin = "0.0", ClampMax = "360.0"))
|
|
||||||
float MinYaw = 0.0f;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration|Placement",
|
|
||||||
meta = (EditCondition = "bRandomYaw", ClampMin = "0.0", ClampMax = "360.0"))
|
|
||||||
float MaxYaw = 360.0f;
|
|
||||||
|
|
||||||
// Wall props only: exclude downward-facing OVERHANGS. A "wall" is any surface between floor and
|
|
||||||
// ceiling (|normal.Z| <= 0.5), which still includes surfaces that lean slightly DOWNWARD (overhang
|
|
||||||
// ceilings). For props that must sit on upright walls (vines, wall torches) set this so only normals
|
|
||||||
// with Z >= 0 (vertical or up-leaning) qualify. Ignored unless the point resolves as a wall.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration|Placement")
|
|
||||||
bool bWallExcludeOverhangs = false;
|
|
||||||
|
|
||||||
// Offset along the surface normal (world units). Positive = lift off the surface,
|
|
||||||
// negative = sink into it. Useful to embed roots or float crystals slightly.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration|Placement")
|
|
||||||
float SurfaceOffset = 0.0f;
|
|
||||||
|
|
||||||
// Hard cap on how many of THIS decoration spawn per chunk (perf safety).
|
// Hard cap on how many of THIS decoration spawn per chunk (perf safety).
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration|Placement", meta = (ClampMin = "1"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration", meta = (ClampMin = "1"))
|
||||||
int32 MaxPerChunk = 40;
|
int32 MaxPerChunk = 40;
|
||||||
|
|
||||||
// Only place where this is below the strate water line (true) or above it (false).
|
// Cluster satellites scattered around each placed instance of THIS decoration (F7 relational placement) —
|
||||||
// Ignored unless RequireWaterRelative is set. Lets you put seaweed underwater and
|
// e.g. a tree → rocks + mushrooms. Deterministic, one level, inherits this entry's surface point.
|
||||||
// grass above water in the same strate.
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration|Companions")
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration|Placement")
|
TArray<FDecoCompanion> Companions;
|
||||||
bool bRequireWaterRelative = false;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration|Placement",
|
|
||||||
meta = (EditCondition = "bRequireWaterRelative"))
|
|
||||||
bool bPlaceBelowWater = false;
|
|
||||||
|
|
||||||
// ----- Performance (HISM render tuning — only affects the InstancedMesh path) -----
|
|
||||||
|
|
||||||
// Distance (world units / cm) past which instances stop rendering. This is THE lever that makes
|
|
||||||
// DENSE groundcover affordable: grass can be placed thickly but only drawn near the player, so the
|
|
||||||
// GPU cost is bounded by area-within-cull, not by the whole streaming radius. 0 = never cull (the
|
|
||||||
// default — correct for trees / large props you want visible to the horizon).
|
|
||||||
// 0 → no cull (props, trees)
|
|
||||||
// 2000 → ~20 m, typical grass / small ground clutter
|
|
||||||
// 4000 → ~40 m, taller plants you want visible a bit further
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration|Performance", meta = (ClampMin = "0.0"))
|
|
||||||
float CullDistance = 0.0f;
|
|
||||||
|
|
||||||
// Whether these instances cast dynamic shadows. Dense instanced shadows are the single biggest cost
|
|
||||||
// of heavy foliage — turn this OFF for grass / small clutter (an unlit-from-below tuft loses almost
|
|
||||||
// nothing visually). Leave ON for trees and anything large enough that its shadow reads as grounding.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decoration|Performance")
|
|
||||||
bool bCastShadow = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FStrateLandmark — A RARE, large, far-visible object placed on a coarse HASH LATTICE (§8.5).
|
* ELandmarkAnchor — how a landmark's candidate anchor points are chosen. Feature-conditioning
|
||||||
|
* (peaks / wet lowlands / biome edges) rides on TOP of either mode via Profile.Conditions.
|
||||||
|
*/
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class ELandmarkAnchor : uint8
|
||||||
|
{
|
||||||
|
HashLattice UMETA(DisplayName = "Hash Lattice (scattered)"), // scatter on a coarse lattice
|
||||||
|
PassageMouth UMETA(DisplayName = "Passage Mouth (at descents)"), // at passage endpoints in this strate
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FStrateLandmark — a RARE, deliberately-placed object: mini-suns, ruins, shrines, monuments (§8.5, F7).
|
||||||
|
* The one placement primitive for "notable things you navigate by" (set-pieces folded in here 2026-07-06).
|
||||||
*
|
*
|
||||||
* This is the right primitive for things like the underground "mini-suns" (in-lore light sources): one
|
* This is the right primitive for things like the underground "mini-suns" (in-lore light sources): one
|
||||||
* object per ~`SpacingChunks` lattice cell, so the work scales with how MANY landmarks are in range
|
* object per ~`SpacingChunks` lattice cell, so the work scales with how MANY landmarks are in range
|
||||||
@@ -1866,109 +2150,97 @@ struct VOXELFORGE_API FStrateLandmark
|
|||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
// ----- What to spawn (one of these; ActorClass wins if both set) -----
|
FStrateLandmark()
|
||||||
|
{
|
||||||
|
// Landmark defaults that differ from FPlacementProfile's neutral defaults: suns sit on the sky-cap
|
||||||
|
// ceiling and stay world-upright regardless of the ceiling tilt.
|
||||||
|
Profile.SurfacePlacement = ESurfaceType::Ceiling;
|
||||||
|
Profile.bAlignToSurface = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Real actor — use this for a sun that carries its own LIGHT / logic. Rare, so the per-actor cost is fine.
|
// Shared placement/transform/render settings (what to spawn, surface/slope/water gates, transform,
|
||||||
|
// cull/shadow). A sun that carries its own light/logic goes in Profile.ActorClass; a plain glowing mesh
|
||||||
|
// in Profile.InstancedMesh.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark", meta = (ShowOnlyInnerProperties))
|
||||||
|
FPlacementProfile Profile;
|
||||||
|
|
||||||
|
// How candidate anchor points are chosen. Feature-conditioning (peaks / wet lowlands / biome edges)
|
||||||
|
// rides on TOP of either mode via Profile.Conditions.
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark")
|
||||||
TSubclassOf<AActor> ActorClass;
|
ELandmarkAnchor AnchorMode = ELandmarkAnchor::HashLattice;
|
||||||
|
|
||||||
// OR a plain static mesh (spawned as one StaticMeshComponent — no actor/tick overhead). An emissive
|
// ----- Hash lattice (AnchorMode == HashLattice — scattered placement; cheap at any radius) -----
|
||||||
// material glows at distance without a light. Ignored if ActorClass is set.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark")
|
|
||||||
UStaticMesh* InstancedMesh = nullptr;
|
|
||||||
|
|
||||||
// ----- Rarity / spacing (the hash lattice — this is what makes it cheap) -----
|
// Average spacing between instances, IN CHUNKS = the lattice cell size (one candidate per cell, so cost
|
||||||
|
// scales with (radius/spacing)²). Large = rare & far apart. 16 → frequent · 64 → sparse · 256+ → km-scale
|
||||||
// Average spacing between landmarks, IN CHUNKS. This is the lattice cell size: exactly one candidate is
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Lattice",
|
||||||
// considered per SpacingChunks×SpacingChunks cell, so cost scales with (radius/spacing)². This is also
|
meta = (EditCondition = "AnchorMode == ELandmarkAnchor::HashLattice", EditConditionHides, ClampMin = "1.0"))
|
||||||
// the primary "distance between two instances" control. Large = rare & far apart.
|
|
||||||
// 16 → fairly frequent landmarks · 64 → sparse (good default) · 256+ → one every few km
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Spacing", meta = (ClampMin = "1.0"))
|
|
||||||
float SpacingChunks = 64.0f;
|
float SpacingChunks = 64.0f;
|
||||||
|
|
||||||
// How far within its cell a candidate may wander (0 = dead-centre grid, 1 = anywhere in the cell).
|
// How far within its cell a candidate may wander (0 = dead-centre, 1 = anywhere). Min spacing between two
|
||||||
// The effective MINIMUM spacing between two instances ≈ SpacingChunks·(1 − JitterFraction); keep it
|
// ≈ SpacingChunks·(1 − JitterFraction).
|
||||||
// below 1 to preserve a spacing guarantee while still breaking up the grid regularity.
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Lattice",
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Spacing", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
meta = (EditCondition = "AnchorMode == ELandmarkAnchor::HashLattice", EditConditionHides, ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
float JitterFraction = 0.5f;
|
float JitterFraction = 0.5f;
|
||||||
|
|
||||||
// Probability that a lattice cell actually contains this landmark (0-1). Combine with SpacingChunks for
|
// Probability that a lattice cell actually contains this instance (0-1).
|
||||||
// "rare AND well-spaced": SpacingChunks sets the grid, SpawnProbability sets how many slots fill.
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Lattice",
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Spacing", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
meta = (EditCondition = "AnchorMode == ELandmarkAnchor::HashLattice", EditConditionHides, ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
float SpawnProbability = 1.0f;
|
float SpawnProbability = 1.0f;
|
||||||
|
|
||||||
// How far out (in chunks) landmarks stream / stay visible. CHEAP to make large here (the lattice means a
|
// ----- Passage mouths (AnchorMode == PassageMouth — at the passages threading this strate) -----
|
||||||
// 2048-chunk radius is still only ~(2048/Spacing)² candidates). Set big enough that a massive object
|
|
||||||
// never pops in at a jarring distance.
|
// Place at the DESCENT mouth — where a passage LEAVES this strate downward (the hole going down; e.g. a
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Spacing", meta = (ClampMin = "1"))
|
// guardian over the descent).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Passage",
|
||||||
|
meta = (EditCondition = "AnchorMode == ELandmarkAnchor::PassageMouth", EditConditionHides))
|
||||||
|
bool bAtDescentMouths = true;
|
||||||
|
|
||||||
|
// Place at the ARRIVAL mouth — where a passage ENTERS this strate from above (where you land; e.g. a
|
||||||
|
// shrine at the bottom of the climb).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Passage",
|
||||||
|
meta = (EditCondition = "AnchorMode == ELandmarkAnchor::PassageMouth", EditConditionHides))
|
||||||
|
bool bAtArrivalMouths = true;
|
||||||
|
|
||||||
|
// Per-mouth chance to place (0-1). Deterministic.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Passage",
|
||||||
|
meta = (EditCondition = "AnchorMode == ELandmarkAnchor::PassageMouth", EditConditionHides, ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float MouthProbability = 1.0f;
|
||||||
|
|
||||||
|
// ----- Shared -----
|
||||||
|
|
||||||
|
// How far out (in chunks) instances stream / stay visible. Cheap to make large (lattice candidate count
|
||||||
|
// scales with (radius/spacing)²; the passage list is finite).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark", meta = (ClampMin = "1"))
|
||||||
int32 StreamRadiusChunks = 256;
|
int32 StreamRadiusChunks = 256;
|
||||||
|
|
||||||
// ----- Placement restriction (mirrors the base decoration gates) -----
|
// ----- Exclusion (relational self-awareness — optional) -----
|
||||||
|
|
||||||
// Optional: only place inside this biome (resolved at the candidate XY). Null = any biome in the strate.
|
// A placed instance suppresses OTHERS whose anchor falls within this radius (chunks) so two never overlap.
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Placement")
|
// 0 = OFF (the default — pure scatter; mini-suns don't exclude). Conflicts resolve deterministically:
|
||||||
UVoxelBiomeDefinition* RequiredBiome = nullptr;
|
// higher Priority wins, ties by hash. (v1 resolves within the streamed set; a fully position-independent,
|
||||||
|
// pop-free resolve is a planned follow-up.)
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Exclusion", meta = (ClampMin = "0.0"))
|
||||||
|
float ExclusionRadiusChunks = 0.0f;
|
||||||
|
|
||||||
// Which surface to snap to. Suns typically sit on the sky-cap CEILING; set Floor for ground monuments,
|
// Higher wins an exclusion conflict (a major shrine outranks scattered ruins).
|
||||||
// Any for the first surface found. Wall-leaning surfaces are matched by the same normal test as decos.
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Exclusion")
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Placement")
|
int32 Priority = 0;
|
||||||
ESurfaceType SurfacePlacement = ESurfaceType::Ceiling;
|
|
||||||
|
|
||||||
// Surface-tilt band (deg from flat = acos(|normal.Z|); 0 = flat, 90 = vertical). MaxSlopeAngle rejects
|
// ----- Decoration footprint (clear groundcover under the object so it doesn't clip through) -----
|
||||||
// surfaces STEEPER than it (90 = no filter); MinSlopeAngle rejects surfaces FLATTER than it (0 = none).
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Placement", meta = (ClampMin = "0.0", ClampMax = "90.0"))
|
|
||||||
float MaxSlopeAngle = 90.0f;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Placement", meta = (ClampMin = "0.0", ClampMax = "90.0"))
|
// Remove decorations (grass etc.) within a radius of this landmark, so foliage doesn't poke through a
|
||||||
float MinSlopeAngle = 0.0f;
|
// temple floor. Uses the same instance-removal as player digging (cleared on spawn AND when decorations
|
||||||
|
// stream in near it). A landmark mesh doesn't change density, so this is the only thing that clears under it.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Footprint")
|
||||||
|
bool bSuppressDecorationsUnder = false;
|
||||||
|
|
||||||
// Water-relative gate (ignored unless the strate has a water table): place only below (true) / above
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Footprint",
|
||||||
// (false) the water line when bRequireWaterRelative is set.
|
meta = (EditCondition = "bSuppressDecorationsUnder", ClampMin = "0.0"))
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Placement")
|
float SuppressRadiusChunks = 2.0f;
|
||||||
bool bRequireWaterRelative = false;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Placement", meta = (EditCondition = "bRequireWaterRelative"))
|
// (Placement gates, transform tweaks, and cull/shadow render tuning live on the shared `Profile` above —
|
||||||
bool bPlaceBelowWater = false;
|
// see FPlacementProfile. Landmark-specific defaults: Ceiling surface + no surface-align.)
|
||||||
|
|
||||||
// ----- Transform tweaks (foliage-style) -----
|
|
||||||
|
|
||||||
// Rotate the object so its up-axis follows the surface normal. OFF by default — a sun usually wants to
|
|
||||||
// stay world-upright regardless of the ceiling tilt. ON makes it lie against the surface.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Transform")
|
|
||||||
bool bAlignToSurface = false;
|
|
||||||
|
|
||||||
// WORLD-space position offset (cm) added after the surface snap. E.g. +Z lifts a sun up off the
|
|
||||||
// sky-cap into the open cavern; use X/Y to nudge it off the exact column.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Transform")
|
|
||||||
FVector LocationOffset = FVector::ZeroVector;
|
|
||||||
|
|
||||||
// Fixed rotation applied on top of the (optional) surface alignment.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Transform")
|
|
||||||
FRotator RotationOffset = FRotator::ZeroRotator;
|
|
||||||
|
|
||||||
// Per-axis RANDOM rotation range (degrees) — each instance gets a hash-deterministic ±value/2 on each
|
|
||||||
// axis (Pitch/Yaw/Roll). 0 on an axis = no randomisation there. Yaw alone = spin variety; all three =
|
|
||||||
// tumbled debris look.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Transform")
|
|
||||||
FRotator RandomRotation = FRotator::ZeroRotator;
|
|
||||||
|
|
||||||
// Uniform scale range (hash-random per instance).
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Transform")
|
|
||||||
float MinScale = 1.0f;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Transform")
|
|
||||||
float MaxScale = 1.0f;
|
|
||||||
|
|
||||||
// ----- Render tuning (the InstancedMesh / StaticMeshComponent path) -----
|
|
||||||
|
|
||||||
// Distance (cm) past which the mesh stops drawing. 0 = NEVER cull (the right choice for a far-visible
|
|
||||||
// sun). Only affects the InstancedMesh path.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Performance", meta = (ClampMin = "0.0"))
|
|
||||||
float CullDistance = 0.0f;
|
|
||||||
|
|
||||||
// Whether the mesh casts a shadow. Only affects the InstancedMesh path.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Performance")
|
|
||||||
bool bCastShadow = true;
|
|
||||||
|
|
||||||
// ----- MINI-SUN LIGHT ORB (feeds the terrain material's raymarched shadows) -----
|
// ----- MINI-SUN LIGHT ORB (feeds the terrain material's raymarched shadows) -----
|
||||||
// When set, this landmark is also a LIGHT SOURCE: the terrain material marches the density volume
|
// When set, this landmark is also a LIGHT SOURCE: the terrain material marches the density volume
|
||||||
|
|||||||
@@ -231,6 +231,12 @@ struct FVoxelMeshData
|
|||||||
TArray<FColor> Colors; // Masques matériau F6 (R=palette biome dominant, G=pente,
|
TArray<FColor> Colors; // Masques matériau F6 (R=palette biome dominant, G=pente,
|
||||||
// B=poids de fondu de bordure, A=palette biome voisin)
|
// B=poids de fondu de bordure, A=palette biome voisin)
|
||||||
|
|
||||||
|
// F17 — classe de surface par TRIANGLE, portée par l'ORDRE du buffer d'indices : les
|
||||||
|
// NumCeilingTriangles DERNIERS triangles de Triangles sont la classe "plafond sky-cap"
|
||||||
|
// (polygroup 1 → slot matériau 1, sans ombre) ; tout ce qui précède est "sol/roche"
|
||||||
|
// (polygroup 0). Runs contigus exigés par RMC (une section par polygroup).
|
||||||
|
int32 NumCeilingTriangles = 0;
|
||||||
|
|
||||||
void Clear()
|
void Clear()
|
||||||
{
|
{
|
||||||
Vertices.Empty();
|
Vertices.Empty();
|
||||||
@@ -238,6 +244,7 @@ struct FVoxelMeshData
|
|||||||
UVs.Empty();
|
UVs.Empty();
|
||||||
Normals.Empty();
|
Normals.Empty();
|
||||||
Colors.Empty();
|
Colors.Empty();
|
||||||
|
NumCeilingTriangles = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsEmpty() const { return Vertices.Num() == 0; }
|
bool IsEmpty() const { return Vertices.Num() == 0; }
|
||||||
|
|||||||
@@ -27,6 +27,39 @@ class UMaterialInterface;
|
|||||||
class UMaterialInstanceDynamic;
|
class UMaterialInstanceDynamic;
|
||||||
namespace RealtimeMesh { struct FRealtimeMeshStreamSet; } // T1.f — worker-built geometry buffers
|
namespace RealtimeMesh { struct FRealtimeMeshStreamSet; } // T1.f — worker-built geometry buffers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How a streaming anchor wants its tiles built (ARCHITECTURE §9.3). The local player is an implicit
|
||||||
|
* FullVisual anchor (the clipmap). Extra anchors — AI, and later remote players — are registered so
|
||||||
|
* their surroundings stream too (so they physically exist away from the local camera).
|
||||||
|
*
|
||||||
|
* `CollisionOnly` tiles cook collision but are hidden (`SetVisibility(false)`) — no draw / VSM cost —
|
||||||
|
* UNLESS the player clipmap also wants that exact level-0 tile (then it renders normally). This is the
|
||||||
|
* §9.4 render-skip: it kills the cost of terrain around AI/remote players far from the local camera.
|
||||||
|
* (Geometry streams are still built on the worker — off the frame; a deeper "no stream build at all"
|
||||||
|
* collision-only path is a later optimization.)
|
||||||
|
*/
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EVoxelAnchorPolicy : uint8
|
||||||
|
{
|
||||||
|
CollisionOnly UMETA(DisplayName = "Collision Only"), // enough to stand / pathfind / hit
|
||||||
|
FullVisual UMETA(DisplayName = "Full Visual") // rendered too (a second local viewpoint)
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A non-player streaming center: keep a small box of level-0 tiles loaded around Actor so it has
|
||||||
|
* collision wherever it is. Plain internal struct (weak ptr is GC-safe without reflection). */
|
||||||
|
struct FVoxelStreamingAnchor
|
||||||
|
{
|
||||||
|
TWeakObjectPtr<AActor> Actor;
|
||||||
|
EVoxelAnchorPolicy Policy = EVoxelAnchorPolicy::CollisionOnly;
|
||||||
|
// Thin box around the anchor (level-0 chunks). Default = the chunk it's in + 1 on each horizontal
|
||||||
|
// edge (3×3) + 1 chunk BELOW (ground safety when its capsule sits near a chunk's bottom); nothing
|
||||||
|
// above (a grounded NPC doesn't need it — raise ZAbove for flyers). Empty tiles in the box are ~free.
|
||||||
|
int32 XYRadiusChunks = 1; // horizontal Chebyshev radius (0 = its own column)
|
||||||
|
int32 ZBelowChunks = 1; // chunks below the anchor's chunk
|
||||||
|
int32 ZAboveChunks = 0; // chunks above
|
||||||
|
FIntVector LastChunk = FIntVector(MAX_int32, MAX_int32, MAX_int32); // move detection (rebuild trigger)
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AVoxelWorld - The main voxel terrain actor
|
* AVoxelWorld - The main voxel terrain actor
|
||||||
*
|
*
|
||||||
@@ -51,11 +84,18 @@ struct FChunkResult
|
|||||||
TSharedPtr<RealtimeMesh::FRealtimeMeshStreamSet> Streams;
|
TSharedPtr<RealtimeMesh::FRealtimeMeshStreamSet> Streams;
|
||||||
uint32 Epoch = 0; // Generation epoch — discard if stale
|
uint32 Epoch = 0; // Generation epoch — discard if stale
|
||||||
bool bEmpty = true; // true ⇒ all-air tile (Streams null); still marked loaded so we don't re-submit
|
bool bEmpty = true; // true ⇒ all-air tile (Streams null); still marked loaded so we don't re-submit
|
||||||
// Ceiling classification from the ACTUAL mesh normals (down-facing geometry = sky-cap ceiling),
|
// F17 — the mesher classifies every triangle semantically (sky-cap = down-facing near the
|
||||||
// computed on the worker where Normals are free. Authoritative — can't disagree with the rendered
|
// column's CeilSurf; overhangs/cave roofs stay ground) and packs them as two contiguous runs
|
||||||
// view the way a game-thread height-oracle sample did (it misclassified coarse far tiles). The
|
// (ground then cap) in the index buffer → polygroups 0/1 → two RMC sections with their own
|
||||||
// game thread still gates this to SurfaceWorld strates before applying CeilingMaterial / no-shadow.
|
// material + shadow flag. These tell the apply path which sections exist (RMC only creates a
|
||||||
bool bIsCeiling = false;
|
// section for a non-empty polygroup — configuring a missing one is invalid).
|
||||||
|
bool bHasGroundTris = false;
|
||||||
|
bool bHasCeilingTris = false;
|
||||||
|
// STRATE CONTENT CUT — the chunk-Z band this tile was MESHED with (MIN/MAX = uncut). The
|
||||||
|
// apply path clamps its strate/material lookups into it (a coarse tile's raw min corner can
|
||||||
|
// sit in a strate whose content was cut out of the mesh entirely).
|
||||||
|
int32 BandChunkLo = MIN_int32;
|
||||||
|
int32 BandChunkHi = MAX_int32;
|
||||||
// CAPTURE-DURING-MESHING: the tile's CHUNK_SIZE³ R8 density grid, captured by the mesher (no extra
|
// CAPTURE-DURING-MESHING: the tile's CHUNK_SIZE³ R8 density grid, captured by the mesher (no extra
|
||||||
// GetDensityAt). Non-empty only for capture-eligible tiles (level 0, full-res). The game thread hands
|
// GetDensityAt). Non-empty only for capture-eligible tiles (level 0, full-res). The game thread hands
|
||||||
// it to UVoxelDensityVolume::IngestTileCapture so the density clipmap reuses the mesher's samples
|
// it to UVoxelDensityVolume::IngestTileCapture so the density clipmap reuses the mesher's samples
|
||||||
@@ -151,7 +191,26 @@ public:
|
|||||||
* because FVoxelTileKey isn't a USTRUCT key). */
|
* because FVoxelTileKey isn't a USTRUCT key). */
|
||||||
TMap<FVoxelTileKey, URealtimeMeshComponent*> TileComponents;
|
TMap<FVoxelTileKey, URealtimeMeshComponent*> TileComponents;
|
||||||
|
|
||||||
|
/** T2.c — COMPONENT POOL. Unloading a tile parks its component here (geometry + collision
|
||||||
|
* stripped via RemoveSectionGroup, hidden, still registered) instead of DestroyComponent;
|
||||||
|
* ApplyMeshToTile pops from here instead of NewObject + RegisterComponent. Kills the
|
||||||
|
* create/register/GC churn of fast travel and regen bursts. Same GC-safety rationale as
|
||||||
|
* TileComponents (registered components are owned by the actor). Bounded — overflow is
|
||||||
|
* destroyed for real. */
|
||||||
|
TArray<URealtimeMeshComponent*> TileComponentPool;
|
||||||
|
static constexpr int32 MaxPooledTileComponents = 256;
|
||||||
|
|
||||||
|
/** Pop a pooled tile component (made visible again) or create + register a fresh one. */
|
||||||
|
URealtimeMeshComponent* AcquireTileComponent();
|
||||||
|
|
||||||
|
/** Park a tile component in the pool (strip geometry/collision, hide) — or destroy it
|
||||||
|
* for real when the pool is full. */
|
||||||
|
void ReleaseTileComponent(URealtimeMeshComponent* Comp);
|
||||||
|
|
||||||
|
/** T2.d — the effective concurrent gen-task budget: the asset's MaxConcurrentTasks,
|
||||||
|
* capped to (logical cores − 2) so small CPUs don't thrash on a flat 16 (background
|
||||||
|
* priority stops frame starvation, not the context-switch overhead). */
|
||||||
|
int32 GetMaxConcurrentTasks() const;
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// TERRAIN MODIFICATION (player carving & filling)
|
// TERRAIN MODIFICATION (player carving & filling)
|
||||||
@@ -271,6 +330,37 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Voxel World|Biome")
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Biome")
|
||||||
FVoxelBiomeQuery GetBiomeAtWorldLocation(FVector WorldLocation) const;
|
FVoxelBiomeQuery GetBiomeAtWorldLocation(FVector WorldLocation) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SurfaceWorld ground finder for self-arranging prefabs (ruins/set-pieces authored as Blueprints):
|
||||||
|
* the terrain surface + sky-cap ceiling world-Z under WorldLocation's XY, WITHOUT any line trace or
|
||||||
|
* streamed collision. Deterministic and available before the area meshes. Returns false (outs = the
|
||||||
|
* input Z) when the point isn't a SurfaceWorld heightfield (cave strates) — fall back to a trace there.
|
||||||
|
* Does NOT account for passage/spine carving; re-check with a trace if the spot might be carved.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Query")
|
||||||
|
bool GetVoxelSurfaceHeightAt(FVector WorldLocation, float& OutSurfaceWorldZ, float& OutCeilingWorldZ) const;
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// STREAMING ANCHORS — keep terrain (collision) loaded around actors that aren't the local player
|
||||||
|
// (AI now; remote players later). ARCHITECTURE §9.3. Registering an actor folds a small box of
|
||||||
|
// level-0 tiles around it into the desired set, so it has ground to stand on / pathfind / be hit
|
||||||
|
// even far from the camera. Idempotent per actor (re-register updates the policy/radius).
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
/** Start streaming terrain around Actor. Default box = its chunk + 1 horizontal ring + 1 chunk below
|
||||||
|
* (ground safety), nothing above. Empty tiles are ~free (trivial-tile reject); with the CollisionOnly
|
||||||
|
* policy the SOLID tiles cook collision but don't render (§9.4). Raise ZAbove for flyers/tall NPCs. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Streaming")
|
||||||
|
void RegisterStreamingAnchor(AActor* Actor,
|
||||||
|
EVoxelAnchorPolicy Policy = EVoxelAnchorPolicy::CollisionOnly,
|
||||||
|
int32 XYRadiusChunks = 1,
|
||||||
|
int32 ZBelowChunks = 1,
|
||||||
|
int32 ZAboveChunks = 0);
|
||||||
|
|
||||||
|
/** Stop streaming terrain around Actor. Its tiles are released by the normal delta cull. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Streaming")
|
||||||
|
void UnregisterStreamingAnchor(AActor* Actor);
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// LIGHTING — DENSITY VOLUME (debug / material wiring)
|
// LIGHTING — DENSITY VOLUME (debug / material wiring)
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -307,11 +397,10 @@ private:
|
|||||||
TVP6 = FLinearColor::Black, TVP7 = FLinearColor::Black,
|
TVP6 = FLinearColor::Black, TVP7 = FLinearColor::Black,
|
||||||
TVP8 = FLinearColor::Black, TVP9 = FLinearColor::Black;
|
TVP8 = FLinearColor::Black, TVP9 = FLinearColor::Black;
|
||||||
|
|
||||||
// Change-detection for the per-Tick pushes: MID vector/texture sets and MPC writes each enqueue
|
// Change-detection for the per-Tick MID pushes (MIDs OWN their param values, so skip-if-identical
|
||||||
// render-thread updates, so skip them entirely on the (common) frames where nothing moved.
|
// is safe there — unlike the MPC, whose world instance can reset behind our back; see
|
||||||
|
// UpdateOrbLightMPC, which deliberately rewrites every frame).
|
||||||
TWeakObjectPtr<UVolumeTexture> LastBoundVolTex0; // re-push MIDs if the L0 texture was recreated
|
TWeakObjectPtr<UVolumeTexture> LastBoundVolTex0; // re-push MIDs if the L0 texture was recreated
|
||||||
FLinearColor LastOrbMPC[4] = { FLinearColor(FLT_MAX, 0, 0, 0), FLinearColor(FLT_MAX, 0, 0, 0),
|
|
||||||
FLinearColor(FLT_MAX, 0, 0, 0), FLinearColor(FLT_MAX, 0, 0, 0) };
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
@@ -336,6 +425,15 @@ public:
|
|||||||
UFUNCTION(CallInEditor, BlueprintCallable, Category = "Live Edit")
|
UFUNCTION(CallInEditor, BlueprintCallable, Category = "Live Edit")
|
||||||
void RebuildStrates();
|
void RebuildStrates();
|
||||||
|
|
||||||
|
/** F2 — DETERMINISM VALIDATOR (run during PIE, takes ~a second). Samples a band of
|
||||||
|
* densities at a chunk boundary near the player through TWO cache-window alignments
|
||||||
|
* (thread_local chunk caches warmed from the left chunk, then from the right one, same
|
||||||
|
* points re-sampled) plus a same-alignment repeat. Every delta MUST be exactly 0 —
|
||||||
|
* anything else is a window-invariance regression (ARCHITECTURE §8.4). Logs the verdict
|
||||||
|
* and the first offending voxel. Run it after any "bit-identical" hot-path refactor. */
|
||||||
|
UFUNCTION(CallInEditor, BlueprintCallable, Category = "Live Edit")
|
||||||
|
void ValidateDeterminism();
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// EDITOR BRUSH (manual carve/fill from the Details panel, works in PIE)
|
// EDITOR BRUSH (manual carve/fill from the Details panel, works in PIE)
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -448,7 +546,35 @@ public:
|
|||||||
*
|
*
|
||||||
* @param ChunkCoord - Which chunk to load
|
* @param ChunkCoord - Which chunk to load
|
||||||
*/
|
*/
|
||||||
void LoadTile(const FVoxelTileKey& Tile);
|
void LoadTile(const FVoxelTileKey& Tile, bool bHighPriority = false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Worker-side gen for one tile: classify → GenerateMesh/GenerateSheetMesh → BuildTileStreamSet.
|
||||||
|
* Fills Result (no enqueue, no bookkeeping). Called from the async ChunkGen task AND from the
|
||||||
|
* synchronous carve path (SyncRemeshTile) — reads Generator/Mesher only, so it's safe on either
|
||||||
|
* thread. See LoadTile for how the parameters are derived.
|
||||||
|
*/
|
||||||
|
void GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
||||||
|
int32 Step, int32 Cells, uint32 Epoch, bool bWantCapture,
|
||||||
|
int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi,
|
||||||
|
bool bSheetTile, int32 SheetChunkZ,
|
||||||
|
int32 HoleMinX, int32 HoleMinY, int32 HoleMaxX, int32 HoleMaxY,
|
||||||
|
FChunkResult& Result);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Game-thread apply for one gen result (shared by ProcessPendingChunks + SyncRemeshTile):
|
||||||
|
* epoch check, mark loaded, ingest capture, then either release the tile's component (empty) or
|
||||||
|
* ApplyMeshToTile. Returns true iff a VISIBLE mesh was uploaded (counts against the apply budget).
|
||||||
|
* Does NOT touch PendingTiles — the caller owns that.
|
||||||
|
*/
|
||||||
|
bool ApplyTileResult(FChunkResult& Result);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same-frame level-0 re-mesh on the game thread: gen + apply INLINE so a player carve is visible
|
||||||
|
* THIS frame (no async round-trip). Used for the tile under the brush centre; neighbours re-mesh
|
||||||
|
* async (prioritised) via RemeshDirtyChunks. One full-res tile gen on the game thread — bounded.
|
||||||
|
*/
|
||||||
|
void SyncRemeshTile(const FVoxelTileKey& Tile);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unload a single chunk.
|
* Unload a single chunk.
|
||||||
@@ -466,14 +592,19 @@ public:
|
|||||||
*
|
*
|
||||||
* The vertex/index buffers (Streams) are already BUILT on the worker (T1.f — see
|
* The vertex/index buffers (Streams) are already BUILT on the worker (T1.f — see
|
||||||
* BuildTileStreamSet / FChunkResult), so this only does the game-thread-only work:
|
* BuildTileStreamSet / FChunkResult), so this only does the game-thread-only work:
|
||||||
* ceiling/material resolution, get-or-create the component, CreateSectionGroup(MoveTemp),
|
* material resolution, get-or-create the component, CreateSectionGroup(MoveTemp),
|
||||||
* and section config (collision/shadow). Never called for empty tiles.
|
* and per-section config (collision/shadow). Never called for empty tiles.
|
||||||
*
|
*
|
||||||
* @param Tile - Which clipmap tile this mesh belongs to
|
* F17 — the streams carry TWO polygroups (0 = ground, 1 = sky-cap ceiling, classified
|
||||||
* @param Streams - Pre-built RMC geometry buffers (consumed/moved)
|
* semantically per triangle on the worker): RMC auto-creates one section per non-empty
|
||||||
* @param bGeomCeiling - Worker's geometry-normal ceiling vote (gated to SurfaceWorld here)
|
* group, so a coarse tile spanning both the terrain and the cap gets BOTH materials
|
||||||
|
* (the old whole-tile vote painted the loser with the winner's material).
|
||||||
|
*
|
||||||
|
* Takes the whole FChunkResult (tile key, streams — consumed/moved —, per-group flags and
|
||||||
|
* the strate content band the mesh was cut to; material lookups clamp into that band).
|
||||||
|
* Never called for empty results.
|
||||||
*/
|
*/
|
||||||
void ApplyMeshToTile(const FVoxelTileKey& Tile, RealtimeMesh::FRealtimeMeshStreamSet&& Streams, bool bGeomCeiling);
|
void ApplyMeshToTile(FChunkResult& Result);
|
||||||
|
|
||||||
/** Mini-sun lighting (bounded directional). Each frame writes the nearest 4 active orbs' WORLD
|
/** Mini-sun lighting (bounded directional). Each frame writes the nearest 4 active orbs' WORLD
|
||||||
* positions (+ reach radius in .w) into OrbLightMPC's Orb0..3 vector params; the Directional
|
* positions (+ reach radius in .w) into OrbLightMPC's Orb0..3 vector params; the Directional
|
||||||
@@ -486,8 +617,10 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting")
|
||||||
UMaterialParameterCollection* OrbLightMPC = nullptr;
|
UMaterialParameterCollection* OrbLightMPC = nullptr;
|
||||||
|
|
||||||
/** Build the clipmap desired-tile set (concentric shells) around the player tile. */
|
/** Build the clipmap desired-tile set (concentric shells) around the player tile.
|
||||||
void BuildDesiredTiles(const FIntVector& CenterChunkCoord);
|
* OutLeavers = les tuiles désirées au crossing PRÉCÉDENT qui ne le sont plus — les seuls
|
||||||
|
* candidats au cull (delta), au lieu de re-scanner TOUTES les tuiles chargées par crossing. */
|
||||||
|
void BuildDesiredTiles(const FIntVector& CenterChunkCoord, TArray<FVoxelTileKey>& OutLeavers);
|
||||||
|
|
||||||
/** True if a tile's world footprint is still within the outermost clip shell (so a
|
/** True if a tile's world footprint is still within the outermost clip shell (so a
|
||||||
* not-desired loaded tile there is mid-LOD-transition and must wait for its replacement,
|
* not-desired loaded tile there is mid-LOD-transition and must wait for its replacement,
|
||||||
@@ -515,6 +648,32 @@ public:
|
|||||||
TQueue<FChunkResult, EQueueMode::Mpsc> ProcessQueue;
|
TQueue<FChunkResult, EQueueMode::Mpsc> ProcessQueue;
|
||||||
TSet<FVoxelTileKey> PendingTiles; // tiles with a gen task in flight
|
TSet<FVoxelTileKey> PendingTiles; // tiles with a gen task in flight
|
||||||
|
|
||||||
|
// STRATE CONTENT CUT (see UVoxelSettings::StrateContentCutMinLevel) — the player-strate
|
||||||
|
// chunk-Z band coarse tiles are meshed to (MIN/MAX sentinels = no cut, e.g. in the gap).
|
||||||
|
// Updated each crossing in UpdateChunksAroundPosition; on change (strate transition) the
|
||||||
|
// loaded coarse tiles whose content depends on it are re-queued through BandRemeshQueue
|
||||||
|
// (drained by the budgeted submit loop — LoadTile re-gens in place, no visual pop).
|
||||||
|
int32 MeshBandChunkLo = MIN_int32;
|
||||||
|
int32 MeshBandChunkHi = MAX_int32;
|
||||||
|
TSet<FVoxelTileKey> BandRemeshQueue;
|
||||||
|
|
||||||
|
// DIG RESPONSIVENESS — loaded level-0 tiles touched by a player carve/fill that still need an
|
||||||
|
// async re-mesh (the neighbours of the synchronously-remeshed centre tile, plus any tile that was
|
||||||
|
// mid-gen at carve time). Drained FIRST in the submit loop (ahead of streaming + band) and launched
|
||||||
|
// at BackgroundHigh so a dig never waits behind streaming. Unlike the old inline RemeshDirtyChunks,
|
||||||
|
// a tile that's in flight is KEPT queued (not dropped) so the stale pre-carve result is corrected
|
||||||
|
// once it lands — the source of "the hole shows up a beat late, or not until I move".
|
||||||
|
TSet<FVoxelTileKey> DirtyRemeshQueue;
|
||||||
|
|
||||||
|
// F18 — TROU XY de l'anneau feuille (voxels ; Max EXCLUSIF ; sentinelles MAX/MIN = pas de
|
||||||
|
// trou) : la zone couverte par les coquilles MC (boîte niveau-MaxClipLevel autour du joueur,
|
||||||
|
// rétrécie d'une tuile pour garder un anneau de recouvrement au raccord) est DÉCOUPÉE des
|
||||||
|
// feuilles — sinon une feuille partiellement couverte recouvre le terrain proche avec son
|
||||||
|
// échantillonnage grossier. Mis à jour par crossing ; changement ⇒ re-queue des feuilles
|
||||||
|
// chevauchantes via BandRemeshQueue (re-gen en place).
|
||||||
|
int32 SheetHoleMinXVox = MAX_int32, SheetHoleMinYVox = MAX_int32;
|
||||||
|
int32 SheetHoleMaxXVox = MIN_int32, SheetHoleMaxYVox = MIN_int32;
|
||||||
|
|
||||||
// Set to true during EndPlay — async tasks check this before accessing UObjects
|
// Set to true during EndPlay — async tasks check this before accessing UObjects
|
||||||
std::atomic<bool> bShuttingDown{false};
|
std::atomic<bool> bShuttingDown{false};
|
||||||
|
|
||||||
@@ -524,6 +683,27 @@ public:
|
|||||||
// Player's level-0 tile coord (= chunk coord). The desired set is rebuilt when this changes.
|
// Player's level-0 tile coord (= chunk coord). The desired set is rebuilt when this changes.
|
||||||
FIntVector CurrentCenterChunk = FIntVector::ZeroValue;
|
FIntVector CurrentCenterChunk = FIntVector::ZeroValue;
|
||||||
|
|
||||||
|
// Non-player streaming anchors (AI now, remote players later — ARCHITECTURE §9.3). BuildDesiredTiles
|
||||||
|
// folds each anchor's small level-0 box into the desired set (same DesiredStamped machinery → the
|
||||||
|
// delta cull releases an anchor's tiles automatically when it moves away / is unregistered). The
|
||||||
|
// desired-set rebuild also fires when any anchor crosses a chunk boundary (UpdateChunksAroundPosition).
|
||||||
|
TArray<FVoxelStreamingAnchor> StreamingAnchors;
|
||||||
|
// Adds each anchor's level-0 tiles to DesiredSorted/DesiredStamped (dedup vs the player clipmap)
|
||||||
|
// and records the ones ONLY a CollisionOnly anchor wants in CollisionOnlyTiles.
|
||||||
|
void AddAnchorDesiredTiles();
|
||||||
|
// Forces a desired-set rebuild next Tick even if neither the player nor an anchor crossed a tile
|
||||||
|
// boundary (set by UnregisterStreamingAnchor so a removed anchor's tiles get culled).
|
||||||
|
bool bForceDesiredRebuild = false;
|
||||||
|
|
||||||
|
// §9.4 RENDER-SKIP — level-0 tiles wanted ONLY by CollisionOnly anchors (no player-clipmap / no
|
||||||
|
// FullVisual desirer this crossing): they cook collision but are hidden. Rebuilt each crossing in
|
||||||
|
// BuildDesiredTiles. `Prev` lets ReconcileAnchorTileVisibility toggle just the DELTA on already-
|
||||||
|
// loaded tiles when a tile flips render↔collision-only (player walks toward/away from a cluster),
|
||||||
|
// without an O(loaded) scan. Both empty when there are no CollisionOnly anchors → zero cost.
|
||||||
|
TSet<FVoxelTileKey> CollisionOnlyTiles;
|
||||||
|
TSet<FVoxelTileKey> PrevCollisionOnlyTiles;
|
||||||
|
void ReconcileAnchorTileVisibility();
|
||||||
|
|
||||||
// --- Streaming work-avoidance (perf) ---
|
// --- Streaming work-avoidance (perf) ---
|
||||||
// The desired tile set only changes when the player crosses a level-0 tile boundary.
|
// The desired tile set only changes when the player crosses a level-0 tile boundary.
|
||||||
// We cache it and only rebuild/cull/sort on a real move, and go idle once every desired
|
// We cache it and only rebuild/cull/sort on a real move, and go idle once every desired
|
||||||
@@ -531,7 +711,36 @@ public:
|
|||||||
FIntVector LastUpdateCenter = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
FIntVector LastUpdateCenter = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||||
bool bAllChunksLoaded = false;
|
bool bAllChunksLoaded = false;
|
||||||
TArray<FVoxelTileKey> DesiredSorted; // desired tiles, nearest-first
|
TArray<FVoxelTileKey> DesiredSorted; // desired tiles, nearest-first
|
||||||
TSet<FVoxelTileKey> DesiredSet; // O(1) membership for the cull pass
|
|
||||||
|
// Desired-set membership STAMPÉE : clé → numéro du dernier crossing où la tuile était désirée.
|
||||||
|
// BuildDesiredTiles upserte le stamp courant puis balaie la map UNE fois : les entrées à stamp
|
||||||
|
// périmé sont les "leavers" (retirées + renvoyées). Le cull ne considère que ces leavers + la
|
||||||
|
// TransitionHold — fini le scan O(toutes-les-tuiles-chargées) à chaque crossing (le spike
|
||||||
|
// CullTiles ~1.6 ms/crossing de la trace 2026-07-05).
|
||||||
|
TMap<FVoxelTileKey, uint32> DesiredStamped;
|
||||||
|
uint32 DesiredStamp = 0;
|
||||||
|
bool IsDesired(const FVoxelTileKey& T) const
|
||||||
|
{
|
||||||
|
const uint32* S = DesiredStamped.Find(T);
|
||||||
|
return S && *S == DesiredStamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tuiles chargées qui ont quitté le desired set mais sont RETENUES (load-before-unload : leur
|
||||||
|
// remplacement n'est pas encore complet, ou le backlog a sauté le test de recouvrement).
|
||||||
|
// Re-considérées par BUDGET tournant (curseur sur la queue, ~256/crossing) — re-scanner TOUTE
|
||||||
|
// la hold par crossing redevient le vieux scan O(loaded) dès que le streaming ne "settle"
|
||||||
|
// jamais (mesuré 2.47 ms/crossing en packagé). Garder une tuile plus longtemps est toujours
|
||||||
|
// hole-safe. Le set est la MEMBERSHIP autoritaire ; la queue peut contenir des clés périmées
|
||||||
|
// (retirées paresseusement au scan). Le "settled cull" reste le filet de sécurité plein-scan.
|
||||||
|
TSet<FVoxelTileKey> TransitionHold;
|
||||||
|
TArray<FVoxelTileKey> TransitionHoldQueue;
|
||||||
|
int32 TransitionHoldCursor = 0;
|
||||||
|
void AddToTransitionHold(const FVoxelTileKey& T)
|
||||||
|
{
|
||||||
|
bool bAlready = false;
|
||||||
|
TransitionHold.Add(T, &bAlready);
|
||||||
|
if (!bAlready) { TransitionHoldQueue.Add(T); }
|
||||||
|
}
|
||||||
|
|
||||||
// Tiles approved for removal but whose teardown (component destroy + content actor Destroy())
|
// Tiles approved for removal but whose teardown (component destroy + content actor Destroy())
|
||||||
// is spread across frames. Unbudgeted, a fast traversal culls a whole shell's worth of tiles in
|
// is spread across frames. Unbudgeted, a fast traversal culls a whole shell's worth of tiles in
|
||||||
@@ -550,6 +759,9 @@ public:
|
|||||||
* fresh with the diff layer when loaded normally).
|
* fresh with the diff layer when loaded normally).
|
||||||
*
|
*
|
||||||
* @param DirtyCoords - Chunk coordinates that need re-meshing
|
* @param DirtyCoords - Chunk coordinates that need re-meshing
|
||||||
|
* @param ExcludeTile - optional level-0 tile already handled synchronously this frame
|
||||||
|
* (SyncRemeshTile): skipped for the async re-queue, but still marked dirty
|
||||||
|
* for the density volume.
|
||||||
*/
|
*/
|
||||||
void RemeshDirtyChunks(const TArray<FIntVector>& DirtyCoords);
|
void RemeshDirtyChunks(const TArray<FIntVector>& DirtyCoords, const FVoxelTileKey* ExcludeTile = nullptr);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user