#pragma once #include "CoreMinimal.h" #include #include "Containers/Queue.h" #include "Core/DescentHandle.h" #include "Core/DescentSimSubsystem.h" #include "Core/DescentStreamFocus.h" #include "Core/DescentWorldSubsystem.h" #include "Core/Tuning/DescentTune_Terrain.h" #include "World/DescentSDF.h" #include "World/TerrainChunk.h" #include "DescentTerrainSubsystem.generated.h" using Descent::World::FTerrainChunk; class ADescentTerrainActor; class UProceduralMeshComponent; class UDescentOreSubsystem; namespace Descent::World { class FDescentWorldDef; /** What a worker is asked to do for one chunk, and what it hands back. */ struct FTerrainJobInput { FIntVector Key = FIntVector::ZeroValue; uint32 Version = 0; bool bFill = false; // the chunk has no data yet: fill it first TArray Data; // a copy of the chunk's data when !bFill TArray Cut; TArray Deposits; // deposits that may touch the chunk TArray Exhausted; // parallel: 1 = mined out }; struct FTerrainJobResult { FIntVector Key = FIntVector::ZeroValue; uint32 Version = 0; bool bWasFill = false; bool bMixed = false; // after a fill: the chunk holds a surface (keep Data) TArray Data; TArray Cut; FProcMeshSection Section; int32 Triangles = 0; bool bHasSurface = false; double FillMs = 0.0; double MeshMs = 0.0; }; } /** * The terrain (DESIGN_UE.md section 2.3 - 2.6): a hash map of 20 m chunks of the world's density field, meshed by * surface nets on worker threads and streamed around the stream focus points, edited by Carve / CleanupFloating, * queried for density, gradient, ray hits and push-out. There is no bound anywhere (requirement U7): a chunk is * created on first touch, and outside the designed voids the field is solid rock. Everything here is game-thread * only; workers get copies. Rock has no physics collision: every interaction queries the density field. */ UCLASS() class THEDESCENT_API UDescentTerrainSubsystem : public UDescentWorldSubsystem { GENERATED_BODY() public: static UDescentTerrainSubsystem* Get(const UWorld* World); virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override; /** The sim step is registered here, not in Initialize: a test world that never begins play has no terrain callback. */ virtual void OnWorldBeginPlay(UWorld& InWorld) override; /** The actor that owns the chunk components (set by the boot step; null in tests: chunks then have data but no component). */ void SetTerrainActor(ADescentTerrainActor* Actor); ADescentTerrainActor* GetTerrainActor() const; // ---- queries (metres; game thread; never wait for jobs: a chunk without data uses the analytic field) ---- double Density(const FVector& Pm); /** Normalised gradient, points INTO rock; (0,0,1) if degenerate. */ FVector Gradient(const FVector& Pm, double H = Descent::Tune::Terrain::GradientH); /** Sphere-traced ray: distance to rock, -1 if none within MaxD, 0 if the origin is in rock. */ double Raycast(const FVector& Om, const FVector& Dir, double MaxD, double MinStep = Descent::Tune::Terrain::RaycastMinStep); /** Moves a sphere out of rock: pen = Radius + Density, centre -= Gradient * pen. True if it moved. */ bool PushOutSphere(FVector& InOutCentreM, double RadiusM, FVector* OutNormal = nullptr); // ---- edits ---- /** Carves a smooth sphere; returns the number of core samples that went from rock to water. */ int32 Carve(const FVector& CentreM, double RadiusM, double Rate, double Dt, bool bIgnoreHardness = false); /** Removes small disconnected rock bits near a carve; returns the number of samples freed. */ int32 CleanupFloating(const FVector& CentreM, double RadiusM); /** Called with the position and the number of samples after a cleanup freed rock (the mining milestone makes debris). */ TFunction OnRockFreed; // ---- streaming ---- /** Loads (blocking, on all cores) every chunk within RadiusM of FocusM. Section 1.5 step 4. */ void StreamInitialBlocking(const FVector& FocusM, double RadiusM); /** True when no job is running or queued, nothing waits to be applied or remeshed, and the plan is current. */ bool IsStreamIdle() const; /** Plans the stream again at the next tick (a scenario that teleported calls this before waiting). */ void RequestReplan() { bReplanNeeded = true; } /** Section 6.12: every chunk with bModified (density as int16, step 4/32767, and the cut bits), zlib-compressed. */ void SaveModified(TArray& OutCompressed, int32& OutRawSize); /** Puts saved chunks back. Chunks carved since the save that are not in it are generated again. */ bool LoadModified(const TArray& Compressed, int32 RawSize); void SetStreamingEnabled(bool bEnabled) { bStreamingEnabled = bEnabled; } /** Runs every queued job on the calling thread and applies the results (tests). */ void FlushJobsSync(); double GetLoadRadiusM() const; // ---- chunk access (tests, tools) ---- FTerrainChunk* FindChunk(const FIntVector& Key); FTerrainChunk* FindOrCreateChunk(const FIntVector& Key); /** Makes sure the chunk has data (fills it on this thread) and returns it. */ FTerrainChunk& EnsureData(FTerrainChunk& Chunk); static FIntVector KeyOfPoint(const FVector& Pm); /** The padded sample at global index G (any chunk); the analytic field where a chunk has no data. */ float GetSampleGlobal(int32 Gx, int32 Gy, int32 Gz); /** Writes one sample into every chunk that holds a copy of it (core or padding); the way cleanup frees rock. */ void SetSampleGlobal(int32 Gx, int32 Gy, int32 Gz, float V); // ---- numbers for the `terrain` section and the scenarios ---- struct FStats { int32 ChunksKnown = 0, ChunksFilled = 0, ChunksMeshed = 0, ChunksModified = 0; int32 Triangles = 0; int32 PendingJobs = 0; int32 ComponentsActive = 0; double ApplyMs = 0.0; // game-thread time spent applying results in the last tick double RemeshMs = 0.0; // and remeshing carved chunks double FillMsAvg = 0.0, MeshMsAvg = 0.0; int64 JobsDone = 0; }; FStats GetStats() const; // Results of the last console / test operations (state section `terrain`). double LastDensity = 0.0; int32 LastRemoved = 0; int32 FloatingAfterCleanup = 0; int32 LastCleanupFreed = 0; /** Counts the rock samples in the flood-fill region around a point that are not connected to the region's border. */ int32 CountFloatingRock(const FVector& CentreM, double RadiusM); private: struct FWanted { double Dist; FIntVector Key; }; void Tick(const FDescentSimFrame& Frame); void PlanStream(const TArray& Foci); void LaunchJobs(); void ApplyResults(double BudgetMs, bool bUnlimited); void ApplyResult(Descent::World::FTerrainJobResult& R); void ProcessDirty(double BudgetMs); void UnloadFar(const TArray& Foci); void ClassifyChunk(FTerrainChunk& C); void FillChunkSync(FTerrainChunk& C); void SetMesh(FTerrainChunk& C, FProcMeshSection& Section, int32 Triangles, bool bHasSurface); void Unmesh(FTerrainChunk& C); void MeshChunkNow(FTerrainChunk& C); TSharedRef MakeJobInput(FTerrainChunk& C, bool bCopyData); float GenQuickGen(const FVector& Pm); void RefreshDeposits(FTerrainChunk& C); UDescentOreSubsystem* Ore(); static Descent::World::FTerrainJobResult RunJob(const Descent::World::FTerrainJobInput& In); TMap> Chunks; TSet MeshedSet; TSet DirtySet; TArray Queue; int32 QueueHead = 0; TQueue, EQueueMode::Mpsc> Results; std::atomic InFlight{ 0 }; std::atomic ResultsWaiting{ 0 }; TMap> QuickCache; TWeakObjectPtr TerrainActor; TWeakObjectPtr OreSub; FDescentHandle StepHandle; FTerrainChunk* LastChunk = nullptr; FIntVector LastChunkKey = FIntVector(INT32_MAX, 0, 0); bool bStreamingEnabled = true; bool bReplanNeeded = true; double TimeSincePlan = 1e9; double TimeSinceUnload = 0.0; FVector PlanFocus0 = FVector(1e18, 0, 0); int64 TotalTriangles = 0; int64 FrameCounter = 0; double ApplyMsLast = 0.0, RemeshMsLast = 0.0; double FillMsSum = 0.0, MeshMsSum = 0.0; int64 FillCount = 0, MeshCount = 0, JobsDoneCount = 0; };