Another pass

This commit is contained in:
Fr0zka committed 2026-06-23 08:30:13 +02:00
1 parent db558d9e14
commit e6cd852129
17 files changed
+2340 -878

No files matched your search

+172 -51
View File
@@ -1,40 +1,48 @@
// VoxelContentManager.h
// Per-chunk content population: deterministic decoration/actor scatter from the
// strate content pools, plus self-contained aesthetic water surfaces.
// Decoration scatter (distance-based world grid, ASYNC surface march) + aesthetic water surfaces.
//
// LIFECYCLE (driven by AVoxelWorld on the GAME THREAD):
// - PopulateChunk(coord, meshdata) after a chunk's mesh is applied
// - ClearChunk(coord) when a chunk unloads / is re-meshed
// - ClearAll() on regenerate / season reset
// - SetActiveStrate(playerStrate) each Tick (no-op unless the strate changed)
// TWO INDEPENDENT SUBSYSTEMS:
//
// DETERMINISM: every placement decision is a pure hash of (chunk, surface index,
// entry index, seed), so the same world re-populates identically. Spawning itself
// must run on the game thread (UWorld::SpawnActor), which it does — ApplyMeshToChunk
// is game-thread.
// 1) DECORATIONS — distance-based WORLD GRID (the no-pop system).
// Decorations are placed on a fixed world XY cell grid (1 cell = 1 chunk footprint) and streamed
// by DISTANCE from the player, completely decoupled from clipmap tiles / LOD. Each candidate column
// is ray-marched vertically through the player's strate Z-band via UVoxelGenerator::GetDensityAt and
// snapped to the real surface crossings. Every placement is a pure hash of (cell, column, crossing,
// entry, seed) + the density surface snap → a prop sits at the SAME world position no matter which
// LOD tile meshes the ground under it: NO pop/teleport on LOD swaps.
//
// RENDERING PATHS per decoration entry (FStrateDecoration):
// - ActorClass → real actors (lights, logic, interaction). Keep MaxLODLevel=0.
// - InstancedMesh → HISM instances batched per chunk: no tick, no per-actor cost,
// engine-culled. Safe to allow at LOD 1-2 so visual props don't
// pop out with LOD0 (emissive materials still glow at distance).
// THREADING (critical): the column ray-march is EXPENSIVE (many GetDensityAt per column). It runs on
// a WORKER thread (UE::Tasks, like mesh gen — GetDensityAt is thread-safe), producing a list of spawn
// commands. Only the SpawnActor/AddInstance (which MUST be game-thread) happens on the game thread,
// budgeted. The game thread never does decoration density math. Flow (UpdateDecorations each Tick):
// - recompute desired cell set on player cell-boundary / strate change
// - LaunchDecoTasks: fire async march tasks (capped by MaxConcurrentDecorationTasks)
// - ProcessDecoResults: drain finished tasks' results, apply (spawn) budgeted, epoch-guarded
// Decorations exist ONLY in the player's current strate (march is strate-bounded) → a strate change
// wipes + rebuilds them, and there is no cross-strate light bleed to cull.
//
// STRATE LIGHT CULLING: a light in another strate can never legitimately reach the
// player (seals + bedrock gap are solid), but a shadowless light BLEEDS through rock
// and a shadowed one pays full cost to render black. So light components on spawned
// decoration actors are only visible while the player is in the SAME strate — the
// analytical form of occlusion culling, computed once per strate change.
// 2) WATER — ONE strate-global ocean plane that follows the player (UpdateWater). Terrain pokes
// through it, so it reads as water at every LOD / to the horizon with no per-tile gaps. One draw.
//
// RENDERING PATHS / DISTANCE TIERS per entry (FStrateDecoration): non-instanced ActorClass entries with
// MaxLODLevel==0 are near-only (DecorationActorRadiusChunks — pricey actors stay close); InstancedMesh
// (HISM) entries + MaxLODLevel>=1 actor entries are any-distance (DecorationRadiusChunks).
//
// DETERMINISM: same seed + world ⇒ identical placement. Spawning runs on the game thread.
#pragma once
#include "CoreMinimal.h"
#include "Containers/Queue.h"
#include "VoxelTypes.h"
#include "VoxelStrateTypes.h" // FStrateDecoration (resolved per dominant biome)
#include <atomic>
#include "VoxelContentManager.generated.h"
class UVoxelStrateManager;
class UVoxelStrateDefinition;
class UVoxelGenerator;
class UVoxelSettings;
class UStaticMesh;
class UStaticMeshComponent;
class UHierarchicalInstancedStaticMeshComponent;
@@ -47,39 +55,121 @@ class VOXELFORGE_API UVoxelContentManager : public UObject
public:
/** Wire up services. Owner is the AVoxelWorld actor that owns spawned content.
* Generator supplies the dominant-biome query for per-biome content selection. */
* Generator supplies GetDensityAt (surface snapping) + GetDominantBiomeAt (per-biome content).
* Settings supplies the decoration grid tunables (radii / spacing / march / budget). */
void Initialize(AActor* InOwner, UVoxelStrateManager* InStrateManager,
UVoxelGenerator* InGenerator, int32 InSeed);
UVoxelGenerator* InGenerator, UVoxelSettings* InSettings, int32 InSeed);
/** Update the seed used for placement hashing (season reset). */
void SetSeed(int32 InSeed) { Seed = InSeed; }
/** Populate decorations + water for a chunk. Clears any previous content first.
* Each decoration entry spawns only while LOD <= its MaxLODLevel; water at any LOD. */
void PopulateChunk(const FIntVector& ChunkCoord, const FVoxelMeshData& MeshData, int32 LODLevel = 0);
//--- WATER (single strate-global ocean plane, follows the player) ---------
/** Maintain ONE large water plane at the player strate's water level, centred on the player
* (snapped to a coarse grid). Terrain pokes through it, so it reads as water at EVERY LOD and
* out to the horizon with no per-tile gaps (the old per-tile planes only appeared on tiles that
* had terrain geometry in the water band → gaps over deep water + level-0 only). One draw, cheap.
* Call every Tick. Uses the strate's WaterMaterial (per-biome override not applied to the global
* plane). */
void UpdateWater(const FVector& PlayerWorldPos);
/** Destroy all spawned content (actors + instances + water plane) for one chunk. */
void ClearChunk(const FIntVector& ChunkCoord);
//--- DECORATIONS (distance-based world grid, async march) ----------------
/** Stream decoration cells around the player: recompute the desired set on cell/strate change,
* launch async march tasks (capped), and apply finished results budgeted. Call every Tick. */
void UpdateDecorations(const FVector& PlayerWorldPos);
/** Destroy all spawned content for every chunk. */
/** Destroy all spawned content (decorations + water). Regenerate / season reset. Bumps the deco
* epoch so any in-flight march tasks' results are discarded. */
void ClearAll();
/** Player strate changed → toggle decoration lights (strict: lights only live in the
* player's strate). Cheap no-op while the strate stays the same — call every Tick. */
void SetActiveStrate(int32 PlayerStrateIndex);
virtual void BeginDestroy() override; // flag shutdown so in-flight march tasks don't touch us
/** Flag shutdown + block until in-flight march tasks drain. Call from AVoxelWorld::EndPlay BEFORE
* UObject teardown (worker tasks read the Generator). */
void NotifyShutdown();
//--- async-task plumbing (public so the worker lambda can reach them) -----
/** One placement decided off-thread; spawned on the game thread from FDecoCellResult::Entries. */
struct FDecoSpawn
{
int32 EntryIdx = 0;
bool bInstanced = false;
FTransform Xf = FTransform::Identity;
};
/** A finished cell march: a snapshot of the resolved decoration list + the spawn commands. */
struct FDecoCellResult
{
FIntPoint Cell = FIntPoint::ZeroValue;
uint32 BuildId = 0; // identity of the region build this cell belongs to
TArray<FStrateDecoration> Entries; // snapshot the game thread spawns from (by EntryIdx)
TArray<FDecoSpawn> Spawns;
};
private:
void SpawnDecorations(const FIntVector& ChunkCoord, const FVoxelMeshData& MeshData,
const TArray<FStrateDecoration>& Decorations, int32 LODLevel,
TArray<TWeakObjectPtr<AActor>>& Out);
void SpawnWater(const FIntVector& ChunkCoord, const UVoxelStrateDefinition* Def,
UMaterialInterface* WaterMaterial);
// Per-REGION spawned content (weak — actors live in the level, components owned by the owner actor).
// A region groups RxR cells (DecorationRegionSizeCells); ALL placements in the region share ONE HISM
// per mesh, so the render thread walks ~R^2 fewer components. Regions load/unload as a unit, so
// clearing is a plain DestroyComponent — no per-instance RemoveInstances index remapping.
struct FDecoRegionContent
{
TArray<TWeakObjectPtr<AActor>> Actors;
TArray<TWeakObjectPtr<UHierarchicalInstancedStaticMeshComponent>> Instances;
};
/** Strate index a chunk belongs to (center-Z lookup). */
int32 GetChunkStrateIndex(const FIntVector& ChunkCoord) const;
// Instanced transforms for one mesh, accumulated across all of a region's cells → one batched HISM.
struct FRegionMeshBucket
{
FStrateDecoration Deco; // representative entry (mesh + HISM render tuning: cull/shadow/scale)
TArray<FTransform> Xforms;
};
// A non-instanced actor placement, spawned when the region is applied.
struct FRegionActorSpawn
{
TSubclassOf<AActor> ActorClass;
FTransform Xf = FTransform::Identity;
};
// A region being assembled: cell march results merged in as they land; APPLIED (HISMs built, actors
// spawned) only once every cell has reported, so the whole region is one batched build per mesh.
struct FDecoRegionBuild
{
TMap<TWeakObjectPtr<UStaticMesh>, FRegionMeshBucket> MeshBuckets;
TArray<FRegionActorSpawn> ActorSpawns;
int32 CellsRemaining = 0; // cells still to account for before this region can apply
uint32 BuildId = 0; // unique, monotonic — stale in-flight cell results fail to match
};
// Constant per-update strate context (a strate is a horizontal slab → same for every cell). Carries
// only PODs/Z-bounds so it is safe to copy into a worker task (no UObject deref on the worker).
struct FDecoContext
{
const UVoxelStrateDefinition* Def = nullptr; // game-thread only (biome/decoration resolve)
int32 RepChunkZ = 0; // representative chunk-Z for biome lookups
float TopVoxelZ = 0.0f; // strate band, voxel coords (march range)
float BottomVoxelZ = 0.0f;
float WaterLocalZ = -FLT_MAX; // water surface, actor-local cm (-FLT_MAX = no water)
bool bHasWater = false;
bool bSurfaceWorld = false; // heightfield archetype → use the GetSurfaceHeightAt oracle
};
/** WORKER-THREAD surface find → fills OutSpawns for one cell. SurfaceWorld uses the height oracle
* (cheap, O(1)/column); other archetypes ray-march the density column. No UObject access except
* Generator (thread-safe). Determinism-critical. */
static void BuildCellSpawns(const UVoxelGenerator* Gen, const FTransform& OwnerXf,
const FIntPoint& Cell, const FDecoContext& Ctx,
const TArray<FStrateDecoration>& Entries, uint32 InSeed,
int32 Spacing, float Step, int32 MaxCrossings, float ColumnDepth,
TArray<FDecoSpawn>& OutSpawns);
void LaunchDecoTasks(const FIntPoint& PlayerCell);
void ProcessDecoResults(const FIntPoint& PlayerCell, int32 FarR);
void MergeCellResult(const FDecoCellResult& Result); // fold one cell's spawns into its region build
void MarkCellDone(const FIntPoint& Region, uint32 BuildId); // decrement region's remaining-cell count
void ApplyRegion(const FIntPoint& Region, FDecoRegionBuild& Build);
void RebuildDesiredCells(const FIntPoint& PlayerCell);
void ClearDecorationRegion(const FIntPoint& Region);
void ClearAllDecorations();
// Region size in cells, clamped (>=1). Cell↔region math lives in file-static helpers in the .cpp.
int32 RegionSize() const;
/** Show/hide every light component on a decoration actor. */
static void SetActorLightsEnabled(AActor* Actor, bool bEnabled);
TWeakObjectPtr<AActor> Owner;
@@ -89,23 +179,54 @@ private:
UPROPERTY()
UVoxelGenerator* Generator = nullptr;
int32 Seed = 0;
UPROPERTY()
UVoxelSettings* Settings = nullptr;
// Player's current strate (INT32_MIN until first SetActiveStrate). Lights on spawned
// actors are enabled iff their chunk's strate matches this.
int32 ActiveStrateIndex = INT32_MIN;
int32 Seed = 0;
// Engine unit plane (/Engine/BasicShapes/Plane) reused for every water surface.
UPROPERTY()
UStaticMesh* PlaneMesh = nullptr;
// Spawned decoration/ambient actors per chunk (weak — they live in the level).
TMap<FIntVector, TArray<TWeakObjectPtr<AActor>>> SpawnedActors;
// Loaded decoration regions (FIntPoint = region XY). One HISM per mesh per region. Not a UPROPERTY
// (weak ptrs inside; the owner actor keeps the components alive).
TMap<FIntPoint, FDecoRegionContent> DecoRegions;
// HISM components per chunk (weak — registered components are owned by the actor).
TMap<FIntVector, TArray<TWeakObjectPtr<UHierarchicalInstancedStaticMeshComponent>>> ChunkInstances;
// Regions currently being marched cell-by-cell; merged here until every cell reports, then applied.
TMap<FIntPoint, FDecoRegionBuild> RegionBuilds;
// Water surface component per chunk.
// Cells that are desired but need a march task launched (nearest-first).
TArray<FIntPoint> PendingLaunch;
// Cells with a march task in flight (awaiting a result).
TSet<FIntPoint> InFlightCells;
// Worker tasks enqueue here (Mpsc: many workers, one game-thread consumer).
TQueue<FDecoCellResult, EQueueMode::Mpsc> DecoResults;
// Regions whose last cell just landed, awaiting budgeted game-thread apply (HISM build + actor spawn).
TArray<FIntPoint> CompletedRegions;
// Monotonic id stamped on each region build + the cell tasks it launches. A cell result merges only
// if its BuildId still matches the live build for that region → a region that was cleared and later
// re-marched (same coords, new BuildId) never absorbs a stale in-flight cell from its prior life.
uint32 NextBuildId = 1;
// Set in BeginDestroy; worker tasks check it before touching us.
std::atomic<bool> bShuttingDown{false};
// Streaming state. INT_MIN sentinels force a full rebuild on the first update / after ClearAll.
FIntPoint LastDecoCell = FIntPoint(INT32_MIN, INT32_MIN);
int32 LastStrateIndex = INT32_MIN;
// Shared strate context for the current update (recomputed each UpdateDecorations; the launch step
// copies the PODs into each task).
FDecoContext CurrentCtx;
// Single strate-global ocean plane, repositioned to follow the player (see UpdateWater).
UPROPERTY()
TMap<FIntVector, UStaticMeshComponent*> WaterPlanes;
UStaticMeshComponent* WaterPlane = nullptr;
// Cached state so UpdateWater is a cheap no-op when neither the water level nor the snapped
// player cell changed since last frame.
float LastWaterZ = -FLT_MAX; // strate water level (voxel Z)
FIntPoint LastWaterCell = FIntPoint(INT32_MIN, INT32_MIN); // snapped XY grid cell
};