standalone: the owner's world outside Unreal -- bit-identical density, D3D11 fly-through viewer

Standalone/ (C++20, MSVC /O2 /arch:AVX2 /fp:precise, Build.ps1): copied density pipeline, op stack,
cave morphology, passages, strate manager/composer, noise, DetSinCos, mesher, clipmap selection,
with a UE compatibility layer (Compat/). Owner data exported once to Standalone/data/owner_world.json
by a default-off explore-commandlet exporter (the only plugin-source change). 25 cm voxels.
Parity: seed 0, 3,436 samples (strate edges, negative/far coords, rooms, tunnels, passages,
landings): bit-identical. Viewer: full 7-level clipmap, per-level toggles 1-7, T = round winding
tunnels (standalone option), N/P seed, F wireframe.
Timing NOT yet a baseline: 515 s for 12,054 tiles (L0 70 s, L6 296 s); the compat TMap/TSet are
vector-backed with linear lookup, which the plugin's caches do not expect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-02 12:14:04 +02:00
1 parent a443eb8546
commit 05f62cff36
114 files changed
+68206 -3

No files matched your search

+323
View File
@@ -0,0 +1,323 @@
#pragma once
#include "CoreMinimal.h"
#include "Json.h"
#include "VoxelBiomeDefinition.h"
#include "VoxelGenerator.h"
#include "VoxelSettings.h"
#include "VoxelStrateDefinition.h"
#include "VoxelStrateManager.h"
#include "VoxelTerrainOpDefinition.h"
struct FStandaloneParitySample
{
FVector PositionVoxels;
float PluginDensity = 0.0f;
std::string Kind;
int32 StrateIndex = INDEX_NONE;
};
struct FStandaloneOwnerWorld
{
std::unique_ptr<UVoxelSettings> Settings;
std::unique_ptr<UVoxelStrateManager> Manager;
std::unique_ptr<UVoxelGenerator> Generator;
std::vector<std::unique_ptr<UVoxelStrateDefinition>> DefinitionStorage;
std::vector<std::unique_ptr<UVoxelBiomeDefinition>> BiomeStorage;
std::vector<std::unique_ptr<UVoxelTerrainOpDefinition>> OperationStorage;
std::vector<FStandaloneParitySample> DensitySamples;
FVector ViewerStartVoxels = FVector::ZeroVector;
bool ViewerStartFound = false;
int32 Seed = 0;
int32 ClipRadius = 6;
int32 MaxClipLevel = 6;
int32 FullResClipLevels = 2;
int32 CoarseTileCells = 16;
int32 RenderDistanceChunks = 1536;
int32 StrateContentCutMinLevel = 9;
bool bGenerateSkirts = true;
int32 SkirtCells = 2;
int32 LODOctaveDrop = 0;
};
namespace StandaloneOwnerLoader
{
template<class T>
inline void ReadField(const VFJson::Value& Object, const char* Name, T& Out)
{
const VFJson::Value& Value = Object.Get(Name);
if constexpr (std::is_same_v<T, bool>)
{
if (std::holds_alternative<bool>(Value.Data)) Out = Value.Boolean(Out);
else if (Value.IsNumber()) Out = Value.Number() != 0.0;
}
else if constexpr (std::is_enum_v<T>)
{
if (Value.IsNumber()) Out = static_cast<T>(Value.Integer());
}
else if constexpr (std::is_arithmetic_v<T>)
{
if (Value.IsNumber()) Out = static_cast<T>(Value.Number());
}
}
inline void ReadText(const VFJson::Value& Object, const char* Name, FText& Out)
{
const VFJson::Value& Value = Object.Get(Name);
if (Value.IsString()) Out = FText::FromString(FString(Value.String()));
}
inline void ReadLinearColor(const VFJson::Value& Object, FLinearColor& Out)
{
ReadField(Object, "R", Out.R); ReadField(Object, "G", Out.G);
ReadField(Object, "B", Out.B); ReadField(Object, "A", Out.A);
}
inline void ReadVector(const VFJson::Value& Object, FVector& Out)
{
ReadField(Object, "x", Out.X); ReadField(Object, "y", Out.Y); ReadField(Object, "z", Out.Z);
}
inline void ReadGenerationParams(const VFJson::Value& J, FStrateGenerationParams& P)
{
#define VF_LOAD_STRATE_FIELD(Name) ReadField(J, #Name, P.Name);
VF_STRATE_PARAM_FIELDS(VF_LOAD_STRATE_FIELD, VF_LOAD_STRATE_FIELD)
#undef VF_LOAD_STRATE_FIELD
ReadField(J, "bTunnelFloorEnabled", P.bTunnelFloorEnabled);
}
inline void ReadDisturbances(const VFJson::Value& J, FStrateDisturbanceParams& P)
{
ReadField(J,"ChasmDensity",P.ChasmDensity); ReadField(J,"ChasmSpacing",P.ChasmSpacing);
ReadField(J,"ChasmRadius",P.ChasmRadius); ReadField(J,"BridgeDensity",P.BridgeDensity);
ReadField(J,"BridgeSpacing",P.BridgeSpacing); ReadField(J,"BridgeRadius",P.BridgeRadius);
ReadField(J,"RidgeDensity",P.RidgeDensity); ReadField(J,"RidgeSpacing",P.RidgeSpacing);
ReadField(J,"RidgeHeight",P.RidgeHeight); ReadField(J,"RidgeThickness",P.RidgeThickness);
ReadField(J,"BaseDensity",P.BaseDensity); ReadField(J,"BoundarySealThickness",P.BoundarySealThickness);
}
inline void ReadPassageConfig(const VFJson::Value& J, FStratePassageConfig& P)
{
ReadField(J,"Connections",P.Connections); ReadField(J,"Style",P.Style);
ReadField(J,"MouthRadius",P.MouthRadius); ReadField(J,"MidRadius",P.MidRadius);
ReadField(J,"ReachMin",P.ReachMin); ReadField(J,"ReachMax",P.ReachMax);
ReadField(J,"DistanceMin",P.DistanceMin); ReadField(J,"DistanceMax",P.DistanceMax);
ReadField(J,"Wander",P.Wander); ReadField(J,"Segments",P.Segments);
ReadField(J,"VerticalWobble",P.VerticalWobble); ReadField(J,"SpiralRadius",P.SpiralRadius);
ReadField(J,"SpiralTurns",P.SpiralTurns); ReadField(J,"CascadeSteps",P.CascadeSteps);
ReadField(J,"CascadeLedge",P.CascadeLedge);
}
inline void ReadSurfaceParams(const VFJson::Value& J, FSurfaceGenerationParams& P)
{
#define VF_SURFACE_FIELD(Name) ReadField(J, #Name, P.Name);
VF_SURFACE_FIELD(BaseGroundRelative) VF_SURFACE_FIELD(ElevationRange)
VF_SURFACE_FIELD(ContinentFrequency) VF_SURFACE_FIELD(MountainStrength)
VF_SURFACE_FIELD(MountainFrequency) VF_SURFACE_FIELD(DetailFrequency)
VF_SURFACE_FIELD(SurfaceRoughness) VF_SURFACE_FIELD(HeightWarpStrength)
VF_SURFACE_FIELD(HeightWarpFrequency) VF_SURFACE_FIELD(ReliefFrequency)
VF_SURFACE_FIELD(ReliefStrength) VF_SURFACE_FIELD(ReliefContrast)
VF_SURFACE_FIELD(TerraceStrength) VF_SURFACE_FIELD(TerraceHeight)
VF_SURFACE_FIELD(TerraceHardness) VF_SURFACE_FIELD(LayerLineDepth)
VF_SURFACE_FIELD(LayerLineSpacing) VF_SURFACE_FIELD(CliffStrength)
VF_SURFACE_FIELD(CliffSlopeThreshold) VF_SURFACE_FIELD(CliffSharpness)
VF_SURFACE_FIELD(CliffSampleDist) VF_SURFACE_FIELD(OverhangStrength)
VF_SURFACE_FIELD(OverhangReach) VF_SURFACE_FIELD(OverhangHeight)
VF_SURFACE_FIELD(OverhangFrequency) VF_SURFACE_FIELD(OverhangZScale)
VF_SURFACE_FIELD(OverhangSlopeThreshold) VF_SURFACE_FIELD(WaterLevelRelative)
VF_SURFACE_FIELD(BeachWidth) VF_SURFACE_FIELD(CeilingRelative)
VF_SURFACE_FIELD(CeilingRoughness) VF_SURFACE_FIELD(CeilingRoughnessFrequency)
VF_SURFACE_FIELD(CeilingUndulation) VF_SURFACE_FIELD(CeilingUndulationFrequency)
VF_SURFACE_FIELD(CeilingRidgeStrength) VF_SURFACE_FIELD(CeilingRidgeFrequency)
VF_SURFACE_FIELD(CeilingWarpStrength) VF_SURFACE_FIELD(CeilingWarpFrequency)
VF_SURFACE_FIELD(BoundarySealThickness) VF_SURFACE_FIELD(BaseDensity)
#undef VF_SURFACE_FIELD
}
inline void ReadBiomeMap(const VFJson::Value& J, FBiomeMapParams& P)
{
ReadField(J,"CellSize",P.CellSize); ReadField(J,"WarpStrength",P.WarpStrength);
ReadField(J,"WarpFrequency",P.WarpFrequency); ReadField(J,"BorderBlend",P.BorderBlend);
ReadField(J,"ReliefFrequency",P.ReliefFrequency); ReadField(J,"ReliefContrast",P.ReliefContrast);
ReadField(J,"MoistureFrequency",P.MoistureFrequency);
}
inline void ReadOperation(const VFJson::Value& J, UVoxelTerrainOpDefinition& O)
{
ReadField(J,"Type",O.Type);
#define VF_OPERATION_FIELD(Name) ReadField(J, #Name, O.Name);
VF_OPERATION_FIELD(TerraceStepHeight) VF_OPERATION_FIELD(TerraceHardness)
VF_OPERATION_FIELD(TerraceNoiseDisplacement) VF_OPERATION_FIELD(LayerLineSpacing)
VF_OPERATION_FIELD(LayerLineDepth) VF_OPERATION_FIELD(RibbingSpacing)
VF_OPERATION_FIELD(RibbingDepth) VF_OPERATION_FIELD(CliffStrength)
VF_OPERATION_FIELD(ScallopStrength) VF_OPERATION_FIELD(ScallopFrequency)
VF_OPERATION_FIELD(OverhangStrength) VF_OPERATION_FIELD(OverhangDepth)
VF_OPERATION_FIELD(OverhangFrequency) VF_OPERATION_FIELD(ArchDensity)
VF_OPERATION_FIELD(ArchMinRadius) VF_OPERATION_FIELD(ArchMaxRadius)
VF_OPERATION_FIELD(ColumnDensity) VF_OPERATION_FIELD(ColumnMinRadius)
VF_OPERATION_FIELD(ColumnMaxRadius) VF_OPERATION_FIELD(PitDensity)
VF_OPERATION_FIELD(PitMinRadius) VF_OPERATION_FIELD(PitMaxRadius)
VF_OPERATION_FIELD(PitDepth) VF_OPERATION_FIELD(ChimneyDensity)
VF_OPERATION_FIELD(ChimneyMinRadius) VF_OPERATION_FIELD(ChimneyMaxRadius)
VF_OPERATION_FIELD(ChimneyHeight) VF_OPERATION_FIELD(DomeDensity)
VF_OPERATION_FIELD(DomeMinRadius) VF_OPERATION_FIELD(DomeMaxRadius)
VF_OPERATION_FIELD(DomeHeightRatio) VF_OPERATION_FIELD(PinchDensity)
VF_OPERATION_FIELD(PinchStrength) VF_OPERATION_FIELD(PinchLength)
#undef VF_OPERATION_FIELD
}
inline void ReadBiome(const VFJson::Value& J, UVoxelBiomeDefinition& B)
{
ReadText(J,"BiomeName",B.BiomeName); ReadText(J,"BiomeDescription",B.BiomeDescription);
ReadLinearColor(J.Get("DebugColor"),B.DebugColor);
ReadField(J,"ReliefMin",B.ReliefMin); ReadField(J,"ReliefMax",B.ReliefMax);
ReadField(J,"MoistureMin",B.MoistureMin); ReadField(J,"MoistureMax",B.MoistureMax);
ReadField(J,"bOverrideTerrain",B.bOverrideTerrain); ReadField(J,"GeneratorType",B.GeneratorType);
ReadSurfaceParams(J.Get("SurfaceParams"),B.SurfaceParams);
ReadField(J,"bOverrideAtmosphere",B.bOverrideAtmosphere);
ReadLinearColor(J.Get("FogColor"),B.FogColor); ReadField(J,"FogDensity",B.FogDensity);
ReadLinearColor(J.Get("AmbientLightColor"),B.AmbientLightColor);
ReadField(J,"AmbientLightIntensity",B.AmbientLightIntensity);
ReadField(J,"MaterialPaletteIndex",B.MaterialPaletteIndex);
}
inline UVoxelStrateDefinition* ReadDefinition(
FStandaloneOwnerWorld& World, const VFJson::Value& Slot, int32 SlotIndex)
{
const VFJson::Value& J = Slot.Get("asset");
auto Definition = std::make_unique<UVoxelStrateDefinition>();
ReadText(J,"StrateName",Definition->StrateName);
ReadText(J,"StrateDescription",Definition->StrateDescription);
ReadField(J,"StrateHeightInChunks",Definition->StrateHeightInChunks);
ReadField(J,"TransitionType",Definition->TransitionType);
ReadField(J,"TransitionBlendChunks",Definition->TransitionBlendChunks);
ReadField(J,"GeneratorType",Definition->GeneratorType);
ReadField(J,"bEnableWorms",Definition->bEnableWorms);
ReadGenerationParams(Slot.Get("generation_params"),Definition->GenerationParams);
ReadDisturbances(Slot.Get("disturbances"),Definition->Disturbances);
ReadPassageConfig(Slot.Get("passage_config"),Definition->PassageConfig);
ReadSurfaceParams(J.Get("SurfaceParams"),Definition->SurfaceParams);
ReadBiomeMap(J.Get("BiomeMapParams"),Definition->BiomeMapParams);
ReadField(J,"bHasWater",Definition->bHasWater);
const auto& Biomes = J.Get("Biomes").AsArray();
Definition->Biomes.Reserve(static_cast<int32>(Biomes.size()));
for (const VFJson::Value& BiomeJson : Biomes)
{
auto Biome = std::make_unique<UVoxelBiomeDefinition>();
ReadBiome(BiomeJson,*Biome);
Definition->Biomes.Add(Biome.get());
World.BiomeStorage.push_back(std::move(Biome));
}
const auto& OperationEntries = Slot.Get("terrain_operation_assets").AsArray();
Definition->TerrainOperations.Reserve(static_cast<int32>(OperationEntries.size()));
for (int32 I = 0; I < static_cast<int32>(OperationEntries.size()); ++I)
{
const VFJson::Value& EntryJson = OperationEntries[static_cast<size_t>(I)];
const VFJson::Value& AssetJson = EntryJson.Get("asset");
const VFJson::Value& PathJson = EntryJson.Get("asset_path");
const FString Path = PathJson.IsString() ? FString(PathJson.String()) : FString{};
FStrateTerrainOpEntry Entry;
ReadField(EntryJson,"weight",Entry.Weight);
ReadField(EntryJson,"probability",Entry.Probability);
if (AssetJson.IsObject())
{
auto Operation = std::make_unique<UVoxelTerrainOpDefinition>();
ReadOperation(AssetJson,*Operation);
UVoxelTerrainOpDefinition* Raw = Operation.get();
World.OperationStorage.push_back(std::move(Operation));
Entry.Operation.Set(Raw,Path);
RegisterStandaloneAsset(Path,Raw);
}
else if (!Path.IsEmpty()) Entry.Operation = TSoftObjectPtr<UVoxelTerrainOpDefinition>(Path);
Definition->TerrainOperations.Add(std::move(Entry));
}
const FString SyntheticPath = FString::Printf("/Standalone/Strate_%d",SlotIndex);
Definition->PostLoad();
UVoxelStrateDefinition* Raw = Definition.get();
World.DefinitionStorage.push_back(std::move(Definition));
RegisterStandaloneAsset(SyntheticPath,Raw);
return Raw;
}
inline bool Load(const std::string& Path, FStandaloneOwnerWorld& Out, std::string& Error)
{
VFJson::Value Root;
if (!VFJson::Load(Path,Root,Error)) return false;
if (Root.Get("voxel_size_cm").Number() != VOXEL_SIZE)
{
Error = "export voxel_size_cm does not match the standalone VOXEL_SIZE";
return false;
}
Out.Seed = static_cast<int32>(Root.Get("seed").Integer());
Out.ClipRadius = static_cast<int32>(Root.Get("clip_radius").Integer(6));
Out.MaxClipLevel = static_cast<int32>(Root.Get("max_clip_level").Integer(6));
Out.FullResClipLevels = static_cast<int32>(Root.Get("full_res_clip_levels").Integer(2));
Out.CoarseTileCells = static_cast<int32>(Root.Get("coarse_tile_cells").Integer(16));
Out.RenderDistanceChunks = static_cast<int32>(Root.Get("render_distance_chunks").Integer(1536));
Out.StrateContentCutMinLevel = static_cast<int32>(Root.Get("strate_content_cut_min_level").Integer(9));
Out.bGenerateSkirts = Root.Get("generate_skirts").Boolean(true);
Out.SkirtCells = static_cast<int32>(Root.Get("skirt_cells").Integer(2));
Out.LODOctaveDrop = static_cast<int32>(Root.Get("lod_octave_drop").Integer(0));
ReadVector(Root.Get("viewer_start"),Out.ViewerStartVoxels);
Out.ViewerStartFound = Root.Get("viewer_start").Get("found_open_space").Boolean();
for (const VFJson::Value& SampleJson : Root.Get("density_samples").AsArray())
{
FStandaloneParitySample Sample;
ReadField(SampleJson,"x",Sample.PositionVoxels.X);
ReadField(SampleJson,"y",Sample.PositionVoxels.Y);
ReadField(SampleJson,"z",Sample.PositionVoxels.Z);
ReadField(SampleJson,"density",Sample.PluginDensity);
ReadField(SampleJson,"strate_index",Sample.StrateIndex);
Sample.Kind = SampleJson.Get("kind").String();
Out.DensitySamples.push_back(std::move(Sample));
}
Out.Settings = std::make_unique<UVoxelSettings>();
const VFJson::Value& SettingsJson = Root.Get("settings_asset");
Out.Settings->Seed = Out.Seed;
ReadField(Root,"origin_spine_radius",Out.Settings->OriginSpineRadius);
ReadField(Root,"world_radius_voxels",Out.Settings->WorldRadiusVoxels);
ReadField(Root,"edge_seal_thickness",Out.Settings->EdgeSealThickness);
ReadField(Root,"open_surface_entry",Out.Settings->bOpenSurfaceEntry);
ReadField(Root,"inter_strate_gap_chunks",Out.Settings->InterStrateGapChunks);
ReadField(SettingsJson,"total_strates",Out.Settings->TotalStrates);
ReadField(Root,"clip_radius",Out.Settings->ClipRadius);
ReadField(Root,"max_clip_level",Out.Settings->MaxClipLevel);
ReadField(Root,"full_res_clip_levels",Out.Settings->FullResClipLevels);
ReadField(Root,"coarse_tile_cells",Out.Settings->CoarseTileCells);
ReadField(Root,"render_distance_chunks",Out.Settings->RenderDistanceChunks);
ReadField(Root,"generate_skirts",Out.Settings->bGenerateSkirts);
ReadField(Root,"skirt_cells",Out.Settings->SkirtCells);
ReadField(Root,"lod_octave_drop",Out.Settings->LODOctaveDrop);
ReadField(Root,"strate_content_cut_min_level",Out.Settings->StrateContentCutMinLevel);
ReadField(SettingsJson,"TotalStrates",Out.Settings->TotalStrates);
ReadField(SettingsJson,"InterStrateGapChunks",Out.Settings->InterStrateGapChunks);
Out.Settings->StratePool.Reset();
Out.Settings->FixedStrates.Empty();
const auto& Strates = Root.Get("strates").AsArray();
Out.Settings->TotalStrates = static_cast<int32>(Strates.size());
for (int32 I = 0; I < static_cast<int32>(Strates.size()); ++I)
{
UVoxelStrateDefinition* Definition = ReadDefinition(Out,Strates[static_cast<size_t>(I)],I);
const FString SyntheticPath = FString::Printf("/Standalone/Strate_%d",I);
TSoftObjectPtr<UVoxelStrateDefinition> SoftDefinition;
SoftDefinition.Set(Definition,SyntheticPath);
Out.Settings->FixedStrates.Add(I,std::move(SoftDefinition));
}
if (Strates.empty()) { Error = "owner JSON contains no resolved strates"; return false; }
Out.Manager = std::make_unique<UVoxelStrateManager>();
if (!Out.Manager->Initialize(Out.Settings.get(),Out.Seed))
{
Error = "copied strate manager rejected the owner settings snapshot";
return false;
}
Out.Generator = std::make_unique<UVoxelGenerator>();
Out.Generator->InitializeSettings(Out.Settings.get());
Out.Generator->SetStrateManager(Out.Manager.get());
return true;
}
}
@@ -0,0 +1,128 @@
#pragma once
#include <atomic>
// Standalone-only port of the research field's round, winding tunnel profile.
// The cubic centerline, variable radius and elliptical vertical section follow
// GeneratedTunnelEdgeFromColumnProfile in VoxelRedefinedDensityField.cpp.
namespace FStandaloneResearchTunnels
{
inline std::atomic<bool> Enabled{false};
inline std::atomic<float> CenterZVoxels{0.0f};
inline uint32 Mix(uint32 X)
{
X ^= X >> 16;
X *= 0x7FEB352Du;
X ^= X >> 15;
X *= 0x846CA68Bu;
X ^= X >> 16;
return X;
}
inline uint32 EdgeHash(int32 X, int32 Y, int32 Axis, uint32 Seed)
{
uint32 H = Seed ^ 0x9E3779B9u;
H = Mix(H ^ static_cast<uint32>(X) * 0x85EBCA6Bu);
H = Mix(H ^ static_cast<uint32>(Y) * 0xC2B2AE35u);
return Mix(H ^ static_cast<uint32>(Axis) * 0x27D4EB2Fu);
}
inline float Unit(uint32 X)
{
return static_cast<float>(Mix(X) >> 8) * (1.0f / 16777216.0f);
}
inline void Set(bool bEnabled, float CenterZ)
{
CenterZVoxels.store(CenterZ,std::memory_order_relaxed);
Enabled.store(bEnabled,std::memory_order_release);
}
inline float Apply(float BaseDensity, float X, float Y, float Z, uint32 Seed)
{
if (!Enabled.load(std::memory_order_acquire)) return BaseDensity;
const float CenterZ = CenterZVoxels.load(std::memory_order_relaxed);
if (FMath::Abs(Z-CenterZ) > 36.0f) return BaseDensity;
constexpr float Pitch = 144.0f;
constexpr float RadiusBase = 28.0f;
constexpr float HalfHeight = 26.0f;
const int32 BaseCellX = FMath::FloorToInt(X/Pitch);
const int32 BaseCellY = FMath::FloorToInt(Y/Pitch);
float Best = -FLT_MAX;
bool bHasEdge = false;
const auto CellCenter = [&](int32 CX, int32 CY, float& OutX, float& OutY)
{
const uint32 H = EdgeHash(CX,CY,3,Seed);
OutX = (static_cast<float>(CX)+0.5f)*Pitch
+ (Unit(H^0xA511E9B3u)-0.5f)*Pitch*0.18f;
OutY = (static_cast<float>(CY)+0.5f)*Pitch
+ (Unit(H^0x63D83595u)-0.5f)*Pitch*0.18f;
};
const auto EvaluateEdge = [&](int32 CX, int32 CY, int32 Axis)
{
const uint32 H = EdgeHash(CX,CY,Axis,Seed);
if (Unit(H^0x68E31DA4u) > 0.56f) return;
bHasEdge = true;
const int32 NX = CX + (Axis == 0 ? 1 : 0);
const int32 NY = CY + (Axis == 1 ? 1 : 0);
float AX,AY,BX,BY;
CellCenter(CX,CY,AX,AY);
CellCenter(NX,NY,BX,BY);
const float ABX = BX-AX, ABY = BY-AY;
const float LengthSquared = ABX*ABX+ABY*ABY;
float T = LengthSquared > 1.0e-8f
? FMath::Clamp(((X-AX)*ABX+(Y-AY)*ABY)/LengthSquared,0.0f,1.0f)
: 0.0f;
const float Length = FMath::Sqrt(LengthSquared);
const float NXrm = Length > 1.0e-4f ? -ABY/Length : 0.0f;
const float NYrm = Length > 1.0e-4f ? ABX/Length : 0.0f;
const float BendA = (Unit(H^0xB5297A4Du)*2.0f-1.0f)*Pitch*0.16f;
const float BendB = (Unit(H^0x1B56C4E9u)*2.0f-1.0f)*Pitch*0.16f;
const float C1X=AX+ABX/3.0f+NXrm*BendA, C1Y=AY+ABY/3.0f+NYrm*BendA;
const float C2X=AX+ABX*(2.0f/3.0f)+NXrm*BendB;
const float C2Y=AY+ABY*(2.0f/3.0f)+NYrm*BendB;
for (int32 Iteration=0;Iteration<4;++Iteration)
{
const float U=T, O=1.0f-U;
const float PX=O*O*O*AX+3.0f*O*O*U*C1X+3.0f*O*U*U*C2X+U*U*U*BX;
const float PY=O*O*O*AY+3.0f*O*O*U*C1Y+3.0f*O*U*U*C2Y+U*U*U*BY;
const float DX=3.0f*O*O*(C1X-AX)+6.0f*O*U*(C2X-C1X)+3.0f*U*U*(BX-C2X);
const float DY=3.0f*O*O*(C1Y-AY)+6.0f*O*U*(C2Y-C1Y)+3.0f*U*U*(BY-C2Y);
const float DDX=6.0f*O*(C2X-2.0f*C1X+AX)+6.0f*U*(BX-2.0f*C2X+C1X);
const float DDY=6.0f*O*(C2Y-2.0f*C1Y+AY)+6.0f*U*(BY-2.0f*C2Y+C1Y);
const float Den=DX*DX+DY*DY+(PX-X)*DDX+(PY-Y)*DDY;
if (FMath::Abs(Den)>1.0e-4f)
T=FMath::Clamp(U-((PX-X)*DX+(PY-Y)*DY)/Den,0.0f,1.0f);
}
const float O=1.0f-T;
const float PX=O*O*O*AX+3.0f*O*O*T*C1X+3.0f*O*T*T*C2X+T*T*T*BX;
const float PY=O*O*O*AY+3.0f*O*O*T*C1Y+3.0f*O*T*T*C2Y+T*T*T*BY;
const float DX=X-PX,DY=Y-PY;
const float Distance=FMath::Sqrt(DX*DX+DY*DY);
const float Phase=Unit(H^0xA511E9B3u)*6.2831853071795864769f;
const float Radius=RadiusBase*(0.82f+0.36f*Unit(H^0x63D83595u))
*(1.0f+0.22f*FMath::Sin(6.2831853071795864769f*T+Phase));
const float VerticalWave=4.0f*FMath::Sin(3.14159265358979323846f*T)
*FMath::Sin(6.2831853071795864769f*T+Phase);
const float VerticalOffset=Z-(CenterZ+VerticalWave);
const float HorizontalDistance=FMath::Max(0.0f,Distance);
const float Normalized=FMath::Sqrt(
FMath::Square(HorizontalDistance/FMath::Max(Radius,1.0f))
+FMath::Square(VerticalOffset/FMath::Max(HalfHeight,1.0f)));
Best=FMath::Max(Best,(1.0f-Normalized)*FMath::Min(Radius,HalfHeight));
};
for (int32 CY=BaseCellY-1;CY<=BaseCellY+1;++CY)
for (int32 CX=BaseCellX-1;CX<=BaseCellX+1;++CX)
{
EvaluateEdge(CX,CY,0);
EvaluateEdge(CX,CY,1);
}
return bHasEdge ? FMath::Max(BaseDensity,Best) : BaseDensity;
}
}
+492
View File
@@ -0,0 +1,492 @@
#include "OwnerWorldLoader.h"
#include "WorldMesher.h"
#include "StandaloneResearchTunnels.h"
#ifdef TEXT
#undef TEXT
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <d3d11.h>
#include <d3dcompiler.h>
#include <dxgi.h>
#include <windowsx.h>
#include <bit>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <map>
#include <sstream>
#pragma comment(lib,"d3d11.lib")
#pragma comment(lib,"dxgi.lib")
#pragma comment(lib,"d3dcompiler.lib")
#pragma comment(lib,"user32.lib")
#pragma comment(lib,"gdi32.lib")
namespace
{
struct V3 { float X=0.0f,Y=0.0f,Z=0.0f; };
V3 Add(V3 A,V3 B){return {A.X+B.X,A.Y+B.Y,A.Z+B.Z};}
V3 Sub(V3 A,V3 B){return {A.X-B.X,A.Y-B.Y,A.Z-B.Z};}
V3 Mul(V3 A,float S){return {A.X*S,A.Y*S,A.Z*S};}
float Dot(V3 A,V3 B){return A.X*B.X+A.Y*B.Y+A.Z*B.Z;}
V3 Cross(V3 A,V3 B){return {A.Y*B.Z-A.Z*B.Y,A.Z*B.X-A.X*B.Z,A.X*B.Y-A.Y*B.X};}
V3 Normalize(V3 A){return Mul(A,1.0f/std::sqrt(std::max(1.0e-20f,Dot(A,A))));}
struct M4 { float M[4][4]{}; };
M4 Identity(){M4 R{};for(int I=0;I<4;++I)R.M[I][I]=1.0f;return R;}
M4 Transpose(M4 A){M4 B{};for(int I=0;I<4;++I)for(int J=0;J<4;++J)B.M[I][J]=A.M[J][I];return B;}
M4 Perspective(float Fov,float Aspect,float NearZ,float FarZ)
{
M4 R{};const float Y=1.0f/std::tan(Fov*0.5f);
R.M[0][0]=Y/Aspect;R.M[1][1]=Y;R.M[2][2]=FarZ/(FarZ-NearZ);
R.M[2][3]=1.0f;R.M[3][2]=-NearZ*FarZ/(FarZ-NearZ);return R;
}
M4 View(V3 Position,V3 Forward,V3 Right,V3 Up)
{
M4 R=Identity();
R.M[0][0]=Right.X;R.M[1][0]=Right.Y;R.M[2][0]=Right.Z;R.M[3][0]=-Dot(Right,Position);
R.M[0][1]=Up.X;R.M[1][1]=Up.Y;R.M[2][1]=Up.Z;R.M[3][1]=-Dot(Up,Position);
R.M[0][2]=Forward.X;R.M[1][2]=Forward.Y;R.M[2][2]=Forward.Z;R.M[3][2]=-Dot(Forward,Position);
return R;
}
struct FGpuVertex { float Position[3]; float Normal[3]; };
struct FConstants
{
M4 ViewProjection;
float Camera[3]; float FogStart;
float Forward[3]; float FogEnd;
float LightDirection[3]; float Ambient;
float Padding[4];
};
constexpr int32 MaxLevels=8;
struct FLevelGpu
{
ID3D11Buffer* VertexBuffer=nullptr;
ID3D11Buffer* IndexBuffer=nullptr;
UINT IndexCount=0;
uint64 VertexCount=0;
double Seconds=0.0;
uint64 Tiles=0;
uint64 Samples=0;
bool Ready=false;
};
struct FReadyLevel { int32 Level=0; FStandaloneCpuLevel Mesh; };
struct FApp
{
HWND Window=nullptr;
ID3D11Device* Device=nullptr;
ID3D11DeviceContext* Context=nullptr;
IDXGISwapChain* SwapChain=nullptr;
ID3D11RenderTargetView* RenderTarget=nullptr;
ID3D11DepthStencilView* DepthView=nullptr;
ID3D11VertexShader* VertexShader=nullptr;
ID3D11PixelShader* PixelShader=nullptr;
ID3D11InputLayout* InputLayout=nullptr;
ID3D11Buffer* Constants=nullptr;
ID3D11RasterizerState* SolidRasterizer=nullptr;
ID3D11RasterizerState* WireRasterizer=nullptr;
FLevelGpu Levels[MaxLevels+1];
std::unique_ptr<FStandaloneOwnerWorld> World;
std::string DataPath;
std::mutex ReadyMutex;
std::vector<FReadyLevel> ReadyLevels;
std::thread GenerationThread;
std::atomic<bool> GenerationDone{false};
std::atomic<bool> CancelGeneration{false};
FStandaloneCpuClipmap LastClipmap;
bool bGenerationRunning=false;
bool bRebuildPending=false;
bool bParityChecked=false;
bool bRoundTunnels=false;
bool bWireframe=false;
bool bCaptured=false;
bool bShowLevel[MaxLevels+1]={true,true,true,true,true,true,true,true,true};
bool Keys[256]{};
int32 Seed=0;
int32 Width=1280,Height=800;
float Yaw=0.0f,Pitch=0.0f,MoveSpeed=8.0f;
V3 Position{};
POINT LastMouse{};
std::chrono::steady_clock::time_point PreviousFrame=std::chrono::steady_clock::now();
double FrameMs=0.0;
uint64 TotalVertices=0;
uint64 TotalIndices=0;
FIntVector BuildTargetChunk=FIntVector::ZeroValue;
~FApp(){
StopGeneration(true);ReleaseMeshes();
if(SolidRasterizer)SolidRasterizer->Release();if(WireRasterizer)WireRasterizer->Release();
if(Constants)Constants->Release();if(InputLayout)InputLayout->Release();
if(VertexShader)VertexShader->Release();if(PixelShader)PixelShader->Release();
if(RenderTarget)RenderTarget->Release();if(DepthView)DepthView->Release();
if(SwapChain)SwapChain->Release();if(Context)Context->Release();if(Device)Device->Release();
}
void ReleaseMeshes()
{
for(FLevelGpu& L:Levels){if(L.VertexBuffer)L.VertexBuffer->Release();if(L.IndexBuffer)L.IndexBuffer->Release();L={};}
TotalVertices=0;TotalIndices=0;
}
void StopGeneration(bool bCancel=false)
{
if(bCancel)CancelGeneration.store(true,std::memory_order_relaxed);
if(GenerationThread.joinable())GenerationThread.join();
bGenerationRunning=false;
}
V3 Forward() const
{return Normalize({std::sin(Yaw)*std::cos(Pitch),std::cos(Yaw)*std::cos(Pitch),std::sin(Pitch)});}
FIntVector CameraChunk() const
{return StandaloneWorldMesher::CameraChunk(FVector(Position.X*4.0,Position.Y*4.0,Position.Z*4.0));}
bool NeedsRecenter(const FIntVector& Center) const
{
const FIntVector Current=CameraChunk();
const int32 Threshold=FMath::Max(1,(World?World->ClipRadius:6)/2);
return FMath::Abs(Current.X-Center.X)>=Threshold
||FMath::Abs(Current.Y-Center.Y)>=Threshold
||FMath::Abs(Current.Z-Center.Z)>=Threshold;
}
bool MakeBuffer(ID3D11Buffer** Out,const void* Data,size_t Bytes,UINT Bind)
{
if(Bytes==0||Bytes>std::numeric_limits<UINT>::max())return false;
D3D11_BUFFER_DESC Desc{};Desc.ByteWidth=static_cast<UINT>(Bytes);
Desc.Usage=D3D11_USAGE_DEFAULT;Desc.BindFlags=Bind;
D3D11_SUBRESOURCE_DATA Initial{};Initial.pSysMem=Data;
return SUCCEEDED(Device->CreateBuffer(&Desc,&Initial,Out));
}
bool Resize(UINT NewWidth,UINT NewHeight)
{
if(!Device||NewWidth==0||NewHeight==0)return false;
Width=static_cast<int32>(NewWidth);Height=static_cast<int32>(NewHeight);
if(RenderTarget){RenderTarget->Release();RenderTarget=nullptr;}
if(DepthView){DepthView->Release();DepthView=nullptr;}
if(SwapChain->ResizeBuffers(0,NewWidth,NewHeight,DXGI_FORMAT_UNKNOWN,0)!=S_OK)return false;
ID3D11Texture2D* Back=nullptr;
if(FAILED(SwapChain->GetBuffer(0,IID_PPV_ARGS(&Back))))return false;
const HRESULT RtvResult=Device->CreateRenderTargetView(Back,nullptr,&RenderTarget);Back->Release();
if(FAILED(RtvResult))return false;
D3D11_TEXTURE2D_DESC DepthDesc{};DepthDesc.Width=NewWidth;DepthDesc.Height=NewHeight;
DepthDesc.MipLevels=1;DepthDesc.ArraySize=1;DepthDesc.Format=DXGI_FORMAT_D24_UNORM_S8_UINT;
DepthDesc.SampleDesc.Count=1;DepthDesc.Usage=D3D11_USAGE_DEFAULT;DepthDesc.BindFlags=D3D11_BIND_DEPTH_STENCIL;
ID3D11Texture2D* Depth=nullptr;
if(FAILED(Device->CreateTexture2D(&DepthDesc,nullptr,&Depth)))return false;
const HRESULT DsvResult=Device->CreateDepthStencilView(Depth,nullptr,&DepthView);Depth->Release();
return SUCCEEDED(DsvResult);
}
bool InitializeDevice()
{
DXGI_SWAP_CHAIN_DESC Desc{};Desc.BufferCount=1;Desc.BufferDesc.Format=DXGI_FORMAT_R8G8B8A8_UNORM;
Desc.BufferUsage=DXGI_USAGE_RENDER_TARGET_OUTPUT;Desc.OutputWindow=Window;Desc.SampleDesc.Count=1;
Desc.Windowed=TRUE;Desc.SwapEffect=DXGI_SWAP_EFFECT_DISCARD;
D3D_FEATURE_LEVEL Feature{};
if(FAILED(D3D11CreateDeviceAndSwapChain(nullptr,D3D_DRIVER_TYPE_HARDWARE,nullptr,0,nullptr,0,
D3D11_SDK_VERSION,&Desc,&SwapChain,&Device,&Feature,&Context)))return false;
const char* ShaderSource=R"(
cbuffer C:register(b0){float4x4 VP;float3 Camera;float FogStart;float3 Forward;float FogEnd;float3 LightDir;float Ambient;float4 Pad;}
struct I{float3 p:POSITION;float3 n:NORMAL;};
struct O{float4 p:SV_POSITION;float3 world:TEXCOORD0;float3 n:NORMAL;};
O VS(I i){O o;o.p=mul(float4(i.p,1),VP);o.world=i.p;o.n=normalize(i.n);return o;}
float4 PS(O i):SV_TARGET{
float3 N=normalize(i.n);float3 V=normalize(Camera-i.world);float3 L=normalize(-LightDir);
float Diffuse=saturate(dot(N,L));float Head=saturate(dot(N,V));
float Dist=length(Camera-i.world);float Atten=1.0-saturate(Dist/95.0);Head=Head*Head*Atten;
float3 Rock=lerp(float3(0.28,0.34,0.38),float3(0.43,0.47,0.48),saturate(N.z*0.5+0.5));
float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.62,0.76,0.9)*Head*1.25;
float Fog=saturate((Dist-FogStart)/(FogEnd-FogStart));
Lit=lerp(Lit,float3(0.10,0.14,0.19),Fog);return float4(Lit,1);
})";
ID3DBlob* VsCode=nullptr;ID3DBlob* PsCode=nullptr;ID3DBlob* Errors=nullptr;
HRESULT Hr=D3DCompile(ShaderSource,std::strlen(ShaderSource),nullptr,nullptr,nullptr,"VS","vs_5_0",0,0,&VsCode,&Errors);
if(FAILED(Hr)){if(Errors)std::fprintf(stderr,"Vertex shader compile: %s\n",static_cast<const char*>(Errors->GetBufferPointer()));if(Errors)Errors->Release();return false;}
Hr=D3DCompile(ShaderSource,std::strlen(ShaderSource),nullptr,nullptr,nullptr,"PS","ps_5_0",0,0,&PsCode,&Errors);
if(FAILED(Hr)){if(Errors)std::fprintf(stderr,"Pixel shader compile: %s\n",static_cast<const char*>(Errors->GetBufferPointer()));if(Errors)Errors->Release();VsCode->Release();return false;}
Hr=Device->CreateVertexShader(VsCode->GetBufferPointer(),VsCode->GetBufferSize(),nullptr,&VertexShader);
if(SUCCEEDED(Hr))Hr=Device->CreatePixelShader(PsCode->GetBufferPointer(),PsCode->GetBufferSize(),nullptr,&PixelShader);
D3D11_INPUT_ELEMENT_DESC Elements[]={{"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0},
{"NORMAL",0,DXGI_FORMAT_R32G32B32_FLOAT,0,12,D3D11_INPUT_PER_VERTEX_DATA,0}};
if(SUCCEEDED(Hr))Hr=Device->CreateInputLayout(Elements,2,VsCode->GetBufferPointer(),VsCode->GetBufferSize(),&InputLayout);
VsCode->Release();PsCode->Release();if(FAILED(Hr))return false;
D3D11_BUFFER_DESC ConstantDesc{};ConstantDesc.ByteWidth=sizeof(FConstants);ConstantDesc.Usage=D3D11_USAGE_DEFAULT;
ConstantDesc.BindFlags=D3D11_BIND_CONSTANT_BUFFER;
if(FAILED(Device->CreateBuffer(&ConstantDesc,nullptr,&Constants)))return false;
D3D11_RASTERIZER_DESC Raster{};Raster.FillMode=D3D11_FILL_SOLID;Raster.CullMode=D3D11_CULL_NONE;
Raster.DepthClipEnable=TRUE;if(FAILED(Device->CreateRasterizerState(&Raster,&SolidRasterizer)))return false;
Raster.FillMode=D3D11_FILL_WIREFRAME;if(FAILED(Device->CreateRasterizerState(&Raster,&WireRasterizer)))return false;
return Resize(Width,Height);
}
void PrintParity()
{
if(bParityChecked||!World)return;
bParityChecked=true;size_t MismatchCount=0;
std::map<std::string,size_t> Kinds;
for(size_t I=0;I<World->DensitySamples.size();++I)
{
const FStandaloneParitySample& S=World->DensitySamples[I];
const float Actual=World->Generator->GetDensityAt(static_cast<float>(S.PositionVoxels.X),
static_cast<float>(S.PositionVoxels.Y),static_cast<float>(S.PositionVoxels.Z));
++Kinds[S.Kind];
if(std::bit_cast<uint32>(Actual)!=std::bit_cast<uint32>(S.PluginDensity))
{
++MismatchCount;
std::fprintf(stderr,"PARITY MISMATCH %s strate=%d voxels=(%.6f,%.6f,%.6f) plugin=%.9g standalone=%.9g\n",
S.Kind.c_str(),S.StrateIndex,S.PositionVoxels.X,S.PositionVoxels.Y,S.PositionVoxels.Z,S.PluginDensity,Actual);
}
}
std::printf("Density parity: %zu/%zu bit-identical; mismatches=%zu\n",
World->DensitySamples.size()-MismatchCount,World->DensitySamples.size(),MismatchCount);
for(const auto& [Kind,Count]:Kinds)std::printf(" %-24s %zu\n",Kind.c_str(),Count);
std::fflush(stdout);
std::fflush(stdout);
}
bool ConfigureWorld(std::string& Error)
{
World=std::make_unique<FStandaloneOwnerWorld>();
if(!StandaloneOwnerLoader::Load(DataPath,*World,Error))return false;
if(!bParityChecked&&Seed==World->Seed&&!bRoundTunnels)PrintParity();
World->Generator.reset();World->Manager.reset();
World->Settings->Seed=Seed;World->Seed=Seed;
if(bRoundTunnels)
{
// The research tunnel graph replaces the owner's first-strate corridor graph.
for(auto& Pair:World->Settings->FixedStrates)
{
UVoxelStrateDefinition* Definition=Pair.Value.Get();
if(Pair.Key==0&&Definition&&Definition->GeneratorType==ECaveGeneratorType::TunnelNetwork)
{
Definition->GenerationParams.TunnelDensity=0.0f;
Definition->GenerationParams.TunnelWarpStrength=0.0f;
}
}
FStandaloneResearchTunnels::Set(true,Position.Z*4.0f);
}
else FStandaloneResearchTunnels::Set(false,0.0f);
World->Manager=std::make_unique<UVoxelStrateManager>();
if(!World->Manager->Initialize(World->Settings.get(),Seed))
{Error="standalone copied strate manager failed while applying viewer seed";return false;}
World->Generator=std::make_unique<UVoxelGenerator>();
World->Generator->InitializeSettings(World->Settings.get());
World->Generator->SetStrateManager(World->Manager.get());
return true;
}
void StartGeneration()
{
StopGeneration();
ReleaseMeshes();
std::string Error;
if(!ConfigureWorld(Error))
{std::fprintf(stderr,"World load failed: %s\n",Error.c_str());MessageBoxA(Window,Error.c_str(),"VoxelForge viewer",MB_ICONERROR);return;}
const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
BuildTargetChunk=StandaloneWorldMesher::CameraChunk(CameraVoxels);
ReadyLevels.clear();CancelGeneration.store(false,std::memory_order_relaxed);
GenerationDone.store(false,std::memory_order_relaxed);bGenerationRunning=true;bRebuildPending=false;
std::printf("Building seed %d%s full clipmap (six workers)...\n",Seed,bRoundTunnels?" with research round/winding tunnels":"");
GenerationThread=std::thread([this,CameraVoxels]()
{
LastClipmap=StandaloneWorldMesher::Build(*World,CameraVoxels,6,
[this](int32 Level,FStandaloneCpuLevel&& Mesh)
{
std::lock_guard<std::mutex> Lock(ReadyMutex);
ReadyLevels.push_back({Level,std::move(Mesh)});
},false,&CancelGeneration);
GenerationDone.store(true,std::memory_order_release);
});
}
void RequestSeed(int32 Delta)
{
const uint32 Next=static_cast<uint32>(Seed)+static_cast<uint32>(Delta);
Seed=static_cast<int32>(Next);bRebuildPending=true;
CancelGeneration.store(true,std::memory_order_relaxed);
std::printf("Seed changed to %d; rebuilding after the active clipmap finishes.\n",Seed);
}
bool UploadLevel(int32 Index,FStandaloneCpuLevel&& Mesh)
{
FLevelGpu& L=Levels[Index];
if(L.VertexBuffer)L.VertexBuffer->Release();if(L.IndexBuffer)L.IndexBuffer->Release();L={};
L.Seconds=Mesh.Seconds;L.Tiles=Mesh.TileCount;L.Samples=Mesh.DensitySamples;
L.VertexCount=Mesh.VertexCount;L.IndexCount=static_cast<uint64>(Mesh.Indices.size());
if(Mesh.Vertices.empty()||Mesh.Indices.empty()){L.Ready=true;return true;}
if(Mesh.Indices.size()>std::numeric_limits<UINT>::max())
{std::fprintf(stderr,"LOD %d has too many indices for one D3D11 draw.\n",Index);return false;}
const bool bVertex=MakeBuffer(&L.VertexBuffer,Mesh.Vertices.data(),Mesh.Vertices.size()*sizeof(FStandaloneCpuVertex),D3D11_BIND_VERTEX_BUFFER);
const bool bIndex=bVertex&&MakeBuffer(&L.IndexBuffer,Mesh.Indices.data(),Mesh.Indices.size()*sizeof(uint32),D3D11_BIND_INDEX_BUFFER);
if(!bIndex){if(L.VertexBuffer){L.VertexBuffer->Release();L.VertexBuffer=nullptr;}std::fprintf(stderr,"D3D11 buffer upload failed for LOD %d.\n",Index);return false;}
L.IndexCount=static_cast<UINT>(Mesh.Indices.size());L.Ready=true;
TotalVertices+=L.VertexCount;TotalIndices+=L.IndexCount;
std::printf("LOD %d uploaded: %llu vertices, %.3f s, %llu tiles.\n",Index,
static_cast<unsigned long long>(L.VertexCount),L.Seconds,static_cast<unsigned long long>(L.Tiles));
return true;
}
void PumpGeneration()
{
std::vector<FReadyLevel> Ready;
{std::lock_guard<std::mutex> Lock(ReadyMutex);Ready.swap(ReadyLevels);}
for(FReadyLevel& R:Ready)UploadLevel(R.Level,std::move(R.Mesh));
if(bGenerationRunning&&GenerationDone.load(std::memory_order_acquire))
{
StopGeneration();
if(!LastClipmap.bCancelled)
std::printf("Full clipmap ready: %.3f s on %d workers; %llu vertices, %llu indices.\n",
LastClipmap.TotalSeconds,LastClipmap.WorkerCount,
static_cast<unsigned long long>(TotalVertices),static_cast<unsigned long long>(TotalIndices));
for(int32 L=0;L<=World->MaxClipLevel;++L)
std::printf(" LOD %d: %.3f s, %llu tiles, %llu vertices, %.2f M density samples\n",L,
LastClipmap.Levels[L].Seconds,static_cast<unsigned long long>(LastClipmap.Levels[L].TileCount),
static_cast<unsigned long long>(LastClipmap.Levels[L].VertexCount),
static_cast<double>(LastClipmap.Levels[L].DensitySamples)/1.0e6);
std::fflush(stdout);
if(bRebuildPending)StartGeneration();
}
else if(!bGenerationRunning&&bRebuildPending)StartGeneration();
}
void Capture(bool bOn)
{
bCaptured=bOn;
if(bOn){SetCapture(Window);ShowCursor(FALSE);GetCursorPos(&LastMouse);POINT C{Width/2,Height/2};ClientToScreen(Window,&C);SetCursorPos(C.x,C.y);}
else{ReleaseCapture();ShowCursor(TRUE);}
}
void UpdateTitle()
{
uint64 VisibleIndices=0;for(int32 I=0;I<=MaxLevels;++I)if(bShowLevel[I])VisibleIndices+=Levels[I].IndexCount;
std::ostringstream S;S<<"VoxelForge standalone | seed "<<Seed<<" | "<<(bRoundTunnels?"round/winding research tunnels":"base world")
<<" | "<<VisibleIndices/3<<" tris | "<<FrameMs<<" ms | "<<(bGenerationRunning?"generating":"ready")
<<" | pos "<<std::fixed<<std::setprecision(1)<<Position.X<<", "<<Position.Y<<", "<<Position.Z<<" m";
SetWindowTextA(Window,S.str().c_str());
}
void Draw()
{
PumpGeneration();
const auto Now=std::chrono::steady_clock::now();
const float Dt=std::clamp(std::chrono::duration<float>(Now-PreviousFrame).count(),0.0f,0.1f);
PreviousFrame=Now;FrameMs=Dt*1000.0;
const V3 F=Forward(),R=Normalize(Cross(F,{0,0,1})),U=Normalize(Cross(R,F));
const float Step=MoveSpeed*Dt*(Keys[VK_SHIFT]?4.0f:1.0f);
if(Keys['W'])Position=Add(Position,Mul(F,Step));if(Keys['S'])Position=Sub(Position,Mul(F,Step));
if(Keys['A'])Position=Sub(Position,Mul(R,Step));if(Keys['D'])Position=Add(Position,Mul(R,Step));
if(Keys[VK_SPACE]||Keys['E'])Position.Z+=Step;if(Keys[VK_CONTROL]||Keys['Q'])Position.Z-=Step;
if(bGenerationRunning&&NeedsRecenter(BuildTargetChunk))
{bRebuildPending=true;CancelGeneration.store(true,std::memory_order_relaxed);}
else if(!bGenerationRunning&&World&&NeedsRecenter(LastClipmap.CenterChunk))
{bRebuildPending=true;}
const float Clear[4]={0.055f,0.075f,0.105f,1.0f};
Context->OMSetRenderTargets(1,&RenderTarget,DepthView);Context->ClearRenderTargetView(RenderTarget,Clear);
Context->ClearDepthStencilView(DepthView,D3D11_CLEAR_DEPTH,1.0f,0);
D3D11_VIEWPORT Viewport{0,0,static_cast<float>(Width),static_cast<float>(Height),0,1};Context->RSSetViewports(1,&Viewport);
Context->RSSetState(bWireframe?WireRasterizer:SolidRasterizer);Context->IASetInputLayout(InputLayout);
Context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
FConstants C{};const M4 V=View(Position,F,R,U);const M4 P=Perspective(1.04719755f,static_cast<float>(Width)/std::max(1,Height),0.05f,5000.0f);
M4 Combined{};for(int I=0;I<4;++I)for(int J=0;J<4;++J)for(int K=0;K<4;++K)Combined.M[I][J]+=V.M[I][K]*P.M[K][J];
C.ViewProjection=Transpose(Combined);C.Camera[0]=Position.X;C.Camera[1]=Position.Y;C.Camera[2]=Position.Z;
C.Forward[0]=F.X;C.Forward[1]=F.Y;C.Forward[2]=F.Z;C.LightDirection[0]=-0.5f;C.LightDirection[1]=-0.72f;C.LightDirection[2]=-0.48f;
C.FogStart=260.0f;C.FogEnd=2600.0f;C.Ambient=0.36f;
Context->UpdateSubresource(Constants,0,nullptr,&C,0,0);Context->VSSetConstantBuffers(0,1,&Constants);Context->PSSetConstantBuffers(0,1,&Constants);
Context->VSSetShader(VertexShader,nullptr,0);Context->PSSetShader(PixelShader,nullptr,0);
for(int32 I=0;I<=MaxLevels;++I)
{
const FLevelGpu& L=Levels[I];if(!bShowLevel[I]||!L.VertexBuffer||!L.IndexBuffer||!L.IndexCount)continue;
const UINT Stride=sizeof(FGpuVertex),Offset=0;Context->IASetVertexBuffers(0,1,&L.VertexBuffer,&Stride,&Offset);
Context->IASetIndexBuffer(L.IndexBuffer,DXGI_FORMAT_R32_UINT,0);Context->DrawIndexed(L.IndexCount,0,0);
}
SwapChain->Present(1,0);UpdateTitle();
}
};
FApp* GApp=nullptr;
LRESULT CALLBACK WindowProc(HWND H,UINT Message,WPARAM W,LPARAM L)
{
if(!GApp)return DefWindowProc(H,Message,W,L);
switch(Message)
{
case WM_SIZE:if(W!=SIZE_MINIMIZED)GApp->Resize(LOWORD(L),HIWORD(L));return 0;
case WM_KEYDOWN:
{
const bool bWasDown=(L&(1ll<<30))!=0;if(W<256)GApp->Keys[W]=true;
if(W==VK_ESCAPE){if(GApp->bCaptured)GApp->Capture(false);else DestroyWindow(H);}
if(!bWasDown&&W=='F')GApp->bWireframe=!GApp->bWireframe;
if(!bWasDown&&W=='T'){GApp->bRoundTunnels=!GApp->bRoundTunnels;GApp->bRebuildPending=true;GApp->CancelGeneration.store(true,std::memory_order_relaxed);}
if(!bWasDown&&(W=='N'||W=='P'))GApp->RequestSeed(W=='N'?1:-1);
if(!bWasDown&&W>='1'&&W<='8'){const int32 I=static_cast<int32>(W-'1');GApp->bShowLevel[I]=!GApp->bShowLevel[I];}
if(!bWasDown&&W=='9')GApp->bShowLevel[8]=!GApp->bShowLevel[8];
if(!bWasDown&&W=='0')for(bool& B:GApp->bShowLevel)B=!B;
return 0;
}
case WM_KEYUP:if(W<256)GApp->Keys[W]=false;return 0;
case WM_RBUTTONDOWN:GApp->Capture(true);return 0;
case WM_RBUTTONUP:GApp->Capture(false);return 0;
case WM_MOUSEMOVE:
if(GApp->bCaptured){POINT C{GApp->Width/2,GApp->Height/2};ClientToScreen(H,&C);POINT P{GET_X_LPARAM(L),GET_Y_LPARAM(L)},Screen=P;ClientToScreen(H,&Screen);const int32 DX=Screen.x-C.x,DY=Screen.y-C.y;if(DX||DY){GApp->Yaw+=DX*0.0035f;GApp->Pitch=std::clamp(GApp->Pitch-DY*0.0035f,-1.55f,1.55f);SetCursorPos(C.x,C.y);}}return 0;
case WM_MOUSEWHEEL:GApp->MoveSpeed=std::clamp(GApp->MoveSpeed*(GET_WHEEL_DELTA_WPARAM(W)>0?1.25f:0.8f),0.25f,1000.0f);return 0;
case WM_DESTROY:GApp->CancelGeneration.store(true,std::memory_order_relaxed);PostQuitMessage(0);return 0;
}
return DefWindowProc(H,Message,W,L);
}
std::filesystem::path DefaultDataPath()
{
wchar_t Module[MAX_PATH]{};GetModuleFileNameW(nullptr,Module,MAX_PATH);
const std::filesystem::path Exe(Module);
return Exe.parent_path().parent_path()/L"data"/L"owner_world.json";
}
}
int main(int Argc,char** Argv)
{
FApp App;App.DataPath=Argc>1?Argv[1]:DefaultDataPath().string();
std::string Error;
App.World=std::make_unique<FStandaloneOwnerWorld>();
if(!StandaloneOwnerLoader::Load(App.DataPath,*App.World,Error)){std::fprintf(stderr,"Owner data load failed: %s\n",Error.c_str());return 2;}
App.Seed=App.World->Seed;
App.Position={static_cast<float>(App.World->ViewerStartVoxels.X*0.25),
static_cast<float>(App.World->ViewerStartVoxels.Y*0.25),static_cast<float>(App.World->ViewerStartVoxels.Z*0.25)};
App.World.reset();
WNDCLASSA Class{};Class.lpfnWndProc=WindowProc;Class.hInstance=GetModuleHandleA(nullptr);Class.lpszClassName="VoxelForgeStandaloneWorld";
Class.hCursor=LoadCursor(nullptr,IDC_ARROW);RegisterClassA(&Class);
RECT Rect{0,0,App.Width,App.Height};AdjustWindowRect(&Rect,WS_OVERLAPPEDWINDOW,FALSE);
App.Window=CreateWindowA(Class.lpszClassName,"VoxelForge standalone world",WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,
Rect.right-Rect.left,Rect.bottom-Rect.top,nullptr,nullptr,Class.hInstance,nullptr);
GApp=&App;
if(!App.Window||!App.InitializeDevice()){MessageBoxA(nullptr,"Direct3D 11 initialization failed.","VoxelForge standalone",MB_ICONERROR);return 3;}
ShowWindow(App.Window,SW_SHOW);UpdateWindow(App.Window);
std::printf("Controls: hold right mouse to look; WASD move; Space/Ctrl (E/Q) up/down; Shift faster; wheel changes speed; F wireframe; 1-9 toggle LOD; 0 toggle all levels; N/P change seed; T swap to round/winding research tunnels; Esc quit.\n");
std::printf("Owner data: %s; starting at first-strate open-space sample (%.2f, %.2f, %.2f) m.\n",
App.DataPath.c_str(),App.Position.X,App.Position.Y,App.Position.Z);
std::fflush(stdout);
App.StartGeneration();
MSG Message{};
while(Message.message!=WM_QUIT)
{
if(PeekMessage(&Message,nullptr,0,0,PM_REMOVE)){TranslateMessage(&Message);DispatchMessage(&Message);}
else App.Draw();
}
GApp=nullptr;
return 0;
}
@@ -0,0 +1,5 @@
// VoxelBiomeDefinition.cpp
// All data lives in the header; this TU exists so UHT generates the asset's body and
// the module has an object file for it.
#include "VoxelBiomeDefinition.h"
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,152 @@
#pragma once
#include "VoxelTypes.h"
namespace VoxelClipmapDesiredTiles
{
struct FParameters
{
int32 ClipRadius = 3;
int32 MaxClipLevel = 4;
int32 RenderDistanceChunks = 0;
};
FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor)
{
return Value >= 0 ? (Value / Divisor) : -(((-Value) + Divisor - 1) / Divisor);
}
FORCEINLINE FIntVector FloorDiv(const FIntVector& Value, int32 Divisor)
{
return FIntVector(FloorDiv(Value.X, Divisor), FloorDiv(Value.Y, Divisor),
FloorDiv(Value.Z, Divisor));
}
FORCEINLINE int32 CeilDiv(int32 Value, int32 Divisor)
{
return -FloorDiv(-Value, Divisor);
}
FORCEINLINE void AlignedRingBounds(const FIntVector& Center, int32 Radius, int32 Alignment,
FIntVector& OutMin, FIntVector& OutMax)
{
const auto AlignAxis = [Radius, Alignment](int32 CenterAxis,
int32& OutAxisMin, int32& OutAxisMax)
{
const int32 BaseMin = CenterAxis - Radius;
const int32 BaseEnd = CenterAxis + Radius + 1;
OutAxisMin = FloorDiv(BaseMin, Alignment) * Alignment;
OutAxisMax = CeilDiv(BaseEnd, Alignment) * Alignment - 1;
};
AlignAxis(Center.X, OutMin.X, OutMax.X);
AlignAxis(Center.Y, OutMin.Y, OutMax.Y);
AlignAxis(Center.Z, OutMin.Z, OutMax.Z);
}
FORCEINLINE int32 OuterShellRadius(const FParameters& Parameters, int32 Radius, int32 MaxLevel)
{
if (Parameters.RenderDistanceChunks <= 0)
{
return Radius;
}
return FMath::Max(Radius,
(Parameters.RenderDistanceChunks + (1 << MaxLevel) - 1) >> MaxLevel);
}
FORCEINLINE void OuterShell(const FParameters& Parameters, int32 Radius, int32 MaxLevel,
int32& OutLevel, int32& OutRadius)
{
OutLevel = MaxLevel;
OutRadius = OuterShellRadius(Parameters, Radius, MaxLevel);
}
FORCEINLINE void ResolveVerticalBounds(
int32 CenterChunkZ, int32 ViewDistanceUp, int32 ViewDistanceDown,
int32 StrateViewMarginChunks, bool bClampToStrate, bool bHasStrate,
int32 StrateTopChunkZ, int32 StrateBottomChunkZ,
int32& OutMinChunkZ, int32& OutMaxChunkZ)
{
OutMinChunkZ = MIN_int32;
OutMaxChunkZ = MAX_int32;
if (!bClampToStrate)
{
return;
}
OutMinChunkZ = CenterChunkZ - ViewDistanceDown;
OutMaxChunkZ = CenterChunkZ + ViewDistanceUp;
if (bHasStrate)
{
OutMinChunkZ = StrateBottomChunkZ - StrateViewMarginChunks;
OutMaxChunkZ = StrateTopChunkZ + StrateViewMarginChunks;
}
}
FORCEINLINE void Build(const FIntVector& Center, int32 MinChunkZ, int32 MaxChunkZ,
const FParameters& Parameters, TArray<FVoxelTileKey>& OutTiles)
{
OutTiles.Reset();
const int32 Radius = FMath::Max(1, Parameters.ClipRadius);
const int32 MaxLevel = FMath::Clamp(Parameters.MaxClipLevel, 0, 8);
const int32 OuterRadius = OuterShellRadius(Parameters, Radius, MaxLevel);
FIntVector PreviousRingMin = FIntVector::ZeroValue;
FIntVector PreviousRingMax = FIntVector::ZeroValue;
for (int32 Level = 0; Level <= MaxLevel; ++Level)
{
const int32 Pow = 1 << Level;
const FIntVector CenterLevel = FloorDiv(Center, Pow);
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
// Every finer shell must end on a complete child-pair boundary so a coarser tile is
// either wholly inside it or wholly outside it. The outermost ring has no coarser
// neighbour and needs no alignment.
const int32 RingAlignment = Level < MaxLevel ? 2 : 1;
FIntVector RingMin, RingMax;
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
int32 DeltaZMin = RingMin.Z - CenterLevel.Z;
int32 DeltaZMax = RingMax.Z - CenterLevel.Z;
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
{
DeltaZMin = FMath::Max(DeltaZMin, FloorDiv(MinChunkZ, Pow) - CenterLevel.Z);
DeltaZMax = FMath::Min(DeltaZMax, FloorDiv(MaxChunkZ, Pow) - CenterLevel.Z);
}
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
for (int32 DeltaY = RingMin.Y - CenterLevel.Y;
DeltaY <= RingMax.Y - CenterLevel.Y; ++DeltaY)
for (int32 DeltaX = RingMin.X - CenterLevel.X;
DeltaX <= RingMax.X - CenterLevel.X; ++DeltaX)
{
const FIntVector Tile = CenterLevel + FIntVector(DeltaX, DeltaY, DeltaZ);
const int32 TileMinZ = Tile.Z << Level;
const int32 TileMaxZ = ((Tile.Z + 1) << Level) - 1;
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
{
continue;
}
if (Level > 0)
{
const bool bCovered =
(2 * Tile.X >= PreviousRingMin.X)
&& (2 * Tile.X + 1 <= PreviousRingMax.X)
&& (2 * Tile.Y >= PreviousRingMin.Y)
&& (2 * Tile.Y + 1 <= PreviousRingMax.Y)
&& (2 * Tile.Z >= PreviousRingMin.Z)
&& (2 * Tile.Z + 1 <= PreviousRingMax.Z);
if (bCovered)
{
continue;
}
}
OutTiles.Emplace(Tile, Level);
}
PreviousRingMin = RingMin;
PreviousRingMax = RingMax;
}
}
}
@@ -0,0 +1,131 @@
// VoxelDensityAblation.cpp
// Development-only TunnelNetwork ablation controls.
#include "VoxelDensityAblation.h"
#include "HAL/IConsoleManager.h"
#include "Misc/CommandLine.h"
#include "Misc/Parse.h"
#include <atomic>
namespace
{
constexpr uint32 UnresolvedMask = 0xFFFFFFFFu;
std::atomic<uint32> GResolvedMask(UnresolvedMask);
#if !UE_BUILD_SHIPPING
int32 GCaveWarp = 0;
int32 GDetailOps = 0;
int32 GRoomSDF = 0;
int32 GTunnelSDF = 0;
int32 GTunnelCore = 0;
int32 GPassageCarving = 0;
int32 GPassageStructuralPosts = 0;
int32 GNativeFloor = 0;
int32 GDisturbances = 0;
int32 GOriginSpine = 0;
int32 GBoundarySeal = 0;
int32 GLandingPosts = 0;
int32 GXYEdgeSeal = 0;
int32 GPitChimneySDF = 0;
#define VF_ABLATION_CVAR(Name, Storage) \
FAutoConsoleVariableRef CVar##Storage( \
Name, Storage, \
TEXT("WORLD-CHANGING DEVELOPMENT-ONLY measurement override. Forces this " \
"TunnelNetwork density stage off; never use as a gameplay switch. " \
"All peers and regenerated worlds must use the same value. Resolved once " \
"per process. 0=on, 1=off."), ECVF_Default)
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateCaveWarp"), GCaveWarp);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateDetailOps"), GDetailOps);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateRoomSDF"), GRoomSDF);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateTunnelSDF"), GTunnelSDF);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateTunnelCore"), GTunnelCore);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblatePassageCarving"), GPassageCarving);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblatePassageStructuralPosts"), GPassageStructuralPosts);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateNativeFloor"), GNativeFloor);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateDisturbances"), GDisturbances);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateOriginSpine"), GOriginSpine);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateBoundarySeal"), GBoundarySeal);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateLandingPosts"), GLandingPosts);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateXYEdgeSeal"), GXYEdgeSeal);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblatePitChimneySDF"), GPitChimneySDF);
#undef VF_ABLATION_CVAR
uint32 ResolveValue(const TCHAR* Name, int32& Storage)
{
int32 Value = Storage;
FParse::Value(FCommandLine::Get(), Name, Value);
Storage = Value != 0 ? 1 : 0;
return Storage != 0 ? 1u : 0u;
}
uint32 ResolveMask()
{
uint32 Mask = 0;
Mask |= ResolveValue(TEXT("voxel.TunnelAblateCaveWarp="), GCaveWarp)
* static_cast<uint32>(VoxelDensityAblation::EStage::CaveWarp);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateDetailOps="), GDetailOps)
* static_cast<uint32>(VoxelDensityAblation::EStage::DetailOps);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateRoomSDF="), GRoomSDF)
* static_cast<uint32>(VoxelDensityAblation::EStage::RoomSDF);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateTunnelSDF="), GTunnelSDF)
* static_cast<uint32>(VoxelDensityAblation::EStage::TunnelSDF);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateTunnelCore="), GTunnelCore)
* static_cast<uint32>(VoxelDensityAblation::EStage::TunnelCore);
Mask |= ResolveValue(TEXT("voxel.TunnelAblatePassageCarving="), GPassageCarving)
* static_cast<uint32>(VoxelDensityAblation::EStage::PassageCarving);
Mask |= ResolveValue(
TEXT("voxel.TunnelAblatePassageStructuralPosts="), GPassageStructuralPosts)
* static_cast<uint32>(VoxelDensityAblation::EStage::PassageStructuralPosts);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateNativeFloor="), GNativeFloor)
* static_cast<uint32>(VoxelDensityAblation::EStage::NativeFloor);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateDisturbances="), GDisturbances)
* static_cast<uint32>(VoxelDensityAblation::EStage::Disturbances);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateOriginSpine="), GOriginSpine)
* static_cast<uint32>(VoxelDensityAblation::EStage::OriginSpine);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateBoundarySeal="), GBoundarySeal)
* static_cast<uint32>(VoxelDensityAblation::EStage::BoundarySeal);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateLandingPosts="), GLandingPosts)
* static_cast<uint32>(VoxelDensityAblation::EStage::LandingPosts);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateXYEdgeSeal="), GXYEdgeSeal)
* static_cast<uint32>(VoxelDensityAblation::EStage::XYEdgeSeal);
Mask |= ResolveValue(TEXT("voxel.TunnelAblatePitChimneySDF="), GPitChimneySDF)
* static_cast<uint32>(VoxelDensityAblation::EStage::PitChimneySDF);
return Mask;
}
#endif
}
namespace VoxelDensityAblation
{
uint32 GetResolvedMask()
{
uint32 Existing = GResolvedMask.load(std::memory_order_acquire);
if (Existing != UnresolvedMask)
{
return Existing;
}
#if UE_BUILD_SHIPPING
constexpr uint32 Resolved = 0;
#else
const uint32 Resolved = ResolveMask();
#endif
uint32 Expected = UnresolvedMask;
if (!GResolvedMask.compare_exchange_strong(
Expected, Resolved, std::memory_order_release, std::memory_order_acquire))
{
return Expected;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeTunnelAblation] resolved mask=0x%08x "
"(world-changing development measurement overrides; 0=all on)"),
Resolved);
return Resolved;
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,994 @@
#include "VoxelDensityProfile.h"
#include "Misc/Crc.h"
namespace VoxelDensityProfile
{
std::atomic<bool> GEnabled(false);
std::atomic<EMode> GMode(EMode::Disabled);
namespace
{
std::atomic<uint32> GSampleInterval(DefaultSampleInterval);
std::atomic<uint64> GTimerPairCycles(0);
// The old implementation performed a contended atomic fetch_add for every timer and
// counter. A TunnelNetwork sample can close twenty-plus ranges and add several
// candidate counters, so profiling changed the workload it was supposed to measure.
// Keep one accumulator per worker and only aggregate at Snapshot(). Commandlet resets
// happen between completed jobs, so the deliberately light-weight list is sufficient.
struct FThreadState
{
uint64 Calls[BucketCount]{};
uint64 Cycles[BucketCount]{};
uint64 Samples[BucketCount]{};
uint64 Counters[CounterCount]{};
uint32 SampleSequence[BucketCount]{};
uint32 BlockCallCount[BucketCount]{};
uint64 BlockStartCycles[BucketCount]{};
bool BlockActive[BucketCount]{};
FSnapshot::FClassifierStats Classifier;
uint64 TunnelCacheCapacityEntries = 0;
uint64 TunnelCacheValidEntries = 0;
uint64 TunnelCacheStaticBytes = 0;
uint64 TunnelCacheDynamicBytes = 0;
uint64 TunnelCacheEntryBytes = 0;
uint64 TunnelCacheLargestEntryBytes = 0;
uint64 TunnelCacheLargestWorkerValidEntries = 0;
FCacheMemoryBreakdown TunnelCacheBreakdown;
uint64 RoomGraphCacheCapacityEntries = 0;
uint64 RoomGraphCacheValidEntries = 0;
uint64 RoomGraphCacheStaticBytes = 0;
uint64 RoomGraphCacheDynamicBytes = 0;
uint64 RoomGraphCacheEntryBytes = 0;
uint64 RoomGraphCacheLargestEntryBytes = 0;
uint64 RoomGraphCacheLargestWorkerValidEntries = 0;
FCacheMemoryBreakdown RoomGraphCacheBreakdown;
std::atomic<FThreadState*> Next{nullptr};
};
std::atomic<FThreadState*> GThreadStates{nullptr};
thread_local FThreadState* GThreadState = nullptr;
thread_local uint32 GScopeDepth = 0;
thread_local bool GScopeSampled = false;
FThreadState& GetThreadState()
{
if (GThreadState == nullptr)
{
GThreadState = new FThreadState();
FThreadState* Head = GThreadStates.load(std::memory_order_relaxed);
do
{
GThreadState->Next.store(Head, std::memory_order_relaxed);
}
while (!GThreadStates.compare_exchange_weak(
Head, GThreadState,
std::memory_order_release,
std::memory_order_relaxed));
}
return *GThreadState;
}
int32 ToIndex(EBucket Bucket)
{
const int32 Index = static_cast<int32>(Bucket);
return (Index >= 0 && Index < BucketCount)
? Index
: static_cast<int32>(EBucket::OtherOp);
}
bool IsSampledScopeEnabled(EBucket Bucket)
{
// Sampled mode pays for the trustworthy parent and mesh containers only. Attribution
// additionally exposes the component scopes, but still samples them; Full remains the
// deliberately invasive mode for per-call scope timing.
switch (Bucket)
{
case EBucket::GetDensityAt:
case EBucket::MesherGenerateMesh:
case EBucket::MesherDensityGrid:
case EBucket::MesherOther:
return true;
default:
return false;
}
}
bool IsAttributionScope(EBucket Bucket)
{
switch (Bucket)
{
case EBucket::GetDensityAt:
case EBucket::ApplyDisturbances:
case EBucket::PassageModifier:
case EBucket::PassageLandingAir:
case EBucket::PassageLandingFloor:
case EBucket::PassageLandingRoomFloor:
case EBucket::PassageTunnelAir:
case EBucket::PassageStructuralPosts:
case EBucket::TunnelCoreSupport:
case EBucket::TunnelCoreSdf:
case EBucket::TunnelCorePosts:
case EBucket::StructuralTail:
case EBucket::RoomGraphSource:
case EBucket::SdfCarve:
case EBucket::FusedEvaluator:
case EBucket::InterpretedOpStack:
case EBucket::OperatorBlock:
case EBucket::RoomGraphBuild:
case EBucket::RoomGraphSdf:
case EBucket::TunnelCoreWorld:
case EBucket::FusedDetail:
case EBucket::FusedWorm:
case EBucket::FusedStructural:
case EBucket::ClassifierTotal:
case EBucket::ClassifierIntervalProof:
case EBucket::ClassifierExactCore:
case EBucket::ClassifierExactFinal:
case EBucket::ClassifierRoomTail:
case EBucket::RuntimeStreamBuilding:
case EBucket::DensityPrologue:
case EBucket::DensityCore:
case EBucket::DensityDisturbances:
case EBucket::DensityStructuralPosts:
case EBucket::DensityBoundarySeal:
case EBucket::DensityDiffLayer:
case EBucket::DensityTail:
case EBucket::MesherGenerateMesh:
case EBucket::MesherDensityGrid:
case EBucket::MesherCellClassification:
case EBucket::MesherGradientNormals:
case EBucket::MesherVertexInterpolation:
case EBucket::MesherStreamBuilding:
case EBucket::MesherOther:
return true;
default:
return false;
}
}
bool IsAttributionAlwaysTimed(EBucket Bucket)
{
switch (Bucket)
{
case EBucket::MesherGenerateMesh:
case EBucket::MesherDensityGrid:
case EBucket::MesherOther:
case EBucket::RoomGraphBuild:
return true;
default:
return false;
}
}
uint64 ScaleSampledValue(uint64 Value, uint64 Calls, uint64 Samples)
{
if (Samples == 0 || Calls <= Samples)
{
return Value;
}
const double Scaled = static_cast<double>(Value)
* static_cast<double>(Calls) / static_cast<double>(Samples);
return Scaled > static_cast<double>(MAX_uint64)
? MAX_uint64
: static_cast<uint64>(Scaled + 0.5);
}
void AddClassifierStatsScaled(
FSnapshot::FClassifierStats& Destination,
const FSnapshot::FClassifierStats& Source,
uint64 Calls,
uint64 Samples)
{
Destination.RefineNodes += ScaleSampledValue(Source.RefineNodes, Calls, Samples);
Destination.StackBoxCalls += ScaleSampledValue(Source.StackBoxCalls, Calls, Samples);
Destination.WholeMixedNodes += ScaleSampledValue(Source.WholeMixedNodes, Calls, Samples);
Destination.WholeSolidNodes += ScaleSampledValue(Source.WholeSolidNodes, Calls, Samples);
Destination.WholeAirNodes += ScaleSampledValue(Source.WholeAirNodes, Calls, Samples);
Destination.NeedsFinalFieldNodes += ScaleSampledValue(Source.NeedsFinalFieldNodes, Calls, Samples);
Destination.SplitNodes += ScaleSampledValue(Source.SplitNodes, Calls, Samples);
Destination.MaxRefinementDepth = FMath::Max(
Destination.MaxRefinementDepth, Source.MaxRefinementDepth);
Destination.ExactCoreSamples += ScaleSampledValue(Source.ExactCoreSamples, Calls, Samples);
Destination.ExactFinalSamples += ScaleSampledValue(Source.ExactFinalSamples, Calls, Samples);
Destination.ExactCoreCacheHits += ScaleSampledValue(Source.ExactCoreCacheHits, Calls, Samples);
Destination.ExactFinalCacheHits += ScaleSampledValue(Source.ExactFinalCacheHits, Calls, Samples);
Destination.ExactCoreLeaves += ScaleSampledValue(Source.ExactCoreLeaves, Calls, Samples);
Destination.ExactFinalLeaves += ScaleSampledValue(Source.ExactFinalLeaves, Calls, Samples);
Destination.StackBoxCycles += ScaleSampledValue(Source.StackBoxCycles, Calls, Samples);
Destination.ExactCoreCycles += ScaleSampledValue(Source.ExactCoreCycles, Calls, Samples);
Destination.ExactFinalCycles += ScaleSampledValue(Source.ExactFinalCycles, Calls, Samples);
Destination.RoomTailQueries += ScaleSampledValue(Source.RoomTailQueries, Calls, Samples);
Destination.RoomTailEvaluated += ScaleSampledValue(Source.RoomTailEvaluated, Calls, Samples);
Destination.RoomTailCycles += ScaleSampledValue(Source.RoomTailCycles, Calls, Samples);
Destination.RoomPropagateCycles += ScaleSampledValue(Source.RoomPropagateCycles, Calls, Samples);
Destination.RoomExactPrimitiveCycles += ScaleSampledValue(Source.RoomExactPrimitiveCycles, Calls, Samples);
Destination.RoomCacheWindowCycles += ScaleSampledValue(Source.RoomCacheWindowCycles, Calls, Samples);
// These are topology descriptors, not per-call work counts. Keep the largest observed
// descriptor instead of multiplying one sampled graph description by the classifier
// call expansion factor.
Destination.RoomNumRooms = FMath::Max(Destination.RoomNumRooms, Source.RoomNumRooms);
Destination.RoomNumTunnels = FMath::Max(Destination.RoomNumTunnels, Source.RoomNumTunnels);
Destination.RoomNumRoomFloorJoins = FMath::Max(
Destination.RoomNumRoomFloorJoins, Source.RoomNumRoomFloorJoins);
Destination.RoomNumPits = FMath::Max(Destination.RoomNumPits, Source.RoomNumPits);
Destination.RoomNumChimneys = FMath::Max(Destination.RoomNumChimneys, Source.RoomNumChimneys);
}
}
void SetMode(EMode Mode, uint32 SampleInterval)
{
if (SampleInterval == 0)
{
SampleInterval = DefaultSampleInterval;
}
GSampleInterval.store(SampleInterval, std::memory_order_relaxed);
GMode.store(Mode, std::memory_order_relaxed);
GEnabled.store(Mode != EMode::Disabled, std::memory_order_relaxed);
if (Mode != EMode::Disabled && GTimerPairCycles.load(std::memory_order_relaxed) == 0)
{
CalibrateTimer();
}
}
bool IsEnabled()
{
return GEnabled.load(std::memory_order_relaxed);
}
bool IsCycleTimingEnabled()
{
return GMode.load(std::memory_order_relaxed) != EMode::Disabled;
}
EMode GetMode()
{
return GMode.load(std::memory_order_relaxed);
}
uint32 GetSampleInterval()
{
return GSampleInterval.load(std::memory_order_relaxed);
}
uint64 GetTimerPairCycles()
{
return GTimerPairCycles.load(std::memory_order_relaxed);
}
void CalibrateTimer()
{
constexpr uint32 Iterations = 100000;
volatile uint64 Sink = 0;
const uint64 Start = FPlatformTime::Cycles64();
for (uint32 Index = 0; Index < Iterations; ++Index)
{
const uint64 A = FPlatformTime::Cycles64();
const uint64 B = FPlatformTime::Cycles64();
Sink ^= (B - A);
}
const uint64 End = FPlatformTime::Cycles64();
const uint64 Total = End - Start;
const uint64 PairCycles = Total > 0
? FMath::Max<uint64>(1, Total / Iterations)
: 1;
GTimerPairCycles.store(PairCycles, std::memory_order_relaxed);
(void)Sink;
}
void Reset()
{
for (FThreadState* State = GThreadStates.load(std::memory_order_acquire);
State != nullptr;
State = State->Next.load(std::memory_order_acquire))
{
for (int32 Index = 0; Index < BucketCount; ++Index)
{
State->Calls[Index] = 0;
State->Cycles[Index] = 0;
State->Samples[Index] = 0;
}
for (int32 Index = 0; Index < CounterCount; ++Index)
{
State->Counters[Index] = 0;
}
for (int32 Index = 0; Index < BucketCount; ++Index)
{
State->SampleSequence[Index] = 0;
State->BlockCallCount[Index] = 0;
State->BlockStartCycles[Index] = 0;
State->BlockActive[Index] = false;
}
State->Classifier = FSnapshot::FClassifierStats();
State->TunnelCacheCapacityEntries = 0;
State->TunnelCacheValidEntries = 0;
State->TunnelCacheStaticBytes = 0;
State->TunnelCacheDynamicBytes = 0;
State->TunnelCacheEntryBytes = 0;
State->TunnelCacheLargestEntryBytes = 0;
State->TunnelCacheBreakdown = FCacheMemoryBreakdown();
State->RoomGraphCacheCapacityEntries = 0;
State->RoomGraphCacheValidEntries = 0;
State->RoomGraphCacheStaticBytes = 0;
State->RoomGraphCacheDynamicBytes = 0;
State->RoomGraphCacheEntryBytes = 0;
State->RoomGraphCacheLargestEntryBytes = 0;
State->RoomGraphCacheBreakdown = FCacheMemoryBreakdown();
}
GScopeDepth = 0;
GScopeSampled = false;
GSampledScopeActive = false;
}
FSnapshot Snapshot()
{
FSnapshot Result;
for (FThreadState* State = GThreadStates.load(std::memory_order_acquire);
State != nullptr;
State = State->Next.load(std::memory_order_acquire))
{
for (int32 Index = 0; Index < BucketCount; ++Index)
{
Result.Calls[Index] += State->Calls[Index];
Result.Samples[Index] += State->Samples[Index];
if (State->Samples[Index] == 0)
{
Result.Cycles[Index] += State->Cycles[Index];
}
else
{
// Store raw sampled cycles in the worker-local state. Estimate the bucket
// from its observed call/sample ratio at aggregation time. This matters for
// small buckets: multiplying every sample by 64 would overstate a bucket with
// only eight calls where all eight were sampled.
const double Scale = static_cast<double>(State->Calls[Index])
/ static_cast<double>(State->Samples[Index]);
const double EstimatedCycles = static_cast<double>(State->Cycles[Index]) * Scale;
Result.Cycles[Index] += EstimatedCycles > static_cast<double>(MAX_uint64)
? MAX_uint64
: static_cast<uint64>(EstimatedCycles + 0.5);
}
if (State->Calls[Index] > 0)
{
++Result.ActiveWorkers[Index];
const double EstimatedWorkerCycles = State->Samples[Index] == 0
? static_cast<double>(State->Cycles[Index])
: static_cast<double>(State->Cycles[Index])
* static_cast<double>(State->Calls[Index])
/ static_cast<double>(State->Samples[Index]);
Result.WallCycles[Index] = FMath::Max(
Result.WallCycles[Index],
EstimatedWorkerCycles > static_cast<double>(MAX_uint64)
? MAX_uint64
: static_cast<uint64>(EstimatedWorkerCycles + 0.5));
}
}
for (int32 Index = 0; Index < CounterCount; ++Index)
{
Result.Counters[Index] += State->Counters[Index];
}
Result.Classifier.Calls += State->Classifier.Calls;
Result.Classifier.SampledCalls += State->Classifier.SampledCalls;
AddClassifierStatsScaled(
Result.Classifier,
State->Classifier,
State->Classifier.Calls,
State->Classifier.SampledCalls);
const uint64 TunnelWorkerBytes = State->TunnelCacheStaticBytes
+ State->TunnelCacheDynamicBytes;
if (State->TunnelCacheCapacityEntries > 0)
{
++Result.TunnelCacheWorkers;
Result.TunnelCacheCapacityEntries += State->TunnelCacheCapacityEntries;
Result.TunnelCacheValidEntries += State->TunnelCacheValidEntries;
Result.TunnelCacheStaticBytes += State->TunnelCacheStaticBytes;
Result.TunnelCacheDynamicBytes += State->TunnelCacheDynamicBytes;
Result.TunnelCacheEntryBytes += State->TunnelCacheEntryBytes;
Result.TunnelCacheLargestEntryBytes = FMath::Max(
Result.TunnelCacheLargestEntryBytes,
State->TunnelCacheLargestEntryBytes);
Result.TunnelCacheLargestWorkerValidEntries = FMath::Max(
Result.TunnelCacheLargestWorkerValidEntries,
State->TunnelCacheValidEntries);
Result.TunnelCacheLargestWorkerBytes = FMath::Max(
Result.TunnelCacheLargestWorkerBytes, TunnelWorkerBytes);
Result.TunnelCacheBreakdown += State->TunnelCacheBreakdown;
}
const uint64 RoomGraphWorkerBytes = State->RoomGraphCacheStaticBytes
+ State->RoomGraphCacheDynamicBytes;
if (State->RoomGraphCacheCapacityEntries > 0)
{
++Result.RoomGraphCacheWorkers;
Result.RoomGraphCacheCapacityEntries += State->RoomGraphCacheCapacityEntries;
Result.RoomGraphCacheValidEntries += State->RoomGraphCacheValidEntries;
Result.RoomGraphCacheStaticBytes += State->RoomGraphCacheStaticBytes;
Result.RoomGraphCacheDynamicBytes += State->RoomGraphCacheDynamicBytes;
Result.RoomGraphCacheEntryBytes += State->RoomGraphCacheEntryBytes;
Result.RoomGraphCacheLargestEntryBytes = FMath::Max(
Result.RoomGraphCacheLargestEntryBytes,
State->RoomGraphCacheLargestEntryBytes);
Result.RoomGraphCacheLargestWorkerValidEntries = FMath::Max(
Result.RoomGraphCacheLargestWorkerValidEntries,
State->RoomGraphCacheValidEntries);
Result.RoomGraphCacheLargestWorkerBytes = FMath::Max(
Result.RoomGraphCacheLargestWorkerBytes, RoomGraphWorkerBytes);
Result.RoomGraphCacheBreakdown += State->RoomGraphCacheBreakdown;
}
}
Result.TimerPairCycles = GetTimerPairCycles();
Result.SampleInterval = GetSampleInterval();
Result.Mode = GetMode();
return Result;
}
FSnapshot SnapshotCurrentThread()
{
FSnapshot Result;
FThreadState* State = GThreadState;
if (State == nullptr)
{
Result.TimerPairCycles = GetTimerPairCycles();
Result.SampleInterval = GetSampleInterval();
Result.Mode = GetMode();
return Result;
}
for (int32 Index = 0; Index < BucketCount; ++Index)
{
Result.Calls[Index] = State->Calls[Index];
Result.Samples[Index] = State->Samples[Index];
if (State->Samples[Index] == 0)
{
Result.Cycles[Index] = State->Cycles[Index];
}
else
{
const double Scale = static_cast<double>(State->Calls[Index])
/ static_cast<double>(State->Samples[Index]);
const double EstimatedCycles = static_cast<double>(State->Cycles[Index]) * Scale;
Result.Cycles[Index] = EstimatedCycles > static_cast<double>(MAX_uint64)
? MAX_uint64
: static_cast<uint64>(EstimatedCycles + 0.5);
}
Result.WallCycles[Index] = Result.Cycles[Index];
Result.ActiveWorkers[Index] = State->Calls[Index] > 0 ? 1 : 0;
}
for (int32 Index = 0; Index < CounterCount; ++Index)
{
Result.Counters[Index] = State->Counters[Index];
}
Result.Classifier = State->Classifier;
if (State->Classifier.SampledCalls > 0)
{
FSnapshot::FClassifierStats Raw = State->Classifier;
Result.Classifier = FSnapshot::FClassifierStats();
Result.Classifier.Calls = State->Classifier.Calls;
Result.Classifier.SampledCalls = State->Classifier.SampledCalls;
AddClassifierStatsScaled(
Result.Classifier,
Raw,
State->Classifier.Calls,
State->Classifier.SampledCalls);
}
Result.TimerPairCycles = GetTimerPairCycles();
Result.SampleInterval = GetSampleInterval();
Result.Mode = GetMode();
return Result;
}
void AddCounter(ECounter Counter, uint64 Amount)
{
if (!IsEnabledFast())
{
return;
}
const int32 Index = static_cast<int32>(Counter);
if (Index >= 0 && Index < CounterCount)
{
GetThreadState().Counters[Index] += Amount;
}
}
void AddMeasurement(EBucket Bucket, uint64 Cycles, uint64 Calls)
{
const int32 Index = ToIndex(Bucket);
FThreadState& State = GetThreadState();
State.Calls[Index] += Calls;
State.Cycles[Index] += Cycles;
}
void RecordCall(EBucket Bucket)
{
const int32 Index = ToIndex(Bucket);
GetThreadState().Calls[Index] += 1;
}
void RecordClassifierCall(bool bSampled)
{
FThreadState& State = GetThreadState();
++State.Classifier.Calls;
if (bSampled)
{
++State.Classifier.SampledCalls;
}
++State.Calls[ToIndex(EBucket::ClassifierTotal)];
}
void AddClassifierStats(const FSnapshot::FClassifierStats& Stats)
{
FThreadState& State = GetThreadState();
State.Classifier.RefineNodes += Stats.RefineNodes;
State.Classifier.StackBoxCalls += Stats.StackBoxCalls;
State.Classifier.WholeMixedNodes += Stats.WholeMixedNodes;
State.Classifier.WholeSolidNodes += Stats.WholeSolidNodes;
State.Classifier.WholeAirNodes += Stats.WholeAirNodes;
State.Classifier.NeedsFinalFieldNodes += Stats.NeedsFinalFieldNodes;
State.Classifier.SplitNodes += Stats.SplitNodes;
State.Classifier.MaxRefinementDepth = FMath::Max(
State.Classifier.MaxRefinementDepth, Stats.MaxRefinementDepth);
State.Classifier.ExactCoreSamples += Stats.ExactCoreSamples;
State.Classifier.ExactFinalSamples += Stats.ExactFinalSamples;
State.Classifier.ExactCoreCacheHits += Stats.ExactCoreCacheHits;
State.Classifier.ExactFinalCacheHits += Stats.ExactFinalCacheHits;
State.Classifier.ExactCoreLeaves += Stats.ExactCoreLeaves;
State.Classifier.ExactFinalLeaves += Stats.ExactFinalLeaves;
State.Classifier.StackBoxCycles += Stats.StackBoxCycles;
State.Classifier.ExactCoreCycles += Stats.ExactCoreCycles;
State.Classifier.ExactFinalCycles += Stats.ExactFinalCycles;
State.Classifier.RoomTailQueries += Stats.RoomTailQueries;
State.Classifier.RoomTailEvaluated += Stats.RoomTailEvaluated;
State.Classifier.RoomTailCycles += Stats.RoomTailCycles;
State.Classifier.RoomPropagateCycles += Stats.RoomPropagateCycles;
State.Classifier.RoomExactPrimitiveCycles += Stats.RoomExactPrimitiveCycles;
State.Classifier.RoomCacheWindowCycles += Stats.RoomCacheWindowCycles;
State.Classifier.RoomNumRooms = FMath::Max(
State.Classifier.RoomNumRooms, Stats.RoomNumRooms);
State.Classifier.RoomNumTunnels = FMath::Max(
State.Classifier.RoomNumTunnels, Stats.RoomNumTunnels);
State.Classifier.RoomNumRoomFloorJoins = FMath::Max(
State.Classifier.RoomNumRoomFloorJoins, Stats.RoomNumRoomFloorJoins);
State.Classifier.RoomNumPits = FMath::Max(
State.Classifier.RoomNumPits, Stats.RoomNumPits);
State.Classifier.RoomNumChimneys = FMath::Max(
State.Classifier.RoomNumChimneys, Stats.RoomNumChimneys);
}
bool ShouldSample(EBucket Bucket)
{
if (!IsCycleTimingEnabled())
{
return false;
}
if (GetMode() == EMode::Full)
{
return true;
}
FThreadState& State = GetThreadState();
const int32 Index = ToIndex(Bucket);
const uint32 Sequence = State.SampleSequence[Index]++;
const uint32 Interval = GetSampleInterval();
if ((Interval & (Interval - 1u)) == 0u)
{
return (Sequence & (Interval - 1u)) == 0u;
}
return (Sequence % Interval) == 0u;
}
void AddSampledMeasurement(EBucket Bucket, uint64 RawCycles)
{
const int32 Index = ToIndex(Bucket);
FThreadState& State = GetThreadState();
const uint64 TimerCost = GetTimerPairCycles();
const uint64 AdjustedCycles = RawCycles > TimerCost
? RawCycles - TimerCost
: 0;
State.Cycles[Index] += AdjustedCycles;
State.Samples[Index] += 1;
}
void SetWorkerTunnelCacheFootprint(
uint64 CapacityEntries, uint64 ValidEntries,
uint64 StaticBytes, uint64 DynamicBytes,
uint64 EntryBytes, uint64 LargestEntryBytes,
uint64 LargestWorkerValidEntries,
const FCacheMemoryBreakdown& Breakdown)
{
if (!IsEnabled()) { return; }
FThreadState& State = GetThreadState();
State.TunnelCacheCapacityEntries = CapacityEntries;
State.TunnelCacheValidEntries = ValidEntries;
State.TunnelCacheStaticBytes = StaticBytes;
State.TunnelCacheDynamicBytes = DynamicBytes;
State.TunnelCacheEntryBytes = EntryBytes;
State.TunnelCacheLargestEntryBytes = LargestEntryBytes;
State.TunnelCacheLargestWorkerValidEntries = LargestWorkerValidEntries;
State.TunnelCacheBreakdown = Breakdown;
}
void SetWorkerRoomGraphCacheFootprint(
uint64 CapacityEntries, uint64 ValidEntries,
uint64 StaticBytes, uint64 DynamicBytes,
uint64 EntryBytes, uint64 LargestEntryBytes,
uint64 LargestWorkerValidEntries,
const FCacheMemoryBreakdown& Breakdown)
{
if (!IsEnabled()) { return; }
FThreadState& State = GetThreadState();
State.RoomGraphCacheCapacityEntries = CapacityEntries;
State.RoomGraphCacheValidEntries = ValidEntries;
State.RoomGraphCacheStaticBytes = StaticBytes;
State.RoomGraphCacheDynamicBytes = DynamicBytes;
State.RoomGraphCacheEntryBytes = EntryBytes;
State.RoomGraphCacheLargestEntryBytes = LargestEntryBytes;
State.RoomGraphCacheLargestWorkerValidEntries = LargestWorkerValidEntries;
State.RoomGraphCacheBreakdown = Breakdown;
}
const TCHAR* CounterName(ECounter Counter)
{
switch (Counter)
{
case ECounter::TunnelCacheLookup: return TEXT("TunnelCacheLookup");
case ECounter::TunnelCacheHit: return TEXT("TunnelCacheHit");
case ECounter::TunnelCacheMiss: return TEXT("TunnelCacheMiss");
case ECounter::SdfCacheBuild: return TEXT("SdfCacheBuild");
case ECounter::SdfSpatialIndexBuild: return TEXT("SdfSpatialIndexBuild");
case ECounter::SdfBrickIndexBuild: return TEXT("SdfBrickIndexBuild");
case ECounter::RoomGraphBuildGeneratorTile: return TEXT("RoomGraphBuildGeneratorTile");
case ECounter::RoomGraphBuildGeneratorTunnelCore: return TEXT("RoomGraphBuildGeneratorTunnelCore");
case ECounter::RoomGraphBuildOpShared: return TEXT("RoomGraphBuildOpShared");
case ECounter::RoomGraphBuildOpLocal: return TEXT("RoomGraphBuildOpLocal");
case ECounter::RoomGraphBuildClassifierShared: return TEXT("RoomGraphBuildClassifierShared");
case ECounter::RoomGraphBuildClassifierLocal: return TEXT("RoomGraphBuildClassifierLocal");
case ECounter::RoomGraphBuildUnknown: return TEXT("RoomGraphBuildUnknown");
case ECounter::CaveRoomCandidates:return TEXT("CaveRoomCandidates");
case ECounter::CaveRoomEvaluated: return TEXT("CaveRoomEvaluated");
case ECounter::CaveTunnelCandidates:return TEXT("CaveTunnelCandidates");
case ECounter::CaveTunnelEvaluated:return TEXT("CaveTunnelEvaluated");
case ECounter::TunnelCoreCandidates:return TEXT("TunnelCoreCandidates");
case ECounter::TunnelCoreEvaluated:return TEXT("TunnelCoreEvaluated");
case ECounter::TunnelSupportColumnBuilds:return TEXT("TunnelSupportColumnBuilds");
case ECounter::TunnelSupportColumnCandidates:return TEXT("TunnelSupportColumnCandidates");
case ECounter::TunnelSupportFloorQueries:return TEXT("TunnelSupportFloorQueries");
case ECounter::TunnelSupportFloorChecks:return TEXT("TunnelSupportFloorChecks");
case ECounter::TunnelSupportFloorBackstopFires:return TEXT("TunnelSupportFloorBackstopFires");
case ECounter::TunnelRoomFloorBackstopFires:return TEXT("TunnelRoomFloorBackstopFires");
case ECounter::TunnelAuthoredFloorSamples:return TEXT("TunnelAuthoredFloorSamples");
case ECounter::TunnelAuthoredFloorProtectionClamps:return TEXT("TunnelAuthoredFloorProtectionClamps");
case ECounter::TunnelAuthoredFloorDisturbanceSkips:return TEXT("TunnelAuthoredFloorDisturbanceSkips");
case ECounter::TunnelFloorProfileFallbacks:return TEXT("TunnelFloorProfileFallbacks");
case ECounter::TunnelFloorProfileBuilds:return TEXT("TunnelFloorProfileBuilds");
case ECounter::TunnelFloorProfileSegments:return TEXT("TunnelFloorProfileSegments");
case ECounter::TunnelFloorProfileLedges:return TEXT("TunnelFloorProfileLedges");
case ECounter::TunnelFloorProfileLedgesBelow2Voxels:return TEXT("TunnelFloorProfileLedgesBelow2Voxels");
case ECounter::TunnelFloorProfileLedges2To4Voxels:return TEXT("TunnelFloorProfileLedges2To4Voxels");
case ECounter::TunnelFloorProfileLedges4To8Voxels:return TEXT("TunnelFloorProfileLedges4To8Voxels");
case ECounter::TunnelFloorProfileLedges8PlusVoxels:return TEXT("TunnelFloorProfileLedges8PlusVoxels");
case ECounter::TunnelFloorSteepEdges:return TEXT("TunnelFloorSteepEdges");
case ECounter::TunnelFloorWindingEdges:return TEXT("TunnelFloorWindingEdges");
case ECounter::TunnelFloorDropEdges:return TEXT("TunnelFloorDropEdges");
case ECounter::TunnelFloorMultiStepEdges:return TEXT("TunnelFloorMultiStepEdges");
case ECounter::PassageSupportFloorBackstopFires:return TEXT("PassageSupportFloorBackstopFires");
case ECounter::PassageNativeFloorCompositions:return TEXT("PassageNativeFloorCompositions");
case ECounter::PassageCandidates: return TEXT("PassageCandidates");
case ECounter::PassageEvaluated: return TEXT("PassageEvaluated");
case ECounter::MesherBlockTests: return TEXT("MesherBlockTests");
case ECounter::MesherBlockAllSolid:return TEXT("MesherBlockAllSolid");
case ECounter::MesherBlockAllAir: return TEXT("MesherBlockAllAir");
case ECounter::TileClassifyCalls: return TEXT("TileClassifyCalls");
case ECounter::TileClassifyAllSolid: return TEXT("TileClassifyAllSolid");
case ECounter::TileClassifyAllAir: return TEXT("TileClassifyAllAir");
case ECounter::TileClassifyMixed: return TEXT("TileClassifyMixed");
case ECounter::FusedTunnelSamples: return TEXT("FusedTunnelSamples");
case ECounter::FusedTunnelDetailSamples: return TEXT("FusedTunnelDetailSamples");
case ECounter::FusedTunnelDetailSkipped: return TEXT("FusedTunnelDetailSkipped");
case ECounter::WormEligibleSamples: return TEXT("WormEligibleSamples");
case ECounter::WormBlockProofs: return TEXT("WormBlockProofs");
case ECounter::WormBlockSkippedSamples: return TEXT("WormBlockSkippedSamples");
case ECounter::OpBlockBuilds: return TEXT("OpBlockBuilds");
case ECounter::OpBlockSamples: return TEXT("OpBlockSamples");
case ECounter::OpBlockOperators: return TEXT("OpBlockOperators");
case ECounter::OpBlockActiveOperators: return TEXT("OpBlockActiveOperators");
case ECounter::OpBlockPrunedOperators: return TEXT("OpBlockPrunedOperators");
case ECounter::MesherDensityGridBytes: return TEXT("MesherDensityGridBytes");
case ECounter::MesherSharedGridReadBytes: return TEXT("MesherSharedGridReadBytes");
case ECounter::MesherOutputArrayBytes: return TEXT("MesherOutputArrayBytes");
case ECounter::MesherOutputAllocatedBytes: return TEXT("MesherOutputAllocatedBytes");
case ECounter::OpBlockScratchBytes: return TEXT("OpBlockScratchBytes");
case ECounter::OpBlockCopyBytes: return TEXT("OpBlockCopyBytes");
case ECounter::Count: break;
}
return TEXT("Unknown");
}
EBucket BucketFromName(const TCHAR* Name)
{
if (Name == nullptr) { return EBucket::OtherOp; }
struct FNameBucket
{
const TCHAR* Name;
EBucket Bucket;
};
static const FNameBucket Names[] =
{
{ TEXT("ApplyDisturbances"), EBucket::ApplyDisturbances },
{ TEXT("PassageModifier"), EBucket::PassageModifier },
{ TEXT("PassageLandingAir"), EBucket::PassageLandingAir },
{ TEXT("PassageLandingFloor"), EBucket::PassageLandingFloor },
{ TEXT("PassageLandingRoomFloor"), EBucket::PassageLandingRoomFloor },
{ TEXT("PassageTunnelAir"), EBucket::PassageTunnelAir },
{ TEXT("PassageStructuralPosts"), EBucket::PassageStructuralPosts },
{ TEXT("TunnelCoreSupport"), EBucket::TunnelCoreSupport },
{ TEXT("TunnelCoreSdf"), EBucket::TunnelCoreSdf },
{ TEXT("TunnelCorePosts"), EBucket::TunnelCorePosts },
{ TEXT("StructuralTail"), EBucket::StructuralTail },
{ TEXT("RoomGraphSource"), EBucket::RoomGraphSource },
{ TEXT("SdfCarve"), EBucket::SdfCarve },
{ TEXT("SdfConvertOp"), EBucket::SdfCarve },
{ TEXT("CaveRoughnessMod"), EBucket::CaveRoughnessMod },
{ TEXT("CaveTerraceMod"), EBucket::CaveTerraceMod },
{ TEXT("LayerLineMod"), EBucket::LayerLineMod },
{ TEXT("RibbingMod"), EBucket::RibbingMod },
{ TEXT("CaveOverhangMod"), EBucket::CaveOverhangMod },
{ TEXT("CaveCliffMod"), EBucket::CaveCliffMod },
{ TEXT("ScallopMod"), EBucket::ScallopMod },
{ TEXT("CaveArchMod"), EBucket::CaveArchMod },
{ TEXT("RoomColumnMod"), EBucket::RoomColumnMod },
{ TEXT("DomeMod"), EBucket::DomeMod },
{ TEXT("PinchMod"), EBucket::PinchMod },
{ TEXT("FloorBiasMod"), EBucket::FloorBiasMod },
{ TEXT("WormFieldSource"), EBucket::WormFieldSource },
{ TEXT("OriginSpineOp"), EBucket::OriginSpineOp },
{ TEXT("BoundarySealOp"), EBucket::BoundarySealOp },
{ TEXT("PassageCarveOp"), EBucket::PassageCarveOp },
{ TEXT("XYEdgeSealOp"), EBucket::XYEdgeSealOp },
{ TEXT("FusedEvaluator"), EBucket::FusedEvaluator },
{ TEXT("InterpretedOpStack"),EBucket::InterpretedOpStack },
{ TEXT("OperatorBlock"), EBucket::OperatorBlock },
{ TEXT("RoomGraphBuild"), EBucket::RoomGraphBuild },
{ TEXT("RoomGraphSdf"), EBucket::RoomGraphSdf },
{ TEXT("TunnelCoreWorld"), EBucket::TunnelCoreWorld },
{ TEXT("FusedDetail"), EBucket::FusedDetail },
{ TEXT("FusedWorm"), EBucket::FusedWorm },
{ TEXT("FusedStructural"), EBucket::FusedStructural },
{ TEXT("ClassifierTotal"), EBucket::ClassifierTotal },
{ TEXT("ClassifierIntervalProof"), EBucket::ClassifierIntervalProof },
{ TEXT("ClassifierExactCore"), EBucket::ClassifierExactCore },
{ TEXT("ClassifierExactFinal"), EBucket::ClassifierExactFinal },
{ TEXT("ClassifierRoomTail"), EBucket::ClassifierRoomTail },
{ TEXT("RuntimeStreamBuilding"), EBucket::RuntimeStreamBuilding },
{ TEXT("DensityPrologue"), EBucket::DensityPrologue },
{ TEXT("DensityCore"), EBucket::DensityCore },
{ TEXT("DensityDisturbances"), EBucket::DensityDisturbances },
{ TEXT("DensityStructuralPosts"), EBucket::DensityStructuralPosts },
{ TEXT("DensityBoundarySeal"), EBucket::DensityBoundarySeal },
{ TEXT("DensityDiffLayer"), EBucket::DensityDiffLayer },
{ TEXT("DensityTail"), EBucket::DensityTail },
{ TEXT("MesherGenerateMesh"), EBucket::MesherGenerateMesh },
{ TEXT("MesherDensityGrid"), EBucket::MesherDensityGrid },
{ TEXT("MesherCellClassification"), EBucket::MesherCellClassification },
{ TEXT("MesherGradientNormals"), EBucket::MesherGradientNormals },
{ TEXT("MesherVertexInterpolation"), EBucket::MesherVertexInterpolation },
{ TEXT("MesherStreamBuilding"), EBucket::MesherStreamBuilding },
{ TEXT("MesherOther"), EBucket::MesherOther },
};
static const TMap<uint32, TArray<const FNameBucket*>> NamesByHash = []()
{
TMap<uint32, TArray<const FNameBucket*>> Lookup;
for (const FNameBucket& Entry : Names)
{
Lookup.FindOrAdd(FCrc::StrCrc32(Entry.Name)).Add(&Entry);
}
return Lookup;
}();
const TArray<const FNameBucket*>* Candidates = NamesByHash.Find(FCrc::StrCrc32(Name));
if (Candidates != nullptr)
{
for (const FNameBucket* Entry : *Candidates)
{
if (Entry != nullptr && FCString::Strcmp(Name, Entry->Name) == 0)
{
return Entry->Bucket;
}
}
}
return EBucket::OtherOp;
}
const TCHAR* BucketName(EBucket Bucket)
{
switch (Bucket)
{
case EBucket::GetDensityAt: return TEXT("GetDensityAt");
case EBucket::ApplyDisturbances:return TEXT("ApplyDisturbances");
case EBucket::PassageModifier: return TEXT("PassageModifier");
case EBucket::PassageLandingAir:return TEXT("PassageLandingAir");
case EBucket::PassageLandingFloor:return TEXT("PassageLandingFloor");
case EBucket::PassageLandingRoomFloor:return TEXT("PassageLandingRoomFloor");
case EBucket::PassageTunnelAir:return TEXT("PassageTunnelAir");
case EBucket::PassageStructuralPosts:return TEXT("PassageStructuralPosts");
case EBucket::TunnelCoreSupport:return TEXT("TunnelCoreSupport");
case EBucket::TunnelCoreSdf: return TEXT("TunnelCoreSdf");
case EBucket::TunnelCorePosts: return TEXT("TunnelCorePosts");
case EBucket::StructuralTail: return TEXT("StructuralTail");
case EBucket::RoomGraphSource: return TEXT("RoomGraphSource");
case EBucket::SdfCarve: return TEXT("SdfCarve");
case EBucket::CaveRoughnessMod: return TEXT("CaveRoughnessMod");
case EBucket::CaveTerraceMod: return TEXT("CaveTerraceMod");
case EBucket::LayerLineMod: return TEXT("LayerLineMod");
case EBucket::RibbingMod: return TEXT("RibbingMod");
case EBucket::CaveOverhangMod: return TEXT("CaveOverhangMod");
case EBucket::CaveCliffMod: return TEXT("CaveCliffMod");
case EBucket::ScallopMod: return TEXT("ScallopMod");
case EBucket::CaveArchMod: return TEXT("CaveArchMod");
case EBucket::RoomColumnMod: return TEXT("RoomColumnMod");
case EBucket::DomeMod: return TEXT("DomeMod");
case EBucket::PinchMod: return TEXT("PinchMod");
case EBucket::FloorBiasMod: return TEXT("FloorBiasMod");
case EBucket::WormFieldSource: return TEXT("WormFieldSource");
case EBucket::OriginSpineOp: return TEXT("OriginSpineOp");
case EBucket::BoundarySealOp: return TEXT("BoundarySealOp");
case EBucket::PassageCarveOp: return TEXT("PassageCarveOp");
case EBucket::XYEdgeSealOp: return TEXT("XYEdgeSealOp");
case EBucket::OtherOp: return TEXT("OtherOp");
case EBucket::FusedEvaluator: return TEXT("FusedEvaluator");
case EBucket::InterpretedOpStack:return TEXT("InterpretedOpStack");
case EBucket::OperatorBlock: return TEXT("OperatorBlock");
case EBucket::RoomGraphBuild: return TEXT("RoomGraphBuild");
case EBucket::RoomGraphSdf: return TEXT("RoomGraphSdf");
case EBucket::TunnelCoreWorld: return TEXT("TunnelCoreWorld");
case EBucket::FusedDetail: return TEXT("FusedDetail");
case EBucket::FusedWorm: return TEXT("FusedWorm");
case EBucket::FusedStructural: return TEXT("FusedStructural");
case EBucket::ClassifierTotal: return TEXT("ClassifierTotal");
case EBucket::ClassifierIntervalProof:return TEXT("ClassifierIntervalProof");
case EBucket::ClassifierExactCore:return TEXT("ClassifierExactCore");
case EBucket::ClassifierExactFinal:return TEXT("ClassifierExactFinal");
case EBucket::ClassifierRoomTail:return TEXT("ClassifierRoomTail");
case EBucket::RuntimeStreamBuilding:return TEXT("RuntimeStreamBuilding");
case EBucket::DensityPrologue: return TEXT("DensityPrologue");
case EBucket::DensityCore: return TEXT("DensityCore");
case EBucket::DensityDisturbances:return TEXT("DensityDisturbances");
case EBucket::DensityStructuralPosts:return TEXT("DensityStructuralPosts");
case EBucket::DensityBoundarySeal:return TEXT("DensityBoundarySeal");
case EBucket::DensityDiffLayer: return TEXT("DensityDiffLayer");
case EBucket::DensityTail: return TEXT("DensityTail");
case EBucket::MesherGenerateMesh:return TEXT("MesherGenerateMesh");
case EBucket::MesherDensityGrid:return TEXT("MesherDensityGrid");
case EBucket::MesherCellClassification:return TEXT("MesherCellClassification");
case EBucket::MesherGradientNormals:return TEXT("MesherGradientNormals");
case EBucket::MesherVertexInterpolation:return TEXT("MesherVertexInterpolation");
case EBucket::MesherStreamBuilding:return TEXT("MesherStreamBuilding");
case EBucket::MesherOther: return TEXT("MesherOther");
case EBucket::Count: break;
}
return TEXT("Unknown");
}
FScopeToken BeginScope(EBucket Bucket)
{
FScopeToken Token{};
if (!IsEnabledFast())
{
return Token;
}
const EMode Mode = GetMode();
const bool bSampledMode = Mode == EMode::Sampled || Mode == EMode::Attribution;
if ((Mode == EMode::Sampled && !IsSampledScopeEnabled(Bucket))
|| (Mode == EMode::Attribution && !IsAttributionScope(Bucket)))
{
return Token;
}
Token.bEntered = true;
Token.bPreviousSampled = GScopeSampled;
Token.bPreviousSampledScopeActive = GSampledScopeActive;
++GScopeDepth;
// For the hot parent, amortise the timer over a deterministic block of calls. A timer
// pair around one ~100 ns operation is mostly the timer; a pair around 64 calls is cheap
// and estimates the parent from real elapsed work rather than instrumentation latency.
if (bSampledMode && Bucket == EBucket::GetDensityAt)
{
FThreadState& State = GetThreadState();
const int32 Index = ToIndex(Bucket);
const uint32 Sequence = State.SampleSequence[Index]++;
Token.bBlockTiming = true;
const uint32 Interval = GetSampleInterval();
const bool bBlockBoundary = (Interval & (Interval - 1u)) == 0u
? (Sequence & (Interval - 1u)) == 0u
: (Sequence % Interval) == 0u;
if (bBlockBoundary && !State.BlockActive[Index])
{
State.BlockActive[Index] = true;
State.BlockCallCount[Index] = 0;
State.BlockStartCycles[Index] = FPlatformTime::Cycles64();
}
// The selected call admits all nested component scopes. Every other call exits the
// inline FScopedTimer fast path before crossing into this translation unit.
GScopeSampled = bBlockBoundary;
GSampledScopeActive = bBlockBoundary;
Token.bSampled = bBlockBoundary;
return Token;
}
// Every bucket samples independently, but only one nested non-container scope owns a
// timer at a time. Otherwise a sampled GetDensityAt would also start timers for every
// phase and operation inside that same call, measuring instrumentation instead of work.
// Mesher container ranges deliberately remain transparent so their grid can still expose
// density samples underneath them.
const bool bContainerScope = Bucket == EBucket::MesherGenerateMesh
|| Bucket == EBucket::MesherDensityGrid;
const bool bInheritedAttributionSample = Mode == EMode::Attribution
&& GSampledScopeActive;
Token.bSampled = Mode == EMode::Full
? true
: ((Mode == EMode::Attribution && IsAttributionAlwaysTimed(Bucket))
? true
: (bInheritedAttributionSample
? true
: (Mode == EMode::Attribution && IsAttributionScope(Bucket)
? ShouldSample(Bucket)
: (!GSampledScopeActive && ShouldSample(Bucket)))));
GScopeSampled = Token.bSampled;
if (Token.bSampled)
{
if ((Mode == EMode::Sampled || Mode == EMode::Attribution) && !bContainerScope)
{
GSampledScopeActive = true;
}
Token.StartCycles = FPlatformTime::Cycles64();
}
return Token;
}
void EndScope(EBucket Bucket, const FScopeToken& Token)
{
if (!Token.bEntered)
{
return;
}
RecordCall(Bucket);
if (Token.bBlockTiming)
{
FThreadState& State = GetThreadState();
const int32 Index = ToIndex(Bucket);
++State.BlockCallCount[Index];
const uint32 Interval = GetSampleInterval();
if (State.BlockActive[Index] && State.BlockCallCount[Index] >= Interval)
{
const uint64 RawCycles = FPlatformTime::Cycles64() - State.BlockStartCycles[Index];
const uint64 TimerCost = GetTimerPairCycles();
const uint64 AdjustedCycles = RawCycles > TimerCost
? RawCycles - TimerCost
: 0;
State.Cycles[Index] += AdjustedCycles / FMath::Max<uint32>(1, Interval);
State.Samples[Index] += 1;
State.BlockActive[Index] = false;
State.BlockCallCount[Index] = 0;
State.BlockStartCycles[Index] = 0;
}
}
else if (Token.bSampled)
{
AddSampledMeasurement(Bucket, FPlatformTime::Cycles64() - Token.StartCycles);
}
check(GScopeDepth > 0);
--GScopeDepth;
GScopeSampled = Token.bPreviousSampled;
GSampledScopeActive = Token.bPreviousSampledScopeActive;
}
}
@@ -0,0 +1,469 @@
// VoxelDiffLayer.cpp
// Runtime terrain modification storage and evaluation.
#include "VoxelDiffLayer.h"
namespace
{
std::atomic<uint64> GNextDiffLayerLifetimeId{0};
bool IsFiniteVector(const FVector& Value)
{
return VoxelMath::IsFinite(Value.X)
&& VoxelMath::IsFinite(Value.Y)
&& VoxelMath::IsFinite(Value.Z);
}
bool IsValidModificationGeometry(const FVoxelModification& Mod)
{
if (!IsFiniteVector(Mod.Center) || !IsFiniteVector(Mod.BoxExtent)
|| !IsFiniteVector(Mod.CapsuleEnd)
|| !VoxelMath::IsFinite(Mod.Radius)
|| !VoxelMath::IsFinite(Mod.Strength)
|| !VoxelMath::IsFinite(Mod.Falloff)
|| Mod.Radius <= 0.0f || Mod.Falloff < 0.0f)
{
return false;
}
switch (Mod.Shape)
{
case EVoxelBrushShape::Sphere:
return true;
case EVoxelBrushShape::Box:
return Mod.BoxExtent.X > 0.0f
&& Mod.BoxExtent.Y > 0.0f
&& Mod.BoxExtent.Z > 0.0f;
case EVoxelBrushShape::Capsule:
return true; // A coincident pair of endpoints is a valid spherical capsule.
default:
return false;
}
}
double GetModificationBudgetVolume(const FVoxelModification& Mod)
{
switch (Mod.Shape)
{
case EVoxelBrushShape::Box:
{
// The SDF's falloff is a rounded outer band, so the expanded axis-aligned box is a
// conservative charge and remains cheap to compute.
const double Ex = static_cast<double>(Mod.BoxExtent.X) + static_cast<double>(Mod.Falloff);
const double Ey = static_cast<double>(Mod.BoxExtent.Y) + static_cast<double>(Mod.Falloff);
const double Ez = static_cast<double>(Mod.BoxExtent.Z) + static_cast<double>(Mod.Falloff);
return 8.0 * Ex * Ey * Ez;
}
case EVoxelBrushShape::Capsule:
{
// Charge the full capsule including its falloff shell: cylinder + two hemispheres.
const double OuterRadius = static_cast<double>(Mod.Radius) + static_cast<double>(Mod.Falloff);
const double SegmentLength = static_cast<double>(FVector::Dist(Mod.Center, Mod.CapsuleEnd));
return PI * OuterRadius * OuterRadius * SegmentLength
+ (4.0 / 3.0) * PI * OuterRadius * OuterRadius * OuterRadius;
}
case EVoxelBrushShape::Sphere:
default:
{
const double Radius = static_cast<double>(Mod.Radius);
return (4.0 / 3.0) * PI * Radius * Radius * Radius;
}
}
}
}
uint64 UVoxelDiffLayer::GetCacheLifetimeId() const
{
uint64 LifetimeId = CacheLifetimeId.load(std::memory_order_acquire);
if (LifetimeId != 0)
{
return LifetimeId;
}
const uint64 NewLifetimeId = GNextDiffLayerLifetimeId.fetch_add(1, std::memory_order_relaxed) + 1;
if (CacheLifetimeId.compare_exchange_strong(
LifetimeId,
NewLifetimeId,
std::memory_order_release,
std::memory_order_acquire))
{
return NewLifetimeId;
}
// Another worker initialized the identity first. Its value is written into LifetimeId by
// compare_exchange_strong on failure.
return LifetimeId;
}
//=============================================================================
// BUDGET CONFIGURATION
//=============================================================================
void UVoxelDiffLayer::SetBudget(int32 InMaxMods, float InMaxRadius, float InMaxVolume)
{
BudgetMaxMods = InMaxMods;
BudgetMaxRadius = InMaxRadius;
BudgetMaxVolume = InMaxVolume;
UE_LOG(LogTemp, Log, TEXT("[DiffLayer] Budget set: MaxMods=%d, MaxRadius=%.1f, MaxVolume=%.0f (0=unlimited)"),
BudgetMaxMods, BudgetMaxRadius, BudgetMaxVolume);
}
bool UVoxelDiffLayer::CanModify(float Radius) const
{
if (!VoxelMath::IsFinite(Radius) || Radius <= 0.0f)
{
return false;
}
// Check radius limit (always enforced, even if "unlimited" count/volume)
if (BudgetMaxRadius > 0.0f && Radius > BudgetMaxRadius)
{
return false;
}
// Check modification count limit
if (BudgetMaxMods > 0 && ModificationCount >= BudgetMaxMods)
{
return false;
}
// Check volume limit: would this brush push us over?
if (BudgetMaxVolume > 0.0f)
{
const double BrushVolume = (4.0 / 3.0) * PI
* static_cast<double>(Radius) * static_cast<double>(Radius) * static_cast<double>(Radius);
const double NewTotal = static_cast<double>(AccumulatedVolume) + BrushVolume;
if (!VoxelMath::IsFinite(BrushVolume) || BrushVolume > static_cast<double>(FLT_MAX)
|| !VoxelMath::IsFinite(NewTotal)
|| NewTotal > static_cast<double>(BudgetMaxVolume))
{
return false;
}
}
return true;
}
bool UVoxelDiffLayer::CanModify(const FVoxelModification& Mod) const
{
if (!IsValidModificationGeometry(Mod)) return false;
// Radius is the radial cap for spheres/capsules. A box's largest half-extent is its analogous
// brush radius; checking it separately prevents a direct API caller from hiding a huge box behind
// an unrelated small Radius field.
const float GeometryRadius = (Mod.Shape == EVoxelBrushShape::Box)
? FMath::Max3(Mod.BoxExtent.X, Mod.BoxExtent.Y, Mod.BoxExtent.Z)
: Mod.Radius;
if (BudgetMaxRadius > 0.0f && GeometryRadius > BudgetMaxRadius) return false;
if (BudgetMaxMods > 0 && ModificationCount >= BudgetMaxMods) return false;
const double BrushVolume = GetModificationBudgetVolume(Mod);
const double NewTotal = static_cast<double>(AccumulatedVolume) + BrushVolume;
if (!VoxelMath::IsFinite(BrushVolume) || BrushVolume > static_cast<double>(FLT_MAX)
|| !VoxelMath::IsFinite(NewTotal)) return false;
if (BudgetMaxVolume > 0.0f && NewTotal > static_cast<double>(BudgetMaxVolume)) return false;
return true;
}
int32 UVoxelDiffLayer::GetRemainingModifications() const
{
if (BudgetMaxMods <= 0) return -1; // Unlimited
return FMath::Max(0, BudgetMaxMods - ModificationCount);
}
float UVoxelDiffLayer::GetRemainingVolume() const
{
if (BudgetMaxVolume <= 0.0f) return -1.0f; // Unlimited
return FMath::Max(0.0f, BudgetMaxVolume - AccumulatedVolume);
}
//=============================================================================
// APPLY MODIFICATION
//=============================================================================
TArray<FIntVector> UVoxelDiffLayer::ApplyModification(const FVoxelModification& Mod)
{
TArray<FIntVector> AffectedChunks;
// Validate the complete geometry before radius clamping, budget accounting, or AABB math.
// FMath::Min(NaN, limit) and the shape switch's default sphere path would otherwise turn
// malformed API input into either a counter update or an invalid chunk range.
if (!IsValidModificationGeometry(Mod))
{
UE_LOG(LogTemp, Warning,
TEXT("[DiffLayer] Modification REJECTED — non-finite or invalid brush geometry"));
return AffectedChunks;
}
// --- Budget enforcement ---
// Preserve the historical radius clamp for spheres/capsules when a finite per-brush cap is set;
// the shape-aware check below still charges the resulting geometry and rejects oversized boxes.
const float ClampedRadius = (BudgetMaxRadius > 0.0f)
? FMath::Min(Mod.Radius, BudgetMaxRadius) : Mod.Radius;
FVoxelModification ClampedMod = Mod;
ClampedMod.Radius = ClampedRadius;
if (!CanModify(ClampedMod))
{
UE_LOG(LogTemp, Warning,
TEXT("[DiffLayer] Modification REJECTED — budget exceeded (count=%d/%d, volume=%.0f/%.0f)"),
ModificationCount, BudgetMaxMods, AccumulatedVolume, BudgetMaxVolume);
return AffectedChunks; // Empty = rejected
}
// Complete the world-AABB calculation before mutating the budget counters. Finite input can
// still overflow during Center +/- extent or address a chunk outside FIntVector's domain;
// neither case is a modification that this layer can store safely.
FVector BoundsMin, BoundsMax;
ClampedMod.GetWorldBounds(BoundsMin, BoundsMax);
auto WorldToChunk = [](float WorldCoord, int32& OutChunk) -> bool
{
if (!VoxelMath::IsFinite(WorldCoord))
{
return false;
}
const double ChunkCoord = FMath::FloorToDouble(
static_cast<double>(WorldCoord) / static_cast<double>(CHUNK_SIZE));
if (!VoxelMath::IsFinite(ChunkCoord)
|| ChunkCoord < static_cast<double>(MIN_int32)
|| ChunkCoord > static_cast<double>(MAX_int32))
{
return false;
}
OutChunk = static_cast<int32>(ChunkCoord);
return true;
};
int32 MinCX = 0, MaxCX = 0, MinCY = 0, MaxCY = 0, MinCZ = 0, MaxCZ = 0;
if (!WorldToChunk(BoundsMin.X, MinCX) || !WorldToChunk(BoundsMax.X, MaxCX)
|| !WorldToChunk(BoundsMin.Y, MinCY) || !WorldToChunk(BoundsMax.Y, MaxCY)
|| !WorldToChunk(BoundsMin.Z, MinCZ) || !WorldToChunk(BoundsMax.Z, MaxCZ))
{
UE_LOG(LogTemp, Warning,
TEXT("[DiffLayer] Modification REJECTED — brush bounds are not representable"));
return TArray<FIntVector>();
}
// Update budget counters
ModificationCount++;
const double BrushVolume = GetModificationBudgetVolume(ClampedMod);
AccumulatedVolume += static_cast<float>(BrushVolume);
{
// Write lock: blocks worker-thread readers (HasModifications / GetDensityOffset) while the map
// is mutated/rehashed. AffectedChunks is local; populate it in the same pass.
FWriteScopeLock Lock(ModsLock);
for (int32 CZ = MinCZ;; ++CZ)
{
for (int32 CY = MinCY;; ++CY)
{
for (int32 CX = MinCX;; ++CX)
{
FIntVector ChunkCoord(CX, CY, CZ);
// Store the modification in this chunk's list
ChunkMods.FindOrAdd(ChunkCoord).Add(ClampedMod);
// Track which chunks need re-meshing
AffectedChunks.Add(ChunkCoord);
if (CX == MaxCX) break;
}
if (CY == MaxCY) break;
}
if (CZ == MaxCZ) break;
}
// 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);
ModsVersion.fetch_add(1, std::memory_order_release);
}
UE_LOG(LogTemp, Log,
TEXT("[DiffLayer] Modification #%d at (%.0f, %.0f, %.0f) R=%.1f S=%.1f -> %d chunks (budget: %d/%d mods, %.0f/%.0f vol)"),
ModificationCount,
ClampedMod.Center.X, ClampedMod.Center.Y, ClampedMod.Center.Z,
ClampedMod.Radius, ClampedMod.Strength,
AffectedChunks.Num(),
ModificationCount, BudgetMaxMods,
AccumulatedVolume, BudgetMaxVolume);
return AffectedChunks;
}
//=============================================================================
// DENSITY EVALUATION
//=============================================================================
float UVoxelDiffLayer::GetDensityOffset(const FIntVector& ChunkCoord,
float WorldX, float WorldY, float WorldZ) const
{
// Lock-free fast reject: no carves anywhere -> nothing to offset.
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
// 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);
const TArray<FVoxelModification>* Mods = ChunkMods.Find(ChunkCoord);
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;
if (MinChunk.X > MaxChunk.X || MinChunk.Y > MaxChunk.Y || MinChunk.Z > MaxChunk.Z)
{
return false;
}
// ApplyModification registers a mod in every chunk its AABB overlaps, so looking up the keys in
// the queried range is conservative and proportional to that range. Do not walk ChunkMods:
// the edit history can grow without bound while a sealed-solid candidate is being proved.
FReadScopeLock Lock(ModsLock);
for (int32 CZ = MinChunk.Z;; ++CZ)
{
for (int32 CY = MinChunk.Y;; ++CY)
{
for (int32 CX = MinChunk.X;; ++CX)
{
if (const TArray<FVoxelModification>* Mods = ChunkMods.Find(FIntVector(CX, CY, CZ)))
{
if (Mods->Num() > 0) return true;
}
if (CX == MaxChunk.X) break;
}
if (CY == MaxChunk.Y) break;
}
if (CZ == MaxChunk.Z) break;
}
return false;
}
float UVoxelDiffLayer::EvaluateMods(const TArray<FVoxelModification>& Mods,
float WorldX, float WorldY, float WorldZ)
{
float TotalOffset = 0.0f;
const FVector Pos(WorldX, WorldY, WorldZ);
// smoothstep helper (full strength when t<=0, zero when t>=1).
auto Smooth01 = [](float T) -> float
{
T = FMath::Clamp(T, 0.0f, 1.0f);
return 1.0f - T * T * (3.0f - 2.0f * T);
};
for (const FVoxelModification& Mod : Mods)
{
float Falloff = 0.0f;
switch (Mod.Shape)
{
case EVoxelBrushShape::Box:
{
// Box SDF (negative inside), then fade over the Falloff band outside.
const FVector Q = (Pos - Mod.Center).GetAbs() - Mod.BoxExtent;
const float Outside = FVector(FMath::Max(Q.X, 0.0f), FMath::Max(Q.Y, 0.0f), FMath::Max(Q.Z, 0.0f)).Size();
const float Inside = FMath::Min(FMath::Max3(Q.X, Q.Y, Q.Z), 0.0f);
const float S = Outside + Inside; // <=0 inside the box
const float Band = FMath::Max(Mod.Falloff, 0.01f);
if (S >= Band) continue;
Falloff = Smooth01(FMath::Max(S, 0.0f) / Band);
break;
}
case EVoxelBrushShape::Capsule:
{
// Distance to the segment minus the tube radius, faded over Falloff.
const FVector AB = Mod.CapsuleEnd - Mod.Center;
const FVector AP = Pos - Mod.Center;
const float T = FMath::Clamp(FVector::DotProduct(AP, AB) / FMath::Max(FVector::DotProduct(AB, AB), KINDA_SMALL_NUMBER), 0.0f, 1.0f);
const float SegDist = FVector::Dist(Pos, Mod.Center + AB * T);
const float S = SegDist - Mod.Radius; // <=0 inside the tube
const float Band = FMath::Max(Mod.Falloff, 0.01f);
if (S >= Band) continue;
Falloff = Smooth01(FMath::Max(S, 0.0f) / Band);
break;
}
case EVoxelBrushShape::Sphere:
default:
{
const float Dist = FVector::Dist(Pos, Mod.Center);
if (Dist >= Mod.Radius) continue;
Falloff = Smooth01(Dist / Mod.Radius);
break;
}
}
TotalOffset += Mod.Strength * Falloff;
}
return TotalOffset;
}
bool UVoxelDiffLayer::HasModifications(const FIntVector& ChunkCoord) const
{
// Lock-free fast reject: no carves anywhere -> the map is empty, skip lock + lookup entirely.
if (!bHasAnyMods.load(std::memory_order_acquire)) return false;
FReadScopeLock Lock(ModsLock); // worker threads read concurrently; serialised vs. writers
const TArray<FVoxelModification>* Mods = ChunkMods.Find(ChunkCoord);
return Mods && Mods->Num() > 0;
}
//=============================================================================
// MANAGEMENT
//=============================================================================
void UVoxelDiffLayer::Clear()
{
int32 Count = GetTotalModificationCount();
{
// Write lock: workers may be mid-read. Flip the fast-path flag false BEFORE emptying so any
// reader that loads it afterwards skips the map without locking.
FWriteScopeLock Lock(ModsLock);
bHasAnyMods.store(false, std::memory_order_release);
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
ModificationCount = 0;
AccumulatedVolume = 0.0f;
UE_LOG(LogTemp, Log, TEXT("[DiffLayer] Cleared %d modifications, budget reset"), Count);
}
int32 UVoxelDiffLayer::GetTotalModificationCount() const
{
FReadScopeLock Lock(ModsLock);
int32 Total = 0;
for (const auto& Pair : ChunkMods)
{
Total += Pair.Value.Num();
}
return Total;
}
int32 UVoxelDiffLayer::GetModifiedChunkCount() const
{
FReadScopeLock Lock(ModsLock);
return ChunkMods.Num();
}
@@ -0,0 +1,54 @@
// VoxelForgeModule.cpp
// Module implementation - boilerplate
#include "VoxelForgeModule.h"
#include "VoxelStackSampler.h"
#include "VoxelGenerator.h"
#include "VoxelMarchingCubesMesher.h"
#include "Misc/CommandLine.h"
#include "Misc/Parse.h"
#if PLATFORM_WINDOWS
#include <math.h>
#endif
// This macro registers our module with Unreal
// "VoxelForge" must match the module name in VoxelForge.Build.cs and .uplugin
IMPLEMENT_MODULE(FVoxelForgeModule, VoxelForge)
void FVoxelForgeModule::StartupModule()
{
// Called when plugin loads
// Keep this diagnostic before any plugin-owned generation can begin. The UCRT may select
// its FMA3 math implementations during process startup; this switch lets the canonical export
// compare that path with the non-FMA3 implementation on the same machine.
#if PLATFORM_WINDOWS
const int32 Fma3Before = _get_FMA3_enable();
int32 RequestedFma3 = 0;
const bool bHasFma3Override = FParse::Value(
FCommandLine::Get(), TEXT("voxel.CrtFma3="), RequestedFma3);
if (bHasFma3Override)
{
_set_FMA3_enable(RequestedFma3 != 0 ? 1 : 0);
}
const int32 Fma3After = _get_FMA3_enable();
UE_LOG(LogTemp, Log,
TEXT("VoxelForge CRT FMA3 diagnostic: before=%d override=%s requested=%d after=%d"),
Fma3Before,
bHasFma3Override ? TEXT("yes") : TEXT("no"),
RequestedFma3,
Fma3After);
#endif
// Resolve the command line once while the module is starting, before any generation task is
// submitted, instead of lazily from worker-side density/mesher calls.
VoxelGenLOD::InitializeConsoleSwitches();
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
UE_LOG(LogTemp, Log, TEXT("VoxelForge module started!"));
}
void FVoxelForgeModule::ShutdownModule()
{
// Called when plugin unloads
FVoxelStackSampler::Get().Shutdown();
UE_LOG(LogTemp, Log, TEXT("VoxelForge module shutdown."));
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,497 @@
// VoxelHeightOpStack.cpp
// Les cinq opérateurs d'espace-hauteur de SurfaceWorld.
// The five height-space operators of SurfaceWorld.
//
// FIDÉLITÉ / FIDELITY
// Chaque corps est une transcription LITTÉRALE du bloc correspondant de
// `SampleSurfaceStructuralZ` / `ComputeSurfaceTerrainZ` — mêmes offsets, mêmes octaves, même ordre
// d'opérations flottantes. Depuis que `FPSemantics = Precise` est posé (AUDIT §C9), l'égalité
// BIT À BIT est atteignable et atteinte pour Maze et Slab : c'est donc la barre ici aussi, et
// `VoxelForge.OpStack.SurfaceHeightEquivalence` la vérifie.
//
// ⚠️ LE DÉTOUR PAR `FVector` EST DÉLIBÉRÉ, comme ailleurs dans ce refactor : `FractalNoise3D` prend
// un `FVector` (donc des DOUBLES en UE5) et re-descend en float. Passer directement des floats
// saute un arrondi. Reproduire le détour, c'est reproduire l'arrondi.
// The FVector round-trip is deliberate: FVector is double in UE5, so the original rounds through a
// double. Going straight through floats skips a rounding step.
#include "VoxelHeightOp.h"
#include "VoxelCaveMorphology.h" // VoxelHash::SeedOffset — AUDIT §C1 (bounded, site-salted)
#include "VoxelNoise.h" // VoxelNoise::FBM / Ridged / Perlin3D
#include "VoxelTypes.h" // SmoothStep01, VOXEL_NOISE_SCALE
namespace
{
//=========================================================================
// HELPERS — les mêmes enveloppes que VoxelGenerator.cpp, transcrites
//=========================================================================
// `FractalNoise3D` et `RidgedNoise3D` sont `static` dans VoxelGenerator.cpp, donc invisibles
// ici. Elles sont recopiées à l'identique plutôt qu'exportées : les exporter changerait leur
// contexte d'inlining, et sous /fp:precise comme sous /fp:fast la règle est la même — on ne
// touche à rien de ce qui entoure une expression flottante qu'on veut reproduire.
FORCEINLINE float HFractalNoise3D(const FVector& Position, int32 Octaves = 4,
float Lacunarity = 2.0f, float Persistence = 0.5f)
{
return VoxelNoise::FBM((float)Position.X, (float)Position.Y, (float)Position.Z,
Octaves, Lacunarity, Persistence);
}
FORCEINLINE float HRidgedNoise3D(const FVector& Position, int32 Octaves = 4,
float Lacunarity = 2.0f, float Persistence = 0.5f)
{
return VoxelNoise::Ridged((float)Position.X, (float)Position.Y, (float)Position.Z,
Octaves, Lacunarity, Persistence);
}
/** Transcription de `UVoxelGenerator::SampleRelief`. Champ [0,1] partagé avec la carte de
* biomes, pour que la géographie et le terrain qu'elle module restent d'accord. */
FORCEINLINE float HSampleRelief(float WorldX, float WorldY, uint32 SeedU,
float Frequency, float Contrast)
{
float R = HFractalNoise3D(FVector(
WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 7.3f),
WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 2.1f),
VoxelHash::SeedOffset(SeedU, 0.5f)), 2) * 0.5f + 0.5f; // [0,1]
R = FMath::Clamp((R - 0.5f) * Contrast + 0.5f, 0.0f, 1.0f);
return SmoothStep01(R);
}
//=========================================================================
// SOURCE — CHAMP STRUCTUREL / STRUCTURAL HEIGHT FIELD
//=========================================================================
// Continents + montagnes + détail, sous une frame de domain-warp. Produit les DEUX canaux.
class FStructuralHeightSource final : public IVoxelHeightOp
{
public:
FStructuralHeightSource(const FSurfaceGenerationParams& InP, int32 InSeed)
: P(InP), SeedU((uint32)InSeed) {}
void Eval(float WorldX, float WorldY, FVoxelHeightSample& InOut) const override
{
float M = 1.0f;
InOut.Height = SampleZ(WorldX, WorldY, M); // Replace : racine de pile
InOut.Relief = M;
}
/**
* Le champ nu, exposé parce que `FCliffHeightMod` doit le RÉ-ÉCHANTILLONNER en différences
* centrées. C'est une dépendance réelle du code d'origine (`ComputeSurfaceTerrainZ` appelle
* `SampleSurfaceStructuralZ` quatre fois de plus), pas un raccourci : la pente doit venir du
* champ STRUCTUREL, sans rétroaction des ops, sinon le cliff se nourrirait de lui-même.
*/
float SampleZ(float WorldX, float WorldY, float& OutM) const
{
const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ;
const float BottomZ = P.StrateBottomWorldZ;
const float GroundBase = BottomZ + H * P.BaseGroundRelative;
// Domain-warp des coords STRUCTURELLES (continents + montagnes). Le bruit de détail
// reste sur le vrai XY pour que les bosses fines restent nettes et décorrélées.
float QX = WorldX, QY = WorldY;
if (P.HeightWarpStrength > 0.0f)
{
const float WF = P.HeightWarpFrequency;
const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.31f), WorldY * WF + 4.2f, VoxelHash::SeedOffset(SeedU, 1.7f)));
const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 8.6f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.53f), VoxelHash::SeedOffset(SeedU, 2.9f)));
QX += wx * VOXEL_NOISE_SCALE * P.HeightWarpStrength;
QY += wy * VOXEL_NOISE_SCALE * P.HeightWarpStrength;
}
const float Relief = HSampleRelief(WorldX, WorldY, SeedU, P.ReliefFrequency, P.ReliefContrast);
const float M = FMath::Lerp(1.0f, Relief, P.ReliefStrength);
float Cont = HFractalNoise3D(FVector(
QX * P.ContinentFrequency + VoxelHash::SeedOffset(SeedU, 3.1f),
QY * P.ContinentFrequency + VoxelHash::SeedOffset(SeedU, 5.7f),
VoxelHash::SeedOffset(SeedU, 0.7f)), 4); // [-1,1]
float Detail = HFractalNoise3D(FVector(
WorldX * P.DetailFrequency + 11.0f,
WorldY * P.DetailFrequency + 22.0f,
VoxelHash::SeedOffset(SeedU, 1.3f)), 3); // [-1,1]
float Mountain = 0.0f;
if (P.MountainStrength > 0.0f)
{
float Ridge = HRidgedNoise3D(FVector(
QX * P.MountainFrequency + 99.0f,
QY * P.MountainFrequency + 77.0f,
VoxelHash::SeedOffset(SeedU, 0.9f)), 4); // [-1,1]
Ridge = Ridge * 0.5f + 0.5f; // [0,1] sommets
Mountain = Ridge * P.MountainStrength * M; // les montagnes ne montent qu'en haut relief
}
// Les plaines gardent une fraction du gonflement continental ; les hautes terres tout.
const float ContScale = FMath::Lerp(0.45f, 1.0f, M);
float Terrain = GroundBase
+ Cont * P.ElevationRange * 0.5f * ContScale
+ Mountain * P.ElevationRange
+ Detail * P.SurfaceRoughness;
OutM = M;
return Terrain;
}
// Une SOURCE pose l'altitude, elle ne la déplace pas : la notion de « déplacement max » ne
// s'applique pas. La borne d'une colonne se calcule à partir de la source elle-même
// (GroundBase ± ElevationRange ± SurfaceRoughness), pas ici — d'où FLT_MAX, honnête.
float MaxDisplacement() const override { return FLT_MAX; }
private:
FSurfaceGenerationParams P;
uint32 SeedU;
};
//=========================================================================
// MOD — FALAISE / CLIFF (raidissement conditionné par la pente)
//=========================================================================
class FCliffHeightMod final : public IVoxelHeightOp
{
public:
FCliffHeightMod(const FSurfaceGenerationParams& InP, const FStructuralHeightSource* InSrc)
: P(InP), Src(InSrc) {}
void Eval(float WorldX, float WorldY, FVoxelHeightSample& InOut) const override
{
if (P.CliffStrength <= 0.0f || Src == nullptr) { return; }
const float D = FMath::Max(P.CliffSampleDist, 0.5f);
float Ms; // relief scratch — on ne veut que les hauteurs
const float Zxp = Src->SampleZ(WorldX + D, WorldY, Ms);
const float Zxm = Src->SampleZ(WorldX - D, WorldY, Ms);
const float Zyp = Src->SampleZ(WorldX, WorldY + D, Ms);
const float Zym = Src->SampleZ(WorldX, WorldY - D, Ms);
const float dZdX = (Zxp - Zxm) / (2.0f * D);
const float dZdY = (Zyp - Zym) / (2.0f * D);
const float Slope = FMath::Sqrt(dZdX * dZdX + dZdY * dZdY);
const float Thr = FMath::Max(P.CliffSlopeThreshold, 0.05f);
const float SlopeGate = FMath::Clamp((Slope - Thr) / Thr, 0.0f, 1.0f);
if (SlopeGate > 0.0f)
{
const float Ref = 0.25f * (Zxp + Zxm + Zyp + Zym);
const float Gain = P.CliffStrength * SlopeGate * P.CliffSharpness;
InOut.Height += (InOut.Height - Ref) * Gain;
}
}
private:
FSurfaceGenerationParams P;
const FStructuralHeightSource* Src;
};
//=========================================================================
// MOD — TERRASSES / TERRACE (gaté par le relief : le canal Relief sert ICI)
//=========================================================================
class FTerraceHeightMod final : public IVoxelHeightOp
{
public:
explicit FTerraceHeightMod(const FSurfaceGenerationParams& InP) : P(InP) {}
void Eval(float, float, FVoxelHeightSample& InOut) const override
{
if (P.TerraceStrength <= 0.0f || P.TerraceHeight <= 0.0f) { return; }
const float StepH = P.TerraceHeight;
const float T = InOut.Height / StepH;
const float K = FMath::FloorToFloat(T);
const float Frac = T - K;
const float W = FMath::Lerp(0.5f, 0.03f, FMath::Clamp(P.TerraceHardness, 0.0f, 1.0f));
const float Fs = SmoothStep01(FMath::Clamp((Frac - (0.5f - W)) / (2.0f * W), 0.0f, 1.0f));
const float Stepped = (K + Fs) * StepH;
// `* InOut.Relief` : c'est le `* M` de l'original — la raison d'être du second canal.
InOut.Height = FMath::Lerp(InOut.Height, Stepped, P.TerraceStrength * InOut.Relief);
}
// Le terrace interpole VERS une hauteur quantifiée : l'écart ne dépasse jamais un palier.
float MaxDisplacement() const override
{
return (P.TerraceStrength > 0.0f) ? FMath::Max(P.TerraceHeight, 0.0f) : 0.0f;
}
private:
FSurfaceGenerationParams P;
};
//=========================================================================
// MOD — LIGNES DE STRATES / LAYER LINES
//=========================================================================
class FLayerLineHeightMod final : public IVoxelHeightOp
{
public:
explicit FLayerLineHeightMod(const FSurfaceGenerationParams& InP) : P(InP) {}
void Eval(float, float, FVoxelHeightSample& InOut) const override
{
if (P.LayerLineDepth <= 0.0f || P.LayerLineSpacing <= 0.0f) { return; }
const float Phase = InOut.Height * (2.0f * PI / P.LayerLineSpacing);
InOut.Height -= VoxelMath::DetSin(Phase) * P.LayerLineDepth;
}
// `sin` ∈ [-1,1] ⇒ borne exacte.
float MaxDisplacement() const override
{
return (P.LayerLineSpacing > 0.0f) ? FMath::Max(P.LayerLineDepth, 0.0f) : 0.0f;
}
private:
FSurfaceGenerationParams P;
};
//=========================================================================
// MOD — PLAGE / BEACH (aplatissement vers la ligne d'eau)
//=========================================================================
class FBeachHeightMod final : public IVoxelHeightOp
{
public:
explicit FBeachHeightMod(const FSurfaceGenerationParams& InP) : P(InP) {}
void Eval(float, float, FVoxelHeightSample& InOut) const override
{
// Le niveau d'eau est GLOBAL à la strate (forcé depuis la strate) pour que le plan
// d'eau reste continu — d'où le calcul depuis les bornes de strate, pas depuis un param
// par biome.
const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ;
const float WaterZ = P.StrateBottomWorldZ + H * P.WaterLevelRelative;
if (P.WaterLevelRelative <= 0.0f || P.BeachWidth <= 0.0f) { return; }
const float DAbs = FMath::Abs(InOut.Height - WaterZ);
if (DAbs < P.BeachWidth)
{
float T = SmoothStep01(DAbs / P.BeachWidth);
InOut.Height = FMath::Lerp(WaterZ, InOut.Height, T);
}
}
// N'agit que dans `BeachWidth` de l'eau, et ne fait qu'y RAPPROCHER.
float MaxDisplacement() const override
{
return (P.WaterLevelRelative > 0.0f) ? FMath::Max(P.BeachWidth, 0.0f) : 0.0f;
}
private:
FSurfaceGenerationParams P;
};
//=========================================================================
// SOURCE — LE CIEL / SKY CAP (c'est une ALTITUDE, donc c'est un op de hauteur)
//=========================================================================
// `ComputeSurfaceCeiling` rend un Z, exactement comme le terrain. `OPSTACK-DECOMPOSITION §5` le
// range en `FSkyCapSource` côté DENSITÉ (« Subtract »), mais c'est le même glissement que pour
// les ops de terrain : ce que la fonction produit est une hauteur, et la soustraction n'arrive
// qu'après, dans le combine. Le mettre ici lui donne gratuitement l'invariance de fenêtre
// testée, la pureté XY garantie par le type, et le cache de colonne.
// The sky cap returns a Z, so it belongs in height space; the subtraction happens later, in the
// density-side combine.
class FSkyCapHeightSource final : public IVoxelHeightOp
{
public:
FSkyCapHeightSource(const FSurfaceGenerationParams& InP, int32 InSeed)
: P(InP), SeedU((uint32)InSeed) {}
void Eval(float WorldX, float WorldY, FVoxelHeightSample& InOut) const override
{
const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ;
float CeilZ = P.StrateBottomWorldZ + H * P.CeilingRelative;
// Domain-warp des coords larges/ridge (miroir du HeightWarp du sol). Les bosses fines
// restent sur le vrai XY pour rester nettes et décorrélées. 0 ⇒ pas de warp.
float QX = WorldX, QY = WorldY;
if (P.CeilingWarpStrength > 0.0f)
{
const float WF = P.CeilingWarpFrequency;
const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.71f), WorldY * WF + 2.3f, VoxelHash::SeedOffset(SeedU, 3.3f)));
const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 6.1f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.19f), VoxelHash::SeedOffset(SeedU, 4.7f)));
QX += wx * VOXEL_NOISE_SCALE * P.CeilingWarpStrength;
QY += wy * VOXEL_NOISE_SCALE * P.CeilingWarpStrength;
}
// Gonflement large SIGNÉ : monte/descend toute la voûte.
if (P.CeilingUndulation > 0.0f)
{
const float Swell = HFractalNoise3D(FVector(
QX * P.CeilingUndulationFrequency + VoxelHash::SeedOffset(SeedU, 1.9f),
QY * P.CeilingUndulationFrequency + 13.0f,
VoxelHash::SeedOffset(SeedU, 0.5f)), 3); // [-1,1]
CeilZ += Swell * VOXEL_NOISE_SCALE * P.CeilingUndulation;
}
// Pendage vers le BAS uniquement : tout est >= 0, donc rien ne perce vers le haut dans
// le seal. Bosses fines + lames ridgées s'additionnent.
float Hang = 0.0f;
if (P.CeilingRoughness > 0.0f)
{
Hang += FMath::Abs(HFractalNoise3D(FVector(
WorldX * P.CeilingRoughnessFrequency + 5.0f,
WorldY * P.CeilingRoughnessFrequency + 6.0f,
VoxelHash::SeedOffset(SeedU, 2.1f)), 3)) * VOXEL_NOISE_SCALE * P.CeilingRoughness;
}
if (P.CeilingRidgeStrength > 0.0f)
{
float Ridge = HRidgedNoise3D(FVector(
QX * P.CeilingRidgeFrequency + 31.0f,
QY * P.CeilingRidgeFrequency + 47.0f,
VoxelHash::SeedOffset(SeedU, 1.1f)), 4); // [-1,1]
Ridge = Ridge * 0.5f + 0.5f; // [0,1] lignes de crête pendantes
Hang += Ridge * P.CeilingRidgeStrength;
}
InOut.Height = CeilZ - Hang; // Replace : racine de sa propre pile
// Relief laissé intact : le ciel n'en produit pas et personne ne le lui demande.
}
float MaxDisplacement() const override { return FLT_MAX; } // source, pas modificateur
private:
FSurfaceGenerationParams P;
uint32 SeedU;
};
//=========================================================================
// COMBINER `Mask` — MÉLANGE DE BIOMES / BIOME BLEND
//=========================================================================
// Une pile complète par biome ; le champ dit lequel domine ; on interpole les HAUTEURS.
//
// ⚠️ Chaque pile calcule SON PROPRE relief `M` en interne et l'utilise pour son propre gate de
// terrace — exactement comme l'original, où `ComputeSurfaceTerrainZ(X, Y, *PD)` et
// `(…, *PN)` sont deux appels complets et indépendants dont seules les SORTIES sont mêlées.
// Le canal `Relief` qui ressort ici est celui du DOMINANT : il est informatif, personne en aval
// ne s'en sert pour re-gater quoi que ce soit.
//
// Each biome stack computes its own relief internally and gates its own terrace with it, exactly
// as the original makes two independent full calls and blends only the OUTPUTS.
class FBiomeBlendHeightSource final : public IVoxelHeightOp
{
public:
FBiomeBlendHeightSource(const TArray<FSurfaceGenerationParams>& PerBiome, int32 Seed,
const IVoxelBiomeField* InField, bool bCeilingOnly)
: Field(InField)
{
Stacks.Reserve(PerBiome.Num());
for (const FSurfaceGenerationParams& BP : PerBiome)
{
FVoxelHeightStack S;
if (bCeilingOnly) { VoxelHeightOps::BuildSurfaceCeilingStack(S, BP, Seed); }
else { VoxelHeightOps::BuildSurfaceHeightStack(S, BP, Seed); }
Stacks.Add(MoveTemp(S));
}
bBlend = !bCeilingOnly; // le plafond SÉLECTIONNE, il ne mélange pas
}
void Eval(float WorldX, float WorldY, FVoxelHeightSample& InOut) const override
{
if (Stacks.Num() == 0) { return; }
FVoxelBiomeWeights W;
if (Field) { W = Field->SampleAt(WorldX, WorldY); }
const int32 D = Stacks.IsValidIndex(W.Dominant) ? W.Dominant : 0;
InOut = Stacks[D].EvalSample(WorldX, WorldY);
// Le plafond ne se mélange pas (voir la fabrique) ; le sol si, et seulement dans la
// bande de frontière où le poids est non nul.
if (bBlend && W.NeighborWeight > 0.0f && Stacks.IsValidIndex(W.Neighbor))
{
const float HN = Stacks[W.Neighbor].EvalHeight(WorldX, WorldY);
InOut.Height = FMath::Lerp(InOut.Height, HN, W.NeighborWeight);
}
}
float MaxDisplacement() const override { return FLT_MAX; } // source composite
private:
TArray<FVoxelHeightStack> Stacks;
const IVoxelBiomeField* Field;
bool bBlend = true;
};
} // ⚠️ FIN DU NAMESPACE ANONYME — TOUT NOUVEL OPÉRATEUR SE MET AU-DESSUS DE CETTE LIGNE.
// En dessous commence `namespace VoxelHeightOps` (les fabriques). Y insérer une classe la sort
// de la liaison interne, et l'accolade qu'on ajoute avec elle ne ferme rien → C2059. Erreur
// commise DEUX fois (7cd2bed, puis à nouveau ici) en s'ancrant sur la bannière « FABRIQUES »,
// qui est de l'autre côté de cette accolade.
// END OF THE ANONYMOUS NAMESPACE — new operators go ABOVE this line. Anchoring on the FACTORIES
// banner below puts them outside it, and the brace added with them closes nothing.
//=============================================================================
// FABRIQUES / FACTORIES
//=============================================================================
namespace VoxelHeightOps
{
TUniquePtr<IVoxelHeightOp> MakeBiomeBlendHeightSource(
const TArray<FSurfaceGenerationParams>& PerBiomeParams, int32 Seed,
const IVoxelBiomeField* Field)
{
return MakeUnique<FBiomeBlendHeightSource>(PerBiomeParams, Seed, Field, /*bCeilingOnly*/false);
}
TUniquePtr<IVoxelHeightOp> MakeBiomeSelectCeilingSource(
const TArray<FSurfaceGenerationParams>& PerBiomeParams, int32 Seed,
const IVoxelBiomeField* Field)
{
return MakeUnique<FBiomeBlendHeightSource>(PerBiomeParams, Seed, Field, /*bCeilingOnly*/true);
}
TUniquePtr<IVoxelHeightOp> MakeSkyCapHeightSource(const FSurfaceGenerationParams& P, int32 Seed)
{
return MakeUnique<FSkyCapHeightSource>(P, Seed);
}
void BuildSurfaceCeilingStack(FVoxelHeightStack& OutStack, const FSurfaceGenerationParams& P, int32 Seed)
{
// Un seul op aujourd'hui — et c'est une information, pas un manque : le plafond n'a pas
// d'équivalent des quatre modificateurs du sol. Le jour où on veut des terrasses au
// plafond, on ajoute la ligne ; c'est exactement le genre de composition que le refactor
// existe pour rendre possible.
OutStack.Add(MakeSkyCapHeightSource(P, Seed));
}
TUniquePtr<IVoxelHeightOp> MakeStructuralHeightSource(const FSurfaceGenerationParams& P, int32 Seed,
const IVoxelHeightOp** OutSource)
{
TUniquePtr<FStructuralHeightSource> Src = MakeUnique<FStructuralHeightSource>(P, Seed);
if (OutSource) { *OutSource = Src.Get(); }
return Src;
}
TUniquePtr<IVoxelHeightOp> MakeCliffHeightMod(const FSurfaceGenerationParams& P,
const IVoxelHeightOp* StructuralSource)
{
// `static_cast` plutôt que `Cast<>` : ce ne sont pas des UObject, et le contrat de la
// fabrique est qu'on lui rend exactement le pointeur sorti de MakeStructuralHeightSource.
return MakeUnique<FCliffHeightMod>(
P, static_cast<const FStructuralHeightSource*>(StructuralSource));
}
TUniquePtr<IVoxelHeightOp> MakeTerraceHeightMod(const FSurfaceGenerationParams& P)
{
return MakeUnique<FTerraceHeightMod>(P);
}
TUniquePtr<IVoxelHeightOp> MakeLayerLineHeightMod(const FSurfaceGenerationParams& P)
{
return MakeUnique<FLayerLineHeightMod>(P);
}
TUniquePtr<IVoxelHeightOp> MakeBeachHeightMod(const FSurfaceGenerationParams& P)
{
return MakeUnique<FBeachHeightMod>(P);
}
void BuildSurfaceHeightStack(FVoxelHeightStack& OutStack, const FSurfaceGenerationParams& P, int32 Seed)
{
// L'ORDRE EST CELUI DE `ComputeSurfaceTerrainZ`, et il porte du sens :
// le cliff raidit le champ brut, le terrace quantifie le résultat raidi, les lignes de
// strates se posent dessus, et la plage écrase tout près de l'eau.
const IVoxelHeightOp* Structural = nullptr;
OutStack.Add(MakeStructuralHeightSource(P, Seed, &Structural));
OutStack.Add(MakeCliffHeightMod(P, Structural));
OutStack.Add(MakeTerraceHeightMod(P));
OutStack.Add(MakeLayerLineHeightMod(P));
OutStack.Add(MakeBeachHeightMod(P));
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,135 @@
#include "VoxelSeasonAsset.h"
#include "VoxelSeasonManifest.h"
#include "VoxelDensityOpStack.h"
#if WITH_EDITOR
#include "Misc/FileHelper.h"
#endif
namespace
{
bool ValidateRuntimeRecipes(const FVoxelSeasonManifest& Manifest, FString& OutReport)
{
for (const FVoxelSeasonStrate& Strate : Manifest.Strates)
{
if (!Strate.bUsesRecipe) continue;
FVoxelOpStack ValidationStack;
FVoxelOpContext ValidationContext;
FString BuildError;
if (!VF_BuildSeasonStrateStack(
Strate, Manifest.OriginSpineRadius, nullptr,
ValidationStack, ValidationContext, &BuildError))
{
OutReport = FString::Printf(
TEXT("season slot %d recipe is not materialisable: %s"),
Strate.DepthIndex, *BuildError);
return false;
}
}
return true;
}
}
FPrimaryAssetId UVoxelSeasonAsset::GetPrimaryAssetId() const
{
return FPrimaryAssetId(TEXT("VoxelSeason"), GetFName());
}
bool UVoxelSeasonAsset::LoadManifest(FVoxelSeasonManifest& OutManifest, FString& OutReport) const
{
if (ManifestJson.IsEmpty())
{
OutReport = TEXT("season asset contains no imported manifest JSON");
OutManifest = FVoxelSeasonManifest();
return false;
}
if (!VF_DeserializeVoxelSeasonManifest(ManifestJson, OutManifest, OutReport))
{
return false;
}
if (!OutManifest.ContentHash.Equals(ManifestContentHash, ESearchCase::IgnoreCase)
|| OutManifest.SchemaVersion != SchemaVersion
|| OutManifest.Season != SeasonNumber
|| OutManifest.Seed != SeasonSeed
|| FMemory::Memcmp(&OutManifest.OriginSpineRadius,
&OriginSpineRadius, sizeof(float)) != 0
|| FMemory::Memcmp(&OutManifest.WorldRadiusVoxels,
&WorldRadiusVoxels, sizeof(float)) != 0)
{
OutReport = TEXT("season asset metadata does not match its embedded manifest; re-import the JSON");
OutManifest.bValid = false;
OutManifest.Error = OutReport;
return false;
}
if (!ValidateRuntimeRecipes(OutManifest, OutReport))
{
OutManifest.bValid = false;
OutManifest.Error = OutReport;
return false;
}
return true;
}
bool UVoxelSeasonAsset::SetManifestJson(const FString& InManifestJson, FString& OutReport)
{
FVoxelSeasonManifest Parsed;
if (!VF_DeserializeVoxelSeasonManifest(InManifestJson, Parsed, OutReport))
{
return false;
}
if (!ValidateRuntimeRecipes(Parsed, OutReport))
{
return false;
}
Modify();
ManifestJson = InManifestJson;
SchemaVersion = Parsed.SchemaVersion;
SeasonNumber = Parsed.Season;
SeasonSeed = Parsed.Seed;
OriginSpineRadius = Parsed.OriginSpineRadius;
WorldRadiusVoxels = Parsed.WorldRadiusVoxels;
ManifestContentHash = Parsed.ContentHash;
ReferencedDefinitions.Reset();
TSet<FString> SeenDefinitionPaths;
for (const FVoxelSeasonStrate& Strate : Parsed.Strates)
{
if (!Strate.SourceDefinitionPath.IsEmpty()
&& !SeenDefinitionPaths.Contains(Strate.SourceDefinitionPath))
{
SeenDefinitionPaths.Add(Strate.SourceDefinitionPath);
ReferencedDefinitions.Add(TSoftObjectPtr<UVoxelStrateDefinition>(
FSoftObjectPath(Strate.SourceDefinitionPath)));
}
}
MarkPackageDirty();
OutReport = FString::Printf(
TEXT("imported season %d seed %d (%d strates, hash %s)"),
SeasonNumber, SeasonSeed, Parsed.Strates.Num(), *ManifestContentHash);
return true;
}
void UVoxelSeasonAsset::ImportSeasonManifestJson()
{
#if WITH_EDITOR
FString JsonText;
FString Report;
if (SourceManifestJson.FilePath.IsEmpty()
|| !FFileHelper::LoadFileToString(JsonText, *SourceManifestJson.FilePath))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelSeasonAsset] Could not read manifest JSON '%s'."),
*SourceManifestJson.FilePath);
return;
}
if (!SetManifestJson(JsonText, Report))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelSeasonAsset] Import failed: %s"), *Report);
return;
}
UE_LOG(LogTemp, Log, TEXT("[VoxelSeasonAsset] %s"), *Report);
#else
UE_LOG(LogTemp, Error, TEXT("[VoxelSeasonAsset] JSON import is editor-only."));
#endif
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,51 @@
#include "VoxelSettings.h"
#include "VoxelSeasonAsset.h"
int32 UVoxelSettings::GetEffectiveWorldSeed() const
{
if (!Season.IsNull())
{
if (const UVoxelSeasonAsset* Asset = Season.LoadSynchronous())
{
return Asset->GetSeasonSeed();
}
}
return Seed;
}
float UVoxelSettings::GetEffectiveOriginSpineRadius() const
{
if (!Season.IsNull())
{
if (const UVoxelSeasonAsset* Asset = Season.LoadSynchronous())
{
return Asset->GetOriginSpineRadius();
}
}
return OriginSpineRadius;
}
float UVoxelSettings::GetEffectiveWorldRadiusVoxels() const
{
if (!Season.IsNull())
{
if (const UVoxelSeasonAsset* Asset = Season.LoadSynchronous())
{
return Asset->GetWorldRadiusVoxels();
}
}
return WorldRadiusVoxels;
}
int32 UVoxelSettings::GetEffectiveSeasonNumber() const
{
if (!Season.IsNull())
{
if (const UVoxelSeasonAsset* Asset = Season.LoadSynchronous())
{
return Asset->GetSeasonNumber();
}
}
return CurrentSeason;
}
@@ -0,0 +1,23 @@
// VoxelStats.cpp
// Definitions for the VoxelForge runtime statistics.
// Définitions des statistiques runtime de VoxelForge.
#include "VoxelStats.h"
DEFINE_STAT(STAT_VoxelForgeTilesClassified);
DEFINE_STAT(STAT_VoxelForgeTilesSkippedAllSolid);
DEFINE_STAT(STAT_VoxelForgeTilesSkippedAllAir);
DEFINE_STAT(STAT_VoxelForgeTilesMeshed);
DEFINE_STAT(STAT_VoxelForgeTilesOpStackSolid);
DEFINE_STAT(STAT_VoxelForgeTilesOpStackAir);
DEFINE_STAT(STAT_VoxelForgeCaveMixOutOfLayout);
DEFINE_STAT(STAT_VoxelForgeCaveMixGap);
DEFINE_STAT(STAT_VoxelForgeCaveMixSurfaceWorld);
DEFINE_STAT(STAT_VoxelForgeCaveBailTwoCaveSlots);
DEFINE_STAT(STAT_VoxelForgeCaveBailParams);
DEFINE_STAT(STAT_VoxelForgeCaveBailStackVerdict);
DEFINE_STAT(STAT_VoxelForgeCaveBailFoldConflict);
DEFINE_STAT(STAT_VoxelForgeCaveBailDisturbance);
DEFINE_STAT(STAT_VoxelForgeCaveBailNoStack);
DEFINE_STAT(STAT_VoxelForgeColumnMemoHit);
DEFINE_STAT(STAT_VoxelForgeColumnMemoMiss);
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,79 @@
// Serialized strate-definition migrations.
#include "VoxelStrateDefinition.h"
#include "Serialization/CustomVersion.h"
#include "VoxelPassageGeometry.h"
namespace
{
// The field name is retained for package compatibility, but version 1 changes its meaning
// from a raw tangent/gradient to authored degrees. This GUID is local to the plugin.
const FGuid GVoxelForgeStrateSerializationVersion(
0xA76D5E31, 0x4B2C4F9A, 0xB8C1D204, 0x6E9F7738);
constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = 1;
FCustomVersionRegistration GVoxelForgeStrateSerializationRegistration(
GVoxelForgeStrateSerializationVersion,
GVoxelForgeStrateGentleSlopeDegreesVersion,
TEXT("VoxelForgeStrateSerialization"));
float LegacyGradientToDegrees(float LegacyGradient)
{
if (!VoxelMath::IsFinite(LegacyGradient) || LegacyGradient <= 0.0f)
{
return 0.0f;
}
if (LegacyGradient >= 4096.0f)
{
return VoxelPassageGeometry::TunnelFloorGentleSlopeMaximumDegrees;
}
// Invert the old gradient without atan/tan. The same deterministic sine/cosine helper
// used by generation makes this migration independent of the host CRT.
float LowDegrees = 0.0f;
float HighDegrees = VoxelPassageGeometry::TunnelFloorGentleSlopeMaximumDegrees;
for (int32 Iteration = 0; Iteration < 32; ++Iteration)
{
const float MidDegrees = (LowDegrees + HighDegrees) * 0.5f;
float SinValue = 0.0f;
float CosValue = 0.0f;
VoxelMath::DetSinCos(
SinValue, CosValue, MidDegrees * (PI / 180.0f));
const float MidGradient = FMath::Min(
FMath::Abs(SinValue) / FMath::Max(FMath::Abs(CosValue), 1.0e-3f),
4096.0f);
if (MidGradient < LegacyGradient)
{
LowDegrees = MidDegrees;
}
else
{
HighDegrees = MidDegrees;
}
}
return FMath::Clamp(
(LowDegrees + HighDegrees) * 0.5f,
0.0f,
VoxelPassageGeometry::TunnelFloorGentleSlopeMaximumDegrees);
}
}
void UVoxelStrateDefinition::PostLoad()
{
Super::PostLoad();
// The field remains reflected solely so old packages deserialize without shifting the
// following members. Tunnel geometry owns the floor now, so every serialized legacy value
// is normalized to the ignored compatibility default before any runtime copy is made.
GenerationParams.bTunnelFloorEnabled = true;
if (GetLinkerCustomVersion(GVoxelForgeStrateSerializationVersion)
>= GVoxelForgeStrateGentleSlopeDegreesVersion)
{
return;
}
const float LegacyGradient = GenerationParams.TunnelFloorGentleSlopeThreshold;
GenerationParams.TunnelFloorGentleSlopeThreshold =
LegacyGradientToDegrees(LegacyGradient);
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,87 @@
// VoxelTerrainOpDefinition.cpp
// Applies a terrain operation's parameters to the generation params struct.
#include "VoxelTerrainOpDefinition.h"
void UVoxelTerrainOpDefinition::ApplyTo(FStrateGenerationParams& OutParams, float Weight) const
{
// Weight scales the "primary" field of each op type — the one that controls
// whether the op is active and how strong it is. Other fields (radii, spacing,
// ratios) are copied directly since they define shape, not intensity.
switch (Type)
{
case EVoxelTerrainOpType::Terrace:
// TerraceStepHeight is the activation field (0 = disabled)
OutParams.TerraceStepHeight = TerraceStepHeight * Weight;
OutParams.TerraceHardness = TerraceHardness;
OutParams.TerraceNoiseDisplacement = TerraceNoiseDisplacement;
break;
case EVoxelTerrainOpType::LayerLines:
// LayerLineSpacing is the activation field (0 = disabled)
OutParams.LayerLineSpacing = LayerLineSpacing * Weight;
OutParams.LayerLineDepth = LayerLineDepth;
break;
case EVoxelTerrainOpType::Ribbing:
// RibbingSpacing is the activation field (0 = disabled)
OutParams.RibbingSpacing = RibbingSpacing * Weight;
OutParams.RibbingDepth = RibbingDepth;
break;
case EVoxelTerrainOpType::Cliff:
OutParams.CliffStrength = CliffStrength * Weight;
break;
case EVoxelTerrainOpType::Scallop:
OutParams.ScallopStrength = ScallopStrength * Weight;
OutParams.ScallopFrequency = ScallopFrequency;
break;
case EVoxelTerrainOpType::Overhang:
OutParams.OverhangStrength = OverhangStrength * Weight;
OutParams.OverhangDepth = OverhangDepth;
OutParams.OverhangFrequency = OverhangFrequency;
break;
case EVoxelTerrainOpType::Arch:
OutParams.ArchDensity = ArchDensity * Weight;
OutParams.ArchMinRadius = ArchMinRadius;
OutParams.ArchMaxRadius = ArchMaxRadius;
break;
case EVoxelTerrainOpType::Column:
OutParams.ColumnDensity = ColumnDensity * Weight;
OutParams.ColumnMinRadius = ColumnMinRadius;
OutParams.ColumnMaxRadius = ColumnMaxRadius;
break;
case EVoxelTerrainOpType::Pit:
OutParams.PitDensity = PitDensity * Weight;
OutParams.PitMinRadius = PitMinRadius;
OutParams.PitMaxRadius = PitMaxRadius;
OutParams.PitDepth = PitDepth;
break;
case EVoxelTerrainOpType::Chimney:
OutParams.ChimneyDensity = ChimneyDensity * Weight;
OutParams.ChimneyMinRadius = ChimneyMinRadius;
OutParams.ChimneyMaxRadius = ChimneyMaxRadius;
OutParams.ChimneyHeight = ChimneyHeight;
break;
case EVoxelTerrainOpType::Dome:
OutParams.DomeDensity = DomeDensity * Weight;
OutParams.DomeMinRadius = DomeMinRadius;
OutParams.DomeMaxRadius = DomeMaxRadius;
OutParams.DomeHeightRatio = DomeHeightRatio;
break;
case EVoxelTerrainOpType::Pinch:
OutParams.PinchDensity = PinchDensity * Weight;
OutParams.PinchStrength = PinchStrength;
OutParams.PinchLength = PinchLength;
break;
}
}
@@ -0,0 +1,88 @@
// VoxelWormField.cpp
// Shared deterministic exact worm-field evaluation.
#include "VoxelWormField.h"
#include "VoxelCaveMorphology.h" // VoxelHash::SeedOffset
#include "VoxelNoise.h"
#include "VoxelTypes.h" // VOXEL_NOISE_SCALE
namespace
{
FORCEINLINE FVector3f MakeWormNoiseBase(
float WorldX,
float WorldY,
float WorldZ,
const VoxelWormField::FParameters& Parameters)
{
const float EffectiveZ = (Parameters.VerticalScale != 1.0f
&& Parameters.VerticalScale > 0.0f)
? WorldZ / Parameters.VerticalScale : WorldZ;
const float WormZFrequency = Parameters.Frequency * Parameters.HorizontalBias;
return FVector3f(
WorldX * Parameters.Frequency
+ VoxelHash::SeedOffset(Parameters.Seed, 1.0f),
WorldY * Parameters.Frequency
+ VoxelHash::SeedOffset(Parameters.Seed, 1.7f),
EffectiveZ * WormZFrequency
+ VoxelHash::SeedOffset(Parameters.Seed, 2.3f));
}
FORCEINLINE float EvaluateN1Field(
float WorldX,
float WorldY,
float WorldZ,
const VoxelWormField::FParameters& Parameters,
float NoiseScale)
{
const FVector3f NoiseBase = MakeWormNoiseBase(
WorldX, WorldY, WorldZ, Parameters);
return FMath::Abs(VoxelNoise::Perlin3D(NoiseBase) * NoiseScale);
}
FORCEINLINE float EvaluateN2Field(
float WorldX,
float WorldY,
float WorldZ,
const VoxelWormField::FParameters& Parameters,
float NoiseScale)
{
const FVector3f NoiseBase = MakeWormNoiseBase(
WorldX, WorldY, WorldZ, Parameters);
const FVector3f N2Position = NoiseBase + FVector3f(137.0f, 259.0f, 431.0f);
return FMath::Abs(VoxelNoise::Perlin3D(N2Position) * NoiseScale);
}
FORCEINLINE float EvaluateExactField(
float WorldX,
float WorldY,
float WorldZ,
float Threshold,
const VoxelWormField::FParameters& Parameters)
{
const float NoiseScale = VOXEL_NOISE_SCALE;
const float N1 = EvaluateN1Field(
WorldX, WorldY, WorldZ, Parameters, NoiseScale);
// N2 is non-negative, so this preserves the exact evaluator's short-circuit.
if (N1 >= Threshold)
{
return N1;
}
return N1 + EvaluateN2Field(
WorldX, WorldY, WorldZ, Parameters, NoiseScale);
}
}
namespace VoxelWormField
{
float Evaluate(
float WorldX,
float WorldY,
float WorldZ,
float Threshold,
const FParameters& Parameters)
{
return EvaluateExactField(WorldX, WorldY, WorldZ, Threshold, Parameters);
}
}
@@ -0,0 +1,344 @@
// MarchingCubesTables.h
// Standard lookup tables for the Marching Cubes algorithm
//
// These tables are reference data — everyone uses the same ones.
// Originally from Paul Bourke's Marching Cubes implementation.
//
// CUBE CORNER LAYOUT:
// -------------------
// 4-------5
// /| /|
// / | / |
// 7-------6 |
// | 0----|--1
// | / | /
// |/ |/
// 3-------2
//
// Corner indices and their positions relative to (X, Y, Z):
// 0: (0, 0, 0) 4: (0, 0, 1)
// 1: (1, 0, 0) 5: (1, 0, 1)
// 2: (1, 1, 0) 6: (1, 1, 1)
// 3: (0, 1, 0) 7: (0, 1, 1)
//
// EDGE INDICES:
// -------------
// Edge 0: corner 0 - corner 1 (bottom front)
// Edge 1: corner 1 - corner 2 (bottom right)
// Edge 2: corner 2 - corner 3 (bottom back)
// Edge 3: corner 3 - corner 0 (bottom left)
// Edge 4: corner 4 - corner 5 (top front)
// Edge 5: corner 5 - corner 6 (top right)
// Edge 6: corner 6 - corner 7 (top back)
// Edge 7: corner 7 - corner 4 (top left)
// Edge 8: corner 0 - corner 4 (vertical left-front)
// Edge 9: corner 1 - corner 5 (vertical right-front)
// Edge 10: corner 2 - corner 6 (vertical right-back)
// Edge 11: corner 3 - corner 7 (vertical left-back)
#pragma once
#include "CoreMinimal.h"
// EdgeTable[caseIndex] = 12-bit bitmask
// Each bit tells you if that edge is intersected by the surface.
// Example: EdgeTable[1] = 0x109 = 0000 0001 0000 1001
// → edges 0, 3, and 8 are intersected
static const int32 MCEdgeTable[256] = {
0x000, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
0x190, 0x099, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90,
0x230, 0x339, 0x033, 0x13a, 0x636, 0x73f, 0x435, 0x53c,
0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
0x3a0, 0x2a9, 0x1a3, 0x0aa, 0x7a6, 0x6af, 0x5a5, 0x4ac,
0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0,
0x460, 0x569, 0x663, 0x76a, 0x066, 0x16f, 0x265, 0x36c,
0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60,
0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0x0ff, 0x3f5, 0x2fc,
0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x055, 0x15c,
0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950,
0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0x0cc,
0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0,
0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc,
0x0cc, 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c,
0x15c, 0x055, 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650,
0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
0x2fc, 0x3f5, 0x0ff, 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0,
0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c,
0x36c, 0x265, 0x16f, 0x066, 0x76a, 0x663, 0x569, 0x460,
0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac,
0x4ac, 0x5a5, 0x6af, 0x7a6, 0x0aa, 0x1a3, 0x2a9, 0x3a0,
0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c,
0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x033, 0x339, 0x230,
0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x099, 0x190,
0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x000
};
// TriTable[caseIndex][up to 16]
// Lists edge indices that form triangles, in groups of 3.
// -1 terminates the list.
// Example: TriTable[1] = {0, 8, 3, -1, ...}
// → one triangle using edges 0, 8, and 3
static const int32 MCTriTable[256][16] = {
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 2, 8, 3, 2, 10, 8, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1},
{ 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 11, 2, 8, 11, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 11, 2, 1, 9, 11, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1},
{ 3, 10, 1, 11, 10, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 10, 1, 0, 8, 10, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1},
{ 3, 9, 0, 3, 11, 9, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1},
{ 9, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 3, 0, 7, 3, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 1, 9, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 1, 9, 4, 7, 1, 7, 3, 1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 4, 7, 3, 0, 4, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1},
{ 9, 2, 10, 9, 0, 2, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1},
{ 2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4, -1, -1, -1, -1},
{ 8, 4, 7, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{11, 4, 7, 11, 2, 4, 2, 0, 4, -1, -1, -1, -1, -1, -1, -1},
{ 9, 0, 1, 8, 4, 7, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1},
{ 4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1, -1, -1, -1, -1},
{ 3, 10, 1, 3, 11, 10, 7, 8, 4, -1, -1, -1, -1, -1, -1, -1},
{ 1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4, -1, -1, -1, -1},
{ 4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3, -1, -1, -1, -1},
{ 4, 7, 11, 4, 11, 9, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1},
{ 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 5, 4, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 5, 4, 1, 5, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 8, 5, 4, 8, 3, 5, 3, 1, 5, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 0, 8, 1, 2, 10, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1},
{ 5, 2, 10, 5, 4, 2, 4, 0, 2, -1, -1, -1, -1, -1, -1, -1},
{ 2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8, -1, -1, -1, -1},
{ 9, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 11, 2, 0, 8, 11, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1},
{ 0, 5, 4, 0, 1, 5, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1},
{ 2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5, -1, -1, -1, -1},
{10, 3, 11, 10, 1, 3, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1},
{ 4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10, -1, -1, -1, -1},
{ 5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3, -1, -1, -1, -1},
{ 5, 4, 8, 5, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1},
{ 9, 7, 8, 5, 7, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 3, 0, 9, 5, 3, 5, 7, 3, -1, -1, -1, -1, -1, -1, -1},
{ 0, 7, 8, 0, 1, 7, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1},
{ 1, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 7, 8, 9, 5, 7, 10, 1, 2, -1, -1, -1, -1, -1, -1, -1},
{10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3, -1, -1, -1, -1},
{ 8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2, -1, -1, -1, -1},
{ 2, 10, 5, 2, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1},
{ 7, 9, 5, 7, 8, 9, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1},
{ 9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11, -1, -1, -1, -1},
{ 2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7, -1, -1, -1, -1},
{11, 2, 1, 11, 1, 7, 7, 1, 5, -1, -1, -1, -1, -1, -1, -1},
{ 9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11, -1, -1, -1, -1},
{ 5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0, -1},
{11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0, -1},
{11, 10, 5, 7, 11, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 0, 1, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 8, 3, 1, 9, 8, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1},
{ 1, 6, 5, 2, 6, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 6, 5, 1, 2, 6, 3, 0, 8, -1, -1, -1, -1, -1, -1, -1},
{ 9, 6, 5, 9, 0, 6, 0, 2, 6, -1, -1, -1, -1, -1, -1, -1},
{ 5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8, -1, -1, -1, -1},
{ 2, 3, 11, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{11, 0, 8, 11, 2, 0, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1},
{ 0, 1, 9, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1},
{ 5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11, -1, -1, -1, -1},
{ 6, 3, 11, 6, 5, 3, 5, 1, 3, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6, -1, -1, -1, -1},
{ 3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9, -1, -1, -1, -1},
{ 6, 5, 9, 6, 9, 11, 11, 9, 8, -1, -1, -1, -1, -1, -1, -1},
{ 5, 10, 6, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 3, 0, 4, 7, 3, 6, 5, 10, -1, -1, -1, -1, -1, -1, -1},
{ 1, 9, 0, 5, 10, 6, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1},
{10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4, -1, -1, -1, -1},
{ 6, 1, 2, 6, 5, 1, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7, -1, -1, -1, -1},
{ 8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6, -1, -1, -1, -1},
{ 7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9, -1},
{ 3, 11, 2, 7, 8, 4, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1},
{ 5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11, -1, -1, -1, -1},
{ 0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1},
{ 9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6, -1},
{ 8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6, -1, -1, -1, -1},
{ 5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11, -1},
{ 0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7, -1},
{ 6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9, -1, -1, -1, -1},
{10, 4, 9, 6, 4, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 10, 6, 4, 9, 10, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1},
{10, 0, 1, 10, 6, 0, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1},
{ 8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10, -1, -1, -1, -1},
{ 1, 4, 9, 1, 2, 4, 2, 6, 4, -1, -1, -1, -1, -1, -1, -1},
{ 3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4, -1, -1, -1, -1},
{ 0, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 8, 3, 2, 8, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1},
{10, 4, 9, 10, 6, 4, 11, 2, 3, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6, -1, -1, -1, -1},
{ 3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10, -1, -1, -1, -1},
{ 6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1, -1},
{ 9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3, -1, -1, -1, -1},
{ 8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1, -1},
{ 3, 11, 6, 3, 6, 0, 0, 6, 4, -1, -1, -1, -1, -1, -1, -1},
{ 6, 4, 8, 11, 6, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 7, 10, 6, 7, 8, 10, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1},
{ 0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10, -1, -1, -1, -1},
{10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0, -1, -1, -1, -1},
{10, 6, 7, 10, 7, 1, 1, 7, 3, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7, -1, -1, -1, -1},
{ 2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9, -1},
{ 7, 8, 0, 7, 0, 6, 6, 0, 2, -1, -1, -1, -1, -1, -1, -1},
{ 7, 3, 2, 6, 7, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7, -1, -1, -1, -1},
{ 2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7, -1},
{ 1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11, -1},
{11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1, -1, -1, -1, -1},
{ 8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6, -1},
{ 0, 9, 1, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0, -1, -1, -1, -1},
{ 7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 0, 8, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 1, 9, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 8, 1, 9, 8, 3, 1, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1},
{10, 1, 2, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, 3, 0, 8, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1},
{ 2, 9, 0, 2, 10, 9, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1},
{ 6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8, -1, -1, -1, -1},
{ 7, 2, 3, 6, 2, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 7, 0, 8, 7, 6, 0, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1},
{ 2, 7, 6, 2, 3, 7, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1},
{ 1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6, -1, -1, -1, -1},
{10, 7, 6, 10, 1, 7, 1, 3, 7, -1, -1, -1, -1, -1, -1, -1},
{10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8, -1, -1, -1, -1},
{ 0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7, -1, -1, -1, -1},
{ 7, 6, 10, 7, 10, 8, 8, 10, 9, -1, -1, -1, -1, -1, -1, -1},
{ 6, 8, 4, 11, 8, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 6, 11, 3, 0, 6, 0, 4, 6, -1, -1, -1, -1, -1, -1, -1},
{ 8, 6, 11, 8, 4, 6, 9, 0, 1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6, -1, -1, -1, -1},
{ 6, 8, 4, 6, 11, 8, 2, 10, 1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6, -1, -1, -1, -1},
{ 4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9, -1, -1, -1, -1},
{10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3, -1},
{ 8, 2, 3, 8, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1},
{ 0, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8, -1, -1, -1, -1},
{ 1, 9, 4, 1, 4, 2, 2, 4, 6, -1, -1, -1, -1, -1, -1, -1},
{ 8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1, -1, -1, -1, -1},
{10, 1, 0, 10, 0, 6, 6, 0, 4, -1, -1, -1, -1, -1, -1, -1},
{ 4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3, -1},
{10, 9, 4, 6, 10, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 9, 5, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, 4, 9, 5, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1},
{ 5, 0, 1, 5, 4, 0, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1},
{11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5, -1, -1, -1, -1},
{ 9, 5, 4, 10, 1, 2, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1},
{ 6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5, -1, -1, -1, -1},
{ 7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2, -1, -1, -1, -1},
{ 3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6, -1},
{ 7, 2, 3, 7, 6, 2, 5, 4, 9, -1, -1, -1, -1, -1, -1, -1},
{ 9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7, -1, -1, -1, -1},
{ 3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0, -1, -1, -1, -1},
{ 6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8, -1},
{ 9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7, -1, -1, -1, -1},
{ 1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4, -1},
{ 4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10, -1},
{ 7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10, -1, -1, -1, -1},
{ 6, 9, 5, 6, 11, 9, 11, 8, 9, -1, -1, -1, -1, -1, -1, -1},
{ 3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5, -1, -1, -1, -1},
{ 0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11, -1, -1, -1, -1},
{ 6, 11, 3, 6, 3, 5, 5, 3, 1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6, -1, -1, -1, -1},
{ 0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10, -1},
{11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5, -1},
{ 6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3, -1, -1, -1, -1},
{ 5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2, -1, -1, -1, -1},
{ 9, 5, 6, 9, 6, 0, 0, 6, 2, -1, -1, -1, -1, -1, -1, -1},
{ 1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8, -1},
{ 1, 5, 6, 2, 1, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6, -1},
{10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0, -1, -1, -1, -1},
{ 0, 3, 8, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{11, 5, 10, 7, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{11, 5, 10, 11, 7, 5, 8, 3, 0, -1, -1, -1, -1, -1, -1, -1},
{ 5, 11, 7, 5, 10, 11, 1, 9, 0, -1, -1, -1, -1, -1, -1, -1},
{10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1, -1, -1, -1, -1},
{11, 1, 2, 11, 7, 1, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11, -1, -1, -1, -1},
{ 9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7, -1, -1, -1, -1},
{ 7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2, -1},
{ 2, 5, 10, 2, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1},
{ 8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5, -1, -1, -1, -1},
{ 9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2, -1, -1, -1, -1},
{ 9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2, -1},
{ 1, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 7, 0, 7, 1, 1, 7, 5, -1, -1, -1, -1, -1, -1, -1},
{ 9, 0, 3, 9, 3, 5, 5, 3, 7, -1, -1, -1, -1, -1, -1, -1},
{ 9, 8, 7, 5, 9, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 5, 8, 4, 5, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1},
{ 5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0, -1, -1, -1, -1},
{ 0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5, -1, -1, -1, -1},
{10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4, -1},
{ 2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8, -1, -1, -1, -1},
{ 0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11, -1},
{ 0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5, -1},
{ 9, 4, 5, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4, -1, -1, -1, -1},
{ 5, 10, 2, 5, 2, 4, 4, 2, 0, -1, -1, -1, -1, -1, -1, -1},
{ 3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9, -1},
{ 5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2, -1, -1, -1, -1},
{ 8, 4, 5, 8, 5, 3, 3, 5, 1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 4, 5, 1, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5, -1, -1, -1, -1},
{ 9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 11, 7, 4, 9, 11, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11, -1, -1, -1, -1},
{ 1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11, -1, -1, -1, -1},
{ 3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4, -1},
{ 4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2, -1, -1, -1, -1},
{ 9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3, -1},
{11, 7, 4, 11, 4, 2, 2, 4, 0, -1, -1, -1, -1, -1, -1, -1},
{11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4, -1, -1, -1, -1},
{ 2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9, -1, -1, -1, -1},
{ 9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7, -1},
{ 3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10, -1},
{ 1, 10, 2, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 9, 1, 4, 1, 7, 7, 1, 3, -1, -1, -1, -1, -1, -1, -1},
{ 4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1, -1, -1, -1, -1},
{ 4, 0, 3, 7, 4, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 0, 9, 3, 9, 11, 11, 9, 10, -1, -1, -1, -1, -1, -1, -1},
{ 0, 1, 10, 0, 10, 8, 8, 10, 11, -1, -1, -1, -1, -1, -1, -1},
{ 3, 1, 10, 11, 3, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 11, 1, 11, 9, 9, 11, 8, -1, -1, -1, -1, -1, -1, -1},
{ 3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9, -1, -1, -1, -1},
{ 0, 2, 11, 8, 0, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 2, 3, 8, 2, 8, 10, 10, 8, 9, -1, -1, -1, -1, -1, -1, -1},
{ 9, 10, 2, 0, 9, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8, -1, -1, -1, -1},
{ 1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 3, 8, 9, 1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}
};
@@ -0,0 +1,159 @@
// VoxelBiomeDefinition.h
// Data asset defining one biome: where it lives (climate), how it reshapes the base
// terrain (modulation), and what content/atmosphere it brings.
//
// HOW TO USE:
// -----------
// 1. Content Browser → Miscellaneous → Data Asset → "VoxelBiomeDefinition".
// 2. Set its climate box (ReliefMin/Max, MoistureMin/Max), its terrain override, and its
// Decorations / atmosphere / water.
// 3. Add it to a UVoxelStrateDefinition's Biomes[] list.
//
// A biome is generator-agnostic: the same asset works inside any archetype strate
// (surface biomes today, cave biomes later). The strate's base archetype params are
// the baseline; this asset modulates on top.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "GameplayTagContainer.h"
#include "VoxelBiomeTypes.h"
#include "VoxelStrateTypes.h" // FStrateDecoration / FStrateAmbientActor
#include "VoxelBiomeDefinition.generated.h"
// IWYU : utilisé en pointeur seulement ⇒ déclaration avant. Le PCH partagé ne la fournit pas :
// `FPSemantics = Precise` nous en sort. / Pointer-only use, so a forward declaration is enough.
// The shared PCH does not provide it (FPSemantics = Precise opts this module out of it).
class UMaterialInterface;
/**
* UVoxelBiomeDefinition — one biome's identity, placement, terrain modulation and content.
*/
UCLASS(BlueprintType)
class VOXELFORGE_API UVoxelBiomeDefinition : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
//=========================================================================
// IDENTITY
//=========================================================================
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Biome")
FText BiomeName;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Biome", meta = (MultiLine = true))
FText BiomeDescription;
// Colour used by the 2D biome-preview bake to identify this biome's regions.
// Pick something distinct per biome so the baked map is readable.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome")
FLinearColor DebugColor = FLinearColor(0.5f, 0.5f, 0.5f, 1.0f);
//=========================================================================
// CLIMATE PLACEMENT
//=========================================================================
// A biome wins the Voronoi cells whose site climate (relief, moisture) falls inside
// — or, failing any match, nearest to — this box. Both axes are [0,1].
// Mountains / snow → high ReliefMin. Lush forest → high Moisture.
// Desert → low Moisture. Plains → mid relief, any moisture.
// Boxes may overlap; ties are broken deterministically per cell.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Climate", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float ReliefMin = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Climate", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float ReliefMax = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Climate", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float MoistureMin = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Climate", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float MoistureMax = 1.0f;
//=========================================================================
// TERRAIN OVERRIDE — a biome is a "mini-strate-variant"
//=========================================================================
// When enabled, this biome supplies its OWN archetype params for its regions,
// replacing the strate's. Heightfield archetypes (SurfaceWorld) blend the output
// across biome borders, so even frequency/shape differences stay seamless.
// Global/structural fields (strate Z bounds, boundary seal, base density, water
// level) are always forced from the strate — a biome can't break seals/connectivity.
//
// GeneratorType must match the strate this biome is used in; if it doesn't, the
// override is ignored and the strate's params are used. (Currently wired: SurfaceWorld.
// Other archetypes' param overrides arrive as each is hooked up — see CODEMAP §8.14.)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Terrain")
bool bOverrideTerrain = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Terrain",
meta = (EditCondition = "bOverrideTerrain"))
ECaveGeneratorType GeneratorType = ECaveGeneratorType::SurfaceWorld;
// Open-sky terrain override (used when bOverrideTerrain && GeneratorType == SurfaceWorld).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Terrain",
meta = (EditCondition = "bOverrideTerrain && GeneratorType == ECaveGeneratorType::SurfaceWorld"))
FSurfaceGenerationParams SurfaceParams;
//=========================================================================
// CONTENT PROFILE (consumed by the content / atmosphere managers)
//=========================================================================
// Decorations placed in this biome's regions. When set, these REPLACE the strate's
// decorations for chunks whose dominant biome is this one (falls back to the strate
// list when empty).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Content")
TArray<FStrateDecoration> Decorations;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Content")
TArray<FStrateAmbientActor> AmbientActors;
//=========================================================================
// ATMOSPHERE OVERRIDE (optional — beats the strate's atmosphere for this biome)
//=========================================================================
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Atmosphere")
bool bOverrideAtmosphere = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Atmosphere", meta = (EditCondition = "bOverrideAtmosphere"))
FLinearColor FogColor = FLinearColor(0.05f, 0.05f, 0.1f, 1.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Atmosphere", meta = (EditCondition = "bOverrideAtmosphere", ClampMin = "0.0", ClampMax = "1.0"))
float FogDensity = 0.3f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Atmosphere", meta = (EditCondition = "bOverrideAtmosphere"))
FLinearColor AmbientLightColor = FLinearColor(0.1f, 0.1f, 0.15f, 1.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Atmosphere", meta = (EditCondition = "bOverrideAtmosphere", ClampMin = "0.0"))
float AmbientLightIntensity = 0.5f;
//=========================================================================
// WATER
//=========================================================================
// Water material override for this biome (null = use the strate's WaterMaterial).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Water")
UMaterialInterface* WaterMaterial = nullptr;
//=========================================================================
// MATERIAL — F6 vertex-colour triplanar palette
//=========================================================================
// Which palette layer this biome's terrain uses in the master triplanar material.
// It is baked into the mesh vertex colour (R channel) at mesh time, so ONE material
// re-skins the terrain per biome and cross-fades across biome borders (the neighbour
// biome's index + a blend weight ride in the A/B channels). The terrain material is
// still the strate's OverrideMaterial / Settings->VoxelMaterial — author it as the
// master palette material and switch its layers on this index. 0 = default layer.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Material", meta = (ClampMin = "0", ClampMax = "255"))
int32 MaterialPaletteIndex = 0;
//=========================================================================
// GAMEPLAY TAGS
//=========================================================================
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Gameplay")
FGameplayTagContainer GameplayTags;
};
@@ -0,0 +1,264 @@
// VoxelBiomeTypes.h
// Shared vocabulary for the biome system (Stage 1).
//
// A biome is a "mini-strate-variant": it can carry a FULL archetype param override
// (e.g. its own FSurfaceGenerationParams) plus a content profile, placed across the
// world by a deterministic, window-invariant XY field (warped Voronoi + climate).
//
// Resolution discipline (protects CODEMAP §8.4 + §8.10):
// - The biome assigned at any XY is a PURE function of world coords + seed + the
// strate's biome context. No dependence on the chunk window.
// - Heightfield archetypes (Surface) resolve dominant + neighbour biome per voxel and
// blend the OUTPUT height — so ANY param difference (even frequencies) stays seamless,
// which per-param blending could never do. The expensive climate classification is
// cached once per chunk; per voxel is just a warp + 3x3 lookup.
// - Structural/global fields (Z bounds, seal, base density, water level) are always
// forced from the STRATE so seals / spine / passages / water plane stay intact.
#pragma once
#include "CoreMinimal.h"
#include "VoxelBiomeTypes.generated.h"
class UVoxelBiomeDefinition;
/**
* FBiomeMapParams — controls the world-XY biome field: how big biome regions are,
* how organic their borders look, and the two climate fields that ASSIGN a biome to
* each Voronoi cell. Lives on UVoxelStrateDefinition (one map per strate).
*/
USTRUCT(BlueprintType)
struct VOXELFORGE_API FBiomeMapParams
{
GENERATED_BODY()
// Average biome cell size in voxels. Larger = bigger, sweeping biome regions.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Map", meta = (ClampMin = "32.0"))
float CellSize = 800.0f;
// Domain-warp the cell lookup by up to this many voxels → organic, non-hexagonal
// borders. 0 = raw Voronoi cells (straight-ish borders).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Map", meta = (ClampMin = "0.0"))
float WarpStrength = 250.0f;
// Frequency of the border-warp noise. Lower = broader, sweeping border bends.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Map")
float WarpFrequency = 0.0018f;
// Width (voxels) of the smooth blend band between neighbouring biomes. The density
// scalars cross-fade across this band so there is no hard wall at a biome border.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Map", meta = (ClampMin = "0.0"))
float BorderBlend = 120.0f;
// ----- Climate fields (assign each cell its biome) -----
//
// IMPORTANT: for COHERENT geography (mountain ranges, desert regions — not confetti)
// the climate must vary much more SLOWLY than CellSize: aim for ~4-6 cells per climate
// feature, i.e. ClimateFrequency ≈ 1 / (5 * CellSize). If the climate wavelength is
// near or below CellSize, neighbouring cells sample unrelated climate → salt-and-pepper.
// Relief ("elevation") field, [0,1]. Mountain / snow biomes live where this is high.
// Default tuned for CellSize≈800 (wavelength ~3300 ≈ 4 cells). Lower → bigger regions.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Climate")
float ReliefFrequency = 0.0003f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Climate", meta = (ClampMin = "0.25", ClampMax = "4.0"))
float ReliefContrast = 2.0f;
// Moisture field, [0,1] — the second climate axis (wet lowlands vs arid). Same
// coherence rule as relief: keep the wavelength several cells wide.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Climate")
float MoistureFrequency = 0.0004f;
};
/**
* Channel selector for the 2D biome-preview bake (AVoxelWorld::BakeBiomePreview).
*/
UENUM(BlueprintType)
enum class EBiomePreviewChannel : uint8
{
Biome UMETA(DisplayName = "Biome (debug colours)"),
Relief UMETA(DisplayName = "Relief / elevation field"),
Moisture UMETA(DisplayName = "Moisture field")
};
/**
* FVoxelBiomeQuery — a Blueprint-readable snapshot of the biome field at one world point.
* Returned by AVoxelWorld::GetBiomeAtWorldLocation so BP can read "which biome is under the
* cursor" — the SAME resolution the decoration scatter uses per column. DecorationCount is the
* effective number of decorations the dominant biome would place (its own list, or the strate's
* when empty): 0 here explains a "band of nothing" — that biome simply has no decorations.
*/
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelBiomeQuery
{
GENERATED_BODY()
// False when the point's strate has no biome field (then only the climate fields are filled).
UPROPERTY(BlueprintReadOnly, Category = "Biome") bool bHasBiomes = false;
// Dominant biome asset at this point (what a decoration column here primarily uses). May be null.
UPROPERTY(BlueprintReadOnly, Category = "Biome") UVoxelBiomeDefinition* DominantBiome = nullptr;
// Nearest neighbouring biome (the one a border blend fades toward). May be null / same as dominant.
UPROPERTY(BlueprintReadOnly, Category = "Biome") UVoxelBiomeDefinition* NeighborBiome = nullptr;
// Dominant biome display name (falls back to the asset name when BiomeName is unset).
UPROPERTY(BlueprintReadOnly, Category = "Biome") FText DominantName;
// Dominant biome's preview/debug colour (handy to tint a BP debug draw to match the bake).
UPROPERTY(BlueprintReadOnly, Category = "Biome") FLinearColor DebugColor = FLinearColor::Black;
// Border blend weight toward the neighbour: 0 deep inside a cell → ~0.5 at the shared border.
UPROPERTY(BlueprintReadOnly, Category = "Biome") float NeighborWeight = 0.0f;
// Climate fields at this XY, both [0,1] (always filled, even when bHasBiomes is false).
UPROPERTY(BlueprintReadOnly, Category = "Biome") float Relief = 0.0f;
UPROPERTY(BlueprintReadOnly, Category = "Biome") float Moisture = 0.0f;
// Effective decoration count for the dominant biome (its list, else the strate's). 0 ⇒ empty band.
UPROPERTY(BlueprintReadOnly, Category = "Biome") int32 DominantDecorationCount = 0;
// Dominant biome's index in the strate's biome context (matches the decoration column tag). -1 = none.
UPROPERTY(BlueprintReadOnly, Category = "Biome") int32 DominantContextIndex = -1;
//--- DECORATION STREAMING STATE of the region covering this point (debug "why is this chunk blank?") ---
// Discriminates the three failure modes for a visibly-bare patch:
// • bDecoRegionApplied && DecoAppliedInstances > 0 → instances EXIST + are uploaded; if still not
// visible it is a RENDER drop (proxy/culling), not data/streaming.
// • bDecoRegionApplied && DecoAppliedInstances == 0 → the region applied but the per-column MARCH
// produced no spawns here (config/determinism), not a render or streaming bug.
// • bDecoRegionBuilding (not applied) → region is STILL marching; if it never finishes
// (DecoCellsAccounted stuck < DecoCellsTotal) it is a streaming/accounting leak.
// • neither flag set → region was never requested (desired-set / range).
// True if the region covering this XY is in DecoRegions (applied; HISMs built).
UPROPERTY(BlueprintReadOnly, Category = "Biome|Deco") bool bDecoRegionApplied = false;
// Total HISM instances actually uploaded across the applied region's components.
UPROPERTY(BlueprintReadOnly, Category = "Biome|Deco") int32 DecoAppliedInstances = 0;
// True if the region is in RegionBuilds (cells still being marched / merged, not yet applied).
UPROPERTY(BlueprintReadOnly, Category = "Biome|Deco") bool bDecoRegionBuilding = false;
// Cells accounted vs the region's total (R*R). Building but accounted < total = in progress; if it
// never reaches total the region is stuck (the permanent-blank-until-regen signature).
UPROPERTY(BlueprintReadOnly, Category = "Biome|Deco") int32 DecoCellsAccounted = 0;
UPROPERTY(BlueprintReadOnly, Category = "Biome|Deco") int32 DecoCellsTotal = 0;
// PER-CELL decisive probe: re-runs the decoration march for the EXACT cell (chunk footprint) under
// this point, synchronously, with the current strate context — i.e. how many spawns the deterministic
// scatter produces here RIGHT NOW. Region state above is R*R-cell coarse and can't see a single blank
// cell; this can. On a visibly-BLANK cell: >0 ⇒ the march works, the spawns were lost in merge/apply or
// not drawn (streaming/render bug); 0 ⇒ the march genuinely makes nothing here (data/config, or — if a
// regen brings grass back at the same spot — a non-determinism bug). -1 ⇒ couldn't run (no strate
// context / point not in the player's current strate).
UPROPERTY(BlueprintReadOnly, Category = "Biome|Deco") int32 DecoLiveMarchSpawns = -1;
// FINAL discriminator, paired with DecoLiveMarchSpawns. Counts the applied region's HISM instances
// that actually fall inside THIS cell's footprint (not the whole region). On a blank cell where the
// live march says N>0:
// • DecoInstancesInCell ≈ N → the instances ARE uploaded here but not visible → RENDER drop.
// • DecoInstancesInCell ≈ 0 → the march's spawns never reached the HISM → MERGE/apply loss.
UPROPERTY(BlueprintReadOnly, Category = "Biome|Deco") int32 DecoInstancesInCell = 0;
};
//=============================================================================
// RUNTIME STRUCTS (plain C++ — not USTRUCT; live in the thread-local hot path)
//=============================================================================
/**
* One biome flattened from its asset for fast, asset-free evaluation. The biome field
* and the per-chunk cache only ever see these PODs — never a UObject — so they stay
* cheap to copy and safe to touch from worker threads.
*/
struct FBiomeResolved
{
// Index back into the owning strate's Biomes[] array (for param / content lookup).
int32 Index = 0;
// Climate placement box in (relief, moisture) space, both [0,1].
float ReliefMin = 0.0f, ReliefMax = 1.0f;
float MoistureMin = 0.0f, MoistureMax = 1.0f;
// Colour used by the 2D preview bake.
FColor DebugColor = FColor::White;
// Palette layer this biome's terrain uses in the master triplanar material (F6).
// Baked into the mesh vertex colour so one material can re-skin per biome. 0 = default.
int32 MaterialPaletteIndex = 0;
};
/**
* Everything the biome field needs for one strate. Built once by StrateManager
* (GetBiomeContextForChunk) from the strate definition. Empty Biomes ⇒ biomes
* disabled for this strate (legacy behaviour, bit-identical output).
*/
struct FBiomeContext
{
TArray<FBiomeResolved> Biomes;
FBiomeMapParams Map;
bool IsValid() const { return Biomes.Num() > 0; }
};
/**
* Result of a single biome query at a world XY: the dominant biome cell, the nearest
* neighbour cell, and a blend weight toward that neighbour (0 deep inside a cell,
* → 0.5 at the shared border). Indices are positions into FBiomeContext::Biomes.
*/
struct FBiomeSample
{
int32 DominantIndex = -1;
int32 NeighborIndex = -1;
float NeighborWeight = 0.0f;
};
/**
* Per-chunk biome cache for the density hot path. The EXPENSIVE part of a biome query
* (classifying each Voronoi cell by its climate — several noise samples per cell) is
* done ONCE here, into a small grid covering the chunk footprint + margin; per voxel
* the resolver then only warps + does a cheap 3x3 lookup + blend.
*
* VALIDITY IS A WORLD-XY BOX (+ ChunkZ + Seed + owner identities), NOT a chunk key (CODEMAP §8.10). The
* grid covers a halo beyond the chunk, so gradient-normal samples and the +X/+Y chunk
* boundary corners stay inside the valid box and DO NOT thrash the noise-heavy rebuild.
* The box logic is identical in spirit to the SDF cache in GetDensityWithParams.
*/
struct FChunkBiomeCache
{
// World-XY box (voxel coords) over which the cached grid answers correctly.
float ValidMinX = 1.0f, ValidMaxX = -1.0f; // start invalid (min > max)
float ValidMinY = 0.0f, ValidMaxY = 0.0f;
int32 ChunkZ = MIN_int32; // which strate slice this was built for
int32 Seed = MIN_int32;
uint64 OwnerId = 0; // generator/world identity
uint64 ManagerLifetimeId = 0; // strate-manager lifetime identity
bool bActive = false; // does this strate have biomes?
FBiomeContext Ctx; // resolved biomes + map (for blending)
// Cell grid (row-major, CellsX × CellsY), each entry = biome index into Ctx.Biomes.
int32 BaseCellX = 0, BaseCellY = 0, CellsX = 0, CellsY = 0;
TArray<int32> CellBiome;
bool Contains(float X, float Y, int32 InChunkZ, int32 InSeed,
uint64 InOwnerId, uint64 InManagerLifetimeId) const
{
return InSeed == Seed && InChunkZ == ChunkZ
&& InOwnerId == OwnerId && InManagerLifetimeId == ManagerLifetimeId
&& X >= ValidMinX && X <= ValidMaxX
&& Y >= ValidMinY && Y <= ValidMaxY;
}
/** AUDIT C2 — force a rebuild on the next query. The validity box says nothing about the
* FBiomeContext the cells were classified AGAINST, so when the strate layout is rebuilt
* (RebuildStrates / an editor live edit) the grid is stale even though the box still
* covers the query. Callers that key on GetLayoutVersion() call this on a version change.
* Le box de validité ne dit rien du contexte de biome ayant servi à classer les cellules :
* après un rebuild de layout, la grille est périmée alors que la boîte couvre encore. */
void Invalidate()
{
ValidMinX = 1.0f; ValidMaxX = -1.0f; // min > max ⇒ Contains() is false everywhere
ChunkZ = MIN_int32;
OwnerId = 0;
ManagerLifetimeId = 0;
}
};
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,60 @@
// VoxelDensityAblation.h
// Development-only, world-changing measurement overrides for TunnelNetwork density stages.
#pragma once
#include "CoreMinimal.h"
#include <atomic>
namespace VoxelDensityAblation
{
/**
* A bit in this mask means that the named stage is forced off. These are measurement
* overrides, not gameplay settings: they change the generated field, are resolved once per
* process, and must match across peers and regenerated worlds.
*/
enum class EStage : uint32
{
CaveWarp = 1u << 0,
DetailOps = 1u << 1,
RoomSDF = 1u << 2,
TunnelSDF = 1u << 3,
TunnelCore = 1u << 4,
PassageCarving = 1u << 5,
PassageStructuralPosts= 1u << 6,
NativeFloor = 1u << 7,
Disturbances = 1u << 8,
OriginSpine = 1u << 9,
BoundarySeal = 1u << 10,
LandingPosts = 1u << 11,
XYEdgeSeal = 1u << 12,
PitChimneySDF = 1u << 13,
};
/** Resolve command-line/CVar values once and return the process-wide mask. */
VOXELFORGE_API uint32 GetResolvedMask();
FORCEINLINE bool IsForcedOff(EStage Stage)
{
return (GetResolvedMask() & static_cast<uint32>(Stage)) != 0;
}
FORCEINLINE bool IsCaveWarpOff() { return IsForcedOff(EStage::CaveWarp); }
FORCEINLINE bool IsDetailOpsOff() { return IsForcedOff(EStage::DetailOps); }
FORCEINLINE bool IsRoomSDFOff() { return IsForcedOff(EStage::RoomSDF); }
FORCEINLINE bool IsTunnelSDFOff() { return IsForcedOff(EStage::TunnelSDF); }
FORCEINLINE bool IsTunnelCoreOff() { return IsForcedOff(EStage::TunnelCore); }
FORCEINLINE bool IsPassageCarvingOff() { return IsForcedOff(EStage::PassageCarving); }
FORCEINLINE bool IsPassageStructuralPostsOff()
{
return IsForcedOff(EStage::PassageStructuralPosts);
}
FORCEINLINE bool IsNativeFloorOff() { return IsForcedOff(EStage::NativeFloor); }
FORCEINLINE bool IsDisturbancesOff() { return IsForcedOff(EStage::Disturbances); }
FORCEINLINE bool IsOriginSpineOff() { return IsForcedOff(EStage::OriginSpine); }
FORCEINLINE bool IsBoundarySealOff() { return IsForcedOff(EStage::BoundarySeal); }
FORCEINLINE bool IsLandingPostsOff() { return IsForcedOff(EStage::LandingPosts); }
FORCEINLINE bool IsXYEdgeSealOff() { return IsForcedOff(EStage::XYEdgeSeal); }
FORCEINLINE bool IsPitChimneySDFOff() { return IsForcedOff(EStage::PitChimneySDF); }
}
@@ -0,0 +1,934 @@
// VoxelDensityOp.h
// LE CONTRAT de la pile d'opérateurs de densité / THE density operator stack CONTRACT.
// Design : Docs/archive/OPSTACK-PLAN.md. The contract is implemented by the stack path in
// UVoxelGenerator::GetDensityAt, which generates every strate.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// POURQUOI / WHY
// ─────────────────────────────────────────────────────────────────────────────────────────
// Chaque archétype (ECaveGeneratorType) est une composition d'opérateurs, pas une fonction de
// densité monolithique qui posséderait le voxel entier. Une idée de monde s'exprime en empilant
// sources, combinateurs et modificateurs — et les idées peuvent donc SE COMBINER : « une strate de
// surface dont les montagnes contiennent un réseau de salles, avec des îles flottantes au-dessus ».
//
// Each archetype (ECaveGeneratorType) is a composition of operators, not a monolithic density
// function that owns the whole voxel. A world idea is expressed by stacking sources, combiners and
// modifiers — so ideas can COMBINE.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// ⚠️ CE N'EST PAS L'ANCIEN SYSTÈME DE « ROOM OPERATIONS » / THIS IS NOT THE OLD ROOM-OPS SYSTEM
// ─────────────────────────────────────────────────────────────────────────────────────────
// UVoxelTerrainOpDefinition (Terrace, Ribbing, Cliff, Scallop, Overhang, Arch, Column, Pit…) ne
// sait que PERTURBER une densité près d'une surface qui existe déjà. Il ne décide jamais ce que le
// champ EST — c'est le rôle des sources de champ ci-dessous.
//
// D'où QUATRE RÔLES, dont l'ancien système n'occupait que le troisième :
//
// 1. FIELD SOURCE — fabrique un champ À PARTIR DE RIEN. C'est ce qui fait qu'une grotte est
// une grotte et qu'un monde ouvert est un monde ouvert. Chaque archétype
// est fondamentalement l'une de ces sources.
// 2. COMBINER — comment deux champs fusionnent (min/max/smooth/mask). C'EST le rôle qui
// achète la composition ; sans lui il n'y a pas de refactor.
// 3. DETAIL MODIFIER — l'ancien système, rétrogradé à un rôle sur quatre. Inchangé.
// 4. STRUCTURAL POST — spine (0,0) → seal vertical → passages → seal XY → diff layer. Des INVARIANTS de monde,
// pas des choix créatifs : toujours ajoutés, dans cet ordre, jamais
// omissibles par l'auteur.
//
// The old system only ever had role 3. Roles 1 and 2 are the new thing, and role 4 is what keeps
// the descent structure intact no matter what an author assembles.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// LA CLÉ DE VOÛTE : EffectOverBox + intervalle SDF / THE KEYSTONE: EffectOverBox + SDF interval
// ─────────────────────────────────────────────────────────────────────────────────────────
// Un verdict de boîte doit être une preuve. Rendre `Both` coûte du CPU ; rendre `Identity` ou une
// direction fausse supprime de la géométrie et peut laisser un joueur tomber au travers du monde.
// En cas de doute, `Mixed`.
//
// Un opérateur qui écrit seulement le canal SDF ne peut pas honnêtement répondre pour le convertisseur
// qui viendra peut-être après lui : il doit propager un intervalle SDF, et le convertisseur doit plier
// cet intervalle avec sa propre formule. Chaque opérateur répond ainsi pour lui-même, et la composition
// reste sûre même pour une combinaison que personne n'a écrite à la main auparavant.
//
// A box verdict is a proof. `Both` costs CPU; a false `Identity` or direction removes geometry and can
// let a player fall through the world. When in doubt, return `Mixed`.
// An SDF-only writer reports an SDF interval; a later converter folds that interval through its own
// response. Each operator answers for itself, so an unverified composition remains sound.
//
// Presque tout opérateur est UNIDIRECTIONNEL : il ne fait que creuser, ou que remplir. La direction
// suffit donc à exprimer GÉNÉRIQUEMENT les gardes de ClassifyTile :
//
// « passages ⇒ bCanSolid = false » EST CarveOnly
// « ponts/arêtes ⇒ bCanAir = false » EST FillOnly
// « aucun passage près de cette boîte » EST Identity
//
// La direction reste le repli conservateur par défaut pour les opérateurs qui ne connaissent pas
// d'intervalle ; elle ne permet jamais à un écrivain SDF d'usurper la réponse d'un voisin.
//
// Direction remains the conservative fallback for operators without an interval implementation;
// it never lets an SDF writer impersonate a downstream neighbour.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// CLASSIFYTILE
// ─────────────────────────────────────────────────────────────────────────────────────────
// ClassifyTile garde ses preuves exactes pour les gaps de bedrock et SurfaceWorld ; les slots de
// cave passent par le pliage `ClassifyBox` de la pile même qui produit leur densité. Le post XY
// fournit en plus une preuve globale de coque : une tuile entièrement dans la bande forcée peut être
// sautée sans connaître l'archétype ou le Z.
//
// ClassifyTile keeps its exact proofs for bedrock gaps and SurfaceWorld; cave slots go through the
// `ClassifyBox` fold of the same stack that produces their density. The XY post adds a global
// shell proof: a tile entirely inside the forced band is skipped without knowing archetype or Z.
//
// NOTE sur les UENUM : ces types sont volontairement du C++ nu (pas d'UHT, pas de .generated.h).
// Ils deviendront UENUM/USTRUCT en Phase 3, quand les opérateurs deviendront des data assets et
// auront besoin d'être édités dans l'éditeur. Les promouvoir plus tôt n'achèterait rien et
// ajouterait une étape UHT à chaque itération.
#pragma once
#include "CoreMinimal.h"
#include "VoxelTypes.h" // CHUNK_SIZE, EVoxelTileClass
struct FBiomeContext;
struct FTunnelCoreWorldEvaluation;
struct FChunkSDFCache;
//=============================================================================
// LES QUATRE RÔLES / THE FOUR ROLES
//=============================================================================
// Le rôle n'est pas décoratif : le compilateur de pile s'en sert pour ORDONNER. Les
// StructuralPost sont toujours ajoutés en dernier, dans l'ordre fixe spine → seal → passage →
// seal XY → diff, quoi que l'auteur ait assemblé. Un auteur ne peut pas les omettre : la descente doit
// rester possible, les seals doivent tenir, les passages doivent percer, les éditions du joueur
// gagnent toujours.
enum class EVoxelOpRole : uint8
{
// Rôle 1 — fabrique un champ à partir de rien. Une pile en a au moins un (sa racine).
FieldSource,
// Rôle 2 — fusionne le champ précédent avec le suivant. Voir EVoxelOpCombine.
Combiner,
// Rôle 3 — perturbe un champ existant près de sa surface (l'ancien UVoxelTerrainOpDefinition).
DetailModifier,
// Rôle 4 — invariants de monde. Ajoutés automatiquement, ordre fixe, non omissibles.
StructuralPost,
};
//=============================================================================
// CANAUX DE L'ÉCHANTILLON / SAMPLE CHANNELS
//=============================================================================
// `FVoxelOpSample` is deliberately a small, explicit state record. Keep its channel set here,
// next to the interface that declares which fields an operator consumes and publishes. The enum
// values are bits so a declaration can name more than one channel without introducing another
// per-voxel object or a dynamic container.
//
// `Density` is the internal positive=solid field; `Sdf` is the standard negative=inside field.
// If a future field is added to `FVoxelOpSample`, it must also be added here and to every operator
// declaration before the composer can treat that field as part of the dependency graph.
enum class EVoxelOpChannel : uint8
{
None = 0,
Density = 1 << 0,
Sdf = 1 << 1,
};
using EVoxelOpChannelMask = uint8;
namespace VoxelOpChannels
{
static constexpr EVoxelOpChannelMask None = 0;
static constexpr EVoxelOpChannelMask Density = static_cast<EVoxelOpChannelMask>(EVoxelOpChannel::Density);
static constexpr EVoxelOpChannelMask Sdf = static_cast<EVoxelOpChannelMask>(EVoxelOpChannel::Sdf);
static constexpr EVoxelOpChannelMask All = Density | Sdf;
}
//=============================================================================
// OPERATOR RESOURCES / RESSOURCES D'OPERATEUR
//=============================================================================
// Sample-channel declarations describe the FVoxelOpSample only. Some operators also consume
// stateful geometry published by an earlier source (the room graph, shaft field, or surface
// column). Keep that dependency explicit as a second, tiny graph so the composer cannot place a
// room modifier on a stack that has no room source merely because its Density/Sdf masks happen to
// fit.
enum class EVoxelOpResource : uint8
{
None = 0,
RoomGeometry = 1 << 0,
ShaftGeometry = 1 << 1,
SurfaceColumn = 1 << 2,
};
using EVoxelOpResourceMask = uint8;
namespace VoxelOpResources
{
static constexpr EVoxelOpResourceMask None = 0;
static constexpr EVoxelOpResourceMask RoomGeometry = static_cast<EVoxelOpResourceMask>(EVoxelOpResource::RoomGeometry);
static constexpr EVoxelOpResourceMask ShaftGeometry = static_cast<EVoxelOpResourceMask>(EVoxelOpResource::ShaftGeometry);
static constexpr EVoxelOpResourceMask SurfaceColumn = static_cast<EVoxelOpResourceMask>(EVoxelOpResource::SurfaceColumn);
static constexpr EVoxelOpResourceMask All = RoomGeometry | ShaftGeometry | SurfaceColumn;
}
//=============================================================================
// COMPOSITION / COMBINERS
//=============================================================================
// Vocabulaire délibérément petit, et il réutilise ce qui existe déjà
// (VoxelSDF::SmoothMin / SmoothMax).
//
// ⚠️⚠️ RAPPEL DE SIGNE — LA source n°1 de confusion du plugin, et il y a DEUX conventions en jeu.
// Lire ceci en entier avant d'écrire un opérateur.
//
// • CANAL DENSITÉ, à l'intérieur de la pile : convention INTERNE, **POSITIF = SOLIDE**.
// C'est celle dans laquelle CHAQUE fonction d'archétype est écrite aujourd'hui. La négation
// vers la convention marching-cubes (négatif = solide) se fait UNE FOIS, tout à la fin, par
// l'appelant. Donc ici : « ajouter du solide » = MAX, « creuser de l'air » = MIN.
//
// • CANAL SDF : convention SDF standard, **NÉGATIF = À L'INTÉRIEUR de la primitive**.
// Réunir deux formes = MIN (c'est `SmoothMin`, ce que fait déjà le code pour salle+puits).
// Le sens de « min » est donc l'INVERSE d'un canal à l'autre. Ce n'est pas une incohérence :
// un SDF décrit une FORME, une densité décrit de la MATIÈRE.
//
// DENSITY channel inside the stack: INTERNAL convention, **POSITIVE = SOLID** (what every
// archetype body already uses; the MC negate happens once, at the end, in the caller). So
// "add solid" is max(), "carve air" is min().
// SDF channel: standard SDF, **NEGATIVE = INSIDE the primitive**; unioning shapes is min().
// The meaning of min() is therefore opposite between the two channels — an SDF describes a
// SHAPE, a density describes MATTER.
enum class EVoxelOpCombine : uint8
{
Replace, // ignore l'entrée — racine de pile (heightfield, densité de base)
Union, // max() sur la DENSITÉ — ajoute du solide : ponts, îles, colonnes
Subtract, // min() sur la DENSITÉ — creuse de l'air : salles, tunnels, passages, spine
SmoothUnion, // VoxelSDF::SmoothMin(k) — jonctions organiques
SmoothSubtract, // VoxelSDF::SmoothMax(k)
Add, // accumulation scalaire — termes de bruit / rugosité
Mask, // met à l'échelle l'opérateur SUIVANT par un champ [0,1]
// (poids de biome, porte de pente, relief, profondeur).
// C'est Mask qui achète le plus d'expressivité : « cet opérateur, mais
// seulement dans les régions à fort relief » devient de la composition
// au lieu d'une garde codée en dur dans chaque opérateur.
};
//=============================================================================
// EFFET SUR UNE BOÎTE / EFFECT OVER A BOX
//=============================================================================
// CONSERVATIF PAR CONSTRUCTION. Rendre `Both` est TOUJOURS SÛR (ça ne coûte que du CPU) ;
// rendre le mauvais est un TROU — pas de géométrie, pas de collision, invisible jusqu'à ce
// qu'un joueur tombe au travers. En cas de doute : `Both`.
//
// CONSERVATIVE BY CONSTRUCTION. Returning `Both` is ALWAYS SAFE (it only costs CPU); returning
// the wrong one is a HOLE. When unsure: `Both`.
enum class EVoxelOpEffect : uint8
{
// Prouvablement aucun effet sur cette boîte. C'est le early-out qui rend la pile rapide,
// et c'est ce qui rendra le bedrock profond sautable pour les strates de grotte.
Identity,
// Ne peut que pousser la densité vers l'AIR ⇒ tue l'hypothèse « tout solide ».
CarveOnly,
// Ne peut que pousser la densité vers le SOLIDE ⇒ tue l'hypothèse « tout air ».
FillOnly,
// Non contraint ⇒ tue les deux hypothèses.
Both,
};
//=============================================================================
// CONTEXTE DE CHUNK / CHUNK CONTEXT
//=============================================================================
// Miroir de ce que le bloc thread_local CP_* résout aujourd'hui dans GetDensityAt.
//
// ⚠️ LayoutVersion est ici PAR CONSTRUCTION, pas par politesse. AUDIT C2 : trois caches
// existants (CP_Chunk, OC_Chunk, BM_Chunk) sont clés sur ChunkCoord SEUL, donc après un
// RebuildStrates ou une édition à chaud un worker dont le cache est encore chaud pour ce chunk
// saute le refetch et génère avec les ANCIENS params. En faisant porter LayoutVersion par le
// contexte, un nouvel opérateur ne PEUT PAS oublier de l'inclure dans sa clé.
//
// LayoutVersion is here BY CONSTRUCTION, not by politeness — see AUDIT C2. Carrying it in the
// context means a new op CANNOT forget to put it in its cache key.
struct FVoxelOpContext
{
FIntVector ChunkCoord = FIntVector::ZeroValue;
// Pas d'échantillonnage LOD (1/2/4…). Un opérateur a le droit de se simplifier quand Step
// est grand — c'est le contrat T2.b : le bruit volumétrique par voxel perd des octaves au
// loin, le bruit de champ XY délibérément non (il alimente des caches box-validés partagés).
int32 Step = 1;
uint32 Seed = 0;
// Compteur de génération du layout (UVoxelStrateManager::GetLayoutVersion()).
// DOIT faire partie de toute clé de cache. Voir AUDIT C2.
uint32 LayoutVersion = 0;
// Global XY world-edge invariant. These are copied from UVoxelSettings by the generator and
// travel with the chunk context so the automatically appended edge op cannot be omitted by a
// builder. Radius 0 is the legacy unbounded/no-op mode.
float WorldRadiusVoxels = 8192.0f;
float EdgeSealThickness = 128.0f;
// Bornes Z de la strate en coords VOXEL (pas cm).
float StrateTopWorldZ = 0.0f;
float StrateBottomWorldZ = 0.0f;
// ClassifyTile may ask box bounds about the exact marching-cubes lattice rather than the
// continuous volume between samples. Production voxel evaluation leaves this disabled;
// when enabled, operators may tighten a conservative bound only for points
// LatticeOriginVoxels + n * Step that lie inside the queried box.
bool bUseLatticeProof = false;
FIntVector LatticeOriginVoxels = FIntVector::ZeroValue;
// A mixed tile may opt into a tighter, still conservative finite-box bound for domain-warped
// sources. The normal classifier leaves this off; ClassifyTile retries only the few boxes
// that the cheap global proof could not resolve.
bool bTightenWarpProof = false;
// null = cette strate n'a pas de champ de biome.
const FBiomeContext* Biome = nullptr;
};
//=============================================================================
// L'ÉTAT QUI TRAVERSE LA PILE / THE STATE THE STACK THREADS THROUGH
//=============================================================================
// DEUX canaux, pas un. Ce n'est pas de la généralité gratuite — c'est ce que le code fait déjà :
//
// CaveSDF = EvaluateSDFCached(salles + tunnels) ← espace SDF
// CaveSDF = SmoothMin(CaveSDF, PitSDF, BlendK) ← espace SDF
// CaveSDF = SmoothMin(CaveSDF, ChimneySDF, BlendK) ← espace SDF
// → UN SEUL carve à la fin : Density -= CarveFactor · BaseDensity · 2
//
// Maze, VerticalShafts et FloatingIslands ont la même forme, et TROIS d'entre eux appliquent la
// rugosité au **SDF** (`MazeSDF += bruit·Rough`), pas à la densité. Sur la densité, le même bruit
// est mis à l'échelle par le gradient local : effet visiblement différent.
//
// Avec un seul canal, un opérateur ne peut qu'ÉCRASER le précédent — les jonctions SmoothMin
// (salle↔puits, et demain « un graphe de salles creusé DANS une montagne ») sont impossibles.
// Un `SmoothMin` entre deux SOURCES différentes est précisément ce qui fait qu'une idée composée
// a l'air d'appartenir au lieu au lieu d'y avoir été percée. Coût : un float.
//
// Two channels, not one — because that is what the code already does, and because SmoothMin between
// two different SOURCES is precisely what makes a composed idea look like it belongs there rather
// than like a hole punched in something else. Cost: one float.
struct FVoxelOpSample
{
// Convention INTERNE : POSITIF = SOLIDE. Négation vers MC une seule fois, par l'appelant.
// INTERNAL convention: POSITIVE = SOLID. Negated to MC once, by the caller.
float Density = 0.0f;
// Convention SDF standard : NÉGATIF = à l'intérieur de la primitive.
// FLT_MAX = « aucune surface à proximité » (l'état initial, et le early-out des sources
// placées quand aucune primitive n'atteint ce voxel).
// FLT_MAX = "no surface nearby" — the initial state, and the early-out placed sources use.
float Sdf = FLT_MAX;
// Block evaluation keeps the room source's per-sample selection explicit. The scalar path
// still uses the source's worker-local state, but a later block-capable modifier must never
// observe the state belonging to the last sample of the preceding operator.
const FChunkSDFCache* RoomCache = nullptr;
int32 NearestRoomIndex = INDEX_NONE;
// The common generator tail consumes the continuous swept-shape metadata after the block has
// left the stack. Keeping it in the sample avoids re-running the world-space tunnel scan or
// consulting a last-sample TLS value after an op-major evaluation.
bool bHasTunnelCoreWorldEvaluation = false;
float TunnelCoreWorldSDF = FLT_MAX;
bool bTunnelCoreRoomFloor = false;
bool bHasTunnelCoreSweptFloor = false;
float TunnelCoreSweptFloorZ = -FLT_MAX;
float TunnelCoreSweptFloorRadius = 0.0f;
};
/**
* A regular world lattice handed to one operator at a time.
*
* The default implementation is deliberately scalar and therefore source-compatible with custom
* operators. An operator may override EvalBlock only when it preserves the exact per-sample
* semantics of Eval. Samples are laid out X-fastest, then Y, then Z; all coordinates are voxel
* coordinates and are formed as Origin + index * Step using the same integer arithmetic as the
* mesher's scalar path.
*/
struct FVoxelOpBlock
{
FIntVector OriginVoxels = FIntVector::ZeroValue;
int32 Step = 1;
int32 SizeX = 0;
int32 SizeY = 0;
int32 SizeZ = 0;
FVoxelOpSample* Samples = nullptr;
int32 NumSamples() const { return SizeX * SizeY * SizeZ; }
FORCEINLINE FVoxelOpSample& At(int32 X, int32 Y, int32 Z) const
{
return Samples[(Z * SizeY + Y) * SizeX + X];
}
};
/**
* Intervalle conservateur du canal SDF sur une boîte.
*
* `Min` et `Max` sont les bornes SUPRÊMES de toutes les valeurs que l'opérateur peut produire dans
* la boîte. `[FLT_MAX, FLT_MAX]` est l'identité exacte : aucune surface connue. Un intervalle inconnu
* est volontairement large ; il force le fold à rester `Mixed` plutôt que de risquer un faux tile
* uniforme. Les setters refusent toute borne non finie ou inversée : un opérateur qui ne peut pas
* prouver son intervalle perd le skip, jamais la géométrie.
*
* Conservative SDF interval over a box. `Min`/`Max` bound every value the operator can produce.
* `[FLT_MAX, FLT_MAX]` is the exact "no surface" identity. Unknown is deliberately wide: it costs
* a skip, never a hole. Invalid bounds become unknown instead of being guessed.
*/
struct FVoxelBoxSdfInterval
{
bool bKnown = true;
float Min = FLT_MAX;
float Max = FLT_MAX;
bool IsKnown() const { return bKnown; }
void SetUnknown()
{
bKnown = false;
Min = -FLT_MAX;
Max = FLT_MAX;
}
void Set(float InMin, float InMax)
{
if (!VoxelMath::IsFinite(InMin) || !VoxelMath::IsFinite(InMax) || InMin > InMax)
{
SetUnknown();
return;
}
bKnown = true;
Min = InMin;
Max = InMax;
}
/** Output of `min(Input, Other)` when both intervals are known. */
void MinWith(const FVoxelBoxSdfInterval& Other)
{
if (!bKnown || !Other.bKnown)
{
SetUnknown();
return;
}
Min = FMath::Min(Min, Other.Min);
Max = FMath::Min(Max, Other.Max);
}
};
// Forward declaration: the interval-aware EffectOverBox overloads consume the hypotheses that
// have already been folded, while the hypotheses themselves are declared immediately below the
// interface.
struct FVoxelBoxHypotheses;
//=============================================================================
// L'INTERFACE / THE INTERFACE
//=============================================================================
// Trois méthodes, et elles FORMALISENT CE QUE LE CODE FAIT DÉJÀ À LA MAIN : chaque archétype
// hisse déjà son travail constant-par-chunk dans un cache thread_local (= PrepareChunk), évalue
// à bas coût par voxel (= Eval), et possède déjà dans ClassifyTile une déclaration écrite à la
// main de ce qu'il peut faire à une tuile (= EffectOverBox). Ce n'est pas une nouvelle
// discipline, c'est la discipline existante, nommée.
class IVoxelDensityOp
{
public:
virtual ~IVoxelDensityOp() = default;
virtual EVoxelOpRole GetRole() const = 0;
/**
* Channel contract for the composer. A read is a field inspected from `InOut` before this op
* publishes its result; a write is a field whose value may differ when the op returns. A
* read/write declaration therefore means "transform the current version". `+=` and `-=` are
* both reads and writes; an assignment that ignores the old field is a write-only producer /
* replacement. Context, coordinates, caches and referenced objects are not sample channels.
*
* These are graph metadata, not voxel work: the stack snapshots them while it is assembled and
* the validator reads the snapshot. They must never be called from `Eval`.
*/
virtual EVoxelOpChannelMask ChannelReads() const = 0;
virtual EVoxelOpChannelMask ChannelWrites() const = 0;
/**
* Non-sample state required by this op. A declaration is satisfied only by a provider that
* appeared earlier in the stack. Defaults are deliberately empty so existing independent
* operators and external test doubles remain source-compatible.
*/
virtual EVoxelOpResourceMask RequiredResources() const
{
return VoxelOpResources::None;
}
/** State published for later consumers, e.g. FRoomGraphSource's room geometry cache. */
virtual EVoxelOpResourceMask ProvidedResources() const
{
return VoxelOpResources::None;
}
/**
* True when this op contributes a delta to its written channels without replacing, clamping,
* selecting, or otherwise depending on the previous value of those written channels. Such an
* op may be shuffled with another compatible additive delta; a false result means its position
* is explicit. A gate on a channel the op itself writes is transformative even if its final
* arithmetic contains `+=`.
*/
virtual bool IsAdditive() const = 0;
/**
* Hisser ici TOUT le travail constant sur le chunk : listes de salles, grilles de biome,
* caches de colonnes, cuissons de treillis. Appelé une fois par chunk et par worker.
* C'est ici que déménagent les caches thread_local d'aujourd'hui.
*
* THREADING : appelé sur des workers. L'opérateur ne doit écrire QUE son propre état
* par-chunk ; le Generator / le Mesher / le StrateManager restent en LECTURE SEULE
* (invariant ARCHITECTURE §8.10). Toute clé de cache DOIT inclure Ctx.LayoutVersion.
*/
virtual void PrepareChunk(const FVoxelOpContext& Ctx) = 0;
/**
* Par voxel. `InOut` est l'état que la pile a produit jusqu'ici (voir FVoxelOpSample).
* Coordonnées en VOXELS, pas en cm.
*
* INVARIANCE DE FENÊTRE (ARCHITECTURE §8.4) : fonction PURE de (coords monde, seed, layout).
* Le même point évalué depuis une autre tuile, un autre ordre, un autre thread doit rendre le
* float BIT-IDENTIQUE. Pas « proche » : 1 ULP d'écart entre deux fenêtres est une couture
* visible, et en multijoueur une divergence de monde. Le test
* VoxelForge.Determinism.DensityPurity vérifie cela.
*/
virtual void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const = 0;
/**
* Op-major evaluation seam. The conservative default is intentionally not clever: it calls
* the canonical scalar Eval for every sample. This lets the stack and mesher be converted in
* reviewable stages while byte-identical output remains the acceptance test. Stateful ops
* may restore their per-sample hand-off from FVoxelOpSample in PrepareBlockSample; the default
* invokes that hook before each scalar fallback call.
*/
virtual void EvalBlock(const FVoxelOpBlock& Block) const
{
if (Block.Samples == nullptr || Block.Step <= 0
|| Block.SizeX <= 0 || Block.SizeY <= 0 || Block.SizeZ <= 0)
{
return;
}
for (int32 Z = 0; Z < Block.SizeZ; ++Z)
{
for (int32 Y = 0; Y < Block.SizeY; ++Y)
{
for (int32 X = 0; X < Block.SizeX; ++X)
{
FVoxelOpSample& Sample = Block.At(X, Y, Z);
PrepareBlockSample(Sample);
Eval(
static_cast<float>(Block.OriginVoxels.X + X * Block.Step),
static_cast<float>(Block.OriginVoxels.Y + Y * Block.Step),
static_cast<float>(Block.OriginVoxels.Z + Z * Block.Step),
Sample);
}
}
}
}
/** Restore a source-owned per-sample context before a scalar fallback in EvalBlock. */
virtual void PrepareBlockSample(const FVoxelOpSample&) const {}
/** Optional hand-off for the common structural tail. The default keeps custom operators
* conservative: the caller uses its canonical cache path when no source publishes a result. */
virtual bool TryGetLastTunnelCoreWorldEvaluation(
FTunnelCoreWorldEvaluation& OutEvaluation) const
{
return false;
}
/**
* CONSERVATIF. The two-argument method is the intrinsic fallback. The state-aware overload
* below is what the stack calls: it sees the SDF interval already produced by earlier ops.
* Returning Both is always safe; returning the wrong direction is a hole.
*
* Le contrat est « conservatif », pas « forme close » : un opérateur A LE DROIT
* D'ÉCHANTILLONNER pour répondre. C'est exactement ce que fait ClassifyTile aujourd'hui pour
* SurfaceWorld — il évalue ComputeSurfaceColumn sur le treillis EXACT du mesher, mêmes
* fonctions, mêmes floats, donc verdict exact plutôt qu'estimé. Cela DOIT survivre au portage.
*
* The contract is "conservative", not "closed-form": an op MAY sample to answer.
*
* An SDF-only source MUST NOT answer for a converter that happens to follow it. It reports its
* own interval through `PropagateSdfOverBox`; the converter's state-aware overload then folds
* the interval through its own formula. This is the isolation rule that makes free composition
* safe.
*/
virtual EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const = 0;
/** State-aware effect. The default preserves old custom operators' conservative direction. */
virtual EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
const FVoxelBoxHypotheses&) const
{
return EffectOverBox(VoxelBox, Ctx);
}
/**
* ⚠️ LE PLIAGE QUI PORTE DES NOMBRES — `OPSTACK-DECOMPOSITION §0.2`, et le plus gros poste de
* perf du plan.
*
* La direction seule ne suffit pas pour les opérateurs FIELDÉS. Un carve à seuil de bruit (les
* vers de TunnelNetwork, la rugosité de paroi) n'a AUCUNE borne spatiale : il rend `CarveOnly`
* sur CHAQUE boîte de CHAQUE strate qui l'active, donc il tue l'hypothèse `AllSolid` partout et
* l'archétype ne saute pas une tuile. Aucun raffinement de `EffectOverBox` ne peut le récupérer,
* parce que la réponse « oui, je peux creuser ici » est VRAIE.
*
* **Mais son AMPLITUDE est bornée, et souvent triviale** : pour un ver, `t ∈ [0,1]` et
* `Mask ∈ [0,1]`, donc il ne peut déplacer la densité vers l'air que de `WormStrength` au plus.
* Si le roc est solide d'une marge SUPÉRIEURE à la somme de tous les carves restants, la boîte
* est prouvablement pleine — quel que soit le bruit.
*
* D'où deux nombres, en unités de DENSITÉ (convention interne, positif = solide) :
* • `MaxCarveOverBox` — de combien AU PLUS cet opérateur peut baisser la densité sur la boîte,
* • `MaxFillOverBox` — de combien AU PLUS il peut la monter.
*
* **`FLT_MAX` = « je ne sais pas », et c'est le DÉFAUT.** Un opérateur qui ne redéfinit rien se
* comporte donc EXACTEMENT comme avant ce changement : le pliage retire `FLT_MAX` à la marge,
* elle passe sous zéro, l'hypothèse meurt. Les treize tests d'équivalence et
* `VoxelForge.OpStack.BoxVerdictFold` ne bougent pas d'un verdict.
*
* ⚠️ SENS DE L'ERREUR : SUR-estimer une amplitude coûte du CPU (une tuile maillée pour rien) ;
* SOUS-estimer produit un TROU. Comme partout ailleurs dans ce fichier, en cas de doute rendre
* `FLT_MAX`. Ce n'est pas une borne « raisonnable », c'est une borne PROUVÉE ou rien.
*
* The fold carries NUMBERS, not just directions. A fielded noise carve has no spatial bound but
* its AMPLITUDE is bounded, so "the rock is solid by more than the sum of every remaining carve"
* becomes provable. FLT_MAX means "unknown" and is the default, so every existing op is
* unchanged. Over-estimating costs CPU; under-estimating is a hole.
*/
virtual float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
{
return FLT_MAX;
}
/** State-aware amplitude. The default is the old, conservative amplitude. */
virtual float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
const FVoxelBoxHypotheses&) const
{
return MaxCarveOverBox(VoxelBox, Ctx);
}
virtual float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
{
return FLT_MAX;
}
/** State-aware amplitude. The default is the old, conservative amplitude. */
virtual float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
const FVoxelBoxHypotheses&) const
{
return MaxFillOverBox(VoxelBox, Ctx);
}
/**
* Propagate this operator's own SDF result through a box query. Only call this for an operator
* whose cached channel declaration writes `Sdf`. The default is unknown by construction: a new
* SDF writer cannot accidentally make a false uniform verdict merely because it forgot this
* method. Implementations that assign SDF replace the interval; min/accumulating writers use
* `FVoxelBoxSdfInterval::MinWith`.
*/
virtual void PropagateSdfOverBox(FVoxelBoxSdfInterval& InOut, const FBox& VoxelBox,
const FVoxelOpContext&) const
{
InOut.SetUnknown();
}
/**
* Pour un opérateur FORÇANT (celui dont `ClassifyBox` rend autre chose que `Mixed`) : de combien
* la densité est-elle garantie du bon côté de zéro, PARTOUT dans la boîte ?
*
* C'est l'autre moitié du pliage numérique. `MaxCarveOverBox` dit ce qu'on peut RETIRER ; ceci
* dit ce qu'il y avait à retirer. Sans les deux, la soustraction n'a pas de premier terme.
*
* Exemple, et c'est LE cas qui compte : `FConstantFieldSource` pose `Density = BaseDensity`
* partout. Sa marge est donc exactement `BaseDensity`. Un ver à `WormStrength = 0.6` sur un roc
* à `BaseDensity = 1.0` laisse 0.4 de marge ⇒ la boîte reste prouvablement pleine.
*
* **0 = « je ne sais pas », et c'est le DÉFAUT** : la marge tombe à zéro, le premier carve la
* fait passer sous zéro, l'hypothèse meurt — le comportement d'avant, à l'identique.
*/
virtual float ForcedMarginOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
{
return 0.0f;
}
/**
* OPÉRATEURS FORÇANTS. Certains opérateurs ne « déplacent » pas la densité d'entrée : ils
* l'ÉCRASENT. La question « dans quelle direction peux-tu bouger ce champ ? » n'a alors pas de
* sens ; la bonne question est « sais-tu prouver que toute cette boîte est d'un seul côté,
* QUELLE QUE SOIT l'entrée ? ».
*
* Deux familles répondent autre chose que Mixed :
* • les SOURCES (rôle 1) — elles posent le champ, donc elles le savent par construction ;
* • les op STRUCTURELS forçants — ApplyBoundarySeal et ApplyXYEdgeSeal — qui, dans leur bande
* prouvée, font `Max(Density, SealFactor·BaseDensity)` avec SealFactor > 0 : le résultat est
* solide garanti quoi qu'il y ait eu avant. C'est exactement ce que ClassifyTile encode
* aujourd'hui avec « bande de seal ⇒ bCanAir = false » — et un simple FillOnly ne suffirait
* PAS à le reproduire (voir VF_FoldOp plus bas).
*
* Par défaut Mixed = « je ne sais pas », toujours sûr. Une source à primitives placées (graphe
* de salles, îles, puits) répond en testant ses bornes ; une source heightfield répond en
* échantillonnant ses colonnes sur le treillis exact, exactement comme aujourd'hui.
*
* FORCING OPS. Some ops do not *move* the input density, they *overwrite* it. Default Mixed =
* "I don't know", always safe. The vertical and XY edge seals are the non-source examples, and
* they are the reason this method exists at all rather than being folded into EffectOverBox.
*/
virtual EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
{
return EVoxelTileClass::Mixed;
}
/**
* Déclare si Eval dépend de Z. Les opérateurs purs en XY reçoivent le traitement du cache de
* colonnes T1.a de façon GÉNÉRIQUE, au lieu que SurfaceWorld en ait un sur mesure.
*
* ⚠️ Le cache de colonnes est clé sur (boîte XY, StrateKey, Seed) SANS ChunkZ et il est
* partagé sur TOUTE la pile verticale de chunks. Mettre une donnée dépendante de Z dans un
* opérateur qui se déclare XY-pur corrompt silencieusement chaque chunk de la colonne, et
* ValidateDeterminism — qui échantillonne le long d'une frontière en X — ne le verrait pas.
*/
virtual bool IsXYPure() const { return false; }
/**
* DIAGNOSTIC UNIQUEMENT — le nom que les rapports de test impriment pour cet opérateur.
*
* ⚠️ POURQUOI CETTE MÉTHODE EXISTE. Sans nom par opérateur, un rapport « 0 tuile prouvée sur
* 40 » ne peut dire que « ou bien les tuiles traversent toutes une grotte, ou bien la source
* n'atteint pas sa branche `Identity` » — deux causes, zéro nombre pour les départager, alors
* que la vraie cause peut être un TROISIÈME opérateur (p. ex. un ver qui rend `CarveOnly`
* partout). Avec un nom par opérateur, `ClassifyBoxAttributed` répond « c'est celui-là » au
* lieu de laisser deviner.
*
* N'entre dans AUCUNE clé de cache, dans aucun hash, dans aucune décision de génération : le
* changer ne peut pas changer le monde. Le défaut est volontairement laconique — un opérateur
* sans nom se repère à son index, ce qui suffit à savoir où regarder.
*
* Diagnostic only: the first build of the spatial EffectOverBox came back green with 0 tiles
* proved, and the report could not name which operator was killing the hypothesis. It was a
* third one nobody was looking at. Never part of a cache key or any generation decision.
*/
virtual const TCHAR* DebugName() const { return TEXT("(unnamed op)"); }
};
//=============================================================================
// LE PLIAGE : comment la pile devient un verdict de tuile
// THE FOLD: how a stack becomes a tile verdict
//=============================================================================
// C'est l'algorithme générique qui remplace le ClassifyTile écrit à la main, et il REPRODUIT
// EXACTEMENT le comportement d'aujourd'hui — vérifié ligne à ligne contre VoxelGenerator.cpp :
//
// source gap bedrock → ClassifyBox = AllSolid
// source SurfaceWorld → ClassifyBox échantillonne les colonnes sur le treillis exact
// ApplyPassageCarving → CarveOnly (tue AllSolid) ≡ « AnyPassageNearBox ⇒ bCanSolid=false »
// ApplyOriginSpine → CarveOnly (tue AllSolid) ≡ le test cercle/boîte XY
// ApplyBoundarySeal → ClassifyBox = AllSolid DANS sa bande (opérateur forçant),
// FillOnly ailleurs ≡ « bande de seal ⇒ bCanAir=false »
// ApplyXYEdgeSeal → ClassifyBox = AllSolid DANS sa bande radiale (forçant) ; il gagne
// après les passages, donc la coque extérieure reste fermée.
// disturbances chasms → CarveOnly ≡ « ChasmDensity > 0 ⇒ bCanSolid=false »
// disturbances ponts/arêtes → FillOnly ≡ « Bridge/RidgeDensity > 0 ⇒ bCanAir=false »
// diff layer → Both si des mods touchent la boîte, sinon Identity
// ≡ « HasAnyModInChunkRange ⇒ Mixed »
//
// et le verdict final « exactement une hypothèse survit, sinon Mixed » est littéralement le
// `if (bCanSolid == bCanAir) return Mixed;` de la fin de ClassifyTile.
//
// This fold reproduces today's hand-written ClassifyTile exactly — verified line by line against
// VoxelGenerator.cpp. That correspondence is the evidence that the abstraction fits this codebase
// rather than being imposed on it.
/** Les deux hypothèses que ClassifyTile poursuit, sous forme d'état pliable. */
struct FVoxelBoxHypotheses
{
bool bCanBeAllSolid = true;
bool bCanBeAllAir = true;
/**
* ⚠️ LES DEUX NOMBRES DU PLIAGE (`OPSTACK-DECOMPOSITION §0.2`).
* `SolidMargin` = de combien la densité est encore garantie AU-DESSUS de zéro partout dans la
* boîte, SOUS l'hypothèse « tout solide ». Un opérateur forçant la pose ; chaque carve en retire
* son amplitude maximale ; quand elle n'est plus strictement positive, l'hypothèse meurt.
* `AirMargin` est son miroir.
*
* **Elles valent 0 sur un état neuf, et c'est ce qui rend le changement rétro-compatible :**
* sans opérateur forçant qui déclare une marge, le premier carve fait `0 − FLT_MAX < 0` et tue
* l'hypothèse — le comportement exact d'avant le pliage numérique.
*/
float SolidMargin = 0.0f;
float AirMargin = 0.0f;
/** SDF interval after all SDF writers folded so far. It starts at the exact no-surface identity. */
FVoxelBoxSdfInterval Sdf;
bool IsDead() const { return !bCanBeAllSolid && !bCanBeAllAir; }
/** Verdict final : exactement une hypothèse doit survivre. Égalité = prudence ⇒ Mixed. */
EVoxelTileClass Resolve() const
{
if (bCanBeAllSolid == bCanBeAllAir) { return EVoxelTileClass::Mixed; }
return bCanBeAllSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir;
}
};
/** Poser l'état depuis un verdict FORÇANT (source, ou seal dans sa bande) : l'opérateur écrase
* l'entrée, donc il écrase aussi tout ce que la pile avait conclu avant lui. Un verdict « tout
* air » affirme du même coup « pas tout solide », et réciproquement. */
/** @param Margin de combien la densité est garantie du bon côté de zéro dans toute la boîte.
* 0 (le défaut) = « je ne sais pas » ⇒ comportement d'avant le pliage numérique. */
FORCEINLINE void VF_ForceHypotheses(FVoxelBoxHypotheses& H, EVoxelTileClass ForcedVerdict,
float Margin = 0.0f)
{
switch (ForcedVerdict)
{
case EVoxelTileClass::AllSolid: H.bCanBeAllSolid = true; H.bCanBeAllAir = false;
H.SolidMargin = Margin; H.AirMargin = 0.0f; break;
case EVoxelTileClass::AllAir: H.bCanBeAllSolid = false; H.bCanBeAllAir = true;
H.SolidMargin = 0.0f; H.AirMargin = Margin; break;
case EVoxelTileClass::Mixed:
default: H.bCanBeAllSolid = false; H.bCanBeAllAir = false;
H.SolidMargin = 0.0f; H.AirMargin = 0.0f; break;
}
}
/**
* Plier l'effet d'un opérateur dans l'état. **Monotone : ne fait que tuer**, jamais ressusciter —
* c'est la propriété de sûreté, et le pliage numérique ne l'affaiblit pas : une marge ne peut que
* DESCENDRE, jamais remonter, en dehors d'un opérateur forçant.
*
* ⚠️ `MaxCarve` / `MaxFill` valent `FLT_MAX` par défaut = « amplitude inconnue ». Une borne finie
* nulle signifie au contraire « identité prouvée » pour cette direction : l'opérateur peut être
* géométriquement présent mais ne modifie aucun échantillon de la boîte. La soustraction
* fait alors passer la marge très en dessous de zéro et l'hypothèse meurt, exactement comme la
* version purement directionnelle de ce pliage. Aucun opérateur existant ne change de verdict.
* (Arithmétique volontairement laissée en float sans garde : `0 − FLT_MAX` vaut `−FLT_MAX`,
* `−FLT_MAX − FLT_MAX` sature à `−inf`, et `−inf > 0` est faux. Pas de NaN possible, les deux
* termes étant de même signe.)
*/
FORCEINLINE void VF_FoldEffect(FVoxelBoxHypotheses& H, EVoxelOpEffect Effect,
float MaxCarve = FLT_MAX, float MaxFill = FLT_MAX)
{
switch (Effect)
{
case EVoxelOpEffect::Identity:
break;
case EVoxelOpEffect::CarveOnly:
// A finite zero bound is a proved identity for this direction. This matters for
// state-aware structural guards: a passage may be geometrically present in a box while
// its exact lattice carve factor is zero, and that must not kill an otherwise valid
// AllSolid hypothesis merely because its margin is zero/unknown.
if (MaxCarve == 0.0f)
{
break;
}
if (!(MaxCarve >= 0.0f))
{
H.bCanBeAllSolid = false;
H.SolidMargin = -FLT_MAX;
break;
}
H.SolidMargin -= MaxCarve;
if (!(H.SolidMargin > 0.0f)) { H.bCanBeAllSolid = false; }
break;
case EVoxelOpEffect::FillOnly:
if (MaxFill == 0.0f)
{
break;
}
if (!(MaxFill >= 0.0f))
{
H.bCanBeAllAir = false;
H.AirMargin = -FLT_MAX;
break;
}
H.AirMargin -= MaxFill;
if (!(H.AirMargin > 0.0f)) { H.bCanBeAllAir = false; }
break;
case EVoxelOpEffect::Both:
default:
if (MaxCarve != 0.0f)
{
if (!(MaxCarve >= 0.0f))
{
H.bCanBeAllSolid = false;
H.SolidMargin = -FLT_MAX;
}
else
{
H.SolidMargin -= MaxCarve;
if (!(H.SolidMargin > 0.0f)) { H.bCanBeAllSolid = false; }
}
}
if (MaxFill != 0.0f)
{
if (!(MaxFill >= 0.0f))
{
H.bCanBeAllAir = false;
H.AirMargin = -FLT_MAX;
}
else
{
H.AirMargin -= MaxFill;
if (!(H.AirMargin > 0.0f)) { H.bCanBeAllAir = false; }
}
}
break;
}
}
/**
* Plier UN opérateur. L'ORDRE COMPTE ICI, et c'est délibéré : un opérateur forçant écrase ce que
* la pile avait conclu AVANT lui, tandis que les opérateurs qui suivent continuent de s'appliquer.
*
* Exemple à garder en tête, parce qu'il est le piège : sur une boîte entièrement dans la bande de
* seal, la source dit peut-être « tout air » (on est au-dessus du terrain), puis le seal FORCE
* « tout solide » — verdict AllSolid, comme aujourd'hui. Si le seal ne savait dire que FillOnly,
* on obtiendrait « les deux hypothèses mortes ⇒ Mixed » : pas un trou, mais la perte pure et
* simple d'une des tuiles triviales que T1.d sait sauter. C'est pour cela que ClassifyBox existe.
*
* Réciproquement, un passage qui traverse cette même boîte rend CarveOnly APRÈS le seal et retue
* l'hypothèse solide ⇒ Mixed. Identique au code actuel, où la garde passage et la garde seal se
* neutralisent en `bCanSolid == bCanAir`.
*
* ORDER MATTERS HERE, deliberately: a forcing op overwrites what the stack concluded before it,
* while ops after it still apply. This is what lets the seal recover an AllSolid verdict that a
* pure FillOnly would have thrown away, while still letting a passage take it back.
*/
FORCEINLINE void VF_FoldOp(FVoxelBoxHypotheses& H, const IVoxelDensityOp& Op,
const FBox& VoxelBox, const FVoxelOpContext& Ctx,
bool bWritesSdf)
{
// Soundness bias: an unknown SDF interval is allowed to kill a skip, never to manufacture a
// uniform tile. The interval is propagated only after this op's own effect has been folded, so
// the next op sees exactly the SDF version that Eval would expose at this point in the stack.
const EVoxelTileClass Forced = Op.ClassifyBox(VoxelBox, Ctx);
if (Forced != EVoxelTileClass::Mixed)
{
VF_ForceHypotheses(H, Forced, Op.ForcedMarginOverBox(VoxelBox, Ctx));
if (bWritesSdf) { Op.PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); }
return;
}
VF_FoldEffect(H, Op.EffectOverBox(VoxelBox, Ctx, H),
Op.MaxCarveOverBox(VoxelBox, Ctx, H),
Op.MaxFillOverBox(VoxelBox, Ctx, H));
if (bWritesSdf) { Op.PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); }
}
/** Backward-compatible helper for unit tests and callers with no cached declaration metadata. */
FORCEINLINE void VF_FoldOp(FVoxelBoxHypotheses& H, const IVoxelDensityOp& Op,
const FBox& VoxelBox, const FVoxelOpContext& Ctx)
{
VF_FoldOp(H, Op, VoxelBox, Ctx,
(Op.ChannelWrites() & VoxelOpChannels::Sdf) != 0);
}
@@ -0,0 +1,730 @@
// VoxelDensityOpStack.h
// La PILE : un conteneur ordonné d'opérateurs, plus les fabriques d'opérateurs concrets.
// The STACK: an ordered container of operators, plus the concrete-operator factories.
//
// ⚠️ CECI EST LE CHEMIN DE GÉNÉRATION DU JEU.
// `UVoxelGenerator::GetDensityAt` construit la pile par chunk pour chaque strate :
// **Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork,
// Underwater (8 sur 8)**. `ClassifyTile` construit la même pile pour les archétypes de cave et plie
// `ClassifyBox`; les gaps et SurfaceWorld gardent leurs preuves exactes dédiées.
//
// THIS IS THE GAME'S GENERATION PATH. GetDensityAt builds the stack per chunk for every strate.
// ClassifyTile uses the same stack for cave archetypes; gaps and SurfaceWorld retain their exact
// hand-written proofs.
//
// POURQUOI CETTE FORME / WHY THIS SHAPE
// La question à laquelle cette forme répond n'est pas « est-ce que ça marche ? » mais
// **« est-ce que la séparation source / modifier tombe naturellement du code existant ? »**
// (OPSTACK-PLAN §4). Chaque archétype a été porté puis comparé bit à bit à sa fonction
// d'origine avant qu'elle ne soit retirée : la réponse a été MESURÉE plutôt qu'une opinion.
#pragma once
#include "CoreMinimal.h"
#include "VoxelDensityOp.h"
#include "VoxelDensityProfile.h"
#include "VoxelStrateTypes.h" // FMazeGenerationParams
#include "VoxelHeightOp.h" // IVoxelBiomeField — BuildSurfaceStack takes ownership of one
class UVoxelGenerator;
class UVoxelStrateManager;
struct FVoxelOpStackRecipe;
struct FVoxelStrateArchetypeParams;
struct FVoxelStrateRegionManifest;
enum class EVoxelStrateOpClass : uint8;
/** The declarations the offline structure roller reads before it ever materialises an op. */
struct VOXELFORGE_API FVoxelStrateOpContract
{
EVoxelOpRole Role = EVoxelOpRole::DetailModifier;
EVoxelOpChannelMask Reads = VoxelOpChannels::None;
EVoxelOpChannelMask Writes = VoxelOpChannels::None;
bool bAdditive = false;
EVoxelOpResourceMask RequiredResources = VoxelOpResources::None;
EVoxelOpResourceMask ProvidedResources = VoxelOpResources::None;
};
/**
* A lowered evaluator selected when a stack has a proven, canonical implementation.
*
* The authored operator list remains present for classification, diagnostics, tests, and the
* fallback evaluator. This is a plan choice made while the stack is assembled, not a runtime
* CPU-feature dispatch and not a second semantic graph.
*/
enum class EVoxelOpFusedEvaluator : uint8
{
None,
TunnelNetwork,
};
/**
* FVoxelOpStack — une liste ordonnée d'opérateurs + le pliage de verdict de boîte.
*
* PROPRIÉTÉ (rôle 4) : les opérateurs STRUCTURELS sont ajoutés par `AppendStructuralPost` et
* l'ordre spine → seal → passage → seal XY est garanti par cette fonction, pas par l'auteur. Un auteur ne
* peut pas les omettre ni les réordonner — ce sont des invariants de monde (la descente doit rester
* possible, les seals doivent tenir, les passages doivent percer).
*
* PROPRIÉTÉ (threading) : la pile est LUE par les workers. Les opérateurs concrets qui ont besoin
* d'un cache par cellule/chunk le tiennent en `thread_local` à l'intérieur de leur `Eval`, comme le
* fait déjà chaque fonction d'archétype. En Phase 3, quand les opérateurs deviendront des assets
* partagés, il faudra un objet d'état PAR WORKER — noté ici pour que ça ne surprenne personne.
*
* THREADING: the stack is READ by workers. Concrete ops that need a per-cell/per-chunk cache keep it
* thread_local inside Eval, exactly as every archetype function already does. Phase 3 (ops as shared
* assets) will need a per-worker state object — flagged here so it is not a surprise.
*/
class FVoxelOpStack
{
private:
/** Metadata is snapshotted when an op enters the stack so no declaration virtual is ever
* dispatched from `EvalSample`'s per-voxel loop. */
struct FOpEntry
{
TUniquePtr<IVoxelDensityOp> Op;
EVoxelOpChannelMask Reads = VoxelOpChannels::None;
EVoxelOpChannelMask Writes = VoxelOpChannels::None;
bool bAdditive = false;
EVoxelOpResourceMask RequiredResources = VoxelOpResources::None;
EVoxelOpResourceMask ProvidedResources = VoxelOpResources::None;
const TCHAR* ProfileName = nullptr;
VoxelDensityProfile::EBucket ProfileBucket = VoxelDensityProfile::EBucket::OtherOp;
};
public:
// DÉPLAÇABLE, PAS COPIABLE — et c'est la bonne sémantique, pas un contournement de compilateur :
// une pile POSSÈDE ses opérateurs de façon unique. La copier voudrait dire cloner des opérateurs
// polymorphes, ce qui n'a pas de sens ici (une pile n'existe qu'une fois par strate).
//
// ⚠️ NOTE COMPILATEUR : ne PAS remettre `VOXELFORGE_API` sur la classe. Sous MSVC, dllexport sur
// une classe force l'instanciation de TOUS ses membres implicites, y compris l'opérateur
// d'affectation par copie — impossible à générer pour un `TArray<FOpEntry>`, d'où
// l'erreur C2280 « fonction supprimée ». L'export va sur la seule méthode hors-ligne.
//
// MOVE-ONLY, and that is the correct semantics rather than a compiler workaround: a stack
// uniquely OWNS its operators. Do NOT put VOXELFORGE_API back on the class — under MSVC,
// dllexport forces instantiation of every implicit member including copy-assignment, which
// cannot be generated for a TArray<FOpEntry> (error C2280). Export the out-of-line
// method instead.
FVoxelOpStack() = default;
FVoxelOpStack(FVoxelOpStack&&) = default;
FVoxelOpStack& operator=(FVoxelOpStack&&) = default;
FVoxelOpStack(const FVoxelOpStack&) = delete;
FVoxelOpStack& operator=(const FVoxelOpStack&) = delete;
void Add(TUniquePtr<IVoxelDensityOp> Op)
{
// Adding an op invalidates any previously selected lowered plan. Factories mark the plan
// only after the complete authored graph has been assembled.
FusedEvaluator = EVoxelOpFusedEvaluator::None;
FOpEntry Entry;
Entry.Op = MoveTemp(Op);
if (Entry.Op.Get() != nullptr)
{
// Assembly-time metadata only. ValidateChannelOrder reuses this snapshot, and Eval
// never asks an operator for its declaration.
Entry.Reads = Entry.Op->ChannelReads();
Entry.Writes = Entry.Op->ChannelWrites();
Entry.bAdditive = Entry.Op->IsAdditive();
Entry.RequiredResources = Entry.Op->RequiredResources();
Entry.ProvidedResources = Entry.Op->ProvidedResources();
Entry.ProfileName = Entry.Op->DebugName();
Entry.ProfileBucket = VoxelDensityProfile::BucketFromName(Entry.ProfileName);
}
Ops.Add(MoveTemp(Entry));
}
int32 Num() const { return Ops.Num(); }
/** Select the lowered evaluator for this complete authored graph. */
void SetFusedEvaluator(EVoxelOpFusedEvaluator InEvaluator)
{
FusedEvaluator = InEvaluator;
}
/** The lowered plan, or None when the general interpreted/block path must be used. */
EVoxelOpFusedEvaluator GetFusedEvaluator() const
{
return FusedEvaluator;
}
/** True when this stack publishes the requested immutable geometry resource. */
bool ProvidesResource(EVoxelOpResourceMask Resource) const
{
if (Resource == VoxelOpResources::None)
{
return false;
}
for (const FOpEntry& Entry : Ops)
{
if ((Entry.ProvidedResources & Resource) == Resource)
{
return true;
}
}
return false;
}
// Allocator-backed bytes owned by the stack, including each concrete operator allocation.
// The enclosing cache entry adds sizeof(FVoxelOpStack) separately.
SIZE_T GetAllocatedSize() const;
/** Hoist chunk-constant work for every op. Une fois par chunk et par worker.
* Non-const : ça MUTE l'état par-chunk des opérateurs, et le prétendre const serait un
* mensonge utile qui finirait par masquer une course. */
void PrepareChunk(const FVoxelOpContext& Ctx)
{
PreparedContext = Ctx;
bHasPreparedContext = true;
for (const FOpEntry& Entry : Ops) { Entry.Op->PrepareChunk(Ctx); }
}
/**
* Évalue la pile complète en un point. Rend la densité en convention INTERNE
* (positif = solide) — l'appelant négate UNE FOIS pour le marching cubes.
*
* Returns INTERNAL-convention density (positive = solid). The caller negates once for MC.
*/
float EvalInternal(float WorldX, float WorldY, float WorldZ) const
{
return EvalSample(WorldX, WorldY, WorldZ).Density;
}
/** L'état COMPLET (densité + SDF) après toute la pile. Diagnostic : quand deux évaluations
* divergent, c'est le canal SDF qui dit si l'écart naît avant ou après la conversion.
* / The full state after the stack — the SDF channel is what says whether a divergence is
* born before or after the carve. */
FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const
{
FVoxelOpSample S;
// Fine operation timers are a diagnostic mode. Keeping their construction out of the
// ordinary scalar path matters for callers that still use fractional samples or an
// archetype without a block session; the block path already times one scope per op/block.
const bool bProfileOps = VoxelDensityProfile::GetMode() == VoxelDensityProfile::EMode::Full;
for (const FOpEntry& Entry : Ops)
{
if (bProfileOps)
{
VoxelDensityProfile::FScopedTimer ProfileTimer(Entry.ProfileBucket);
Entry.Op->Eval(WorldX, WorldY, WorldZ, S);
}
else
{
Entry.Op->Eval(WorldX, WorldY, WorldZ, S);
}
}
// The room-graph source publishes the authored floor ownership while evaluating the
// shape, but it must not be reasserted here. This stack is followed by the common
// disturbance/passage writers; composing the floor at this point would make every authored
// floor sample look like a post-hoc clamp (and counted 100% of them). The generator
// consumes the same immutable core result after those writers and composes the floor once,
// at the final MC boundary. The metadata remains on the sample for that hand-off and for
// diagnostics, but it is not allowed to modify density inside the operator stack.
return S;
}
/**
* Evaluate one regular lattice in operator order. The caller owns and initializes the sample
* array (normally to FVoxelOpSample defaults); each operator transforms that same array before
* the next operator runs. The timer is intentionally one scope per operator/block rather than
* one scope per operator/sample. Operators without a block implementation use the conservative
* scalar fallback supplied by IVoxelDensityOp, so this entry point is safe to introduce before
* converting the concrete operators.
*/
void EvalBlock(const FVoxelOpBlock& Block) const
{
if (Block.Samples == nullptr || Block.Step <= 0
|| Block.SizeX <= 0 || Block.SizeY <= 0 || Block.SizeZ <= 0)
{
return;
}
const bool bProfileOps = VoxelDensityProfile::GetMode() == VoxelDensityProfile::EMode::Full;
for (const FOpEntry& Entry : Ops)
{
if (bProfileOps)
{
VoxelDensityProfile::FScopedTimer ProfileTimer(Entry.ProfileBucket);
Entry.Op->EvalBlock(Block);
}
else
{
Entry.Op->EvalBlock(Block);
}
}
}
/** Evaluate only the entries that the interval fold proved non-identity for this block. */
void EvalBlock(const FVoxelOpBlock& Block, const TArray<int32>& ActiveOps) const
{
if (Block.Samples == nullptr || Block.Step <= 0
|| Block.SizeX <= 0 || Block.SizeY <= 0 || Block.SizeZ <= 0)
{
return;
}
const bool bProfileOps = VoxelDensityProfile::GetMode() == VoxelDensityProfile::EMode::Full;
for (const int32 OpIndex : ActiveOps)
{
if (!Ops.IsValidIndex(OpIndex)) { continue; }
const FOpEntry& Entry = Ops[OpIndex];
if (bProfileOps)
{
VoxelDensityProfile::FScopedTimer ProfileTimer(Entry.ProfileBucket);
Entry.Op->EvalBlock(Block);
}
else
{
Entry.Op->EvalBlock(Block);
}
}
}
/**
* Build a conservative active list for one exact mesher lattice block. An operator may be
* omitted only when its state-aware box contract says Identity and it does not publish SDF.
* SDF writers remain in the list even when their density effect is Identity: their SDF is a
* channel consumed by a later converter, and an unchanged-looking interval is not by itself a
* pointwise proof. Unknown costs the operator visit, never the geometry.
*/
int32 BuildActiveOpList(const FBox& VoxelBox, int32 InStep,
FIntVector InLatticeOrigin, TArray<int32>& OutActiveOps) const
{
OutActiveOps.Reset();
if (!bHasPreparedContext)
{
for (int32 Index = 0; Index < Ops.Num(); ++Index) { OutActiveOps.Add(Index); }
return OutActiveOps.Num();
}
FVoxelOpContext Ctx = PreparedContext;
Ctx.Step = FMath::Max(InStep, 1);
Ctx.LatticeOriginVoxels = InLatticeOrigin;
Ctx.bUseLatticeProof = true;
FVoxelBoxHypotheses H;
for (int32 Index = 0; Index < Ops.Num(); ++Index)
{
const FOpEntry& Entry = Ops[Index];
const bool bWritesSdf = (Entry.Writes & VoxelOpChannels::Sdf) != 0;
const bool bPublishesResource = Entry.ProvidedResources != VoxelOpResources::None;
const EVoxelTileClass Forced = Entry.Op->ClassifyBox(VoxelBox, Ctx);
bool bCanSkip = false;
if (Forced != EVoxelTileClass::Mixed)
{
VF_ForceHypotheses(H, Forced, Entry.Op->ForcedMarginOverBox(VoxelBox, Ctx));
if (bWritesSdf) { Entry.Op->PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); }
}
else
{
const EVoxelOpEffect Effect =
Entry.Op->EffectOverBox(VoxelBox, Ctx, H);
bCanSkip = Effect == EVoxelOpEffect::Identity
&& !bWritesSdf
&& !bPublishesResource;
VF_FoldEffect(H,
Effect,
Entry.Op->MaxCarveOverBox(VoxelBox, Ctx, H),
Entry.Op->MaxFillOverBox(VoxelBox, Ctx, H));
if (bWritesSdf) { Entry.Op->PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); }
}
if (!bCanSkip) { OutActiveOps.Add(Index); }
}
return OutActiveOps.Num();
}
/** Le même, négaté pour le mesher (négatif = solide). */
float EvalMC(float WorldX, float WorldY, float WorldZ) const
{
return -EvalInternal(WorldX, WorldY, WorldZ);
}
/** Return a same-sample structural-core result published by a source in this stack. */
bool TryGetLastTunnelCoreWorldEvaluation(
FTunnelCoreWorldEvaluation& OutEvaluation) const;
/**
* Le pliage générique qui remplacera les gardes écrites à la main dans ClassifyTile.
* Voir `VF_FoldOp` (VoxelDensityOp.h) pour la sémantique — en particulier pourquoi un
* opérateur FORÇANT (les seals dans leurs bandes) écrase ce que la pile avait conclu avant lui.
*/
EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
{
FVoxelBoxHypotheses H;
for (const FOpEntry& Entry : Ops)
{
VF_FoldOp(H, *Entry.Op, VoxelBox, Ctx,
(Entry.Writes & VoxelOpChannels::Sdf) != 0);
// Do not early-out on a dead hypothesis: a later forcing structural post may
// deliberately overwrite it (the XY edge seal is appended after passage tube/landing
// carving and support).
}
return H.Resolve();
}
/**
* LE MÊME PLIAGE, MAIS QUI DIT **QUI** A TUÉ CHAQUE HYPOTHÈSE. Diagnostic, réservé aux tests.
*
* ⚠️ IL DOIT RENDRE EXACTEMENT LE MÊME VERDICT QUE `ClassifyBox` — même boucle, même ordre,
* y compris le fait de laisser un post forçant ultérieur ressusciter une hypothèse morte. Un diagnostic qui emprunte un chemin légèrement différent de celui qu'il explique
* est pire que pas de diagnostic : il envoie chercher le bug ailleurs. Si l'un des deux change,
* l'autre change avec lui.
*
* `OutSolidKiller` / `OutAirKiller` reçoivent l'INDEX du premier opérateur qui fait passer
* l'hypothèse correspondante de vraie à fausse, ou `INDEX_NONE` si elle a survécu. Le nom
* lisible s'obtient par `GetOpDebugName(index)`.
*
* Same fold, but it reports WHICH op killed each hypothesis. Must stay verdict-identical to
* ClassifyBox, including later forcing posts that may overwrite a dead hypothesis — a
* diagnostic that takes a slightly different path sends you hunting in the wrong place.
*/
EVoxelTileClass ClassifyBoxAttributed(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
int32& OutSolidKiller, int32& OutAirKiller) const
{
OutSolidKiller = INDEX_NONE;
OutAirKiller = INDEX_NONE;
FVoxelBoxHypotheses H;
for (int32 i = 0; i < Ops.Num(); ++i)
{
const bool bSolidBefore = H.bCanBeAllSolid;
const bool bAirBefore = H.bCanBeAllAir;
VF_FoldOp(H, *Ops[i].Op, VoxelBox, Ctx,
(Ops[i].Writes & VoxelOpChannels::Sdf) != 0);
if (bSolidBefore && !H.bCanBeAllSolid && OutSolidKiller == INDEX_NONE) { OutSolidKiller = i; }
if (bAirBefore && !H.bCanBeAllAir && OutAirKiller == INDEX_NONE) { OutAirKiller = i; }
}
return H.Resolve();
}
/**
* Per-operator box-bound audit. This deliberately replays the same fold as ClassifyBox while
* also brute-forcing Eval on the queried lattice. It is report/test infrastructure only: it
* must never be used to make a generation decision.
*/
struct FOpBoxDiagnostic
{
int32 Index = INDEX_NONE;
FString Name;
EVoxelOpEffect Effect = EVoxelOpEffect::Identity;
EVoxelTileClass ForcedVerdict = EVoxelTileClass::Mixed;
bool bForced = false;
bool bWritesSdf = false;
float ForcedMargin = 0.0f;
float MaxCarve = 0.0f;
float MaxFill = 0.0f;
bool bHasDensityDelta = false;
float ActualDensityDeltaMin = FLT_MAX;
float ActualDensityDeltaMax = -FLT_MAX;
bool bHasSdfValue = false;
float ActualSdfMin = FLT_MAX;
float ActualSdfMax = -FLT_MAX;
bool bHasSdfBound = false;
float SdfBoundMin = FLT_MAX;
float SdfBoundMax = FLT_MAX;
bool bCarveBoundViolated = false;
bool bFillBoundViolated = false;
uint64 SampleCount = 0;
};
VOXELFORGE_API void DiagnoseBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
int32 SampleStep, TArray<FOpBoxDiagnostic>& OutDiagnostics,
EVoxelTileClass* OutVerdict = nullptr) const;
/** Nom lisible d'un opérateur, pour les rapports de test. Voir `IVoxelDensityOp::DebugName`. */
const TCHAR* GetOpDebugName(int32 Index) const
{
return Ops.IsValidIndex(Index) ? Ops[Index].Op->DebugName() : TEXT("(none)");
}
/**
* Validate the stack's channel dependency DAG without evaluating a voxel.
*
* Rule: each channel has a current version. A channel is unavailable until an earlier op
* writes it. The one deliberate exception is a root `FieldSource` that reads and writes the
* same channel as an identity fold (for example `min(FLT_MAX, Sdf)`); it may consume that
* channel's `FVoxelOpSample` initial identity while publishing the first real version. An op
* may read only the version visible on entry; a read/write op consumes that version and
* publishes the next one. A write-only op is a replacement/producer, so it is legal only as
* the first producer of that channel; allowing a second write-only producer would silently
* clobber the earlier field. An additive op must read every channel it writes, because an
* additive delta cannot introduce or replace a channel. All producer-to-consumer edges
* therefore point forward in the list, and the list is a legal topological ordering of the
* channel DAG. A later read deliberately sees the writer's post-op version; a consumer that
* needs a pre-write value must be placed before that writer. The declarations do not invent
* dependencies from names, roles, or Eval side effects.
*
* This is assembly/diagnostic work only. It must not be called from Eval or any voxel loop.
* Returns false and optionally describes the first violation.
*/
VOXELFORGE_API bool ValidateChannelOrder(FString* OutError = nullptr) const;
/**
* RÔLE 4 — ajoute les invariants de monde, dans l'ordre fixe, à la fin de la pile.
* spine (0,0) → seal de frontière → carve de passage → [posts spécifiques] → seal de limite XY.
*
* ⚠️ La couche de diff (édits joueur) n'est PAS ici : elle vit dans `GetDensityAt`, APRÈS la
* négation MC, avec les disturbances. Elle rejoindra la pile quand les disturbances seront
* portées et que la question de convention MC-vs-interne sera tranchée pour de bon
* (OPSTACK-DECOMPOSITION §10.2). Tant que la pile n'alimente pas le jeu, c'est sans effet.
*
* The diff layer is NOT here: it lives in GetDensityAt, AFTER the MC negate, with disturbances.
* It joins the stack when disturbances are ported. Harmless while the stack feeds nothing.
*
* @param StrateManager peut être nullptr → pas de carve de passage (comme le fallback actuel).
* Le seal XY reste toujours présent ; son rayon/thickness viennent de Ctx
* au moment de PrepareChunk (0 = monde non borné).
*/
VOXELFORGE_API void AppendStructuralPost(float StrateTopWorldZ, float StrateBottomWorldZ,
float SealThickness, float BaseDensity, float SpineRadius,
const UVoxelStrateManager* StrateManager,
bool bAppendEdgeSeal = true);
private:
TArray<FOpEntry> Ops;
FVoxelOpContext PreparedContext;
bool bHasPreparedContext = false;
EVoxelOpFusedEvaluator FusedEvaluator = EVoxelOpFusedEvaluator::None;
};
//=============================================================================
// FABRIQUES / FACTORIES
//=============================================================================
namespace VoxelDensityOps
{
/** Query the concrete op declaration used by the offline recipe roller. */
VOXELFORGE_API bool GetStrateOpContract(EVoxelStrateOpClass OpClass,
FVoxelStrateOpContract& OutContract);
/** Rôle 1 — `Density = BaseDensity` partout. `ClassifyBox` → AllSolid, exact et gratuit.
* Racine de TunnelNetwork, Maze, VerticalShafts et des gaps de bedrock. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeConstantRockSource(float BaseDensity);
/** Rôle 1 — le MÊME opérateur au signe près : `Density = -BaseDensity`, un grand vide ouvert.
* `ClassifyBox` → **AllAir**, ce qu'aucune source n'avait encore su rendre — c'est ce qui rend
* une strate d'îles flottantes (surtout vide) sautable là où aucune île n'arrive. Racine de
* FloatingIslands. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeConstantVoidSource(float BaseDensity);
/** Rôle 1 — les couloirs de Maze : capsules sur les arêtes ouvertes d'un treillis 3D.
* Écrit le canal SDF uniquement. Chaque nœud hors origine choisit un parent vers l'origine;
* les arêtes canoniques (nœud inférieur + axe) testent localement les deux parents. La source
* met en cache les décisions de la fenêtre {-1,0}³ par cellule; les chunks adjacents voient
* donc exactement le même graphe, sans collect global ni couture. */
/** The source answers only for its own SDF field. Its box query publishes an SDF interval;
* each later converter or modifier consumes that interval through the generic fold. It does
* not know, and must not answer for, any downstream operator. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeLatticeCorridorSource(const FMazeGenerationParams& P,
int32 Seed);
/** Rôle 3 — rugosité de paroi appliquée au canal SDF (variante Maze/Shafts/Islands).
* `Frequency` est codée en dur au site d'appel aujourd'hui (0.12 pour Maze) ; l'exposer est
* un gain d'authoring gratuit, et §2.6 autorise explicitement le re-tune. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeSdfRoughnessMod(float Strength, float Frequency,
int32 BaseOctaves, float ApplyWithin);
/** Rôle 2 — conversion SDF → densité : creuse de l'air là où le SDF est à l'intérieur.
* Les six mêmes lignes apparaissent aujourd'hui dans TunnelNetwork, Maze et VerticalShafts.
* @param MinDivisor plancher du diviseur `Blend·2`. **TunnelNetwork passe 1.0** (son original
* écrit `FMath::Max(SDFBlendRadius·2, 1)`) ; Maze/Shafts laissent 0, où
* `Max(x,0) == x` exactement. Les deux formules divergent si `Blend·2 < 1`,
* donc ce paramètre est une vraie différence, pas une précaution. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeSdfCarve(float Blend, float BaseDensity,
float MinDivisor = 0.0f);
/** Rôle 2 — la même conversion, signe opposé : REMPLIT du solide là où le SDF est à l'intérieur.
* C'est ce que fait FloatingIslands (`Density += Fill·Base·2`), et la multiplication par ±1
* étant exacte en IEEE-754, le chemin carve reste bit pour bit ce qu'il était. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeSdfFill(float Blend, float BaseDensity);
/** Rôle 1 — la dalle : surface de sol + surface de plafond → champ de vide. **XY-PUR** depuis
* OPSTACK-DECOMPOSITION §3.1 (le terme en Z des deux bruits est parti), ce qui lui donne un
* `ClassifyBox` EXACT sans échantillonnage : les deux surfaces vivent dans des bandes en Z
* bornées par le supremum prouvé de FBM (1.5, not the nominal parameter label). Sert
* FlatPlain **et** CrystalChamber. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeSlabVoidSource(const FSlabGenerationParams& P, int32 Seed);
/** Rôle 3 — cylindres de hauteur infinie sur une grille monde. N'ajoute que du solide ⇒
* `FillOnly` quand une colonne atteint la boîte, `Identity` (le cas courant) sinon. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeGridColumnMod(const FSlabGenerationParams& P, int32 Seed);
/**
* SurfaceWorld, COMPLET : colonne (sol + voûte) → densité, overhang 3D, post structurel, et le
* mélange de biomes quand `PerBiomeParams` est non vide.
*
* @param PerBiomeParams vide ⇒ pas de biomes, chemin d'origine strictement inchangé. Non vide
* ⇒ une pile de hauteur COMPLÈTE par biome, sol mélangé / voûte
* sélectionnée, amplitude d'overhang interpolée (§5, combiner `Mask`).
* @param BiomeField **transféré** à la pile, qui le possède. Doit répondre pour les mêmes
* indices que `PerBiomeParams`. `nullptr` avec des params non vides ⇒
* biome 0 partout (dégradation sûre, pas un crash).
*/
VOXELFORGE_API void BuildSurfaceStack(FVoxelOpStack& OutStack, const FSurfaceGenerationParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
const TArray<FSurfaceGenerationParams>& PerBiomeParams =
TArray<FSurfaceGenerationParams>(),
TUniquePtr<IVoxelBiomeField> BiomeField = nullptr,
bool bAppendStructuralPosts = true);
/**
* FlatPlain ET CrystalChamber — la même pile, **sans branchement sur le type** :
* SlabVoidSource → GridColumnMod → [structural post ×4]
*
* Deux des huit archétypes tiennent dans un opérateur (OPSTACK-PLAN §4) : leur différence est
* un jeu de valeurs par défaut, pas du code.
*/
VOXELFORGE_API void BuildSlabStack(FVoxelOpStack& OutStack, const FSlabGenerationParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
/**
* VerticalShafts — 11 ops, dont les connecteurs air/sol et les quatre structural posts :
* ConstantRock → ShaftField → SdfRoughness → SdfCarve → ShaftLedge
* → ShaftConnectorAir → ShaftConnectorFloor → [structural post ×4]
*
* Trois de ces ops sont ceux de Maze (`§2.5`), avec une source différente et d'autres réglages
* (fréquence 0.1 au lieu de 0.12, fenêtre `rough + 4` au lieu de `R + rough + 2`).
*/
VOXELFORGE_API void BuildVerticalShaftStack(FVoxelOpStack& OutStack, const FVerticalShaftParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
/**
* TunnelNetwork — **COMPLET** :
* ConstantRock → RoomGraph(warp + pits + cheminées) → SdfCarve → CaveRoughness(4b)
* → Terrace → LayerLines → Ribbing → Overhang → Cliff → Scallop → Arch → RoomColumn(4d)
* → Dome(4g) → Pinch(4h) → FloorBias → Worms → [spine, boundary, passage, graph-air, XY seal]
*
* L'override d'op PAR SALLE (étape C1) n'ajoute aucun opérateur : `FRoomGraphSource` publie
* `LocalParams()` — les params de la strate avec l'op de la salle la plus proche appliqué — et
* ONZE des douze modificateurs y lisent leurs champs. La rugosité (4b) lit les params de la
* STRATE, parce que dans l'original elle précède la déclaration du shadow.
*
* ⚠️ `FRoomGraphSource` **APPELLE** `BuildChunkCache`/`EvaluateSDFCached`, il ne les transcrit
* pas : c'est là que vit la discipline d'invariance de fenêtre à deux régions (`ARCHITECTURE
* §8.4`), et en faire une copie serait le pire résultat possible pour un refactor dont le but est
* d'avoir UNE définition de chaque idée.
*/
VOXELFORGE_API void BuildTunnelNetworkStack(FVoxelOpStack& OutStack,
const FStrateGenerationParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
/**
* DIAGNOSTIC — la ventilation par CLASSE DE PRIMITIVE de la dernière propagation d'intervalle
* de `FRoomGraphSource` évaluée sur ce thread. **Tests uniquement. N'entre dans aucune décision
* de génération.**
*
* ⚠️ POURQUOI ÇA EXISTE PLUTÔT QUE D'ÊTRE REFAIT DANS LE TEST. Le test a déjà tout ce qu'il faut
* pour rejouer le critère — il appelle `BuildChunkCache` ailleurs. Le rejouer serait une
* DEUXIÈME définition du critère, qui dériverait de la vraie et mentirait exactement le jour où
* on la croirait. C'est la même raison qui a fait exister `VF_BuildOpStackForChunk`. On expose
* donc ce que l'opérateur a réellement calculé.
*
* `Hit*` = combien de primitives de cette classe atteignent la boîte (0 partout ⇒ `Identity`).
* `Num*` = combien le cache en contenait, ce qui distingue « aucune n'atteint » de « il n'y en
* avait aucune » — deux zéros de sens opposé.
*
* Reads back what the operator actually computed, rather than letting the test re-derive the
* criterion: a second copy would drift and would lie on the day it was believed.
*/
struct FRoomBoxDiagnostic
{
int32 HitRooms = 0, HitTunnels = 0, HitPits = 0, HitChimneys = 0;
int32 NumRooms = 0, NumTunnels = 0, NumPits = 0, NumChimneys = 0;
/** Les mêmes comptes si la dilatation de warp valait ZÉRO, et de combien de voxels la boîte
* est effectivement dilatée. `Hit* - Hit*NoWarp` = la part du blocage due à MA boîte plutôt
* qu'à la géométrie. Cette mesure manquait, et son absence a coûté trois builds de
* resserrement autour du mauvais terme. */
int32 HitRoomsNoWarp = 0, HitTunnelsNoWarp = 0;
float WarpDilation = 0.0f;
/** Conservative candidate for the world-space tunnel-core post that runs after EvalMC.
* This is derived from the final post's own world-chain bounds, not from the warped SDF
* source's broader threshold hit count. */
bool bMayHaveTunnelCoreAir = false;
bool bMayHaveTunnelSupportFloor = false;
bool bMayHaveTunnelCoreTail = false;
uint32 ExactTailQueries = 0;
uint32 ExactTailEvaluated = 0;
uint64 ExactTailCycles = 0;
uint64 PropagateCycles = 0;
uint64 ExactPrimitiveCycles = 0;
uint64 CacheWindowCycles = 0;
int32 NumRoomFloorJoins = 0;
};
VOXELFORGE_API FRoomBoxDiagnostic GetLastRoomBoxDiagnostic();
/** Current worker-local room-graph cache footprint; profiling/diagnostic use only. */
VOXELFORGE_API void ReportWorkerRoomGraphCacheFootprint();
/** Current process-wide immutable room-graph proof-cache footprint; diagnostic use only. */
VOXELFORGE_API void ReportSharedRoomGraphCacheFootprint();
/** Drop completed proof caches from older manager lifetimes, or all of them when zero. */
VOXELFORGE_API void TrimSharedRoomGraphCache(uint64 KeepManagerLifetimeId = 0);
/**
* FloatingIslands — 8 ops, et **la pile tourne à l'ENVERS** :
* ConstantVoid → IslandBlob → SdfRoughness → SdfFill → [structural post ×4]
*
* Les quatre archétypes portés jusqu'ici partent de ROC et CREUSENT ; celui-ci part du VIDE et
* REMPLIT. Aucune des deux extrémités n'a demandé d'opérateur neuf — `FConstantFieldSource` et
* `FSdfConvertOp` sont les mêmes classes au signe près, et `FSdfRoughnessMod` est repris sans
* une ligne de changement (4ᵉ archétype). Seul le blob d'île est nouveau.
*
* The stack that runs backwards: void source + fill instead of rock source + carve, using the
* SAME operators with the opposite sign.
*/
VOXELFORGE_API void BuildFloatingIslandStack(FVoxelOpStack& OutStack, const FFloatingIslandParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
/**
* La pile Maze complète, décomposée — PAS un `FMazeOp` monolithique :
* ConstantRockSource → LatticeCorridorSource → SdfRoughnessMod → SdfCarve → [structural post]
*
* C'est le test de la Phase 1 : si Maze ne se décompose pas ainsi, l'abstraction est mauvaise
* pour ce domaine (OPSTACK-PLAN §4, déclencheur d'arrêt).
*/
VOXELFORGE_API void BuildMazeStack(FVoxelOpStack& OutStack, const FMazeGenerationParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
}
/** Materialise an offline recipe. Structural posts are appended by default; lateral region cores
* deliberately pass false so the parent stack can append the global posts exactly once. */
VOXELFORGE_API bool VF_BuildStackFromRecipe(const FVoxelOpStackRecipe& Recipe,
const FVoxelStrateArchetypeParams& Params,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr,
bool bAppendStructuralPosts = true);
/** Build the region combiner stack. Creative cores are independent; structural posts are global. */
#if WITH_EDITOR
VOXELFORGE_API bool VF_BuildStrateRegionStack(
const FVoxelStrateRegionManifest& Manifest,
float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr);
#endif
@@ -0,0 +1,382 @@
// VoxelDensityPrimitives.h
// Les quatre post-traitements STRUCTURELS partagés par chaque archétype.
// The four STRUCTURAL post-processes every archetype shares.
//
// POURQUOI CE FICHIER EXISTE / WHY THIS FILE EXISTS
// Les trois primitives verticales étaient `static` dans VoxelGenerator.cpp et appelées à l'identique
// par les six fonctions de densité. La pile d'opérateurs a besoin des MÊMES, donc elles montent ici :
// UNE copie partagée. Le scellement XY est ajouté dans ce même fichier pour que les deux chemins
// partagent aussi la nouvelle invariance — les invariants de monde ne sont pas des choix créatifs.
//
// The three vertical primitives were `static` in VoxelGenerator.cpp and called identically by all six
// density functions. The operator stack needs the same ones, so they move here: ONE shared copy. The
// XY seal is added beside them so both paths share the new invariant too.
//
// ⚠️ CONVENTION DE SIGNE — la source n°1 de confusion du plugin.
// Ces primitives travaillent en convention INTERNE : **positif = SOLIDE, négatif = AIR**.
// C'est la convention dans laquelle chaque fonction d'archétype est écrite ; la négation vers la
// convention marching-cubes (négatif = solide) se fait UNE FOIS, sur le `return`.
// SIGN CONVENTION: these work in INTERNAL convention — **positive = SOLID**. The negate to MC
// convention happens ONCE, at the caller's return.
//
// Les trois corps déplacés restent identiques; le quatrième est la nouvelle frontière globale.
// The three moved bodies remain identical; the fourth is the new global boundary.
#pragma once
#include "CoreMinimal.h"
#include "VoxelTypes.h" // SmoothStep01
#include "VoxelPassageGeometry.h" // shared body-sized landing/tunnel geometry
#include "VoxelDensityAblation.h" // development-only world-changing measurements
#include "VoxelTilePostReach.h" // worker-local landing/structural post reach bits
//=============================================================================
// SEAL DE FRONTIÈRE / BOUNDARY SEAL
//=============================================================================
// Seal solide aux bords haut et bas de la strate. Fade smoothstep sur `Thickness` voxels depuis
// chaque bord. N'AJOUTE que de la densité (FMath::Max), jamais n'en enlève → le joueur ne peut
// jamais percer le seal "par accident", seulement via les passages.
//
// ⚠️ C'est un opérateur FORÇANT, pas seulement un FillOnly : à l'intérieur de la bande, avec
// SealFactor > 0 et BaseDensity > 0, le résultat est solide GARANTI quelle qu'ait été l'entrée.
// C'est exactement ce qu'encode `IVoxelDensityOp::ClassifyBox` (voir VoxelDensityOp.h), et la
// raison pour laquelle cette méthode existe.
FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
float StrateTopZ, float StrateBottomZ,
float Thickness, float BaseDensity)
{
if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return;
const float DistTop = StrateTopZ - WorldZ; // + si on est sous le plafond
const float DistBot = WorldZ - StrateBottomZ; // + si on est au-dessus du sol
if (DistTop >= 0.0f && DistTop < Thickness)
{
float SealFactor = 1.0f - (DistTop / Thickness);
SealFactor = SmoothStep01(SealFactor);
Density = FMath::Max(Density, SealFactor * BaseDensity);
}
if (DistBot >= 0.0f && DistBot < Thickness)
{
float SealFactor = 1.0f - (DistBot / Thickness);
SealFactor = SmoothStep01(SealFactor);
Density = FMath::Max(Density, SealFactor * BaseDensity);
}
}
//=============================================================================
// CARVE DE PASSAGE / PASSAGE CARVING
//=============================================================================
// Creuse un passage inter-strates. Évalué APRÈS le seal pour que les passages puissent percer à
// travers le bouchon solide. Le rayon de blend est hard-codé à 4.0f — à exposer via
// UVoxelSettings si on veut pouvoir le tweaker.
FORCEINLINE void VF_ApplyPassageCarving(float& Density, float ModSDF,
float BaseDensity, float SealThickness)
{
if (VoxelDensityAblation::IsPassageCarvingOff()) return;
constexpr float PASSAGE_BLEND_RADIUS = 4.0f;
if (ModSDF >= PASSAGE_BLEND_RADIUS) return;
float CarveFactor = FMath::Clamp(
(PASSAGE_BLEND_RADIUS - ModSDF) / (PASSAGE_BLEND_RADIUS * 2.0f),
0.0f, 1.0f);
CarveFactor = SmoothStep01(CarveFactor);
// FORCE the density toward guaranteed AIR so the passage punches through ANYTHING in
// its path (seals, columns, surface roughness, terrain ops). A plain subtraction can
// be out-paced by stacked density additions, leaving solid plugs mid-tunnel — which is
// why the shaft "didn't go all the way through". Lerp toward a strongly negative target
// and take the min so we only ever make it MORE air (never refill an existing cave).
const float AirTarget = -(BaseDensity * 2.0f + SealThickness + 4.0f);
Density = FMath::Min(Density, FMath::Lerp(Density, AirTarget, CarveFactor));
}
// Landing air is also reasserted after the MC-space disturbance post. Unlike the legacy tube
// carve above, this threshold is independent of the incoming density: applying it a second time
// is therefore bit-stable. The structural passage op and the post-disturbance backstop can share
// the same operation without turning an otherwise unchanged live-vs-direct op-stack comparison
// into a second, deeper smooth carve. The tube intentionally retains its old current-density
// interpolation because that is part of the legacy structural path's contract.
FORCEINLINE void VF_ApplyPassageLandingCarving(float& Density, float LandingSDF,
float BaseDensity, float SealThickness)
{
constexpr float PASSAGE_BLEND_RADIUS = 4.0f;
if (LandingSDF >= PASSAGE_BLEND_RADIUS) return;
float CarveFactor = FMath::Clamp(
(PASSAGE_BLEND_RADIUS - LandingSDF) / (PASSAGE_BLEND_RADIUS * 2.0f),
0.0f, 1.0f);
CarveFactor = SmoothStep01(CarveFactor);
const float AirTarget = -(BaseDensity * 2.0f + SealThickness + 4.0f);
const float LandingThreshold = FMath::Lerp(BaseDensity, AirTarget, CarveFactor);
Density = FMath::Min(Density, LandingThreshold);
}
//=============================================================================
// LANDING ROOM ORIGINE (0,0) / ORIGIN LANDING ROOM
//=============================================================================
// The old implementation carved a full-height cylinder. That made the origin visually open but
// left no floor anywhere except at the bottom, so it was a fall shaft rather than a landing. The
// origin primitive is now a top-anchored rounded room with a real flat floor in every strate.
// It never crosses either vertical seal. The inter-strate seal remains solid until a passage (or
// the player) opens it; there is no default vertical bore between rooms.
FORCEINLINE void VF_ApplyOriginSpine(float& Density, float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
if (VoxelDensityAblation::IsOriginSpineOff() || Radius <= 0.0f) return;
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
VoxelPassageGeometry::BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, Radius);
if (!Geometry.bValid) return;
const float RoomSDF = VoxelPassageGeometry::OriginLandingRoomSDF(
FVector(WorldX, WorldY, WorldZ), Geometry);
VF_ApplyPassageLandingCarving(
Density, RoomSDF, BaseDensity, SealThickness);
// The floor is intentionally written after the room carve. It is also reasserted after the
// passage/disturbance posts by the callers below; doing it here makes the standalone primitive
// itself a complete landing-room operation.
if (WorldZ <= Geometry.FloorZ + KINDA_SMALL_NUMBER
&& WorldZ > Geometry.FloorZ - Geometry.FloorThickness
&& FMath::Abs(WorldX) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f)
&& FMath::Abs(WorldY) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f))
{
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker())
{
Density = FMath::Max(Density, BaseDensity);
}
}
}
/** Reassert only the origin-room air volume after a post-process that may add rock. */
FORCEINLINE void VF_ApplyOriginLandingAir(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
if (VoxelDensityAblation::IsLandingPostsOff() || Radius <= 0.0f) return;
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
VoxelPassageGeometry::BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, Radius);
if (!Geometry.bValid) return;
const float RoomSDF = VoxelPassageGeometry::OriginLandingRoomSDF(
FVector(WorldX, WorldY, WorldZ), Geometry);
VF_ApplyPassageLandingCarving(
Density, RoomSDF, BaseDensity, SealThickness);
}
/** Reassert only the origin-room support slab after an air carve/post-process. */
FORCEINLINE void VF_ApplyOriginLandingFloor(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
if (VoxelDensityAblation::IsLandingPostsOff() || Radius <= 0.0f) return;
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
VoxelPassageGeometry::BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, Radius);
if (!Geometry.bValid) return;
if (WorldZ <= Geometry.FloorZ + KINDA_SMALL_NUMBER
&& WorldZ > Geometry.FloorZ - Geometry.FloorThickness
&& FMath::Abs(WorldX) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f)
&& FMath::Abs(WorldY) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f))
{
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker())
{
Density = FMath::Max(Density, BaseDensity);
}
}
}
/** MC-facing origin-room air backstop for the post-disturbance path. */
FORCEINLINE void VF_ApplyOriginLandingAirMC(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
if (!VoxelGenLOD::IsOriginLandingReachable()
&& !VoxelGenLOD::IsTilePostReachBypassActive())
{
VoxelGenLOD::CompareSkippedPost(Density, [&]()
{
VF_ApplyOriginLandingAirMC(
Density, WorldX, WorldY, WorldZ,
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
}, VoxelGenLOD::ETilePostComparisonKind::OriginAir);
return;
}
float InternalDensity = -Density;
VF_ApplyOriginLandingAir(InternalDensity, WorldX, WorldY, WorldZ,
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
Density = -InternalDensity;
}
/** MC-facing origin-room floor backstop for the post-disturbance path. */
FORCEINLINE void VF_ApplyOriginLandingFloorMC(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
if (!VoxelGenLOD::IsOriginLandingReachable()
&& !VoxelGenLOD::IsTilePostReachBypassActive())
{
VoxelGenLOD::CompareSkippedPost(Density, [&]()
{
VF_ApplyOriginLandingFloorMC(
Density, WorldX, WorldY, WorldZ,
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
}, VoxelGenLOD::ETilePostComparisonKind::OriginFloor);
return;
}
float InternalDensity = -Density;
VF_ApplyOriginLandingFloor(InternalDensity, WorldX, WorldY, WorldZ,
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
Density = -InternalDensity;
}
//=============================================================================
// XY EDGE SEAL / SCELLEMENT DE LA LIMITE XY
//=============================================================================
// The bounded-world rim is a radial solid ramp in actor-space XY. This helper uses the same
// internal convention as VF_ApplyBoundarySeal (positive = solid); the MC-facing wrapper below
// negates it so the output convention remains negative = solid.
//
// Radius == 0 is deliberately checked before any arithmetic: it is a true legacy no-op, not a
// nearly-invisible setting-dependent perturbation. The common interior case also returns from a
// squared-distance test and pays no sqrt.
FORCEINLINE void VF_ApplyXYEdgeSeal(float& Density, float WorldX, float WorldY,
float WorldRadiusVoxels, float Thickness, float BaseDensity)
{
if (VoxelDensityAblation::IsXYEdgeSealOff()
|| !(WorldRadiusVoxels > 0.0f) || !(Thickness > 0.0f) || !(BaseDensity > 0.0f)) return;
if (!VoxelMath::IsFinite(WorldX) || !VoxelMath::IsFinite(WorldY)
|| !VoxelMath::IsFinite(WorldRadiusVoxels) || !VoxelMath::IsFinite(Thickness)
|| !VoxelMath::IsFinite(BaseDensity)) return;
const float InnerRadius = WorldRadiusVoxels - Thickness;
const float InnerRadiusSq = InnerRadius * InnerRadius;
const float DistSq = WorldX * WorldX + WorldY * WorldY;
const float RadiusSq = WorldRadiusVoxels * WorldRadiusVoxels;
// The overwhelmingly common case: well inside the bounded world.
if (InnerRadius > 0.0f && DistSq <= InnerRadiusSq) return;
// Keep the outer-boundary comparison in squared space too. This makes the exact force branch
// and the ClassifyBox proof use the same predicate: at or beyond R the result is BaseDensity,
// with no sqrt-rounding ambiguity in the forced-solid margin.
if (DistSq >= RadiusSq)
{
Density = FMath::Max(Density, BaseDensity);
return;
}
const float DistXY = FMath::Sqrt(DistSq);
// 0 at the inner edge, 1 at the world radius; smoothstep removes a cylindrical cliff.
const float SealFactor = SmoothStep01(FMath::Clamp(
(DistXY - InnerRadius) / Thickness, 0.0f, 1.0f));
// At/beyond WorldRadiusVoxels the branch above already forced exactly BaseDensity.
Density = FMath::Max(Density, SealFactor * BaseDensity);
}
/** MC-facing form used after post-processes that already operate in the output convention. */
FORCEINLINE void VF_ApplyXYEdgeSealMC(float& Density, float WorldX, float WorldY,
float WorldRadiusVoxels, float Thickness, float BaseDensity)
{
if (VoxelDensityAblation::IsXYEdgeSealOff()
|| !(WorldRadiusVoxels > 0.0f) || !(Thickness > 0.0f)) return;
float InternalDensity = -Density;
VF_ApplyXYEdgeSeal(InternalDensity, WorldX, WorldY,
WorldRadiusVoxels, Thickness, BaseDensity);
Density = -InternalDensity;
}
//=============================================================================
// XY EDGE PROOF / PREUVE DE BOITE
//=============================================================================
// The closest point of an AABB to the XY origin gives the minimum radius over the whole box.
// The helpers below are shared by ClassifyBox, ClassifyTile's global shell early-out, and the
// soundness test. They intentionally require one voxel of radial margin in the ramp: at the
// mathematical inner edge the helper is a true no-op, so claiming AllSolid there would be false.
namespace VoxelDensityReach
{
constexpr float PassageBlend = 4.0f;
constexpr float EdgeProofMargin = 1.0f;
}
FORCEINLINE bool VF_IsValidXYEdgeSealProofInput(const FBox& VoxelBox,
float WorldRadiusVoxels, float Thickness, float BaseDensity)
{
return VoxelBox.IsValid
&& VoxelMath::IsFinite(VoxelBox.Min.X) && VoxelMath::IsFinite(VoxelBox.Min.Y)
&& VoxelMath::IsFinite(VoxelBox.Max.X) && VoxelMath::IsFinite(VoxelBox.Max.Y)
&& VoxelMath::IsFinite(WorldRadiusVoxels) && VoxelMath::IsFinite(Thickness)
&& VoxelMath::IsFinite(BaseDensity)
&& WorldRadiusVoxels > 0.0f && Thickness > 0.0f && BaseDensity > 0.0f;
}
FORCEINLINE float VF_XYEdgeClosestRadiusSq(const FBox& VoxelBox)
{
const float ClosestX = (VoxelBox.Min.X > 0.0f) ? VoxelBox.Min.X
: (VoxelBox.Max.X < 0.0f) ? VoxelBox.Max.X : 0.0f;
const float ClosestY = (VoxelBox.Min.Y > 0.0f) ? VoxelBox.Min.Y
: (VoxelBox.Max.Y < 0.0f) ? VoxelBox.Max.Y : 0.0f;
return ClosestX * ClosestX + ClosestY * ClosestY;
}
FORCEINLINE float VF_XYEdgeFarthestRadiusSq(const FBox& VoxelBox)
{
const float FarthestX = FMath::Max(FMath::Abs(VoxelBox.Min.X), FMath::Abs(VoxelBox.Max.X));
const float FarthestY = FMath::Max(FMath::Abs(VoxelBox.Min.Y), FMath::Abs(VoxelBox.Max.Y));
return FarthestX * FarthestX + FarthestY * FarthestY;
}
/**
* Return the actual positive lower-bound margin imposed by the edge seal over a box, or 0 when
* the box is not provably forced. The ramp bound is intentionally reduced by 2× after the
* one-voxel proof margin, so float evaluation error can only make this estimate too small.
*/
FORCEINLINE float VF_XYEdgeSealForcedMarginOverBox(const FBox& VoxelBox,
float WorldRadiusVoxels, float Thickness, float BaseDensity)
{
if (!VF_IsValidXYEdgeSealProofInput(VoxelBox, WorldRadiusVoxels, Thickness, BaseDensity)) return 0.0f;
const float ClosestRadiusSq = VF_XYEdgeClosestRadiusSq(VoxelBox);
const float RadiusSq = WorldRadiusVoxels * WorldRadiusVoxels;
if (ClosestRadiusSq >= RadiusSq)
{
// The per-voxel helper takes the >= WorldRadiusVoxels branch and forces BaseDensity.
return BaseDensity;
}
const float InnerRadius = WorldRadiusVoxels - Thickness;
const float ProofRadius = InnerRadius + VoxelDensityReach::EdgeProofMargin;
if (!(Thickness > VoxelDensityReach::EdgeProofMargin)
|| !(ClosestRadiusSq >= ProofRadius * ProofRadius)) return 0.0f;
const float GuaranteedFactor = SmoothStep01(
FMath::Clamp(VoxelDensityReach::EdgeProofMargin / Thickness, 0.0f, 1.0f));
return BaseDensity * GuaranteedFactor * 0.5f;
}
FORCEINLINE bool VF_XYEdgeSealBoxIsForcedSolid(const FBox& VoxelBox,
float WorldRadiusVoxels, float Thickness, float BaseDensity)
{
return VF_XYEdgeSealForcedMarginOverBox(
VoxelBox, WorldRadiusVoxels, Thickness, BaseDensity) > 0.0f;
}
FORCEINLINE bool VF_XYEdgeSealBoxTouchesBand(const FBox& VoxelBox,
float WorldRadiusVoxels, float Thickness)
{
if (!(WorldRadiusVoxels > 0.0f) || !(Thickness > 0.0f)
|| !VoxelMath::IsFinite(WorldRadiusVoxels) || !VoxelMath::IsFinite(Thickness)) return false;
const float InnerRadius = WorldRadiusVoxels - Thickness;
return VF_XYEdgeFarthestRadiusSq(VoxelBox) > InnerRadius * InnerRadius;
}
//=============================================================================
// PORTÉES / REACHES — les rayons dont ClassifyTile et EffectOverBox ont besoin
//=============================================================================
// The constants are named above so a future bounds test cannot let them drift from the hot path.
@@ -0,0 +1,459 @@
// Opt-in density-path profiling used by the editor commandlet and targeted PIE diagnostics.
// Disabled by default so the shipping density path pays only one predictable branch in the
// scoped timers below.
#pragma once
#include "CoreMinimal.h"
#include "HAL/PlatformTime.h"
#include <atomic>
namespace VoxelDensityProfile
{
enum class EMode : uint8
{
Disabled,
Sampled,
// Low-overhead attribution: keeps the parent and stage ledger trustworthy, samples
// hot component scopes, and collects classifier detail without the old full-scope storm.
Attribution,
Full,
};
// Count-based sampling is deterministic with respect to the profiler's own counters, but the
// report is intentionally diagnostic only. It never participates in density or mesh output.
constexpr uint32 DefaultSampleInterval = 128;
enum class EBucket : uint8
{
GetDensityAt,
ApplyDisturbances,
PassageModifier,
PassageLandingAir,
PassageLandingFloor,
PassageLandingRoomFloor,
PassageTunnelAir,
PassageStructuralPosts,
TunnelCoreSupport,
TunnelCoreSdf,
TunnelCorePosts,
StructuralTail,
RoomGraphSource,
SdfCarve,
CaveRoughnessMod,
CaveTerraceMod,
LayerLineMod,
RibbingMod,
CaveOverhangMod,
CaveCliffMod,
ScallopMod,
CaveArchMod,
RoomColumnMod,
DomeMod,
PinchMod,
FloorBiasMod,
WormFieldSource,
OriginSpineOp,
BoundarySealOp,
PassageCarveOp,
XYEdgeSealOp,
OtherOp,
// Attribution components. These are nested diagnostics; the non-overlapping phase
// buckets below remain the density accounting ledger.
FusedEvaluator,
InterpretedOpStack,
OperatorBlock,
RoomGraphBuild,
RoomGraphSdf,
TunnelCoreWorld,
FusedDetail,
FusedWorm,
FusedStructural,
ClassifierTotal,
ClassifierIntervalProof,
ClassifierExactCore,
ClassifierExactFinal,
ClassifierRoomTail,
RuntimeStreamBuilding,
// Non-overlapping top-level phases. The named operation buckets above are intentionally
// nested diagnostics; these phase buckets are the accounting ledger that must add up to
// GetDensityAt without double-counting a stack op inside its enclosing phase.
DensityPrologue,
DensityCore,
DensityDisturbances,
DensityStructuralPosts,
DensityBoundarySeal,
DensityDiffLayer,
DensityTail,
// Mesher-side accounting. GenerateMesh is the inclusive total; the following buckets
// cover its regular-grid work and its non-grid setup/finalisation.
MesherGenerateMesh,
MesherDensityGrid,
MesherCellClassification,
MesherGradientNormals,
MesherVertexInterpolation,
MesherStreamBuilding,
MesherOther,
Count
};
constexpr int32 BucketCount = static_cast<int32>(EBucket::Count);
enum class ECounter : uint8
{
TunnelCacheLookup,
TunnelCacheHit,
TunnelCacheMiss,
SdfCacheBuild,
SdfSpatialIndexBuild,
SdfBrickIndexBuild,
RoomGraphBuildGeneratorTile,
RoomGraphBuildGeneratorTunnelCore,
RoomGraphBuildOpShared,
RoomGraphBuildOpLocal,
RoomGraphBuildClassifierShared,
RoomGraphBuildClassifierLocal,
RoomGraphBuildUnknown,
CaveRoomCandidates,
CaveRoomEvaluated,
CaveTunnelCandidates,
CaveTunnelEvaluated,
TunnelCoreCandidates,
TunnelCoreEvaluated,
TunnelSupportColumnBuilds,
TunnelSupportColumnCandidates,
TunnelSupportFloorQueries,
TunnelSupportFloorChecks,
TunnelSupportFloorBackstopFires,
TunnelRoomFloorBackstopFires,
TunnelAuthoredFloorSamples,
TunnelAuthoredFloorProtectionClamps,
TunnelAuthoredFloorDisturbanceSkips,
TunnelFloorProfileFallbacks,
TunnelFloorProfileBuilds,
TunnelFloorProfileSegments,
TunnelFloorProfileLedges,
TunnelFloorProfileLedgesBelow2Voxels,
TunnelFloorProfileLedges2To4Voxels,
TunnelFloorProfileLedges4To8Voxels,
TunnelFloorProfileLedges8PlusVoxels,
// One count per authored graph edge. MultiStepEdges is an invariant guard: it must stay
// zero now that old per-segment staircase quantisation is removed.
TunnelFloorSteepEdges,
TunnelFloorWindingEdges,
TunnelFloorDropEdges,
TunnelFloorMultiStepEdges,
PassageSupportFloorBackstopFires,
PassageNativeFloorCompositions,
PassageCandidates,
PassageEvaluated,
MesherBlockTests,
MesherBlockAllSolid,
MesherBlockAllAir,
TileClassifyCalls,
TileClassifyAllSolid,
TileClassifyAllAir,
TileClassifyMixed,
FusedTunnelSamples,
FusedTunnelDetailSamples,
FusedTunnelDetailSkipped,
WormEligibleSamples,
WormBlockProofs,
WormBlockSkippedSamples,
OpBlockBuilds,
OpBlockSamples,
OpBlockOperators,
OpBlockActiveOperators,
OpBlockPrunedOperators,
MesherDensityGridBytes,
MesherSharedGridReadBytes,
MesherOutputArrayBytes,
MesherOutputAllocatedBytes,
OpBlockScratchBytes,
OpBlockCopyBytes,
Count
};
constexpr int32 CounterCount = static_cast<int32>(ECounter::Count);
// Export the one fast-path bit so callers in the editor module can skip instrumentation
// without crossing the DLL boundary for every scoped timer or counter. It is diagnostic
// state only; it never participates in density or mesh decisions.
VOXELFORGE_API extern std::atomic<bool> GEnabled;
VOXELFORGE_API extern std::atomic<EMode> GMode;
inline thread_local bool GSampledScopeActive = false;
// Resident allocator-backed bytes for one worker's generation cache family. The profiler
// aggregates this by worker; the fields intentionally describe allocations owned by the
// cache, not the inline size of the enclosing C++ object. SlotStorageBytes is the one
// exception: it is the inline/static storage reserved for the worker's fixed cache table.
struct FCacheMemoryBreakdown
{
uint64 SlotStorageBytes = 0;
uint64 OpStackBytes = 0;
uint64 RoomsBytes = 0;
uint64 RoomFloorJoinsBytes = 0;
uint64 TunnelsBytes = 0;
uint64 PitsBytes = 0;
uint64 ChimneysBytes = 0;
uint64 ColumnsBytes = 0;
uint64 SupportColumnEntriesBytes = 0;
uint64 SupportColumnsBytes = 0;
uint64 SupportColumnIntervalsBytes = 0;
uint64 SpatialIndexBytes = 0;
uint64 TotalBytes() const
{
return SlotStorageBytes + OpStackBytes
+ RoomsBytes + RoomFloorJoinsBytes + TunnelsBytes + PitsBytes
+ ChimneysBytes + ColumnsBytes + SupportColumnEntriesBytes
+ SupportColumnsBytes + SupportColumnIntervalsBytes + SpatialIndexBytes;
}
uint64 DynamicBytes() const
{
return TotalBytes() - SlotStorageBytes;
}
FCacheMemoryBreakdown& operator+=(const FCacheMemoryBreakdown& Other)
{
SlotStorageBytes += Other.SlotStorageBytes;
OpStackBytes += Other.OpStackBytes;
RoomsBytes += Other.RoomsBytes;
RoomFloorJoinsBytes += Other.RoomFloorJoinsBytes;
TunnelsBytes += Other.TunnelsBytes;
PitsBytes += Other.PitsBytes;
ChimneysBytes += Other.ChimneysBytes;
ColumnsBytes += Other.ColumnsBytes;
SupportColumnEntriesBytes += Other.SupportColumnEntriesBytes;
SupportColumnsBytes += Other.SupportColumnsBytes;
SupportColumnIntervalsBytes += Other.SupportColumnIntervalsBytes;
SpatialIndexBytes += Other.SpatialIndexBytes;
return *this;
}
};
struct FSnapshot
{
uint64 Calls[BucketCount]{};
// Sum of sampled worker time. Useful for CPU-work accounting, but not directly comparable
// with an export wall clock when the mesher runs in parallel.
uint64 Cycles[BucketCount]{};
uint64 Samples[BucketCount]{};
// Largest estimated worker contribution. This is the report-facing parallel wall
// estimate; it avoids mistaking several parallel workers for several exports.
uint64 WallCycles[BucketCount]{};
uint32 ActiveWorkers[BucketCount]{};
uint64 Counters[CounterCount]{};
// Classifier detail is sampled separately from the timer buckets. Calls is the exact
// number of classifier invocations; the remaining fields are worker-local samples
// expanded at Snapshot() using Calls / SampledCalls.
struct FClassifierStats
{
uint64 Calls = 0;
uint64 SampledCalls = 0;
uint64 RefineNodes = 0;
uint64 StackBoxCalls = 0;
uint64 WholeMixedNodes = 0;
uint64 WholeSolidNodes = 0;
uint64 WholeAirNodes = 0;
uint64 NeedsFinalFieldNodes = 0;
uint64 SplitNodes = 0;
uint64 MaxRefinementDepth = 0;
uint64 ExactCoreSamples = 0;
uint64 ExactFinalSamples = 0;
uint64 ExactCoreCacheHits = 0;
uint64 ExactFinalCacheHits = 0;
uint64 ExactCoreLeaves = 0;
uint64 ExactFinalLeaves = 0;
uint64 StackBoxCycles = 0;
uint64 ExactCoreCycles = 0;
uint64 ExactFinalCycles = 0;
uint64 RoomTailQueries = 0;
uint64 RoomTailEvaluated = 0;
uint64 RoomTailCycles = 0;
uint64 RoomPropagateCycles = 0;
uint64 RoomExactPrimitiveCycles = 0;
uint64 RoomCacheWindowCycles = 0;
uint64 RoomNumRooms = 0;
uint64 RoomNumTunnels = 0;
uint64 RoomNumRoomFloorJoins = 0;
uint64 RoomNumPits = 0;
uint64 RoomNumChimneys = 0;
};
FClassifierStats Classifier;
uint64 TimerPairCycles = 0;
uint32 SampleInterval = DefaultSampleInterval;
EMode Mode = EMode::Disabled;
// Opt-in worker-cache footprint snapshot. These are current per-thread values (not
// allocation-event counters), so Snapshot() reports the resident cache footprint at the
// end of a profiling run rather than multiplying bytes by cache churn.
uint64 TunnelCacheWorkers = 0;
uint64 TunnelCacheCapacityEntries = 0;
uint64 TunnelCacheValidEntries = 0;
uint64 TunnelCacheStaticBytes = 0;
uint64 TunnelCacheDynamicBytes = 0;
uint64 TunnelCacheEntryBytes = 0;
uint64 TunnelCacheLargestEntryBytes = 0;
uint64 TunnelCacheLargestWorkerValidEntries = 0;
uint64 TunnelCacheLargestWorkerBytes = 0;
FCacheMemoryBreakdown TunnelCacheBreakdown;
uint64 RoomGraphCacheWorkers = 0;
uint64 RoomGraphCacheCapacityEntries = 0;
uint64 RoomGraphCacheValidEntries = 0;
uint64 RoomGraphCacheStaticBytes = 0;
uint64 RoomGraphCacheDynamicBytes = 0;
uint64 RoomGraphCacheEntryBytes = 0;
uint64 RoomGraphCacheLargestEntryBytes = 0;
uint64 RoomGraphCacheLargestWorkerValidEntries = 0;
uint64 RoomGraphCacheLargestWorkerBytes = 0;
FCacheMemoryBreakdown RoomGraphCacheBreakdown;
};
VOXELFORGE_API void SetMode(EMode Mode, uint32 SampleInterval = DefaultSampleInterval);
// Counters are collected only while a diagnostic mode is enabled. In the ordinary clean
// path this is a single relaxed load, so the measurement hooks stay dormant.
FORCEINLINE bool IsEnabledFast()
{
return GEnabled.load(std::memory_order_relaxed);
}
FORCEINLINE bool AreCountersEnabled() { return IsEnabledFast(); }
FORCEINLINE bool ShouldProbeScopeFast(EBucket Bucket)
{
const EMode Mode = GMode.load(std::memory_order_relaxed);
if (Mode == EMode::Full)
{
return true;
}
if (Mode == EMode::Sampled)
{
return Bucket == EBucket::GetDensityAt
|| Bucket == EBucket::MesherGenerateMesh
|| Bucket == EBucket::MesherDensityGrid
|| Bucket == EBucket::MesherOther;
}
if (Mode == EMode::Attribution)
{
switch (Bucket)
{
case EBucket::GetDensityAt:
case EBucket::MesherGenerateMesh:
case EBucket::MesherDensityGrid:
case EBucket::MesherCellClassification:
case EBucket::MesherGradientNormals:
case EBucket::MesherVertexInterpolation:
case EBucket::MesherStreamBuilding:
case EBucket::MesherOther:
case EBucket::RoomGraphBuild:
return true;
default:
return GSampledScopeActive;
}
}
return false;
}
VOXELFORGE_API bool IsEnabled();
VOXELFORGE_API bool IsCycleTimingEnabled();
VOXELFORGE_API EMode GetMode();
VOXELFORGE_API uint32 GetSampleInterval();
VOXELFORGE_API uint64 GetTimerPairCycles();
VOXELFORGE_API void CalibrateTimer();
VOXELFORGE_API void Reset();
VOXELFORGE_API FSnapshot Snapshot();
// Worker-local snapshot for a tile profile. GenerateTileResult is single-owner on its worker,
// so this avoids attributing another worker's concurrent tile to the current tile diagnostic.
VOXELFORGE_API FSnapshot SnapshotCurrentThread();
VOXELFORGE_API void AddCounter(ECounter Counter, uint64 Amount = 1);
VOXELFORGE_API void AddMeasurement(EBucket Bucket, uint64 Cycles, uint64 Calls = 1);
VOXELFORGE_API void RecordCall(EBucket Bucket);
VOXELFORGE_API void RecordClassifierCall(bool bSampled);
VOXELFORGE_API void AddClassifierStats(const FSnapshot::FClassifierStats& Stats);
VOXELFORGE_API bool ShouldSample(EBucket Bucket);
VOXELFORGE_API void AddSampledMeasurement(EBucket Bucket, uint64 RawCycles);
VOXELFORGE_API void SetWorkerTunnelCacheFootprint(
uint64 CapacityEntries, uint64 ValidEntries,
uint64 StaticBytes, uint64 DynamicBytes,
uint64 EntryBytes, uint64 LargestEntryBytes,
uint64 LargestWorkerValidEntries,
const FCacheMemoryBreakdown& Breakdown);
VOXELFORGE_API void SetWorkerRoomGraphCacheFootprint(
uint64 CapacityEntries, uint64 ValidEntries,
uint64 StaticBytes, uint64 DynamicBytes,
uint64 EntryBytes, uint64 LargestEntryBytes,
uint64 LargestWorkerValidEntries,
const FCacheMemoryBreakdown& Breakdown);
VOXELFORGE_API const TCHAR* CounterName(ECounter Counter);
VOXELFORGE_API EBucket BucketFromName(const TCHAR* Name);
VOXELFORGE_API const TCHAR* BucketName(EBucket Bucket);
struct FScopeToken
{
bool bEntered;
bool bSampled;
bool bBlockTiming;
bool bPreviousSampled;
bool bPreviousSampledScopeActive;
uint64 StartCycles;
};
VOXELFORGE_API FScopeToken BeginScope(EBucket Bucket);
VOXELFORGE_API void EndScope(EBucket Bucket, const FScopeToken& Token);
class FScopedTimer final
{
public:
explicit FScopedTimer(EBucket InBucket)
{
if (IsEnabledFast() && ShouldProbeScopeFast(InBucket))
{
Bucket = InBucket;
Token = BeginScope(InBucket);
bActive = Token.bEntered;
}
}
explicit FScopedTimer(const TCHAR* InName)
{
if (IsEnabledFast())
{
Bucket = BucketFromName(InName);
if (ShouldProbeScopeFast(Bucket))
{
Token = BeginScope(Bucket);
bActive = Token.bEntered;
}
}
}
FORCEINLINE ~FScopedTimer()
{
End();
}
// End a range before its lexical scope ends. This lets GetDensityAt split its enclosing
// total into disjoint prologue/core/post phases without moving the large cache block.
FORCEINLINE void End()
{
if (!bActive)
{
return;
}
EndScope(Bucket, Token);
Token.bEntered = false;
bActive = false;
}
FScopedTimer(const FScopedTimer&) = delete;
FScopedTimer& operator=(const FScopedTimer&) = delete;
private:
EBucket Bucket;
FScopeToken Token;
bool bActive = false;
};
}
@@ -0,0 +1,315 @@
// VoxelDiffLayer.h
// Runtime terrain modification system (player carving & filling).
//
// HOW IT WORKS:
// =============
// The procedural density function generates cave terrain deterministically.
// When the player carves or fills terrain, we DON'T modify the procedural
// output — instead, we store modifications as a "diff layer" on top.
//
// During density evaluation:
// Final density = Procedural density + Diff offset
//
// This keeps procedural generation deterministic while supporting freeform editing.
// Modifications are stored spatially (per-chunk) for O(1) lookup.
//
// MODIFICATION LIFECYCLE:
// 1. Player uses a tool at position P with radius R and strength S
// 2. ApplyModification() stores the mod in all overlapping chunks
// 3. Returns the list of affected chunk coords → world re-meshes those chunks
// 4. During re-meshing, GetDensityOffset() returns the combined diff at each voxel
//
// PERSISTENCE:
// Diffs are stored in memory. The game layer is responsible for saving/loading
// them to disk (server-side) and clearing them on season reset.
#pragma once
#include "CoreMinimal.h"
#include "HAL/CriticalSection.h" // FRWLock
#include "Misc/ScopeRWLock.h" // FReadScopeLock / FWriteScopeLock
#include "VoxelTypes.h"
#include <atomic>
#include "VoxelDiffLayer.generated.h"
/**
* EVoxelBrushShape — geometry of a modification brush.
* Sphere : Center + Radius. Classic round brush.
* Box : Center + BoxExtent (half-extents) + Falloff band.
* Capsule : segment Center→CapsuleEnd + Radius (tube) + Falloff band. Good for tunnels.
*/
UENUM(BlueprintType)
enum class EVoxelBrushShape : uint8
{
Sphere UMETA(DisplayName = "Sphere"),
Box UMETA(DisplayName = "Box"),
Capsule UMETA(DisplayName = "Capsule")
};
/**
* FVoxelModification — A single edit to the density field.
*
* Represents one brush stroke at a world position. Multiple overlapping
* modifications combine additively.
*
* Strength convention (INTERNAL density, before MC negate):
* Negative → subtract density → create air → CARVE (remove rock)
* Positive → add density → create solid → FILL (add rock)
*/
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelModification
{
GENERATED_BODY()
// Actor-local voxel-space center of the brush (endpoint A for Capsule). The AVoxelWorld helper
// converts world centimetres to this space before calling the diff layer.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification")
FVector Center = FVector::ZeroVector;
// Sphere radius / capsule tube radius, in voxels.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification", meta = (ClampMin = "0.5"))
float Radius = 3.0f;
// Density change: negative = carve (remove rock), positive = fill (add rock).
// -10 → strong carve (punches through BaseDensity=8) · +10 → strong fill
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification")
float Strength = -10.0f;
// Brush geometry.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification")
EVoxelBrushShape Shape = EVoxelBrushShape::Sphere;
// Box half-extents in voxels (Box shape only).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification",
meta = (EditCondition = "Shape == EVoxelBrushShape::Box"))
FVector BoxExtent = FVector(5.0f, 5.0f, 5.0f);
// Capsule second endpoint in actor-local voxel coords (Capsule shape only).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification",
meta = (EditCondition = "Shape == EVoxelBrushShape::Capsule"))
FVector CapsuleEnd = FVector::ZeroVector;
// Soft falloff band (voxels) around the Box/Capsule surface for smooth edges.
// Sphere ignores this — its whole Radius is the falloff.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification", meta = (ClampMin = "0.1"))
float Falloff = 2.0f;
// Conservative voxel-space AABB the brush can affect (used to find overlapping chunks).
void GetWorldBounds(FVector& OutMin, FVector& OutMax) const
{
switch (Shape)
{
case EVoxelBrushShape::Box:
{
const FVector E = BoxExtent + FVector(Falloff);
OutMin = Center - E;
OutMax = Center + E;
break;
}
case EVoxelBrushShape::Capsule:
{
const float Pad = Radius + Falloff;
OutMin = FVector(FMath::Min(Center.X, CapsuleEnd.X), FMath::Min(Center.Y, CapsuleEnd.Y), FMath::Min(Center.Z, CapsuleEnd.Z)) - FVector(Pad);
OutMax = FVector(FMath::Max(Center.X, CapsuleEnd.X), FMath::Max(Center.Y, CapsuleEnd.Y), FMath::Max(Center.Z, CapsuleEnd.Z)) + FVector(Pad);
break;
}
case EVoxelBrushShape::Sphere:
default:
OutMin = Center - FVector(Radius);
OutMax = Center + FVector(Radius);
break;
}
}
};
/**
* UVoxelDiffLayer — Stores all player modifications as a spatial diff.
*
* Owned by AVoxelWorld, passed to the generator for density evaluation.
* Modifications are grouped by chunk coord for fast spatial lookup —
* when evaluating density for a chunk, we only check modifications
* stored in that chunk (no global scan).
*
* When a modification sphere overlaps multiple chunks, it's stored in ALL
* of them so each chunk's lookup is self-contained.
*/
UCLASS()
class VOXELFORGE_API UVoxelDiffLayer : public UObject
{
GENERATED_BODY()
public:
/** Process-unique identity for worker-local modification snapshots. */
uint64 GetCacheLifetimeId() const;
//=========================================================================
// BUDGET CONFIGURATION
//=========================================================================
/**
* Set the budget limits for player modifications.
* Called by AVoxelWorld during BeginPlay from VoxelSettings values.
* A value of 0 means unlimited (no cap).
*
* @param InMaxMods - Max number of operations (0 = unlimited)
* @param InMaxRadius - Max brush radius per operation
* @param InMaxVolume - Max cumulative brush volume in cubic voxels (0 = unlimited)
*/
void SetBudget(int32 InMaxMods, float InMaxRadius, float InMaxVolume);
/**
* Check if a modification would be allowed under the current budget.
* Does NOT consume budget — use this for UI feedback (greyed-out tool, etc.)
*
* @param Radius - The brush radius the player wants to use
* @return true if the modification is allowed
*/
bool CanModify(float Radius) const;
/** Shape-aware budget check used by ApplyModification. Boxes charge their affected outer box and
* capsules charge their full swept volume, so a long tunnel cannot be billed as a small sphere. */
bool CanModify(const FVoxelModification& Mod) const;
/**
* Get remaining modification count (how many more operations the player can do).
* Returns -1 if unlimited.
*/
UFUNCTION(BlueprintPure, Category = "Voxel Diff Layer")
int32 GetRemainingModifications() const;
/**
* Get remaining volume budget (cubic voxels of modification still allowed).
* Returns -1.0 if unlimited.
*/
UFUNCTION(BlueprintPure, Category = "Voxel Diff Layer")
float GetRemainingVolume() const;
//=========================================================================
// APPLY MODIFICATIONS
//=========================================================================
/**
* Apply a carve or fill operation.
*
* Stores the modification in every chunk the brush volume overlaps.
* Returns the list of affected chunk coordinates so the world knows
* which chunks need re-meshing.
*
* Budget enforcement: if the modification would exceed any configured
* limit (count, radius, or volume), it is REJECTED and an empty array
* is returned. Check CanModify() first for UI feedback.
*
* @param Mod - The modification to apply
* @return Array of chunk coordinates that need re-meshing (empty if rejected)
*/
TArray<FIntVector> ApplyModification(const FVoxelModification& Mod);
//=========================================================================
// DENSITY EVALUATION
//=========================================================================
/**
* Get the combined density offset from all modifications at a world position.
*
* Called per-voxel by the generator during density evaluation.
* Only checks modifications stored for the given chunk coordinate,
* so it's fast when most chunks have no modifications.
*
* Each modification contributes with a smoothstep falloff from center to edge.
* Multiple overlapping modifications combine additively.
*
* @param ChunkCoord - The chunk being evaluated (for spatial lookup)
* @param WorldX, WorldY, WorldZ - The voxel position
* @return Total density offset (negative = carve, positive = fill)
*/
float GetDensityOffset(const FIntVector& ChunkCoord,
float WorldX, float WorldY, float WorldZ) const;
/**
* Check if a chunk has any modifications (fast reject for hot path).
*
* @param ChunkCoord - The chunk to check
* @return true if there are modifications affecting this chunk
*/
bool HasModifications(const FIntVector& ChunkCoord) const;
//=========================================================================
// 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. La recherche parcourt directement les clés de
* la plage demandée, pas tout l'historique des chunks — 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
//=========================================================================
/** Clear ALL modifications (e.g., on season reset). */
void Clear();
/** Get total number of stored modification entries (for debug/stats). */
int32 GetTotalModificationCount() const;
/** Get number of chunks that have modifications. */
int32 GetModifiedChunkCount() const;
private:
// Never reused for the lifetime of the process: a recycled UObject address must not make a
// worker-local snapshot from another diff layer look current when its chunk/version match.
mutable std::atomic<uint64> CacheLifetimeId{0};
//=========================================================================
// STORAGE
//=========================================================================
// Modifications grouped by chunk coordinate.
// Each chunk's list contains ALL modifications that overlap it
// (including mods centered in neighboring chunks whose radius reaches here).
//
// THREADING: written on the game thread (ApplyModification / Clear) but read by MANY mesher
// WORKER threads (GetDensityAt -> HasModifications / GetDensityOffset). TMap is not thread-safe —
// a read landing during a write's rehash reads freed hash memory (EXCEPTION_ACCESS_VIOLATION in
// TSet::FindId). All access to ChunkMods MUST hold ModsLock (read lock for queries, write lock for
// mutation). bHasAnyMods is a lock-free fast reject: while it's false (the common streaming case,
// no carves) readers skip the map AND the lock entirely, so unmodified worlds pay nothing.
TMap<FIntVector, TArray<FVoxelModification>> ChunkMods;
mutable FRWLock ModsLock;
std::atomic<bool> bHasAnyMods{ false };
std::atomic<uint32> ModsVersion{ 1 }; // see the snapshot API above
//=========================================================================
// BUDGET TRACKING
//=========================================================================
// Configured limits (0 = unlimited)
int32 BudgetMaxMods = 0;
float BudgetMaxRadius = 50.0f;
float BudgetMaxVolume = 0.0f;
// Running totals — reset when Clear() is called
int32 ModificationCount = 0; // Total operations applied
float AccumulatedVolume = 0.0f; // Sum of all brush sphere volumes (4/3 * PI * R^3)
};
@@ -0,0 +1,479 @@
// VoxelGenerator.h
// Fournit le champ de densité du monde (positif = solide, négatif = air).
//
// Pipeline density-only: plus de grille de blocs, plus de surface heightfield.
// Le mesher appelle GetDensityAt() par voxel pour reconstruire la géométrie.
// Chaque strate est générée par sa pile d'opérateurs (VoxelDensityOpStack.h), pilotée par les
// params de strate récupérés auprès du StrateManager.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "VoxelTypes.h"
#include "VoxelStrateTypes.h"
#include "VoxelBiomeTypes.h"
#include "VoxelGenerator.generated.h"
// Forward decls (évite les includes transitifs)
class UVoxelSettings;
class UVoxelStrateManager;
class UVoxelDiffLayer;
class UVoxelBiomeDefinition;
struct FTunnelCoreWorldEvaluation;
/** Guards which may be disabled only by the sealed-solid proof's soundness tests. */
enum class EVoxelSealedSolidProofGuard : uint8
{
None = 0,
DiffLayer = 1u << 0,
Passages = 1u << 1,
OriginLanding = 1u << 2,
DisturbanceChasms = 1u << 3,
};
//=============================================================================
// 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
{
// The TLS variables themselves stay module-local because MSVC forbids exporting thread_local
// data (C2492). These tiny accessors let the editor explorer build a shared LOD grid without
// linking directly to the storage in the game module.
extern thread_local int32 OctaveBias;
// Sampling step of the tile currently being meshed on this worker. GetDensityAt itself has
// no Step parameter because callers outside the mesher are point queries; this TLS is only a
// read-only hint for caches that may safely widen their spatial window for coarse tiles.
extern thread_local int32 SampleStep;
// Exact lattice footprint of the tile currently being meshed. The cache-window switch
// uses this rather than inferring a region from an individual sample coordinate, so every
// sample in one tile shares one deterministic window even at tile/chunk boundaries.
extern thread_local FIntVector TileOriginVoxels;
extern thread_local int32 TileCellsPerAxis;
VOXELFORGE_API int32 GetThreadOctaveBias();
VOXELFORGE_API void SetThreadOctaveBias(int32 Value);
VOXELFORGE_API int32 GetThreadSampleStep();
VOXELFORGE_API void SetThreadSampleStep(int32 Value);
// Called by the module's game-thread startup before any worker can enter GetDensityAt.
VOXELFORGE_API void InitializeConsoleSwitches();
VOXELFORGE_API bool IsTileCacheWindowEnabled(bool bFusedPath = true);
#if WITH_EDITOR
/** Research-only override for the bounded editor region evaluator; default false per thread. */
VOXELFORGE_API void SetResearchRegionCacheWindowEnabled(bool bEnabled);
#endif
VOXELFORGE_API bool GetThreadTileCacheWindow(
FIntVector& OutOriginVoxels, int32& OutStep, int32& OutCellsPerAxis);
VOXELFORGE_API void SetThreadTileContext(
const FIntVector& OriginVoxels, int32 CellsPerAxis);
// 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); }
}
// NOTE: EVoxelTileClass (le verdict T1.d) a déménagé dans VoxelTypes.h — inclus ci-dessus —
// pour que VoxelDensityOp.h puisse le partager sans dépendre d'un header UCLASS.
// EVoxelTileClass (the T1.d verdict) moved to VoxelTypes.h, included above.
/**
* UVoxelGenerator
*
* Objet UObject léger — ne stocke pas de données lourdes.
* Tient les paramètres globaux copiés depuis les settings (seed, spine, bord XY) et les services
* dont il a besoin (StrateManager pour les params par chunk, DiffLayer pour
* les carves du joueur).
*/
UCLASS(BlueprintType)
class VOXELFORGE_API UVoxelGenerator : public UObject
{
GENERATED_BODY()
public:
UVoxelGenerator();
//=========================================================================
// SEED (source unique: Settings->Seed)
//=========================================================================
// Les paramètres globaux sont copiés au démarrage pour éviter un déréférencement par voxel.
// Les formes créatives viennent ensuite des params de strate.
int32 Seed = 0;
// Authored radius (voxels) of the guaranteed flat-floored landing room at world XY (0,0) in
// every strate — the old “spine” setting now sizes a room and never creates a vertical bore.
// 0 disables the origin landing rooms. Copied from VoxelSettings.
float OriginSpineRadius = 28.0f;
// Radius (voxels) of the bounded world in actor-space XY, measured from (0,0).
// 0 keeps the legacy unbounded behavior and is a true no-op for the edge seal.
float WorldRadiusVoxels = 8192.0f;
// Width (voxels) of the smooth solid ramp at the XY world edge.
float EdgeSealThickness = 128.0f;
//=========================================================================
// SERVICES (injectés par AVoxelWorld::BeginPlay)
//=========================================================================
// Manager des strates — fournit les params de cave par chunk.
// Peut être nullptr: dans ce cas on utilise des params par défaut.
UPROPERTY()
const UVoxelStrateManager* StrateManager = nullptr;
// Diff layer des modifications joueur (carve/fill).
// Peut être nullptr: dans ce cas pas de modifs appliquées.
UPROPERTY()
const UVoxelDiffLayer* DiffLayer = nullptr;
void SetStrateManager(const UVoxelStrateManager* InManager)
{
StrateManager = InManager;
}
void SetDiffLayer(const UVoxelDiffLayer* InDiffLayer)
{
DiffLayer = InDiffLayer;
}
// Copie le seed depuis les settings. Appelé au démarrage et lors d'un ChangeSeed.
void InitializeSettings(const UVoxelSettings* Settings);
//=========================================================================
// API DE DENSITÉ
//=========================================================================
/**
* Densité combinée (strates + diff du joueur).
*
* Convention de sortie (MC): négatif = solide, positif = air.
* C'est ce que le marching cubes attend comme champ scalaire.
*
* @param WorldX, WorldY, WorldZ - coordonnées monde en voxels (pas cm)
*/
float GetDensityAt(float WorldX, float WorldY, float WorldZ,
float* OutPreEditDensity = nullptr) const;
/**
* Open a worker-local lattice hand-off for the following GetDensityAt calls. The mesher uses
* this around one tile's density-grid fill. The first sample in each resolved chunk lets the
* generator run the operator stack once in EvalBlock; later samples consume the exact cached
* per-sample result while the ordinary post-stack tail still runs in canonical order.
*
* This is intentionally a scoped service rather than a persistent world cache: its lifetime is
* one tile task, it is thread-local, and EndDensityBlock must be called on every exit path.
*/
void BeginDensityBlock(FIntVector OriginVoxels, int32 Step,
int32 SizeX, int32 SizeY, int32 SizeZ) const;
void EndDensityBlock() const;
#if WITH_EDITOR
/** Editor-only chunk program; supported routes enter GetDensityAt once to seed a whole block. */
bool EvaluateDensityChunk(FIntVector OriginVoxels, int32 SizeX, int32 SizeY, int32 SizeZ,
TArray<float>& OutDensities, int32& OutFallbackSamples,
uint64& OutGeneralPathEntries,
uint64& OutSupportedBlockSetupEntries) const;
#endif
/**
* Prove that the mesher's core lattice is entirely solid from the cheap, structural rules
* that own inter-strate gaps and boundary seals. This never samples density. It is intended
* for an empty-tile fast path; a false answer is harmless, while a true answer must make every
* core lattice vertex negative in the final MC field.
*
* The proof is deliberately narrower than ClassifyTile: it accepts only a tile made entirely
* of gap/seal lattice rows and rejects recipes/composer overrides plus every known air-writing
* structural feature. The mesher's +/-1 normal halo is not part of the cell lattice, matching
* ClassifyTile and GenerateMesh.
*/
bool TryProveSealedSolidTile(const FIntVector& OriginVoxels, int32 Step,
int32 CellsPerAxis) const;
#if WITH_DEV_AUTOMATION_TESTS
/** Test hook: disable named proof guards and require the brute-force oracle to catch it. */
bool TryProveSealedSolidTileForTest(
const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis,
uint8 DisabledGuards) const;
#endif
/**
* Densité pour une strate TunnelNetwork (rooms + tunnels + worm noise).
* C'est l'évaluateur fusionné de la pile TunnelNetwork/Underwater dans GetDensityAt.
* Exposée pour permettre des tests isolés avec des params custom.
*
* ⚠️ `ParamsFingerprint` ET `LayoutVersion` SONT OBLIGATOIRES (`AUDIT §C2`). Le cache SDF
* interne est clé sur (boîte XY, strate, seed) et PAS sur les params. Or `GetGenerationParams` BLENDE les params à l'intérieur d'une même
* strate — `Alpha` dépend du chunk Z en mode `Gradient` (le DÉFAUT, avec
* `TransitionBlendChunks = 2`) et du chunk XY en plus en mode `Interleaved`. Deux chunks de la
* même strate, même seed, donc même clé, mais des params DIFFÉRENTS : le worker évalue le
* deuxième chunk qu'il construit contre les salles du premier. Et comme *quel* chunk vient en
* premier dépend de l'ordre des workers, **deux pairs divergent depuis la même seed** — ce que
* `OPSTACK-PLAN §2.6.1` interdit explicitement.
*
* Pourquoi une empreinte PASSÉE plutôt qu'un `MemCrc32` calculé ici : ce serait ~300 octets de
* CRC PAR VOXEL sur le chemin le plus chaud du plugin. L'appelant la calcule UNE fois par
* chunk, là où le mémo de params vit déjà (`CP_*`), donc le coût par voxel est exactement deux
* comparaisons d'entiers. Pas de valeur par défaut : un appelant qui oublie doit ne pas
* compiler, pas hériter silencieusement du trou (la discipline de `FVoxelOpContext`).
*
* ⚠️ POUR LES TESTS : passez `FCrc::MemCrc32(&Params, sizeof(Params))`. Un oracle qui partage
* le défaut qu'il teste ne prouve rien — c'est précisément ce que la note de
* `VoxelForgeOpStackTunnelTest.cpp` (contrôle 3) décrivait comme le trou de l'original.
*
* The SDF cache key had neither the params nor anything that determines them, while the params
* are blended per chunk INSIDE a strate — so a worker could evaluate one chunk against another
* chunk's rooms, and which came first depends on worker order. Passing a once-per-chunk
* fingerprint keeps the fix off the per-voxel path. No default: forgetting it must not compile.
*/
float GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
const FStrateGenerationParams& Params,
uint32 ParamsFingerprint, uint32 LayoutVersion,
bool bApplyLegacyStructuralPosts = true,
// Optional hand-off used by a lowered canonical evaluator. When
// non-null, this receives the pre-disturbance TunnelNetwork core;
// it does not alter the scalar field result.
FTunnelCoreWorldEvaluation* OutTunnelCore = nullptr,
// Diagnostics are opt-in so ordinary/reference calls do not pay
// for fused-path counters.
bool bCollectFusedDiagnostics = false,
// Optional same-sample hand-off from EvaluateSDFCached. This is
// only a room-shape ownership bit; it does not alter the scalar
// result and avoids a duplicate room-index query.
bool* OutRoomOwnsBottom = nullptr) const;
//=========================================================================
// CLIMATE & BIOME FIELDS (pure XY, deterministic, window-invariant)
//=========================================================================
/**
* Relief ("elevation") field at a world XY → [0,1], contrast-shaped & smoothed.
* The single source of truth for the relief map M: SurfaceWorld terrain and the
* biome climate map both call this, so they agree when given the same freq/contrast.
*/
float SampleRelief(float WorldX, float WorldY, float Frequency, float Contrast) const;
/**
* La chaîne de hauteur complète de SurfaceWorld : structural → cliff → terrace → layer lines →
* plage. Rend une ALTITUDE monde en voxels, pas une densité.
*
* PUBLIQUE pour permettre un test isolé : c'est la référence de
* `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui compare la pile d'opérateurs de hauteur
* (`VoxelHeightOp.h`) à cette fonction point par point.
* Public so the height-op stack can be measured against it.
*/
float ComputeSurfaceTerrainZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const;
/**
* La colonne de surface : terrain Z, plafond, et le gate d'OVERHANG résolu par colonne
* (amplitude + direction amont). C'est la colonne que `ClassifyTile` teste pour SurfaceWorld ;
* publique pour que les tests puissent placer des échantillons dans la fenêtre d'overhang.
* The surface column ClassifyTile tests for SurfaceWorld; public so tests can aim samples at
* the overhang window.
*/
void ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ,
const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx,
const TArray<FSurfaceGenerationParams>& BiomeParams, FChunkBiomeCache& BiomeCache,
float& OutTerrainZ, float& OutCeilSurf,
float& OutOverhangAmp, float& OutDirX, float& OutDirY) const;
/**
* Moisture field at a world XY → [0,1]. The second climate axis for biome placement.
*/
float SampleMoisture(float WorldX, float WorldY, float Frequency) const;
/**
* Resolve the biome at a world XY: dominant cell + nearest neighbour + blend weight.
* Warped Voronoi over a jittered grid; each cell's biome is chosen by its site's
* (relief, moisture) against the context's climate boxes. PURE function of (XY, seed,
* Ctx) — no chunk-window dependence (CODEMAP §8.4). Returns an empty sample when the
* context has no biomes. Used by the 2D preview bake and (next) the density path.
*/
FBiomeSample SampleBiomeAt(float WorldX, float WorldY, const FBiomeContext& Ctx) const;
/**
* Hot-path biome resolve: the dominant + neighbour biome and blend weight at a world
* XY for the given strate slice. Uses (and lazily rebuilds) Cache — a box-validated
* per-chunk grid — so the noise-heavy cell classification happens once per chunk, not
* per voxel. Bit-identical to SampleBiomeAt, so the baked preview matches the terrain.
*/
FBiomeSample ResolveBiomeSampleAt(float WorldX, float WorldY, int32 ChunkZ,
const FBiomeContext& Ctx, FChunkBiomeCache& Cache) const;
/**
* Dominant biome ASSET at a world XY for the given strate slice (chunk Z), or nullptr
* when the strate has no biomes / the point is outside the strate range. Game-thread
* helper for content + atmosphere selection — uses the same field as the density path.
*/
const UVoxelBiomeDefinition* GetDominantBiomeAt(float WorldX, float WorldY, int32 ChunkZ) const;
/**
* Rich biome probe at a world XY for a strate slice (ChunkZ): dominant + neighbour biome assets,
* climate fields, border blend weight, and the dominant biome's EFFECTIVE decoration count. Mirrors
* exactly what the decoration scatter resolves per column, so it is a faithful "what's under here?"
* diagnostic (DominantDecorationCount == 0 explains an empty biome region). Game-thread, uncached.
*/
void QueryBiomeAt(float WorldX, float WorldY, int32 ChunkZ, FVoxelBiomeQuery& Out) const;
/**
* Per-vertex material data for the master triplanar palette material (F6). Resolves the
* biome field at a world XY/Z and returns the dominant + neighbour MaterialPaletteIndex
* and the border blend weight (0 deep in a cell → 0.5 at the border). The mesher packs
* these into vertex colour so one material re-skins terrain per biome and cross-fades
* across biome borders. No biomes ⇒ Dominant=Neighbour=0, Weight=0 (default palette).
* Thread-safe: own per-chunk thread_local context + box cache (clustered tile queries
* stay warm). Window-invariant (ResolveBiomeSampleAt is bit-identical to SampleBiomeAt).
*/
void GetBiomeMaterialAt(float WorldX, float WorldY, float WorldZ,
int32& OutDominantPalette, int32& OutNeighborPalette,
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
* XY for the given strate slice (ChunkZ), WITHOUT ray-marching the density column. Returns false
* (outs untouched) when the chunk is NOT a SurfaceWorld heightfield — callers fall back to marching.
* Shares the density path's surface helpers, so a decoration snapped to OutTerrainZ sits exactly on
* the rendered ground. Does NOT include passage/spine/seal carving — verify with one GetDensityAt at
* the result if a column might be carved. Thread-safe (own per-chunk thread_local cache).
*/
bool GetSurfaceHeightAt(float WorldX, float WorldY, int32 ChunkZ,
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).
*
* GAP (bedrock), hors-layout (air constant) et SurfaceWorld plient leurs hypothèses par Z ;
* SurfaceWorld évalue ses colonnes terrain/plafond avec le MÊME ComputeSurfaceColumn que le
* chemin densité. Un unique slot cave peut ajouter le verdict conservatif de sa pile,
* sous gardes d'archétype et de params bit-identiques. Spine/passages/disturbances/diff restent
* des gardes conservatrices. 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;
EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step,
int32 CellsPerAxis,
struct FVoxelTileClassificationStats* OutStats) const;
/**
* Prove the exact constant-air field above/below the strate layout for a mesher lattice.
* This is separate from TryProveSealedSolidTile: it never supplies collision support and its
* caller records AllAir, not AllSolid. It shares the layout/diff/structural guards with the
* production classifier and returns false for every mixed or uncertain box.
*/
bool TryProveOutOfLayoutAirTile(const FIntVector& OriginVoxels, int32 Step,
int32 CellsPerAxis) const;
private:
/** Identité process-unique du propriétaire des caches `CP_*` thread_local.
* Process-unique owner identity for the `CP_*` thread-local cache key. */
uint64 DensityCacheOwnerId = 0;
EVoxelTileClass ClassifyTileUncached(
const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis,
struct FVoxelTileClassificationStats* OutStats) const;
/** 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;
// ComputeSurfaceTerrainZ a été DÉPLACÉE en `public` (voir plus haut) pour que
// VoxelForge.OpStack.SurfaceHeightEquivalence puisse s'y comparer. Une seule déclaration.
// Moved to public above so the height-stack test can compare against it. One declaration only.
/** 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). */
float ComputeSurfaceCeiling(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const;
/** Resolve a chunk's SurfaceWorld params (strate base + per-biome overrides, structural fields
* forced from the strate). Shared by GetDensityAt's per-chunk cache and the GetSurfaceHeightAt
* oracle so both produce the SAME surface. */
void ResolveSurfaceChunkParams(const FIntVector& ChunkCoord,
FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx,
TArray<FSurfaceGenerationParams>& OutBiomeParams) const;
/** (Re)build the per-chunk biome cell grid covering chunk (X,Y) footprint + margin. */
void RebuildBiomeGrid(int32 ChunkX, int32 ChunkY, int32 ChunkZ,
const FBiomeContext& Ctx, FChunkBiomeCache& Cache,
uint64 OwnerId, uint64 ManagerLifetimeId) const;
};
namespace VoxelGenLOD
{
// -1 uses the measured path/LOD policy; 0/1 force the spatial broad phase off/on for an A/B
// run. The policy affects candidate enumeration only, never cache contents or ordering.
VOXELFORGE_API bool ShouldUseSpatialIndex(bool bFusedPath);
}
/**
* Optional diagnostics for one ClassifyTile call. This is populated only when the caller asks
* for it (the streaming profiler does); it is never consulted by the proof and cannot affect a
* verdict. Keeping the counters beside the classifier lets the game profile distinguish cache,
* interval refinement, and exact-lattice work without sampling a different code path.
*/
struct FVoxelTileClassificationStats
{
uint32 RefineNodes = 0;
uint32 StackBoxCalls = 0;
uint32 WholeMixedNodes = 0;
uint32 WholeSolidNodes = 0;
uint32 WholeAirNodes = 0;
uint32 NeedsFinalFieldNodes = 0;
uint32 SplitNodes = 0;
uint32 MaxRefinementDepth = 0;
uint32 ExactCoreSamples = 0;
uint32 ExactFinalSamples = 0;
uint32 ExactCoreIntervalChecks = 0;
uint32 ExactCoreIntervalDisagreements = 0;
uint32 ExactCoreCacheHits = 0;
uint32 ExactFinalCacheHits = 0;
uint32 ExactCoreLeaves = 0;
uint32 ExactFinalLeaves = 0;
uint64 StackBoxCycles = 0;
uint64 ExactCoreCycles = 0;
uint64 ExactFinalCycles = 0;
uint32 RoomTailQueries = 0;
uint32 RoomTailEvaluated = 0;
uint64 RoomTailCycles = 0;
uint64 RoomPropagateCycles = 0;
uint64 RoomExactPrimitiveCycles = 0;
uint64 RoomCacheWindowCycles = 0;
int32 RoomNumRooms = 0;
int32 RoomNumTunnels = 0;
int32 RoomNumRoomFloorJoins = 0;
int32 RoomNumPits = 0;
int32 RoomNumChimneys = 0;
};
@@ -0,0 +1,273 @@
// VoxelHeightOp.h
// L'ESPACE DES HAUTEURS — une seconde famille d'opérateurs, et pourquoi elle DOIT exister.
// HEIGHT SPACE — a second operator family, and why it has to exist.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// LE CONSTAT QUI FORCE CE FICHIER
// ─────────────────────────────────────────────────────────────────────────────────────────
// `OPSTACK-DECOMPOSITION §5` décompose SurfaceWorld ainsi :
//
// FHeightfieldSource ← toute la chaîne de colonne, XY-pure
// ├─ FStructuralHeightField
// ├─ FCliffHeightMod
// ├─ FTerraceHeightMod
// ├─ FLayerLineHeightMod
// └─ FBeachHeightMod
//
// et note, sans en tirer la conséquence : *« les ops de hauteur opèrent sur des valeurs Z dans la
// colonne, pas sur la densité »*. En lisant `ComputeSurfaceTerrainZ`, c'est littéralement vrai :
// c'est une suite de blocs qui lisent et écrivent **un seul float `Terrain`**, une altitude.
//
// **Ils ne rentrent donc PAS dans `IVoxelDensityOp`.** Sa signature est
// `Eval(x, y, z, FVoxelOpSample&)` — par voxel, deux canaux densité/SDF. Un op de hauteur n'a pas
// de Z d'entrée (il en PRODUIT un), ne veut pas être appelé par voxel (il est XY-pur, une fois par
// colonne), et n'écrit ni densité ni SDF. Les forcer dans le contrat densité demanderait soit un
// troisième canal par voxel — alors que la hauteur est une propriété de COLONNE, pas de voxel —,
// soit de replier les cinq en un seul op opaque, ce que `§2.5` appelle précisément l'échec du
// refactor.
//
// **Donc : une seconde famille, dans son propre espace.** C'est la même leçon que `§0.1` (il fallait
// un canal SDF en plus de la densité), un cran plus loin : certaines choses ne sont pas un canal de
// plus, elles sont un ESPACE de plus.
//
// The height ops read and write a single float ALTITUDE. They have no input Z (they produce one),
// are XY-pure (once per column, not per voxel), and write neither density nor SDF. Forcing them into
// IVoxelDensityOp would need either a per-voxel third channel for what is a COLUMN property, or
// collapsing all five into one opaque op — which §2.5 calls the failure mode. Hence a second family.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// CE QUE ÇA ACHÈTE / WHAT IT BUYS
// ─────────────────────────────────────────────────────────────────────────────────────────
// • **Le cache de colonne T1.a tombe naturellement.** Une pile de hauteur est XY-pure PAR
// CONSTRUCTION — il n'y a pas de Z à mettre dedans par erreur. `AUDIT §6.3` avertit qu'une donnée
// dépendante de Z glissée dans `FSurfaceColumn` corrompt silencieusement toute la pile verticale
// de chunks, et que `ValidateDeterminism` ne le verrait pas. Ici c'est le TYPE qui l'interdit.
// • **La composition d'idées de terrain devient de l'authoring**, comme pour la densité.
// • Les mêmes ops resserviront à VerticalShafts (ledges) et FloatingIslands.
//
// ⚠️ CE FICHIER NE TOUCHE PAS AU JEU. Il est bâti et exercé par
// `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui le compare à `ComputeSurfaceTerrainZ` point par
// point. Le branchement dans le chemin densité est l'étape SUIVANTE (§5 : `FHeightfieldSource`,
// `FSkyCapSource`, `FOverhangShelfMod`), délibérément séparée pour que la question d'architecture
// — *« l'espace des hauteurs se décompose-t-il vraiment ? »* — reçoive une réponse MESURÉE avant
// qu'on écrive l'adaptateur qui en dépend.
#pragma once
#include "CoreMinimal.h"
#include "Templates/UniquePtr.h"
#include "VoxelStrateTypes.h" // FSurfaceGenerationParams
/**
* L'état qui traverse une pile de hauteur. DEUX canaux, exactement comme `FVoxelOpSample` — et
* pour la même raison : le code le fait déjà.
*
* `Relief` (le `M` de `SampleSurfaceStructuralZ`) est PRODUIT par la source structurelle et CONSOMMÉ
* par le gate du terrace (`TerraceStrength * M`). Sans ce second canal, le terrace devrait
* ré-échantillonner le champ de relief — plus lent, et surtout une occasion de diverger de la valeur
* que la source a réellement utilisée.
*
* Two channels, for the same reason as FVoxelOpSample: Relief (the `M` of the structural field) is
* produced by the source and consumed by the terrace gate. Threading it beats resampling it.
*/
struct FVoxelHeightSample
{
/** Altitude monde en VOXELS (pas cm). */
float Height = 0.0f;
/** « Montagnosité » [0,1]. 1 = uniforme (ReliefStrength = 0). */
float Relief = 1.0f;
};
/**
* Le résultat d'une requête de champ de biome en un XY : qui domine, qui est le voisin, et à quel
* poids on va vers lui dans la bande de frontière.
*/
struct FVoxelBiomeWeights
{
int32 Dominant = 0;
int32 Neighbor = -1;
float NeighborWeight = 0.0f; // 0 ⇒ pas de mélange, le dominant seul
};
/**
* LE CHAMP DE BIOMES, VU COMME UNE INTERFACE — et c'est délibérément une interface, pas un pointeur
* vers `UVoxelGenerator`.
*
* Le résolveur de biome réel est une Voronoï warpée avec un cache par chunk, qui vit sur le
* générateur. Un opérateur ne doit PAS en dépendre : la Phase 3 veut que les opérateurs deviennent
* des DONNÉES (des assets), et un op qui tient un `UVoxelGenerator*` ne peut pas le devenir. En
* passant par cette interface, l'adaptateur qui connaît le générateur reste du côté du générateur,
* et l'opérateur ne connaît qu'« un truc qui répond (dominant, voisin, poids) en XY ».
*
* Deliberately an interface rather than a UVoxelGenerator*: Phase 3 wants ops to become data, and an
* op holding a generator pointer never can. The adapter that knows the generator stays on the
* generator's side; the op only knows "something that answers (dominant, neighbour, weight) at XY".
*/
class IVoxelBiomeField
{
public:
virtual ~IVoxelBiomeField() = default;
/** PURE en XY, et bit-identique quel que soit le thread ou l'ordre — même contrat que le reste
* de l'espace-hauteur, puisque le résultat alimente le cache de colonne T1.a. */
virtual FVoxelBiomeWeights SampleAt(float WorldX, float WorldY) const = 0;
};
/**
* Un opérateur d'espace-hauteur. Trois différences avec `IVoxelDensityOp`, toutes voulues :
* • pas de Z d'entrée — la pile en PRODUIT un ;
* • XY-pur par construction, donc pas de `IsXYPure()` à déclarer ni à oublier ;
* • pas de `PrepareChunk` — ces ops sont déjà appelés une fois par colonne, ce qui EST la
* granularité que `PrepareChunk` sert à obtenir côté densité.
*/
class IVoxelHeightOp
{
public:
virtual ~IVoxelHeightOp() = default;
/**
* INVARIANCE DE FENÊTRE (ARCHITECTURE §8.4) : fonction PURE de (X, Y, seed, params). Le même XY
* évalué depuis une autre tuile, un autre ordre, un autre thread doit rendre le float
* BIT-IDENTIQUE — le cache de colonne T1.a est partagé sur toute la pile verticale de chunks,
* donc une impureté ici se propage à tous les Z d'un coup.
*/
virtual void Eval(float WorldX, float WorldY, FVoxelHeightSample& InOut) const = 0;
/**
* Majorant CONSERVATIF du déplacement vertical que cet op peut ajouter, en voxels.
* Sert à borner la colonne pour un futur `ClassifyBox` exact du heightfield — la même logique
* que les bandes de `FSlabVoidSource`, qui prouvent 36-40 tuiles sur 60.
* Rendre trop grand coûte du CPU ; rendre trop petit serait un TROU. `FLT_MAX` = « je ne sais
* pas », toujours sûr, et c'est le défaut.
*/
virtual float MaxDisplacement() const { return FLT_MAX; }
};
/**
* Pile de hauteur : source → modificateurs, dans l'ordre. Déplaçable, pas copiable, exactement
* comme `FVoxelOpStack` et pour la même raison (elle POSSÈDE ses opérateurs).
*/
class FVoxelHeightStack
{
public:
FVoxelHeightStack() = default;
FVoxelHeightStack(FVoxelHeightStack&&) = default;
FVoxelHeightStack& operator=(FVoxelHeightStack&&) = default;
FVoxelHeightStack(const FVoxelHeightStack&) = delete;
FVoxelHeightStack& operator=(const FVoxelHeightStack&) = delete;
void Add(TUniquePtr<IVoxelHeightOp> Op) { Ops.Add(MoveTemp(Op)); }
int32 Num() const { return Ops.Num(); }
/** L'altitude après toute la pile. */
float EvalHeight(float WorldX, float WorldY) const
{
return EvalSample(WorldX, WorldY).Height;
}
/** L'état complet (altitude + relief). */
FVoxelHeightSample EvalSample(float WorldX, float WorldY) const
{
FVoxelHeightSample S;
for (const TUniquePtr<IVoxelHeightOp>& Op : Ops) { Op->Eval(WorldX, WorldY, S); }
return S;
}
/** Somme des majorants. `FLT_MAX` dès qu'un seul op ne sait pas répondre. */
float MaxTotalDisplacement() const
{
float Total = 0.0f;
for (const TUniquePtr<IVoxelHeightOp>& Op : Ops)
{
const float D = Op->MaxDisplacement();
if (D >= FLT_MAX) { return FLT_MAX; }
Total += D;
}
return Total;
}
private:
TArray<TUniquePtr<IVoxelHeightOp>> Ops;
};
//=============================================================================
// FABRIQUES / FACTORIES
//=============================================================================
namespace VoxelHeightOps
{
/**
* La source structurelle : continents + montagnes + détail, sous une frame de warp.
* Produit `Height` ET `Relief`. Transcription littérale de `SampleSurfaceStructuralZ`.
*
* ⚠️ Rend un pointeur NON-POSSÉDANT via `OutSource` : `FCliffHeightMod` doit pouvoir
* RÉ-ÉCHANTILLONNER ce champ (4 fois, en différences centrées) et doit le faire sur la MÊME
* fonction, pas sur une copie qui pourrait dériver. La pile garde la propriété ; la source vit
* donc aussi longtemps que le modificateur qui la référence, parce que le constructeur de pile
* les ajoute ensemble et que la pile ne réordonne jamais.
*/
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeStructuralHeightSource(
const FSurfaceGenerationParams& P, int32 Seed, const IVoxelHeightOp** OutSource);
/** Raidissement conditionné par la pente. Le seul op qui coûte des échantillons en plus
* (4 resamples structurels), et seulement quand il est activé. */
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeCliffHeightMod(
const FSurfaceGenerationParams& P, const IVoxelHeightOp* StructuralSource);
/** Plateaux quantifiés, gatés par le relief (`TerraceStrength * M`) — d'où le canal Relief. */
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeTerraceHeightMod(const FSurfaceGenerationParams& P);
/** Bandes sédimentaires : `Height -= sin(Height · 2π / Spacing) · Depth`. */
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeLayerLineHeightMod(const FSurfaceGenerationParams& P);
/** Aplatissement vers la ligne d'eau dans `BeachWidth`. */
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeBeachHeightMod(const FSurfaceGenerationParams& P);
/**
* La pile de hauteur complète de SurfaceWorld, dans l'ordre de `ComputeSurfaceTerrainZ` :
* structural → cliff → terrace → layer lines → beach
*
* L'ordre n'est PAS négociable : le terrace quantifie une hauteur que le cliff a déjà raidie,
* les layer lines se posent sur le résultat, et la plage écrase tout près de l'eau. C'est
* l'ordre du code d'origine, et le test échouerait bruyamment sur toute permutation.
*/
VOXELFORGE_API void BuildSurfaceHeightStack(FVoxelHeightStack& OutStack,
const FSurfaceGenerationParams& P, int32 Seed);
/** La voûte : warp + gonflement signé + pendage vers le bas uniquement. C'est une ALTITUDE,
* donc un op de hauteur — la soustraction n'arrive qu'au combine côté densité. */
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeSkyCapHeightSource(const FSurfaceGenerationParams& P,
int32 Seed);
/** La pile de plafond de SurfaceWorld. Un seul op aujourd'hui, et c'est une information : le
* plafond n'a pas d'équivalent des quatre modificateurs du sol. */
VOXELFORGE_API void BuildSurfaceCeilingStack(FVoxelHeightStack& OutStack,
const FSurfaceGenerationParams& P, int32 Seed);
/**
* LE COMBINER `Mask` — mélange de biomes, et `OPSTACK-DECOMPOSITION §5` en fait le prototype
* de la Phase 3 entière : « unifier strates et biomes » EST ce mécanisme, généralisé.
*
* Une pile de hauteur COMPLÈTE par biome, plus un champ qui dit lequel domine en (X,Y). Ce sont
* les **HAUTEURS** qui sont interpolées, pas les params — c'est ce que fait déjà le code
* d'origine, et c'est ce qui rend les frontières continues quelle que soit la différence de
* params entre deux biomes (interpoler des params ferait passer un terrace de « fort » à
* « faible » à travers des états intermédiaires qui n'ont de sens pour personne).
*
* Each biome gets a COMPLETE height stack; the HEIGHTS are lerped, not the params — which is
* what keeps borders continuous across any param difference.
*
* @param Field non possédé, doit survivre à la pile. `nullptr` ⇒ biome 0 partout.
*/
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeBiomeBlendHeightSource(
const TArray<FSurfaceGenerationParams>& PerBiomeParams, int32 Seed,
const IVoxelBiomeField* Field);
/** Idem pour le plafond — mais le plafond N'EST PAS mélangé : le code d'origine prend celui du
* biome DOMINANT seul. Reproduit tel quel, pas « amélioré » : une voûte interpolée changerait
* la silhouette du monde et ce portage n'est pas l'endroit pour décider ça. */
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeBiomeSelectCeilingSource(
const TArray<FSurfaceGenerationParams>& PerBiomeParams, int32 Seed,
const IVoxelBiomeField* Field);
}
@@ -0,0 +1,144 @@
// VoxelMarchingCubesMesher.h
// Mesh lisse à partir du champ de densité du générateur.
//
// Principe:
// 1. Chaque cellule du chunk (cube 2x2x2 voxels) lit la densité aux 8 coins.
// 2. Un index 8 bits indique quels coins sont au-dessus du IsoLevel.
// 3. Les tables MC (EdgeTable + TriTable) donnent les triangles à générer.
// 4. On interpole la position du vertex le long de chaque arête traversée.
//
// LOD: Step > 1 échantillonne moins souvent → moins de triangles à distance.
#pragma once
#include "CoreMinimal.h"
#include <atomic>
#include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc.
#include "VoxelGenerator.h"
#include "VoxelMarchingCubesMesher.generated.h"
// A run-scoped, read-only density lattice. The explorer owns the storage and installs the
// pointer only while its canonical tiles are being meshed. The lattice includes the one-point
// halo needed by every tile, so each world sample is generated once and copied into both adjacent
// tile scratch grids. It is deliberately not a UObject or a cache with eviction: ownership,
// lifetime, and iteration order remain explicit and deterministic.
struct FVoxelSharedDensityGrid
{
FIntVector OriginVoxels = FIntVector::ZeroValue;
int32 Step = 1;
int32 Dim = 0;
TArray<float> Samples;
void Reset()
{
OriginVoxels = FIntVector::ZeroValue;
Step = 1;
Dim = 0;
Samples.Reset();
}
};
UCLASS(BlueprintType)
class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject
{
GENERATED_BODY()
public:
// Parse command-line switches on the module's game-thread startup. GenerateMesh is a worker
// entry point and therefore only reads the resulting values.
static void InitializeConsoleSwitches();
/**
* Génère le mesh d'une TUILE de clipmap (voir FVoxelTileKey).
*
* @param OriginVoxels - Coin min de la tuile en coords VOXEL (= Tile.OriginVoxels()).
* @param Step - Taille de cellule en voxels. La tuile couvre CellsPerAxis*Step voxels.
* @param CellsPerAxis - Nombre de cellules par axe. Les tuiles GROSSIÈRES en utilisent MOINS
* (gen moins chère, maillage plus grossier au loin) tout en couvrant la
* même étendue (extent = CellsPerAxis*Step). Niveau 0 = CHUNK_SIZE.
* @param OutCaptureGrid - CAPTURE-DURING-MESHING (optionnel). Si non-null ET CellsPerAxis==CHUNK_SIZE
* (tuile pleine résolution, Step==1<<Level, donc 1:1 avec une cellule du clipmap
* de densité), on y recopie les CHUNK_SIZE³ points intérieurs de la grille de
* densité déjà échantillonnée, quantifiés via VF_QuantizeDensity. Cela évite à
* UVoxelDensityVolume de re-sampler GetDensityAt pour ces cellules (le mesher
* 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. Le runtime ne passe
* jamais de bande aux tuiles LOD0, car elles possèdent la collision du pawn.
* 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,
TArray<uint8>* OutCaptureGrid = nullptr,
int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX,
int64* OutDensitySampleCount = nullptr,
FVoxelSharedDensityGrid* OutPreEditDensityGrid = nullptr,
const FVoxelSharedDensityGrid* InSharedDensityGrid = nullptr);
//=========================================================================
// SERVICES (injectés par AVoxelWorld)
//=========================================================================
// Le générateur fournit la densité par voxel.
UPROPERTY()
const UVoxelGenerator* Generator = nullptr;
void SetGenerator(const UVoxelGenerator* InGenerator) { Generator = InGenerator; }
// AVoxelWorld installs its bShuttingDown flag here. The commandlet path leaves it null.
// This is intentionally read-only: workers may observe shutdown, but never mutate world state.
void SetShutdownFlag(const std::atomic<bool>* InShutdownFlag) { ShutdownFlag = InShutdownFlag; }
// Optional run-scoped shared lattice. The caller must keep it alive and immutable until all
// GenerateMesh calls finish. A malformed/incomplete lattice is ignored and falls back to
// the canonical generator path rather than changing geometry.
void SetSharedDensityGrid(const FVoxelSharedDensityGrid* InGrid) { SharedDensityGrid = InGrid; }
private:
FORCEINLINE bool ShouldAbortWork() const
{
return ShutdownFlag && ShutdownFlag->load(std::memory_order_relaxed);
}
const std::atomic<bool>* ShutdownFlag = nullptr;
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
public:
//=========================================================================
// SETTINGS
//=========================================================================
// Seuil de l'isosurface dans le champ de densité.
// Convention MC: densité < IsoLevel = solide, >= = air.
float IsoLevel = 0.0f;
// SKIRTS — bouchent les fissures aux frontières de tuiles entre niveaux de clipmap voisins
// (résolutions différentes → les iso-surfaces ne se rejoignent pas exactement). Une jupe
// (mur court) est extrudée vers le solide depuis chaque arête de surface posée sur une des 6
// faces externes de la tuile. Voir GenerateMesh.
bool bGenerateSkirts = true;
// Profondeur de la jupe, en CELLULES de la tuile (× Step × VOXEL_SIZE). Four L0 cells preserve
// the 0.5 m default extrusion at 12.5 cm per voxel.
float SkirtCells = 4.0f;
// Conservative 8x8x8 block classifier. Uniform blocks skip the marching-cubes cell loop;
// when every relevant block has the same verdict, the density grid is skipped as well.
// ClassifyTile is proof-only, so no samples, resolution, or geometry are removed.
// The tile classifier is the cheap, whole-tile proof. Keep the optional 8^3-block refinement
// available for focused diagnostics, but leave it disabled in streaming: repeating a proof on
// every mixed tile can cost more than the density grid it is meant to avoid.
bool bUseBlockEarlyOut = false;
// 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;
};
@@ -0,0 +1,438 @@
// VoxelNoise.h
// Float, SIMD-batched gradient-noise core (T2.a). Replaces UE's double-precision
// FMath::PerlinNoise3D on the density hot path (~6.6 ms/chunk was all noise math).
//
// WHY THIS EXISTS
// FMath::PerlinNoise3D is double-precision with a permutation-table lookup. The
// density field calls it >1M times per surface chunk (fBm octaves + domain warps).
// This core is:
// - FLOAT (no double math),
// - table-free hash-gradient Perlin (pure arithmetic → vectorizes cleanly),
// - SIMD-BATCHED across fBm octaves: one FractalNoise/Ridged call evaluates up to
// 4 octaves' Perlin samples in one 4-wide SSE pass.
//
// It is a DIFFERENT noise field than FMath's, so worlds re-tune ONCE (accepted).
//
// DETERMINISM / CACHES
// Perlin3D is a pure function of (x,y,z) exactly like the old call — every box-validity
// cache (SDF §8.10, biome, surface column) stays valid. No invariant changes.
//
// SCALAR vs SIMD
// Perlin3D (scalar) and Perlin3D_x4 (SSE) use the IDENTICAL formula, op-for-op, so on
// x86 (SSE math == scalar-float math, same IEEE rounding) they produce bit-identical
// results. The SSE path is therefore a pure speedup with no second re-tune. If it ever
// fails to build on a given toolchain, force the scalar fallback with one line:
// #define VF_NOISE_USE_SIMD 0 // (before including this header, or here)
// The scalar path alone is still a large win over the old double-precision core.
//
// REQUIRES: SSE4.1 for the SIMD path (UE5 x64 baseline is SSE4.2 → always available).
#pragma once
#include "CoreMinimal.h"
#include "VoxelTypes.h"
#if !defined(VF_NOISE_USE_SIMD)
#if PLATFORM_ENABLE_VECTORINTRINSICS && PLATFORM_CPU_X86_FAMILY
#define VF_NOISE_USE_SIMD 1
#else
#define VF_NOISE_USE_SIMD 0
#endif
#endif
#if VF_NOISE_USE_SIMD
#include <immintrin.h> // SSE4.1: _mm_floor_ps / _mm_mullo_epi32 / _mm_blendv_ps
#endif
namespace VoxelNoise
{
namespace Detail
{
// Table-free integer hash of a lattice corner → gradient selector. Pure mul/xor/shift
// so it vectorizes 1:1 (see the SSE Hash lambda below — must stay in lock-step).
FORCEINLINE uint32 HashCorner(int32 ix, int32 iy, int32 iz)
{
uint32 h = (uint32)ix * 0x9E3779B1u
^ (uint32)iy * 0x85EBCA77u
^ (uint32)iz * 0xC2B2AE3Du;
h ^= h >> 15; h *= 0x2C1B3C6Du;
h ^= h >> 12; h *= 0x297A2D39u;
h ^= h >> 15;
return h;
}
// Quintic fade 6t^5-15t^4+10t^3, factored as t^3 * (t*(6t-15)+10) so the SSE twin
// can mirror the exact grouping.
FORCEINLINE float Fade(float t)
{
const float inner = t * (t * 6.0f - 15.0f) + 10.0f;
const float t3 = t * t * t;
return t3 * inner;
}
// Ken Perlin's 12-gradient dot (hash&15 picks the gradient). Branchy form here;
// the SSE twin reproduces it with selects.
FORCEINLINE float GradDot(uint32 hash, float x, float y, float z)
{
const uint32 h = hash & 15u;
const float u = (h & 8u) == 0u ? x : y;
float v;
if ((h & 12u) == 0u) v = y; // h < 4
else if ((h & 13u) == 12u) v = x; // h == 12 or 14
else v = z;
const float ru = (h & 1u) == 0u ? u : -u;
const float rv = (h & 2u) == 0u ? v : -v;
return ru + rv;
}
FORCEINLINE float Lerp(float a, float b, float t) { return a + t * (b - a); }
}
// Single 3D Perlin sample, ~[-1,1] (typically [-0.7,0.7], same character as the old core
// so VOXEL_NOISE_SCALE still applies). Used for all single-sample domain warps.
FORCEINLINE float Perlin3D(float x, float y, float z)
{
using namespace Detail;
const float xf = FMath::FloorToFloat(x);
const float yf = FMath::FloorToFloat(y);
const float zf = FMath::FloorToFloat(z);
const int32 X = (int32)xf, Y = (int32)yf, Z = (int32)zf;
const float fx = x - xf, fy = y - yf, fz = z - zf;
const float su = Fade(fx), sv = Fade(fy), sw = Fade(fz);
const uint32 h000 = HashCorner(X, Y, Z );
const uint32 h100 = HashCorner(X+1, Y, Z );
const uint32 h010 = HashCorner(X, Y+1, Z );
const uint32 h110 = HashCorner(X+1, Y+1, Z );
const uint32 h001 = HashCorner(X, Y, Z+1);
const uint32 h101 = HashCorner(X+1, Y, Z+1);
const uint32 h011 = HashCorner(X, Y+1, Z+1);
const uint32 h111 = HashCorner(X+1, Y+1, Z+1);
const float fx1 = fx - 1.0f, fy1 = fy - 1.0f, fz1 = fz - 1.0f;
const float n000 = GradDot(h000, fx, fy, fz );
const float n100 = GradDot(h100, fx1, fy, fz );
const float n010 = GradDot(h010, fx, fy1, fz );
const float n110 = GradDot(h110, fx1, fy1, fz );
const float n001 = GradDot(h001, fx, fy, fz1);
const float n101 = GradDot(h101, fx1, fy, fz1);
const float n011 = GradDot(h011, fx, fy1, fz1);
const float n111 = GradDot(h111, fx1, fy1, fz1);
const float x00 = Lerp(n000, n100, su);
const float x10 = Lerp(n010, n110, su);
const float x01 = Lerp(n001, n101, su);
const float x11 = Lerp(n011, n111, su);
const float y0 = Lerp(x00, x10, sv);
const float y1 = Lerp(x01, x11, sv);
return Lerp(y0, y1, sw);
}
FORCEINLINE float Perlin3D(const FVector& P)
{
return Perlin3D((float)P.X, (float)P.Y, (float)P.Z);
}
// Hot density callers already own float voxel coordinates. Keeping this overload separate from
// FVector avoids constructing UE5's double-precision FVector merely to round the coordinates back
// to float at the noise entry point. It is the same scalar formula and remains deterministic on
// every build of the module.
FORCEINLINE float Perlin3D(const FVector3f& P)
{
return Perlin3D(P.X, P.Y, P.Z);
}
//=============================================================================
// LIPSCHITZ BOUND FOR THE ACTUAL 3D FIELD
//=============================================================================
// This is deliberately derived from the implementation above, rather than borrowed from a
// textbook Perlin constant. GradDot's h&15 table has the twelve UNNORMALIZED gradients
//
// (±1,±1,0), (±1,0,±1), (0,±1,±1)
//
// (the four duplicate selectors do not add any vectors). For one partial derivative, write the
// four x-edges of the trilinear interpolation as independent gradient pairs. With
// d = Fade'(x) = 30 x²(1-x)², each edge contributes the support of the actual gradient set at
//
// c0 = (1-x-dx, -dy, -dz), c1 = (x-d(1-x), dy, dz).
//
// The support is max(|a|+|b|, |a|+|c|, |b|+|c|), because every allowed gradient has exactly two
// non-zero ±1 components. Taking the four y/z edge weights and the exact fade derivative, a
// finite case split over those three support branches has supremum 15/4 on [0,1]^3 (attained at
// x=y=z=1/2). This maximization permits every corner hash to choose any member of the actual
// set, so it is an upper bound for every hash cell; it is also the supremum of that finite support
// relaxation, not the looser 1 + 4*max(Fade') = 8.5 bound used by the unrelated 2D floor relief.
//
// The vector bound below follows the same per-axis-bound × sqrt(dimension) method used by the
// floor-relief proof. Abs is 1-Lipschitz and VOXEL_NOISE_SCALE is positive, so the scaled N1
// field has this bound in noise-space coordinates. WormBlockSkip computes its radius after the
// anisotropic world->noise transform, so the HBias and VerticalScale factors are included there.
static constexpr double Perlin3DPartialAbsBound = 15.0 / 4.0;
static constexpr double Perlin3DGradientBound =
Perlin3DPartialAbsBound * 1.732050807568877293527446341505872L;
static constexpr double WormN1NoiseSpaceLipschitzBound =
static_cast<double>(VOXEL_NOISE_SCALE) * Perlin3DGradientBound;
// A four-point side gives the natural small worker-local blocks for the mesher's 35-point
// (32 cells + one-sample normal halo) LOD0 lattice. The cache is only an optimization; a failed
// proof falls through to the original per-sample N1/N2 code.
static constexpr int32 WormBlockSampleSide = 4;
// The center value is evaluated with the same float Perlin expression as N1. The endpoint noise
// coordinates are already float results of monotone positive/negative affine transforms, so their
// min/max box contains every float lattice sample exactly. The 1e-3 scaled-N1 margin covers the
// remaining center-evaluation/interpolation rounding with substantial headroom; the proof remains
// strict because both the Lipschitz term and this margin are subtracted before comparing with T.
static constexpr double WormBlockOutputRoundMargin = 1.0e-3;
FORCEINLINE bool ProvesScaledAbsPerlin3DAbove(
const FVector3f& CenterNoisePosition,
const FVector3f& MinNoisePosition,
const FVector3f& MaxNoisePosition,
float Threshold,
double ScaledFieldLipschitzBound)
{
if (!FMath::IsFinite(CenterNoisePosition.X)
|| !FMath::IsFinite(CenterNoisePosition.Y)
|| !FMath::IsFinite(CenterNoisePosition.Z)
|| !FMath::IsFinite(MinNoisePosition.X)
|| !FMath::IsFinite(MinNoisePosition.Y)
|| !FMath::IsFinite(MinNoisePosition.Z)
|| !FMath::IsFinite(MaxNoisePosition.X)
|| !FMath::IsFinite(MaxNoisePosition.Y)
|| !FMath::IsFinite(MaxNoisePosition.Z)
|| !FMath::IsFinite(Threshold)
|| !(Threshold > 0.0f)
|| !FMath::IsFinite(ScaledFieldLipschitzBound)
|| !(ScaledFieldLipschitzBound > 0.0))
{
return false;
}
const double Dx = FMath::Max(
FMath::Abs(static_cast<double>(MinNoisePosition.X)
- static_cast<double>(CenterNoisePosition.X)),
FMath::Abs(static_cast<double>(MaxNoisePosition.X)
- static_cast<double>(CenterNoisePosition.X)));
const double Dy = FMath::Max(
FMath::Abs(static_cast<double>(MinNoisePosition.Y)
- static_cast<double>(CenterNoisePosition.Y)),
FMath::Abs(static_cast<double>(MaxNoisePosition.Y)
- static_cast<double>(CenterNoisePosition.Y)));
const double Dz = FMath::Max(
FMath::Abs(static_cast<double>(MinNoisePosition.Z)
- static_cast<double>(CenterNoisePosition.Z)),
FMath::Abs(static_cast<double>(MaxNoisePosition.Z)
- static_cast<double>(CenterNoisePosition.Z)));
const double Radius = FMath::Sqrt(Dx * Dx + Dy * Dy + Dz * Dz);
// Keep this operation in float and in the same order as the production N1 expression. The
// double conversion is only for the conservative comparison below.
const float CenterN1 = FMath::Abs(
Perlin3D(CenterNoisePosition) * VOXEL_NOISE_SCALE);
if (!FMath::IsFinite(CenterN1))
{
return false;
}
return static_cast<double>(CenterN1)
- ScaledFieldLipschitzBound * Radius
>= static_cast<double>(Threshold) + WormBlockOutputRoundMargin;
}
//=============================================================================
// 4-WIDE BATCH — the SIMD multiplier. Computes 4 independent Perlin samples.
// Inputs are 4-element arrays; unused lanes must be zero-filled by the caller
// (FBM/Ridged below do). Results written to Out[0..3].
//=============================================================================
#if VF_NOISE_USE_SIMD
FORCEINLINE void Perlin3D_x4(const float* Xs, const float* Ys, const float* Zs, float* Out)
{
const __m128 x = _mm_loadu_ps(Xs);
const __m128 y = _mm_loadu_ps(Ys);
const __m128 z = _mm_loadu_ps(Zs);
const __m128 xf = _mm_floor_ps(x);
const __m128 yf = _mm_floor_ps(y);
const __m128 zf = _mm_floor_ps(z);
const __m128i X = _mm_cvttps_epi32(xf);
const __m128i Y = _mm_cvttps_epi32(yf);
const __m128i Z = _mm_cvttps_epi32(zf);
const __m128 fx = _mm_sub_ps(x, xf);
const __m128 fy = _mm_sub_ps(y, yf);
const __m128 fz = _mm_sub_ps(z, zf);
const __m128 c6 = _mm_set1_ps(6.0f);
const __m128 c15 = _mm_set1_ps(15.0f);
const __m128 c10 = _mm_set1_ps(10.0f);
auto Fade = [&](const __m128 t) -> __m128
{
const __m128 inner = _mm_add_ps(_mm_mul_ps(t, _mm_sub_ps(_mm_mul_ps(t, c6), c15)), c10);
const __m128 t3 = _mm_mul_ps(_mm_mul_ps(t, t), t);
return _mm_mul_ps(t3, inner);
};
const __m128 su = Fade(fx);
const __m128 sv = Fade(fy);
const __m128 sw = Fade(fz);
const __m128i one = _mm_set1_epi32(1);
const __m128i X1 = _mm_add_epi32(X, one);
const __m128i Y1 = _mm_add_epi32(Y, one);
const __m128i Z1 = _mm_add_epi32(Z, one);
const __m128i k1 = _mm_set1_epi32((int32)0x9E3779B1u);
const __m128i k2 = _mm_set1_epi32((int32)0x85EBCA77u);
const __m128i k3 = _mm_set1_epi32((int32)0xC2B2AE3Du);
const __m128i m1 = _mm_set1_epi32((int32)0x2C1B3C6Du);
const __m128i m2 = _mm_set1_epi32((int32)0x297A2D39u);
auto Hash = [&](const __m128i ix, const __m128i iy, const __m128i iz) -> __m128i
{
__m128i h = _mm_xor_si128(_mm_xor_si128(_mm_mullo_epi32(ix, k1),
_mm_mullo_epi32(iy, k2)),
_mm_mullo_epi32(iz, k3));
h = _mm_xor_si128(h, _mm_srli_epi32(h, 15)); h = _mm_mullo_epi32(h, m1);
h = _mm_xor_si128(h, _mm_srli_epi32(h, 12)); h = _mm_mullo_epi32(h, m2);
h = _mm_xor_si128(h, _mm_srli_epi32(h, 15));
return h;
};
const __m128 fx1 = _mm_sub_ps(fx, _mm_set1_ps(1.0f));
const __m128 fy1 = _mm_sub_ps(fy, _mm_set1_ps(1.0f));
const __m128 fz1 = _mm_sub_ps(fz, _mm_set1_ps(1.0f));
const __m128i i8 = _mm_set1_epi32(8);
const __m128i i12 = _mm_set1_epi32(12);
const __m128i i13 = _mm_set1_epi32(13);
const __m128i i15 = _mm_set1_epi32(15);
const __m128i i1 = _mm_set1_epi32(1);
const __m128i i2 = _mm_set1_epi32(2);
const __m128i izero = _mm_setzero_si128();
const __m128 sgn = _mm_set1_ps(-0.0f);
auto Grad = [&](const __m128i hash, const __m128 gx, const __m128 gy, const __m128 gz) -> __m128
{
const __m128i h = _mm_and_si128(hash, i15);
// u = (h&8)==0 ? gx : gy
const __m128 mU = _mm_castsi128_ps(_mm_cmpeq_epi32(_mm_and_si128(h, i8), izero));
const __m128 u = _mm_blendv_ps(gy, gx, mU);
// v = (h&12)==0 ? gy : ((h&13)==12 ? gx : gz)
const __m128 mLt4 = _mm_castsi128_ps(_mm_cmpeq_epi32(_mm_and_si128(h, i12), izero));
const __m128 m1214 = _mm_castsi128_ps(_mm_cmpeq_epi32(_mm_and_si128(h, i13), i12));
const __m128 vTmp = _mm_blendv_ps(gz, gx, m1214);
const __m128 v = _mm_blendv_ps(vTmp, gy, mLt4);
// signs: (h&1)? -u:u + (h&2)? -v:v
const __m128 mNegU = _mm_castsi128_ps(_mm_cmpeq_epi32(_mm_and_si128(h, i1), i1));
const __m128 mNegV = _mm_castsi128_ps(_mm_cmpeq_epi32(_mm_and_si128(h, i2), i2));
const __m128 ru = _mm_blendv_ps(u, _mm_xor_ps(u, sgn), mNegU);
const __m128 rv = _mm_blendv_ps(v, _mm_xor_ps(v, sgn), mNegV);
return _mm_add_ps(ru, rv);
};
const __m128 n000 = Grad(Hash(X, Y, Z ), fx, fy, fz );
const __m128 n100 = Grad(Hash(X1, Y, Z ), fx1, fy, fz );
const __m128 n010 = Grad(Hash(X, Y1, Z ), fx, fy1, fz );
const __m128 n110 = Grad(Hash(X1, Y1, Z ), fx1, fy1, fz );
const __m128 n001 = Grad(Hash(X, Y, Z1), fx, fy, fz1);
const __m128 n101 = Grad(Hash(X1, Y, Z1), fx1, fy, fz1);
const __m128 n011 = Grad(Hash(X, Y1, Z1), fx, fy1, fz1);
const __m128 n111 = Grad(Hash(X1, Y1, Z1), fx1, fy1, fz1);
auto Lerp = [&](const __m128 a, const __m128 b, const __m128 t) -> __m128
{
return _mm_add_ps(a, _mm_mul_ps(t, _mm_sub_ps(b, a)));
};
const __m128 x00 = Lerp(n000, n100, su);
const __m128 x10 = Lerp(n010, n110, su);
const __m128 x01 = Lerp(n001, n101, su);
const __m128 x11 = Lerp(n011, n111, su);
const __m128 y0 = Lerp(x00, x10, sv);
const __m128 y1 = Lerp(x01, x11, sv);
_mm_storeu_ps(Out, Lerp(y0, y1, sw));
}
#else
FORCEINLINE void Perlin3D_x4(const float* Xs, const float* Ys, const float* Zs, float* Out)
{
for (int32 i = 0; i < 4; ++i) Out[i] = Perlin3D(Xs[i], Ys[i], Zs[i]);
}
#endif
// Three independent channels are the hot cave-warp shape. On x86, evaluate them as three lanes
// of the existing four-wide implementation; the unused fourth lane is deliberate. The scalar
// fallback keeps the exact three-call behavior on platforms without the SIMD path. This helper is
// the owner of the cave-warp channel evaluation so generator, op-stack, and room-fit callers cannot
// drift into different noise formulas or channel offsets.
FORCEINLINE void Perlin3D_x3(
float X0, float Y0, float Z0,
float X1, float Y1, float Z1,
float X2, float Y2, float Z2,
float& Out0, float& Out1, float& Out2)
{
#if VF_NOISE_USE_SIMD
const float Xs[4] = { X0, X1, X2, X0 };
const float Ys[4] = { Y0, Y1, Y2, Y0 };
const float Zs[4] = { Z0, Z1, Z2, Z0 };
float Out[4];
Perlin3D_x4(Xs, Ys, Zs, Out);
Out0 = Out[0];
Out1 = Out[1];
Out2 = Out[2];
#else
Out0 = Perlin3D(X0, Y0, Z0);
Out1 = Perlin3D(X1, Y1, Z1);
Out2 = Perlin3D(X2, Y2, Z2);
#endif
}
//=============================================================================
// fBm / Ridged — octaves evaluated 4 at a time through Perlin3D_x4.
// Accumulation stays scalar in octave order, so the result is independent of
// whether the SIMD or scalar Perlin3D_x4 is used (bit-identical either way).
//=============================================================================
FORCEINLINE float FBM(float x, float y, float z,
int32 Octaves = 4, float Lacunarity = 2.0f, float Persistence = 0.5f)
{
float Total = 0.0f, MaxValue = 0.0f, Freq = 1.0f, Amp = 1.0f;
for (int32 o = 0; o < Octaves; )
{
const int32 N = FMath::Min(4, Octaves - o);
float Xs[4] = {}, Ys[4] = {}, Zs[4] = {}, Out[4];
float f = Freq;
for (int32 i = 0; i < N; ++i) { Xs[i] = x * f; Ys[i] = y * f; Zs[i] = z * f; f *= Lacunarity; }
Perlin3D_x4(Xs, Ys, Zs, Out);
for (int32 i = 0; i < N; ++i) { Total += Out[i] * Amp; MaxValue += Amp; Amp *= Persistence; }
Freq = f;
o += N;
}
return Total / MaxValue;
}
FORCEINLINE float Ridged(float x, float y, float z,
int32 Octaves = 4, float Lacunarity = 2.0f, float Persistence = 0.5f)
{
// Matches the original RidgedNoise3D fold exactly (NS scale, square, weight feedback).
static constexpr float NS = 1.25f;
float Total = 0.0f, MaxValue = 0.0f, Freq = 1.0f, Amp = 1.0f, Weight = 1.0f;
for (int32 o = 0; o < Octaves; )
{
const int32 N = FMath::Min(4, Octaves - o);
float Xs[4] = {}, Ys[4] = {}, Zs[4] = {}, Out[4];
float f = Freq;
for (int32 i = 0; i < N; ++i) { Xs[i] = x * f; Ys[i] = y * f; Zs[i] = z * f; f *= Lacunarity; }
Perlin3D_x4(Xs, Ys, Zs, Out);
for (int32 i = 0; i < N; ++i)
{
float Nn = Out[i] * NS;
Nn = 1.0f - FMath::Abs(Nn); // fold → ridge at zero-crossings
Nn = Nn * Nn; // sharpen
Nn *= Weight; // detail follows previous ridge
Weight = FMath::Clamp(Nn * 2.0f, 0.0f, 1.0f);
Total += Nn * Amp; MaxValue += Amp; Amp *= Persistence;
}
Freq = f;
o += N;
}
return (Total / MaxValue) * 2.0f - 1.0f;
}
} // namespace VoxelNoise
@@ -0,0 +1,711 @@
// Shared dimensions and construction laws for walkable passages and origin landings.
//
// This header is deliberately free of UObjects. The density path, the operator stack, the
// passage generator, and the headless measurements all include the same arithmetic so a geometry
// change cannot leave one of those paths with a different room or slope contract.
#pragma once
#include "CoreMinimal.h"
#include "VoxelTypes.h"
namespace VoxelPassageGeometry
{
// Authored scale and player body used by the §6.5 landing calculation.
constexpr float VoxelSizeMeters = VOXEL_SIZE / 100.0f;
constexpr float MetersToVoxels(float Meters)
{
return Meters / VoxelSizeMeters;
}
constexpr float PlayerRadiusVoxels = MetersToVoxels(0.34f);
constexpr float PlayerHalfHeightVoxels = MetersToVoxels(0.88f);
constexpr float PlayerHeightVoxels = 2.0f * PlayerHalfHeightVoxels;
constexpr float HeadroomVoxels = MetersToVoxels(1.0f);
constexpr float MinimumTurnFloorWidthVoxels = MetersToVoxels(3.0f);
constexpr float MinimumLandingHalfWidthVoxels =
MinimumTurnFloorWidthVoxels * 0.5f;
constexpr float MinimumRoomHeightVoxels = MetersToVoxels(3.0f);
constexpr float LandingFloorThicknessVoxels = MetersToVoxels(0.75f);
constexpr float MaxStepHeightVoxels = MetersToVoxels(0.45f);
// The A-to-B walk corridor is budgeted as a full player-width rectangle. Its vertical
// envelope starts above the legal step and extends one player height above that raised floor.
constexpr float WalkCorridorPlayerWidthVoxels = MetersToVoxels(0.68f);
constexpr float WalkCorridorPlayerHeightVoxels = MetersToVoxels(1.76f);
constexpr float WalkCorridorLatticeMarginVoxels = MetersToVoxels(0.25f);
constexpr float SealSafetyMarginVoxels = MetersToVoxels(1.0f);
constexpr float RoomRoundingVoxels = MetersToVoxels(0.375f);
constexpr float LandingCarveBlendVoxels = MetersToVoxels(1.0f);
// The origin landing is reserved for the same straight spine that the vertical-shaft
// generator uses: OriginSpineRadius on each side, with a six-voxel / 0.75 m floor ledge
// around it after the support patch's two-voxel inset. At the default radius (28 voxels) this
// is a 28-voxel / 7 m future shaft diameter.
constexpr float OriginShaftFloorClearanceVoxels = MetersToVoxels(1.0f);
// Level tunnel apron outside each room-floor edge. It covers the room/tunnel SDF blend and
// keeps the ramp from starting while the route is still over the room's square support patch.
constexpr float WalkableTunnelLandingApronVoxels = MetersToVoxels(1.5f);
// A level transition after a bend lets the route turn before its sloped segment begins.
constexpr float WalkableTunnelTurnTransitionVoxels = MetersToVoxels(1.5f);
// Keep the tunnel-air post 12.5 cm above the authored support plane. This is a numerical
// ownership band: the floor writer owns the plane, while the air writer owns the capsule
// volume above it even when two overlapping passage projections differ by a fraction.
constexpr float WalkableTunnelFloorAirClearanceVoxels = MetersToVoxels(0.125f);
// A 15 degree ramp is comfortably below the 44 degree engine walkability ceiling. The
// latter is a scramble limit, not a sensible default for a tunnel the player walks through.
constexpr float WalkableTunnelMaxGradientDegrees = 15.0f;
constexpr float WalkableTunnelMaxGradient = 0.2679491924311227f; // tan(15 degrees)
// Player-fit measurements use the engine's 44 degree walkability contract. The 15 degree
// value above remains the preferred authored tunnel ramp; this wider bound is only the
// fallback threshold at which a room-to-room floor must become a terrace.
constexpr float PlayerWalkableFloorMaxGradient = 0.9656887748070738f; // tan(44 degrees)
// TunnelFloorGentleSlopeThreshold is authored as degrees. Zero is still a legal UI value,
// but the construction path uses this small positive floor so a malformed/overly strict
// value cannot turn the graph collect window into an unbounded switchback search.
constexpr float TunnelFloorGentleSlopeMinimumDegrees = 1.0f;
constexpr float TunnelFloorGentleSlopeMaximumDegrees = 90.0f;
// The route solver uses at most six half-waves. The canonical tunnel envelope is the default
// 360-voxel tunnel reach plus its endpoint correction; round that direct-run envelope up to
// 640 voxels, then derive the total cap from the same wave limit. This leaves room above the
// proven canonical route while keeping the 0.1-degree collect window finite and bounded.
constexpr int32 TunnelFloorMaximumWindWaves = 6;
constexpr float TunnelFloorMaximumSingleWaveRunVoxels = MetersToVoxels(160.0f);
constexpr float TunnelFloorMaximumRouteLengthVoxels =
static_cast<float>(TunnelFloorMaximumWindWaves)
* TunnelFloorMaximumSingleWaveRunVoxels;
FORCEINLINE float EffectiveTunnelFloorGentleSlopeDegrees(float AuthoredDegrees)
{
if (!VoxelMath::IsFinite(AuthoredDegrees))
{
return 44.0f;
}
return FMath::Clamp(
FMath::Max(AuthoredDegrees, TunnelFloorGentleSlopeMinimumDegrees),
TunnelFloorGentleSlopeMinimumDegrees,
TunnelFloorGentleSlopeMaximumDegrees);
}
FORCEINLINE float TunnelFloorGradientFromDegrees(float AuthoredDegrees)
{
const float Degrees = EffectiveTunnelFloorGentleSlopeDegrees(AuthoredDegrees);
float SinValue = 0.0f;
float CosValue = 0.0f;
VoxelMath::DetSinCos(
SinValue, CosValue, Degrees * (PI / 180.0f));
// At 90 degrees the mathematical tangent is unbounded. The route solver only needs a
// finite comparison, so preserve the ordering while keeping every downstream bound finite.
return FMath::Min(
FMath::Abs(SinValue) / FMath::Max(FMath::Abs(CosValue), 1.0e-3f),
4096.0f);
}
// The walkability contract is about the floor carried by the tube, not only its centreline.
// A tapered tube changes that floor by the radius delta, so the construction and the audit
// use this same conservative floor delta.
FORCEINLINE float TunnelFloorZ(const FVector& Centreline, float Radius)
{
return Centreline.Z - FMath::Abs(Radius);
}
FORCEINLINE float NativePassageFloorSupportRadius(float StartRadius, float EndRadius)
{
return FMath::Max(
FMath::Min(FMath::Abs(StartRadius), FMath::Abs(EndRadius))
- MetersToVoxels(0.125f),
PlayerRadiusVoxels);
}
// Keep two switchback floor branches apart far enough that the upper branch's final floor
// post cannot enter the lower player's full-width walk corridor. The 0.25 m margin covers
// the scalar lattice/interpolation boundary; Eval displacement is already followed by the
// MC-facing tunnel and native-floor stages before collision density is returned.
FORCEINLINE float WalkableTunnelTurnLegSeparation(
float StartRadius, float EndRadius)
{
return NativePassageFloorSupportRadius(StartRadius, EndRadius)
+ 0.5f * WalkCorridorPlayerWidthVoxels
+ WalkCorridorLatticeMarginVoxels;
}
// Vertical-shaft tree links are round air capsules, but the player-fit graph needs a
// deterministic support plane to walk between shafts. Keep that plane on the first
// seal-safe ledge shared by the whole strate, so every tree edge has the same walk level.
// This is shaft-network support, not an origin landing connector.
FORCEINLINE float VerticalShaftConnectorFloorThickness(float Radius)
{
return FMath::Min(
LandingFloorThicknessVoxels,
FMath::Max(MetersToVoxels(0.125f), FMath::Abs(Radius) * 0.5f));
}
FORCEINLINE float VerticalShaftConnectorFloorZ(
float StrateBottomZ,
float InteriorBottomZ,
float InteriorTopZ,
float LedgeSpacing,
float LedgeDepth,
float ConnectorRadius)
{
const float FloorThickness = VerticalShaftConnectorFloorThickness(ConnectorRadius);
const float MinimumFloor = InteriorBottomZ
+ FloorThickness + SealSafetyMarginVoxels;
const float MaximumFloor = InteriorTopZ
- PlayerHeightVoxels - SealSafetyMarginVoxels;
if (!VoxelMath::IsFinite(StrateBottomZ)
|| !VoxelMath::IsFinite(InteriorBottomZ)
|| !VoxelMath::IsFinite(InteriorTopZ)
|| !VoxelMath::IsFinite(LedgeSpacing)
|| !VoxelMath::IsFinite(LedgeDepth)
|| !VoxelMath::IsFinite(MinimumFloor)
|| !VoxelMath::IsFinite(MaximumFloor)
|| !(MaximumFloor > MinimumFloor))
{
return 0.5f * (InteriorBottomZ + InteriorTopZ);
}
if (LedgeSpacing > 0.0f && LedgeDepth > 0.0f)
{
const float RelativeBottom = InteriorBottomZ - StrateBottomZ;
const int32 FirstPeriod = FMath::FloorToInt(RelativeBottom / LedgeSpacing);
// The small fixed bound keeps malformed authored values from turning generation into
// an unbounded search. Normal shaft strates find the first or second period.
constexpr int32 MaxPeriods = 64;
for (int32 PeriodOffset = 0; PeriodOffset < MaxPeriods; ++PeriodOffset)
{
const float PeriodStart = static_cast<float>(FirstPeriod + PeriodOffset)
* LedgeSpacing;
const float Candidate = StrateBottomZ + PeriodStart + LedgeDepth;
if (Candidate > MinimumFloor && Candidate < MaximumFloor)
{
return Candidate;
}
}
}
return FMath::Clamp(
0.5f * (InteriorBottomZ + InteriorTopZ), MinimumFloor, MaximumFloor);
}
FORCEINLINE float VerticalShaftConnectorCenterZ(
float StrateBottomZ,
float InteriorBottomZ,
float InteriorTopZ,
float LedgeSpacing,
float LedgeDepth,
float ConnectorRadius)
{
const float SafeRadius = FMath::Abs(ConnectorRadius);
const float FloorThickness = VerticalShaftConnectorFloorThickness(SafeRadius);
return VerticalShaftConnectorFloorZ(
StrateBottomZ, InteriorBottomZ, InteriorTopZ,
LedgeSpacing, LedgeDepth, SafeRadius)
+ SafeRadius - FloorThickness;
}
/** Add a flat, inset support band below one horizontal shaft-tree capsule. */
FORCEINLINE void VF_ApplyVerticalShaftConnectorFloor(
float& Density,
float WorldX,
float WorldY,
float WorldZ,
const FVector& Start,
const FVector& End,
float Radius,
float FloorZ,
float BaseDensity)
{
const float SafeRadius = FMath::Abs(Radius);
const float FloorThickness = VerticalShaftConnectorFloorThickness(SafeRadius);
const float FloorRadius = FMath::Max(
MetersToVoxels(0.125f), SafeRadius - MetersToVoxels(0.125f));
if (!(SafeRadius > 0.0f)
|| !(BaseDensity > 0.0f)
|| !VoxelMath::IsFinite(WorldX)
|| !VoxelMath::IsFinite(WorldY)
|| !VoxelMath::IsFinite(WorldZ)
|| !VoxelMath::IsFinite(FloorZ)
|| !VoxelMath::IsFinite(Start.X) || !VoxelMath::IsFinite(Start.Y)
|| !VoxelMath::IsFinite(End.X) || !VoxelMath::IsFinite(End.Y))
{
return;
}
const FVector2D A(Start.X, Start.Y);
const FVector2D Delta(End.X - Start.X, End.Y - Start.Y);
const float LengthSquared = Delta.SizeSquared();
const float T = LengthSquared > KINDA_SMALL_NUMBER
? FMath::Clamp(
FVector2D::DotProduct(FVector2D(WorldX, WorldY) - A, Delta)
/ LengthSquared,
0.0f, 1.0f)
: 0.0f;
const FVector2D Closest = A + Delta * T;
if ((FVector2D(WorldX, WorldY) - Closest).SizeSquared()
> FMath::Square(FloorRadius))
{
return;
}
if (WorldZ <= FloorZ + KINDA_SMALL_NUMBER
&& WorldZ > FloorZ - FloorThickness)
{
Density = FMath::Max(Density, BaseDensity);
}
}
// ApplyVerticalShaftConnectorAir runs before the generator's shared disturbance pass. Keep
// this one-bit, call-local hand-off so a bridge/ridge cannot refill the core it just opened.
// It is thread-local state, never a per-voxel cache, and is reset around every density query.
FORCEINLINE bool& VerticalShaftConnectorAirMarker()
{
static thread_local bool bMarked = false;
return bMarked;
}
FORCEINLINE void ResetVerticalShaftConnectorAirMarker()
{
VerticalShaftConnectorAirMarker() = false;
}
/** Reassert the walkable air core above one horizontal shaft-tree floor band. */
FORCEINLINE void VF_ApplyVerticalShaftConnectorAir(
float& Density,
float WorldX,
float WorldY,
float WorldZ,
const FVector& Start,
const FVector& End,
float Radius,
float FloorZ,
float BaseDensity)
{
const float SafeRadius = FMath::Abs(Radius);
// Leave a one-voxel wall band for the roughness/solid shell, while retaining more than
// enough width for the 1.36-voxel player capsule and its support stencil.
const float CoreRadius = FMath::Max(
MetersToVoxels(0.125f), SafeRadius - MetersToVoxels(0.25f));
if (!(SafeRadius > 0.0f)
|| !(BaseDensity > 0.0f)
|| !VoxelMath::IsFinite(WorldX)
|| !VoxelMath::IsFinite(WorldY)
|| !VoxelMath::IsFinite(WorldZ)
|| !VoxelMath::IsFinite(FloorZ)
|| !VoxelMath::IsFinite(Start.X) || !VoxelMath::IsFinite(Start.Y)
|| !VoxelMath::IsFinite(Start.Z)
|| !VoxelMath::IsFinite(End.X) || !VoxelMath::IsFinite(End.Y)
|| !VoxelMath::IsFinite(End.Z))
{
return;
}
// The exact floor sample belongs to the air core's lower ownership edge. The floor
// writer only owns the three voxels below FloorZ, so clearing from the half-voxel band
// above it cannot remove support.
if (WorldZ + KINDA_SMALL_NUMBER
< FloorZ + WalkableTunnelFloorAirClearanceVoxels)
{
return;
}
const FVector Delta = End - Start;
const float LengthSquared = Delta.SizeSquared();
const float T = LengthSquared > KINDA_SMALL_NUMBER
? FMath::Clamp(FVector::DotProduct(FVector(WorldX, WorldY, WorldZ) - Start, Delta)
/ LengthSquared, 0.0f, 1.0f)
: 0.0f;
const FVector Closest = Start + Delta * T;
if ((FVector(WorldX, WorldY, WorldZ) - Closest).SizeSquared()
> FMath::Square(CoreRadius))
{
return;
}
// Internal convention: negative is air, positive is solid. This is intentionally a
// post-ledge write; a partial shaft shelf must not cap the tree route's player-height
// envelope after the capsule has already carved it.
VerticalShaftConnectorAirMarker() = true;
Density = FMath::Min(Density, -BaseDensity);
}
FORCEINLINE float TunnelFloorGradient(
const FVector& Start, float StartRadius,
const FVector& End, float EndRadius)
{
const float HorizontalRun = FVector2D(
End.X - Start.X, End.Y - Start.Y).Size();
if (HorizontalRun <= KINDA_SMALL_NUMBER)
{
return FMath::Abs(TunnelFloorZ(End, EndRadius)
- TunnelFloorZ(Start, StartRadius)) > KINDA_SMALL_NUMBER
? FLT_MAX : 0.0f;
}
return FMath::Abs(TunnelFloorZ(End, EndRadius)
- TunnelFloorZ(Start, StartRadius)) / HorizontalRun;
}
FORCEINLINE float RequiredHorizontalRunForFloorDrop(
float FirstFloorDrop, float SecondFloorDrop)
{
return 2.0f * FMath::Max(
FMath::Abs(FirstFloorDrop), FMath::Abs(SecondFloorDrop))
/ WalkableTunnelMaxGradient;
}
/** Project a query into the nearest horizontal control segment of a walkable tunnel. */
FORCEINLINE bool ProjectWalkableTunnelFloor(
const TArray<FVector>& ControlPoints,
const TArray<float>& ControlRadii,
const FVector& Position,
float& OutFloorZ,
float& OutSupportRadius,
int32* OutSegmentIndex = nullptr)
{
if (OutSegmentIndex != nullptr)
{
*OutSegmentIndex = INDEX_NONE;
}
if (ControlPoints.Num() < 2
|| ControlRadii.Num() != ControlPoints.Num())
{
return false;
}
const double QueryX = Position.X;
const double QueryY = Position.Y;
const int32 ControlPointCount = ControlPoints.Num();
const FVector* ControlPointData = ControlPoints.GetData();
const float* ControlRadiusData = ControlRadii.GetData();
float BestDistanceSquared = FLT_MAX;
float BestHorizontalDistanceSquared = FLT_MAX;
bool bFoundSegment = false;
for (int32 SegmentIndex = 0;
SegmentIndex + 1 < ControlPointCount;
++SegmentIndex)
{
const FVector& A = ControlPointData[SegmentIndex];
const FVector& B = ControlPointData[SegmentIndex + 1];
const double AXY_X = A.X;
const double AXY_Y = A.Y;
const double DeltaX = B.X - AXY_X;
const double DeltaY = B.Y - AXY_Y;
const float LengthSquared = static_cast<float>(
DeltaX * DeltaX + DeltaY * DeltaY);
if (LengthSquared <= KINDA_SMALL_NUMBER)
{
continue;
}
const double HorizontalDot = (QueryX - AXY_X) * DeltaX
+ (QueryY - AXY_Y) * DeltaY;
const float HorizontalT = FMath::Clamp(
HorizontalDot / LengthSquared,
0.0f, 1.0f);
const double ClosestX = AXY_X + DeltaX * HorizontalT;
const double ClosestY = AXY_Y + DeltaY * HorizontalT;
const double DistanceX = QueryX - ClosestX;
const double DistanceY = QueryY - ClosestY;
const float HorizontalDistanceSquared = static_cast<float>(
DistanceX * DistanceX + DistanceY * DistanceY);
const float StartRadius = FMath::Abs(ControlRadiusData[SegmentIndex]);
const float EndRadius = FMath::Abs(ControlRadiusData[SegmentIndex + 1]);
const float StartFloorZ = TunnelFloorZ(A, StartRadius);
const float EndFloorZ = TunnelFloorZ(B, EndRadius);
const double FloorDeltaZ = static_cast<double>(EndFloorZ - StartFloorZ);
const double FullLengthSquared =
LengthSquared + FloorDeltaZ * FloorDeltaZ;
if (!(FullLengthSquared > KINDA_SMALL_NUMBER))
{
continue;
}
// A switchback may place two different floors at the same XY. Select the segment
// nearest to the complete query point on the floor curve, so a lower passage leg
// cannot inherit the floor height of a vertically separated upper leg. Keep floor
// interpolation horizontal after choosing the branch: FloorZ remains a function of XY.
const double FullDot = HorizontalDot
+ (static_cast<double>(Position.Z) - StartFloorZ) * FloorDeltaZ;
const float SpatialT = FMath::Clamp(
FullDot / FullLengthSquared,
0.0f, 1.0f);
const double SpatialX = AXY_X + DeltaX * SpatialT;
const double SpatialY = AXY_Y + DeltaY * SpatialT;
const double SpatialZ = StartFloorZ + FloorDeltaZ * SpatialT;
const double SpatialDistanceX = QueryX - SpatialX;
const double SpatialDistanceY = QueryY - SpatialY;
const double SpatialDistanceZ = Position.Z - SpatialZ;
const float DistanceSquared = static_cast<float>(
SpatialDistanceX * SpatialDistanceX
+ SpatialDistanceY * SpatialDistanceY
+ SpatialDistanceZ * SpatialDistanceZ);
if (DistanceSquared >= BestDistanceSquared)
{
continue;
}
OutFloorZ = FMath::Lerp(
StartFloorZ,
EndFloorZ,
HorizontalT);
OutSupportRadius = FMath::Max(
FMath::Min(StartRadius, EndRadius) - MetersToVoxels(0.125f),
PlayerRadiusVoxels);
if (OutSegmentIndex != nullptr)
{
*OutSegmentIndex = SegmentIndex;
}
BestDistanceSquared = DistanceSquared;
BestHorizontalDistanceSquared = HorizontalDistanceSquared;
bFoundSegment = true;
}
return bFoundSegment
&& VoxelMath::IsFinite(OutFloorZ)
&& VoxelMath::IsFinite(OutSupportRadius)
&& OutSupportRadius > 0.0f
&& BestHorizontalDistanceSquared <= FMath::Square(OutSupportRadius);
}
struct FOriginLandingGeometry
{
bool bValid = false;
float FloorZ = 0.0f;
float CeilingZ = 0.0f;
float HalfWidth = 0.0f;
float FloorThickness = LandingFloorThicknessVoxels;
};
FORCEINLINE float LandingHalfWidthForRadius(float Radius)
{
const float SafeRadius = FMath::Max(
FMath::Abs(Radius), MetersToVoxels(0.25f));
return FMath::Max(MinimumLandingHalfWidthVoxels,
SafeRadius + MetersToVoxels(0.5f));
}
FORCEINLINE float LandingHeightForRadius(float Radius)
{
const float SafeRadius = FMath::Max(
FMath::Abs(Radius), MetersToVoxels(0.25f));
return FMath::Max(
FMath::Max(MinimumRoomHeightVoxels, PlayerHeightVoxels + HeadroomVoxels),
2.0f * SafeRadius + MetersToVoxels(0.5f));
}
/**
* Build the one origin landing in a strate.
*
* `OriginRadius` is the old spine setting and becomes the room's authored mouth radius. The
* room dimensions therefore reuse the §6.5 formula exactly:
* half-width = max(1.5 m, radius + 0.5 m),
* height = max(3 m, capsule height + 1 m, 2 * radius + 0.5 m).
* The floor is placed against the top of the safe interior interval. This makes the first
* landing the place reached from above, while every lower landing remains sealed above until
* progression opens its inter-strate passage.
*/
FORCEINLINE FOriginLandingGeometry BuildOriginLandingGeometry(
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
{
FOriginLandingGeometry Result;
if (!(OriginRadius > 0.0f)
|| !VoxelMath::IsFinite(StrateTopZ)
|| !VoxelMath::IsFinite(StrateBottomZ)
|| !VoxelMath::IsFinite(SealThickness)
|| !VoxelMath::IsFinite(OriginRadius))
{
return Result;
}
// Keep a solid walking ledge around the future shaft, not merely a room whose wall is
// tangent to it. The shaft diameter is 2 * OriginRadius; the eight-voxel / 1 m extra
// leaves six voxels / 0.75 m of floor after the support patch's two-voxel / 0.25 m inset.
Result.HalfWidth = FMath::Max(
LandingHalfWidthForRadius(OriginRadius),
FMath::Max(FMath::Abs(OriginRadius), MetersToVoxels(0.25f))
+ OriginShaftFloorClearanceVoxels);
const float DesiredHeight = LandingHeightForRadius(OriginRadius);
const float SafeSeal = FMath::Max(SealThickness, 0.0f);
const float InnerBottom = StrateBottomZ + SafeSeal;
const float InnerTop = StrateTopZ - SafeSeal;
const float MinimumFloor = InnerBottom
+ Result.FloorThickness + SealSafetyMarginVoxels;
const float MaximumFloor = InnerTop
- DesiredHeight - SealSafetyMarginVoxels;
if (!VoxelMath::IsFinite(InnerBottom) || !VoxelMath::IsFinite(InnerTop)
|| !(InnerTop > InnerBottom)
|| !VoxelMath::IsFinite(MinimumFloor)
|| !VoxelMath::IsFinite(MaximumFloor)
|| MaximumFloor < MinimumFloor)
{
Result.HalfWidth = 0.0f;
return Result;
}
Result.FloorZ = MaximumFloor;
Result.CeilingZ = Result.FloorZ + DesiredHeight;
Result.bValid = true;
return Result;
}
FORCEINLINE bool IntersectsClosedInterval(float MinA, float MaxA,
float MinB, float MaxB)
{
return MaxA >= MinB && MinA <= MaxB;
}
/** True when the exact MC lattice has at least one sample in a closed interval. */
FORCEINLINE bool LatticeAxisHasSampleInInterval(
float Min, float Max, float Origin, int32 Step)
{
if (Step <= 0 || !VoxelMath::IsFinite(Min) || !VoxelMath::IsFinite(Max)
|| !VoxelMath::IsFinite(Origin) || Min > Max)
{
return false;
}
const float InvStep = 1.0f / static_cast<float>(Step);
const int32 First = FMath::CeilToInt(
(Min - Origin) * InvStep - 1.0e-4f);
const int32 Last = FMath::FloorToInt(
(Max - Origin) * InvStep + 1.0e-4f);
return First <= Last;
}
FORCEINLINE bool OriginLandingRoomTouchesLattice(
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step,
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius,
float ReachScale = 1.0f)
{
if (!VoxelBox.IsValid) return false;
if (!VoxelMath::IsFinite(ReachScale) || ReachScale <= 0.0f)
{
// This is an invalid proof request, not an empty room. The manager treats a
// malformed scale as conservative/unknown before calling this helper.
return true;
}
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
if (!Geometry.bValid) return false;
const float Pad = LandingCarveBlendVoxels * ReachScale;
return LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.X, -Geometry.HalfWidth - Pad),
FMath::Min((float)VoxelBox.Max.X, Geometry.HalfWidth + Pad),
(float)LatticeOrigin.X, Step)
&& LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.Y, -Geometry.HalfWidth - Pad),
FMath::Min((float)VoxelBox.Max.Y, Geometry.HalfWidth + Pad),
(float)LatticeOrigin.Y, Step)
&& LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.Z, Geometry.FloorZ - Pad),
FMath::Min((float)VoxelBox.Max.Z, Geometry.CeilingZ + Pad),
(float)LatticeOrigin.Z, Step);
}
FORCEINLINE bool OriginLandingFloorTouchesLattice(
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step,
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
{
if (!VoxelBox.IsValid) return false;
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
if (!Geometry.bValid) return false;
constexpr float ProofPad = 1.0f;
const float FloorHalfWidth = FMath::Max(
Geometry.HalfWidth - MetersToVoxels(0.25f), 0.0f);
return LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.X, -FloorHalfWidth - ProofPad),
FMath::Min((float)VoxelBox.Max.X, FloorHalfWidth + ProofPad),
(float)LatticeOrigin.X, Step)
&& LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.Y, -FloorHalfWidth - ProofPad),
FMath::Min((float)VoxelBox.Max.Y, FloorHalfWidth + ProofPad),
(float)LatticeOrigin.Y, Step)
&& LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.Z,
Geometry.FloorZ - Geometry.FloorThickness - ProofPad),
FMath::Min((float)VoxelBox.Max.Z, Geometry.FloorZ + ProofPad),
(float)LatticeOrigin.Z, Step);
}
/** Conservative influence box for the origin room's air carve. */
FORCEINLINE bool OriginLandingRoomTouchesBox(
const FBox& VoxelBox,
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
{
if (!VoxelBox.IsValid) return false;
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
if (!Geometry.bValid) return false;
const float Pad = LandingCarveBlendVoxels;
return IntersectsClosedInterval(
VoxelBox.Min.X, VoxelBox.Max.X,
-Geometry.HalfWidth - Pad, Geometry.HalfWidth + Pad)
&& IntersectsClosedInterval(
VoxelBox.Min.Y, VoxelBox.Max.Y,
-Geometry.HalfWidth - Pad, Geometry.HalfWidth + Pad)
&& IntersectsClosedInterval(
VoxelBox.Min.Z, VoxelBox.Max.Z,
Geometry.FloorZ - Pad, Geometry.CeilingZ + Pad);
}
/** Conservative influence box for the origin room's guaranteed solid floor. */
FORCEINLINE bool OriginLandingFloorTouchesBox(
const FBox& VoxelBox,
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
{
if (!VoxelBox.IsValid) return false;
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
if (!Geometry.bValid) return false;
// One voxel is a proof margin, not a write margin. It keeps ClassifyTile conservative at
// an exact lattice edge and mirrors the existing passage-floor guard.
constexpr float ProofPad = 1.0f;
const float FloorHalfWidth = FMath::Max(
Geometry.HalfWidth - MetersToVoxels(0.25f), 0.0f);
return IntersectsClosedInterval(
VoxelBox.Min.X, VoxelBox.Max.X,
-FloorHalfWidth - ProofPad, FloorHalfWidth + ProofPad)
&& IntersectsClosedInterval(
VoxelBox.Min.Y, VoxelBox.Max.Y,
-FloorHalfWidth - ProofPad, FloorHalfWidth + ProofPad)
&& IntersectsClosedInterval(
VoxelBox.Min.Z, VoxelBox.Max.Z,
Geometry.FloorZ - Geometry.FloorThickness - ProofPad,
Geometry.FloorZ + ProofPad);
}
/** Signed distance for a rounded origin room with a hard flat floor. */
FORCEINLINE float OriginLandingRoomSDF(
const FVector& Position, const FOriginLandingGeometry& Geometry)
{
if (!Geometry.bValid || Geometry.CeilingZ <= Geometry.FloorZ)
{
return FLT_MAX;
}
const FVector Center(0.0f, 0.0f,
(Geometry.FloorZ + Geometry.CeilingZ) * 0.5f);
const float Rounding = RoomRoundingVoxels;
const float HalfZ = (Geometry.CeilingZ - Geometry.FloorZ) * 0.5f;
const FVector HalfExtent(
FMath::Max(Geometry.HalfWidth - Rounding, MetersToVoxels(0.0625f)),
FMath::Max(Geometry.HalfWidth - Rounding, MetersToVoxels(0.0625f)),
FMath::Max(HalfZ - Rounding, MetersToVoxels(0.0625f)));
const FVector D(
FMath::Abs(Position.X - Center.X) - HalfExtent.X,
FMath::Abs(Position.Y - Center.Y) - HalfExtent.Y,
FMath::Abs(Position.Z - Center.Z) - HalfExtent.Z);
const FVector Outside(
FMath::Max(D.X, 0.0f),
FMath::Max(D.Y, 0.0f),
FMath::Max(D.Z, 0.0f));
const float Inside = FMath::Min(
FMath::Max(D.X, FMath::Max(D.Y, D.Z)), 0.0f);
const float RoundedBoxSDF = Outside.Size() + Inside - Rounding;
// Below the floor is rock. Above it the rounded-box air volume is authoritative.
return FMath::Max(RoundedBoxSDF, Geometry.FloorZ - Position.Z);
}
}
@@ -0,0 +1,195 @@
#pragma once
#include "CoreMinimal.h"
#include "VoxelTypes.h"
class UVoxelGenerator;
#if WITH_EDITOR
/**
* Step 2d editor research field. It is intentionally separate from the gameplay evaluator and
* is selected only by the default-off voxel.ResearchRedefinedDensityField console variable.
*
* The field is a connected, archetype-sized cave lattice with an authored walkable floor at each
* passage landing. Its macro shape is evaluated directly in world space; bounded, continuous
* hash-value noise is added at every full-resolution sample. The brick classifier is permitted to
* skip only when a Lipschitz bound on the macro field plus the exact maximum detail amplitude
* proves that the final sign is uniform throughout the brick and its marching-cubes halo.
*/
namespace VoxelForgeRedefinedDensityField
{
/** Proof sample width shared by the research mesher and the region-evaluation harness. */
constexpr int32 ParameterizedBrickSamples = 4;
/** Uniform fill magnitude used after a 16 metre sign proof. */
constexpr float ParameterizedUniformDensity = 16.0f * 100.0f / VOXEL_SIZE;
/** Per-worker, per-brick state. The opaque plan pointer is valid until the layout changes. */
struct FBrickEvaluationContext
{
const void* PlanToken = nullptr;
bool bPlanResolved = false;
bool bMayContainPassage = true;
bool bChunkResolved = false;
bool bIsGapChunk = false;
bool bHasDiffMods = false;
bool bMacroXYPrecomputed = false;
FIntVector BrickOrigin = FIntVector::ZeroValue;
FIntVector BrickSize = FIntVector::ZeroValue;
FIntVector Chunk = FIntVector::ZeroValue;
TArray<float> FloorByXY;
TArray<float> GridAirByXY;
TArray<float> DetailFadeByXY;
TArray<float> LandingHorizontalByXY;
TArray<float> LandingApproachByXY;
TArray<float> LandingBridgeHorizontalByXY;
TArray<float> LandingBridgeFloorByXY;
TArray<int32> PassageCandidates;
};
/** Cell-level controls for the separate editor-only parameterized cave field. */
struct FGenerationParameters
{
float RoomPitchVoxels = 144.0f;
float MinimumRoomRadiusFraction = 0.30f;
float MaximumRoomRadiusFraction = 0.45f;
float RoomHeightVoxels = 192.0f;
float FloorReliefVoxels = 8.0f;
float TunnelRadiusVoxels = 28.0f;
float TunnelHalfHeightVoxels = 26.0f;
float LoopProbability = 0.20f;
float VerticalShaftProbability = 0.10f;
float ShaftRadiusVoxels = 18.0f;
float PillarProbability = 0.10f;
float LedgeProbability = 0.20f;
float FloatingMassProbability = 0.10f;
float SpireProbability = 0.25f;
float BasinProbability = 0.0f;
float BasinDepthVoxels = 0.0f;
float WaterLevelFraction = 0.5f;
};
/** Brick-local immutable room cells used by step 2e evaluation and its final-field proof. */
struct FParameterizedBrickContext
{
const void* PlanToken = nullptr;
const void* RoomCellCacheToken = nullptr;
bool bPlanResolved = false;
bool bChunkResolved = false;
bool bIsGapChunk = false;
bool bHasDiffMods = false;
bool bMayContainPassage = true;
FIntVector BrickOrigin = FIntVector::ZeroValue;
FIntVector BrickSize = FIntVector::ZeroValue;
FIntVector Chunk = FIntVector::ZeroValue;
FIntPoint RoomCellCacheMin = FIntPoint::ZeroValue;
FIntPoint RoomCellCacheSize = FIntPoint::ZeroValue;
TArray<int32> PassageCandidates;
};
enum class EBrickFallbackReason : uint8
{
None,
UnsupportedState,
ChunkOrStrateBoundary,
PassageBounds,
MixedMacroInterval
};
struct FBrickCounters
{
uint64 Tested = 0;
uint64 Skipped = 0;
uint64 EvaluatedSamples = 0;
};
VOXELFORGE_API bool IsEnabled();
VOXELFORGE_API void SetEnabled(bool bEnabled);
VOXELFORGE_API float GetDetailScale();
VOXELFORGE_API void SetDetailScale(float Scale);
/** The parameterized cave field is editor-only and default-off. -1 rolls per strate. */
VOXELFORGE_API bool IsParameterizedEnabled();
VOXELFORGE_API void SetParameterizedEnabled(bool bEnabled);
/** Select the frozen step 2e implementation while the step 2f audit measures its gates. */
VOXELFORGE_API void SetLegacyStep2eAuditMode(bool bEnabled);
VOXELFORGE_API void SetParameterSet(int32 ParameterSet);
VOXELFORGE_API int32 GetParameterSet();
VOXELFORGE_API int32 GetParameterSetCount();
VOXELFORGE_API bool GetParameterSetInfo(
int32 ParameterSet, const TCHAR*& OutName, FGenerationParameters& OutParameters);
/** Evaluate the separate parameterized room / tunnel network, including edits/passages. */
VOXELFORGE_API float EvaluateParameterized(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ,
float* OutPreEditDensity = nullptr, float DetailScale = 1.0f);
/** Evaluate only the parameterized room/tunnel macro, before passage overlays and seals. */
VOXELFORGE_API float EvaluateParameterizedMacroOnly(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ);
/** Describe the candidate room SDF values at a point for step 2f proof diagnosis. */
VOXELFORGE_API FString DescribeParameterizedMacroAt(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ);
/** Return the active parameterized detail signal for the band-limit research gate. */
VOXELFORGE_API float EvaluateParameterizedDetail(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ);
VOXELFORGE_API float EvaluateParameterizedWithDetailValue(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ,
float DetailValue, float* OutPreEditDensity = nullptr, float DetailScale = 1.0f,
const FParameterizedBrickContext* Context = nullptr);
/** Evaluate a Z column while reusing its XY-dependent room graph data. */
VOXELFORGE_API void EvaluateParameterizedColumnWithDetailValues(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZStart,
const float* DetailValues, int32 DetailStride, int32 Count, float* OutValues,
const FParameterizedBrickContext* Context = nullptr, float DetailScale = 1.0f,
int32 SampleStep = 1);
VOXELFORGE_API bool TryClassifyParameterizedBrick(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
bool& bOutAir, float DetailScale = 1.0f,
bool* bOutEvaluatedMacroCentre = nullptr,
EBrickFallbackReason* OutFallbackReason = nullptr,
int32 SampleStep = 1);
VOXELFORGE_API void BuildParameterizedBrickContext(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
FParameterizedBrickContext& OutContext, int32 SampleStep = 1);
/** Evaluate a mixed brick with one shared room/passage setup for all Z samples. */
VOXELFORGE_API void EvaluateParameterizedDensityBrick(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
float DetailScale, FParameterizedBrickContext& InOutContext,
TArray<float>& InOutDetail, TArray<float>& InOutColumnValues,
TArray<float>& OutValues, int32 SampleStep = 1);
/** Evaluate the new field, including existing passage carving and player diffs. */
VOXELFORGE_API float Evaluate(const UVoxelGenerator& Generator, float WorldX, float WorldY,
float WorldZ, float* OutPreEditDensity = nullptr,
float DetailScale = 1.0f);
/** Build mathematically identical multi-scale noise for one integer-lattice brick. */
VOXELFORGE_API void BuildDetailBrick(const UVoxelGenerator& Generator,
FIntVector OriginVoxels, FIntVector Size, float DetailScale,
TArray<float>& OutDetail, FBrickEvaluationContext* OutContext = nullptr,
bool bParameterized = false, int32 SampleStep = 1);
/** Evaluate with a detail value produced by BuildDetailBrick. */
VOXELFORGE_API float EvaluateWithDetailValue(const UVoxelGenerator& Generator,
float WorldX, float WorldY, float WorldZ, float DetailValue,
float* OutPreEditDensity = nullptr, float DetailScale = 1.0f,
const FBrickEvaluationContext* Context = nullptr);
/** Prove a world-aligned box has one final density sign; false means evaluate every point. */
VOXELFORGE_API bool TryClassifyBrick(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
bool& bOutAir, float DetailScale = 1.0f,
bool* bOutEvaluatedMacroCentre = nullptr,
EBrickFallbackReason* OutFallbackReason = nullptr);
/** Reset this thread's immutable plan cache; used by deterministic editor experiments. */
VOXELFORGE_API void ResetThreadCache();
}
#endif
@@ -0,0 +1,71 @@
// Cookable runtime carrier for one reviewed season manifest.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "VoxelSeasonAsset.generated.h"
struct FVoxelSeasonManifest;
class UVoxelStrateDefinition;
/**
* The JSON remains the reviewed/diffable source artifact. This asset embeds that exact text so
* Unreal's cooker can carry it into a packaged build, together with an independently stored digest
* used to detect an accidentally stale or edited asset.
*/
UCLASS(BlueprintType)
class VOXELFORGE_API UVoxelSeasonAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
virtual FPrimaryAssetId GetPrimaryAssetId() const override;
/** Parse and validate the embedded manifest, including both content-hash copies. */
bool LoadManifest(FVoxelSeasonManifest& OutManifest, FString& OutReport) const;
/** Runtime-safe import core used by the editor button and automation. */
bool SetManifestJson(const FString& InManifestJson, FString& OutReport);
int32 GetSeasonNumber() const { return SeasonNumber; }
int32 GetSeasonSeed() const { return SeasonSeed; }
float GetOriginSpineRadius() const { return OriginSpineRadius; }
float GetWorldRadiusVoxels() const { return WorldRadiusVoxels; }
/** Create the asset in Content Browser, choose a JSON file, then press this button. */
UFUNCTION(CallInEditor, Category = "Season")
void ImportSeasonManifestJson();
#if WITH_EDITORONLY_DATA
UPROPERTY(EditAnywhere, Category = "Season|Import", meta = (FilePathFilter = "json"))
FFilePath SourceManifestJson;
#endif
private:
UPROPERTY(VisibleAnywhere, Category = "Season")
int32 SchemaVersion = 0;
UPROPERTY(VisibleAnywhere, Category = "Season")
int32 SeasonNumber = 0;
UPROPERTY(VisibleAnywhere, Category = "Season")
int32 SeasonSeed = 0;
UPROPERTY(VisibleAnywhere, Category = "Season")
float OriginSpineRadius = 0.0f;
UPROPERTY(VisibleAnywhere, Category = "Season")
float WorldRadiusVoxels = 0.0f;
UPROPERTY(VisibleAnywhere, Category = "Season")
FString ManifestContentHash;
UPROPERTY(VisibleAnywhere, Category = "Season", meta = (MultiLine = true))
FString ManifestJson;
// Import-derived cook references for fixed/authored content bags named by the JSON. Generated
// strates legitimately have no source definition and use deterministic C++ defaults at runtime.
UPROPERTY(VisibleAnywhere, Category = "Season")
TArray<TSoftObjectPtr<UVoxelStrateDefinition>> ReferencedDefinitions;
};
@@ -0,0 +1,299 @@
// Offline Tier 4c composition plus the runtime-readable, diffable Tier A manifest.
#pragma once
#include "CoreMinimal.h"
#include "VoxelStrateComposer.h"
#include "VoxelStrateMeasure.h"
class UVoxelSettings;
/** Why the provisional season selector put one surviving candidate in a slot. */
enum class EVoxelSeasonSelectionReason : uint8
{
Grounded,
Outlier,
Fixed,
};
/**
* Explicit, reviewable selection knobs for one season.
*
* None of these values is a settled definition of quality. They are the first policy Jahni can
* change after walking the review page. Hard validation remains separate from this policy.
*/
struct VOXELFORGE_API FVoxelSeasonSelectionPolicy
{
// Placeholder mix only: ordinary strates give an outlier contrast. Revisit after the first
// season review; this is not a learned or final quality score.
float GroundedFraction = 0.75f;
float OutlierFraction = 0.25f;
// Below this measured distance two neighbours look too similar. The selector subtracts a
// penalty proportional to the gap; provisional because the useful contrast is visual.
double AdjacentSimilarityDistanceThreshold = 0.20;
double AdjacentSimilarityPenaltyWeight = 1.0;
// Rewards an unseen archetype/recipe in the already chosen prefix. These are variety
// tie-breakers, not claims that one archetype or recipe is better. Provisional review knobs.
double NewArchetypeBonus = 0.10;
double NewRecipeBonus = 0.05;
// GDD §6 says roughly every five levels, and a boss is intermediary rather than terminal.
// This interval is therefore an explicit provisional layout knob, not a quality score.
int32 BossSlotInterval = 5;
// Tier 2 hard gate: a surviving air component must retain at least this share. Provisional
// only in its threshold; the existence of the gate is non-negotiable for a shippable season.
float MinimumLargestComponentShare = 0.50f;
// Normalizers for the two unbounded measured dimensions in the adjacency distance. Fractions
// already live in [0,1]. These physical scales are provisional and visible in the manifest.
float FeatureScaleNormalizationVoxels = 256.0f;
float ClearanceNormalizationVoxels = 64.0f;
};
/** A caller-supplied fixed slot for tests/tools that already have a complete manifest record. */
struct VOXELFORGE_API FVoxelSeasonFixedStrate
{
int32 Seed = 0;
int32 HeightInChunks = 8;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams Params;
FVoxelOpStackRecipe Recipe;
FVoxelStrateRegionManifest Regions;
bool bUsesRecipe = true;
bool bUsesRegions = false;
FString SourceDefinitionPath;
};
/** Settings for the offline build-box pass. No member is consulted by normal runtime generation. */
struct VOXELFORGE_API FVoxelSeasonCompositionSettings
{
// Optional live-world settings. TotalStrates, spine/gap values, and FixedStrates are read
// from it when present. The pointer is borrowed for the duration of VF_ComposeSeason only.
const UVoxelSettings* WorldSettings = nullptr;
// Optional preloaded corpus. With nullptr the composer loads the project asset corpus on the
// build box, exactly like the existing offline composer tests.
const FVoxelStrateCorpus* Corpus = nullptr;
// 24 gives a useful minute-scale automation pass at the default coarse measurement window.
// Production can raise this to the overnight budget without changing the policy.
int32 CandidateCount = 24;
int32 SelectedStrateCount = 10;
// Generated candidates use one stable height. Fixed authored slots may provide their own
// height. World layout bounds are written into every selected parameter family below.
int32 StrateHeightInChunks = 8;
int32 InterStrateGapChunks = 0;
float OriginSpineRadius = 28.0f;
float WorldRadiusVoxels = 0.0f;
// Coarse measurement is deliberately bounded. Tests may reduce the window; production may
// spend the overnight budget on a finer pass after the selector has been judged.
FVoxelStrateMeasureSettings MeasureSettings;
// Player-fit is a separate, explicitly fine pass. SampleStep values other than one are
// refused by the metric, never interpreted as an approximate capsule answer.
FVoxelStrateMeasureSettings PlayerFitMeasureSettings;
FVoxelSeasonSelectionPolicy SelectionPolicy;
// Explicit composed fixed slots take precedence. When empty, the composer adapts the
// existing UVoxelSettings::FixedStrates map, preserving its absolute slot indices.
TMap<int32, FVoxelSeasonFixedStrate> FixedStrates;
// Empty means ProjectSavedDir()/VoxelForge/Seasons. Files are never written to the runtime
// content directory by this API.
FString OutputDirectory;
bool bWriteArtifacts = true;
bool bWriteReviewPage = true;
bool bIncludeCandidateAudit = true;
// Informational only; when WorldSettings is present CurrentSeason supplies the default.
int32 SeasonNumber = 0;
FVoxelSeasonCompositionSettings()
{
// The existing Tier 4b test uses step 4. Season composition defaults to step 8 so the
// default automation artifact is measured in minutes, while MaxCells remains bounded.
MeasureSettings.SampleStep = 8;
MeasureSettings.RadiusInVoxels = 512;
MeasureSettings.MaxCells = 8000000;
MeasureSettings.MaxRouteRetries = 16;
MeasureSettings.HeadroomCells = 2;
MeasureSettings.InteriorMarginVoxels = -1;
PlayerFitMeasureSettings.SampleStep = 1;
PlayerFitMeasureSettings.RadiusInVoxels = 128;
// A fitted mouth-to-mouth XY window across an 8-chunk (256-voxel) strate can approach
// four million cells. Keep the cap finite and below the old multi-gigabyte retry cost.
PlayerFitMeasureSettings.MaxCells = 4000000;
PlayerFitMeasureSettings.MaxRouteRetries = 16;
PlayerFitMeasureSettings.HeadroomCells = 2;
PlayerFitMeasureSettings.InteriorMarginVoxels = -1;
}
};
/** Compact audit row; the full vectors live only for selected strates. */
struct VOXELFORGE_API FVoxelSeasonCandidateAudit
{
int32 CandidateIndex = INDEX_NONE;
int32 Seed = 0;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
uint32 RecipeHash = 0;
double DistanceFromCorpusCentroid = 0.0;
bool bPassedHardGates = false;
bool bSelected = false;
FString RejectionReason;
};
/** Count of one explicit hard-gate or final-placement rejection reason. */
struct VOXELFORGE_API FVoxelSeasonRejectionCount
{
FString Reason;
int32 Count = 0;
};
/** One ordered Tier A spine entry. */
struct VOXELFORGE_API FVoxelSeasonStrate
{
// DepthIndex is the absolute world-layout slot. CandidateIndex is the offline attempt id.
int32 DepthIndex = INDEX_NONE;
int32 CandidateIndex = INDEX_NONE;
int32 Seed = 0;
int32 HeightInChunks = 0;
int32 TopWorldZ = 0;
int32 BottomWorldZ = 0;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelOpStackRecipe Recipe;
FVoxelStrateArchetypeParams Params;
// When true this is the complete lateral parent description. Params/Archetype remain the
// compatibility identity of region zero; runtime rebuilds use Regions and never blend params.
FVoxelStrateRegionManifest Regions;
FVoxelStrateMeasuredMetrics Metrics;
double DistanceFromCorpusCentroid = 0.0;
FString SelectionReason;
FString SourceDefinitionPath;
bool bGrounded = false;
bool bOutlier = false;
bool bBossSlot = false;
bool bFixed = false;
bool bUsesRecipe = true;
bool bUsesRegions = false;
bool bPassedNonVacuous = false;
bool bPassedLargestComponent = false;
bool bPassedPrimordialLaw = false;
EVoxelConnectivityResult PrimordialLawResult = EVoxelConnectivityResult::OutOfWindow;
};
/**
* The stored Tier A season artifact.
*
* Tier B is intentionally absent: its hash-lattice field remains an arithmetic/enumeration API,
* not a second stored table. Strates is ordered from the surface down and is the only finite spine
* persisted by this task.
*/
struct VOXELFORGE_API FVoxelSeasonManifest
{
static constexpr int32 CurrentSchemaVersion = 2;
bool bValid = false;
int32 SchemaVersion = CurrentSchemaVersion;
FString Error;
int32 Season = 0;
int32 Seed = 0;
uint32 InputCorpusHash = 0;
// SHA-1 of the canonical manifest JSON with this field omitted. This detects stale cooked
// season assets without requiring the editor-only input corpus on a player's machine.
FString ContentHash;
int32 CandidateCount = 0;
int32 SurvivorCount = 0;
int32 SelectedCount = 0;
int32 RejectedCount = 0;
int32 GroundedSelectedCount = 0;
int32 OutlierSelectedCount = 0;
int32 FixedSelectedCount = 0;
int32 TotalStrates = 0;
int32 InterStrateGapChunks = 0;
float OriginSpineRadius = 0.0f;
float WorldRadiusVoxels = 0.0f;
FVoxelSeasonSelectionPolicy SelectionPolicy;
FString PolicyNote;
TArray<FVoxelSeasonStrate> Strates;
TArray<FVoxelSeasonCandidateAudit> CandidateAudit;
TArray<FVoxelSeasonRejectionCount> RejectionCounts;
// Derived artifact locations; intentionally not serialized into the portable JSON.
FString ManifestPath;
FString ReviewPagePath;
FString SerializedJson;
bool IsUsable() const
{
if (!bValid || !Error.IsEmpty() || ContentHash.Len() != 40 || WorldRadiusVoxels != 0.0f
|| Strates.Num() != SelectedCount || SelectedCount <= 0)
{
return false;
}
for (const FVoxelSeasonStrate& Strate : Strates)
{
// Region composition is intentionally not a shippable season format yet. Keeping
// this rejection in the portable manifest contract prevents an editor experiment
// from becoming packaged terrain merely because its density evaluator exists.
if (Strate.bUsesRegions)
{
return false;
}
}
return true;
}
};
/** Compose, validate, select, serialize, and (by default) write one season. Editor/build-box only. */
#if WITH_EDITOR
VOXELFORGE_API FVoxelSeasonManifest VF_ComposeSeason(
int32 SeasonSeed, const FVoxelSeasonCompositionSettings& Settings);
/** Convenience overload that adapts the existing runtime settings asset. */
VOXELFORGE_API FVoxelSeasonManifest VF_ComposeSeason(
int32 SeasonSeed, const UVoxelSettings* WorldSettings);
#endif
/** Serialize/read the complete Tier A manifest as stable pretty JSON. */
VOXELFORGE_API FString VF_SerializeVoxelSeasonManifest(
const FVoxelSeasonManifest& Manifest);
/** Parse an in-memory/cooked JSON manifest and verify its embedded content hash. */
VOXELFORGE_API bool VF_DeserializeVoxelSeasonManifest(
const FString& ManifestJson, FVoxelSeasonManifest& OutManifest, FString& OutReport);
/** Stable SHA-1 over the canonical manifest payload (the content_hash field itself is omitted). */
VOXELFORGE_API FString VF_ComputeVoxelSeasonManifestContentHash(
const FVoxelSeasonManifest& Manifest);
VOXELFORGE_API bool VF_SaveVoxelSeasonManifest(
const FString& ManifestPath, const FVoxelSeasonManifest& Manifest, FString& OutReport);
VOXELFORGE_API bool VF_LoadVoxelSeasonManifest(
const FString& ManifestPath, FVoxelSeasonManifest& OutManifest, FString& OutReport);
/** Rebuild exactly the stack represented by one manifest entry. */
VOXELFORGE_API bool VF_BuildSeasonStrateStack(
const FVoxelSeasonStrate& Strate,
float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr);
/** Verbose alias for tests/tools that want the round-trip operation named explicitly. */
VOXELFORGE_API bool VF_RebuildVoxelSeasonStrate(
const FVoxelSeasonStrate& Strate,
float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr);
@@ -0,0 +1,436 @@
// VoxelSettings.h
// Settings du plugin VoxelForge.
//
// Data asset unique (assigné sur AVoxelWorld) qui regroupe tous les tuning
// du monde: seed, streaming, LOD, pool de strates, budgets de carving.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "VoxelStrateDefinition.h"
#include "VoxelSettings.generated.h"
// IWYU : pointeur seulement (VoxelMaterial). / Pointer-only use.
class UMaterialInterface;
class UVoxelSeasonAsset;
UCLASS(BlueprintType)
class UVoxelSettings : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
/** Season seed/radius replace the mutable authored values only when Season is assigned. */
int32 GetEffectiveWorldSeed() const;
float GetEffectiveOriginSpineRadius() const;
float GetEffectiveWorldRadiusVoxels() const;
int32 GetEffectiveSeasonNumber() const;
//=========================================================================
// STREAMING (distance de vue)
//=========================================================================
// En CHUNKS (CHUNK_SIZE=32). At 12.5 cm/voxel, each view-distance chunk covers 4 m.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 ViewDistanceXY = 32;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 ViewDistanceUp = 10;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 ViewDistanceDown = 10;
// Nombre max de tâches de génération/mesh en parallèle.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 MaxConcurrentTasks = 16;
// Optional hard cap for the generation worker budget. 0 keeps the safe automatic cap of
// logical cores minus two; a positive value makes that cap visible and authorable beside the
// task budget instead of hiding it in AVoxelWorld. The effective task count is still the
// minimum of this value and MaxConcurrentTasks.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0"))
int32 MaxGenerationWorkerCores = 0;
// Keep this many task slots available for collision-critical, edited, and level-0 terrain while
// coarse horizon work is in flight. A positive reserve prevents a full far-field queue from
// delaying a new near tile because running tasks cannot be preempted. The runtime clamps the
// value below the effective task budget; 4 is the conservative default for the ship settings.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0", ClampMax = "16"))
int32 NearTaskReserve = 4;
// Nombre max de meshes appliqués (upload GPU) par frame. Seuls les vrais applies
// comptent (chunks vides/périmés se vident gratuitement). À 32³ chaque apply est léger
// (~0.1 ms) — tunable en live sur l'asset : montez (8-16) si le remplissage traîne,
// baissez si l'apparition des chunks fait des à-coups.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 MaxMeshAppliesPerFrame = 4;
// Wall-clock cap for the complete game-thread result drain, including component teardown and
// collision configuration. The count cap remains the second guard. A zero value disables this
// cap; the default keeps one burst of chunk application below a small frame-time budget without
// changing worker resolution or mesh quality.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0.0"))
float MaxMeshApplyMilliseconds = 2.0f;
// Max tile teardowns (component destroy + content-actor Destroy) per frame. A fast traversal
// culls a whole shell of tiles at once; doing every destroy in one frame is a game-thread spike
// ("stuff torn down behind you"). This spreads it. The drain auto-scales up if the backlog grows
// (PendingUnload/4) so it never falls far behind — raise the floor if teardown lags at high speed.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 MaxUnloadsPerFrame = 6;
// Independent level-0 collision radius around the locally controlled pawn. Render tiles outside
// this cube remain visible but do not queue Chaos cooks; registered streaming anchors keep their
// own authored collision boxes, and the collision gate can extend a landing-support column.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "1", ClampMax = "4"))
int32 PawnCollisionRadiusChunks = 2;
// Strate-aware VERTICAL streaming: don't load chunks into a strate you can't see.
// Seals + the inter-strate bedrock gap are light-tight (§8.7), so the strate above/below
// the one you're in is fully occluded — loading it just wastes scene primitives (the
// game-thread cost). When ON, the vertical view is clamped to the player's strate ±
// StrateViewMarginChunks. In the bedrock GAP itself the view is NOT clamped (you can see
// both sides through the descent passage there), so the next strate streams in ahead.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
bool bClampViewToStrate = true;
// Chunks of vertical look-ahead PAST the strate seal (each side) when bClampViewToStrate
// is on: keeps the descent tunnel / immediate transition loaded and acts as the trailing
// buffer of the strate you're leaving (it sheds naturally as you descend out of view).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0"))
int32 StrateViewMarginChunks = 6;
// 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 1 = cut LOD1 and coarser. LOD0 is deliberately never content-cut because it owns pawn
// collision: cutting the passage, gap, or next-strate support tile would deadlock a walk at the
// current strate boundary. The runtime clamps legacy/hand-authored values below 1 to 1 as well.
// Raise only if a broader near-field view needs full coarse tiles. 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 = "1", ClampMax = "9"))
int32 StrateContentCutMinLevel = 1;
/**
* Return the effective content-cut level used by runtime streaming.
*
* Existing data assets can contain a serialized legacy value of 0 even though the property now
* documents 1 as the minimum. Keep the safety rule in code, at the consumer boundary: level 0
* tiles are the collision tiles and must never receive a strate content band.
*/
int32 GetEffectiveStrateContentCutMinLevel() const
{
return FMath::Clamp(FMath::Max(StrateContentCutMinLevel, 1), 1, 9);
}
//=========================================================================
// CLIPMAP (chunked-LOD streaming — supersedes the ViewDistance box above)
//=========================================================================
// Streaming loads concentric shells of tiles: level 0 = full-res chunks near the player,
// each coarser level doubles tile size (and reach). Total tile/draw/gen count stays ~flat
// regardless of how far you see — so this is the "see to the horizon cheaply" system.
// Total reach ≈ ClipRadius × CHUNK_SIZE × 2^MaxClipLevel voxels.
// Near full-res reach ≈ ClipRadius × CHUNK_SIZE voxels.
// Half-extent (in tiles, per axis) of EACH level's shell. Bigger = more tiles per level
// (more detail / overlap) but more components. 3 → a 7×7×7 box per level (minus the inner
// hole filled by the finer level).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "1"))
int32 ClipRadius = 6;
// Coarsest LOD level. 0 = full-res only (no clipmap). Each level up doubles tile size &
// reach at ~constant cost — raise this to see much farther for cheap. (Level L step = 2^L.)
// Ship default: level 5, the measured horizon-distance balance at 1536 chunks.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0", ClampMax = "8"))
int32 MaxClipLevel = 5;
// How many near levels mesh at FULL resolution (CHUNK_SIZE cells). Levels at or above this
// use the cheaper CoarseTileCells → MUCH faster gen for the far field (it's far, so the
// extra blockiness is hidden, and skirts cover the seams). 2 = levels 0,1 crisp; set to 1
// for the fastest gen (slightly harder LOD0→1 transition).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "1"))
int32 FullResClipLevels = 2;
// Cells per axis for COARSE tiles (levels >= FullResClipLevels). Lower = cheaper gen, blockier
// far terrain. 16 = ~⅛ the gen samples of a full 32-cell tile; 8 = ~1/64. Tile EXTENT is
// unchanged (the shells still tile) — only the mesh resolution within a far tile drops.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "4", ClampMax = "32"))
int32 CoarseTileCells = 16;
// RENDER DISTANCE — custom horizontal reach, in CHUNKS (1 chunk = 4 m; 256 ≈ 1 km, 1536 ≈ 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.
// Ship default: 1536 chunks (about 6.1 km at 4 m/chunk). 0 = off (natural reach).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0"))
int32 RenderDistanceChunks = 1536;
// 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.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap")
bool bGenerateSkirts = true;
// Skirt depth in tile CELLS (× the tile's step). Four L0 cells preserve the former 0.5 m
// default depth at 12.5 cm per voxel. Raise if cracks still show; lower if skirts peek out.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0.5", ClampMax = "8.0"))
float SkirtCells = 4.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;
//==========================================================================
// WORLD BOUNDS (bounded gigantic world in actor-space XY)
//==========================================================================
// Radius (voxels) of the bounded world in XY, measured from actor-space (0,0).
// 0 = unbounded (legacy behaviour, no edge seal).
//
// ⚠️ DEFAULTS TO 0 ON PURPOSE, and it is not the value the GDD wants.
// Existing DA_Settings / BP_VoxelSettings assets predate this property, so they do NOT serialize
// it — they silently inherit whatever C++ default is written here. A non-zero default would
// therefore wall in every existing world the next time the editor opens, with nothing in the
// asset to show why. The former 8192-voxel / 2.05 km target must be 16384 voxels at 12.5 cm.
// 8192 now means a 1.024 km radius.
// SET IT IN THE ASSET, deliberately, once.
//
// FR : 0 par défaut EXPRÈS — les assets existants n'ont pas cette propriété et héritent
// silencieusement de la valeur C++. La valeur de production visée (~8192) se règle dans l'asset.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|World Bounds", meta = (ClampMin = "0.0"))
float WorldRadiusVoxels = 0.0f;
// Solid ramp thickness at the radial world edge, in voxels.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|World Bounds", meta = (ClampMin = "0.0"))
float EdgeSealThickness = 128.0f;
//=========================================================================
// CONTENT — distance-based decoration grid (no LOD pop)
//=========================================================================
// Decorations are placed on a fixed WORLD XY cell grid (1 cell = 1 chunk footprint) and streamed
// by DISTANCE from the player, independent of which clipmap tile (LOD) currently meshes the ground
// under them. Each candidate column is ray-marched through the player's strate via the generator's
// density field and snapped to the real surface — so a given prop keeps the SAME world position at
// every LOD (no teleport/pop on tile swaps). Decorations exist only in the player's current strate.
// FAR-tier stream radius in cells (= chunks): how far FStrateDecoration entries set to EDecoStreamTier::Far
// (the default — trees, landmarks, rare props) stream out. Bigger = props visible farther + more
// spawn/march cost (but the far grid is COARSE — see DecorationFarSpacingVoxels — so far cost is cheap).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1"))
int32 DecorationRadiusChunks = 12;
// NEAR-tier stream radius in cells (= chunks): how far EDecoStreamTier::Near entries (dense groundcover
// like grass) stream out. Keep this SHORT — near entries use the FINE grid (DecorationSpacingVoxels), so
// their cost is the steep one; bounding their radius keeps the far-region HISM build + memory small.
// (Repurposes the old vestigial DecorationActorRadiusChunks; same default.)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1"))
int32 DecorationNearRadiusChunks = 6;
// Decoration cells are grouped into REGIONS of RxR cells, and ALL placements in a region share ONE
// HISM per mesh (instead of one HISM per cell per mesh). Regions load/unload as a unit, so clearing
// stays a plain DestroyComponent — no per-instance index remapping. This is the render-thread lever:
// component count (which InitViews walks every frame) drops by ~R^2. R=4 → ~16 regions in a radius-6
// disk vs 169 cells (~10x fewer components). Bigger R = fewer components but coarser pop-in + heavier
// one-shot cluster-tree build per region (apply is budgeted, so the build is amortised). MUST be >= 1.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "16"))
int32 DecorationRegionSizeCells = 8;
// NEAR-tier column spacing (in voxels) within a cell — the FINE grid. MUST divide CHUNK_SIZE (32):
// 4 → 8×8=64 columns/cell. Smaller = denser placement potential + more march cost. SpawnDensity rolls
// per column-crossing. Used by EDecoStreamTier::Near entries (and is the legacy single-grid spacing).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "32"))
int32 DecorationSpacingVoxels = 8;
// FAR-tier column spacing (in voxels) within a cell — the COARSE grid. MUST divide CHUNK_SIZE (32):
// 16 → 2×2=4 columns/cell (16× fewer worker ray-marches than a spacing-4 grid). This is the lever that
// makes a RARE prop visible at every distance cheap: the far grid samples sparsely, so supporting a
// low-SpawnDensity landmark across the full radius costs a fraction of the fine grid. DEFAULTS to the
// fine value (4) so existing worlds are byte-identical until you raise it; bump to 8–16 for cheap far props.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "32"))
int32 DecorationFarSpacingVoxels = 8;
// COARSE vertical march step (in voxels) when searching a column for surface crossings. The crossing
// Z is then bisection-refined, so accuracy is independent of this — raise it (4-8) to cut the scan
// cost (the column is ray-marched on a WORKER thread, but a smaller step still means more samples).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "8"))
int32 DecorationMarchStepVoxels = 4;
// Max surface crossings placed per column (caps cave columns that pierce many floors/ceilings;
// surface worlds have 1). Bounds worst-case march/spawn cost.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "16"))
int32 DecorationMaxCrossingsPerColumn = 4;
// Stop marching a column after this many voxels of CONTIGUOUS SOLID once it has already entered the
// open space — i.e. cap how far into bedrock BELOW the floor (or below a cavern) we keep scanning.
// The top cap/seal and the air above the ground are always marched first (this only trims the dead
// rock underneath), so surface worlds still get their ground and caves still find layered floors.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "8"))
int32 DecorationColumnDepthVoxels = 320;
// Budget: how many completed decoration REGIONS to APPLY (game-thread HISM build + SpawnActor) per
// frame. A region is applied once ALL its cells have finished marching, in one batched AddInstances
// per mesh; this throttles that burst. The expensive ray-march runs async on workers. (Named "Cells"
// for asset back-compat — the apply unit is now a region of DecorationRegionSizeCells^2 cells.)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "1"))
int32 MaxDecorationCellsPerFrame = 2;
// Max decoration ray-march tasks in flight at once. Keeps decoration marching from crowding the
// mesh-gen workers (which are the streaming bottleneck). 0 disables decorations entirely.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0"))
int32 MaxConcurrentDecorationTasks = 4;
//=========================================================================
// LIGHTING — DENSITY VOLUME (mini-sun raymarched shadows)
//=========================================================================
// A player-centred CLIPMAP of the density field, uploaded to the GPU so the terrain
// material can RAYMARCH it toward the "mini-sun" orbs → from-the-orb, crisp, dynamic
// shadows under FORWARD rendering (Lumen/DF off the table). The volume is the load-bearing
// prerequisite: density is CPU-only (GetDensityAt), so we stream it onto the GPU here.
// Concentric levels: level 0 = full-res near the player (step 1), each level up doubles the
// sampling step & reach (fine near / coarse far — exactly what shadow rays want). Filled on
// WORKER threads (re-evaluating GetDensityAt → deterministic, carves auto-picked-up), with
// toroidal incremental refill on movement and localized refill on carve. See VoxelDensityVolume.
// Master switch. OFF = no volume built, no fill tasks, no GPU cost (terrain unlit by orbs).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting")
bool bEnableDensityVolume = false;
// Per-axis resolution of EACH clip level (cells). Memory per level ≈ Res³ bytes (R8).
// 256 → ~16 MiB/level; 512 → ~128 MiB/level. Doubling Res preserves metre reach at 12.5 cm.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "32", ClampMax = "512"))
int32 DensityVolumeResolution = 256;
// Number of concentric clip levels. Level L samples every (1<<L) voxels and covers
// Res·(1<<L) voxels. At 12.5 cm, Res=512 → near 64 m (full-res) out to ~256 m (coarse).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "1", ClampMax = "5"))
int32 DensityVolumeLevels = 3;
// A fill box is split into Z-slabs of at most this many cells per task, so no single task is
// huge (a full level refill on startup/teleport fans out across workers). Lower = more, smaller
// tasks (better parallelism / latency); higher = fewer, fatter tasks.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "1", ClampMax = "64"))
int32 DensityVolumeFillSlabCells = 8;
// Per-pixel shadow-march step count toward the orb (the terrain material reads this). More = crisper
// occlusion at grazing angles but higher GPU cost. 64 is a sane start; tune against the look/cost.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "4", ClampMax = "256"))
int32 DensityVolumeMarchSteps = 64;
// Upload the clipmap to GPU R8 volume textures (so the terrain material can march it). OFF = the
// CPU volume still streams (debug-draw works) but nothing reaches the GPU — the safe fallback if
// the runtime Texture3D RHI path misbehaves on a given engine build.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting")
bool bDensityVolumeGPUUpload = true;
// DEBUG (step 1a verification, no GPU): draw small boxes for SOLID cells of level 0 within
// DensityVolumeDebugRadiusCells of the player, so you can confirm the volume holds terrain-shaped
// solidity, follows you, and updates on carve — BEFORE the GPU upload + material march land.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting|Debug")
bool bDebugDrawDensityVolume = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting|Debug", meta = (ClampMin = "1", ClampMax = "32"))
int32 DensityVolumeDebugRadiusCells = 12;
//=========================================================================
// RENDERING
//=========================================================================
// Matériau global par défaut (peut être override par strate).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Rendering")
UMaterialInterface* VoxelMaterial;
//=========================================================================
// STRATES
//=========================================================================
// Seed du monde. Pilote toute la génération procédurale.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates")
int32 Seed = 0;
// Optional reviewed/cooked season. When set, its manifest owns layout, seed, bounds and
// density recipes; the authored StratePool/FixedStrates path remains untouched when unset.
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Voxel|Strates")
TSoftObjectPtr<UVoxelSeasonAsset> Season;
// Numéro de saison (incrementé par ChangeSeed). Purement informatif côté gameplay.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates")
int32 CurrentSeason = 1;
// Pool de définitions de strates disponibles pour l'assignation aléatoire.
// Chaque définition a sa propre hauteur — les strates ne sont PAS uniformes.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates")
TArray<TSoftObjectPtr<UVoxelStrateDefinition>> StratePool;
// Strates fixées: {index → définition}. Ex: {5, CrystalCaverns} =
// la strate 5 est toujours CrystalCaverns. Les autres sont tirées du pool.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates")
TMap<int32, TSoftObjectPtr<UVoxelStrateDefinition>> FixedStrates;
// Nombre total de strates à générer vers le bas.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "1"))
int32 TotalStrates = 10;
// Vertical SEPARATION between consecutive strates, in chunks of solid bedrock.
// 0 = strates touch (just the seals between them). Higher = a thick rock layer
// between each biome that the player must dig through at (0,0), and that the
// auto-carved passages tunnel across.
// 0 → adjacent layers (default) · 1-2 → a real bedrock band · 4+ → deep separation
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "0", ClampMax = "16"))
int32 InterStrateGapChunks = 0;
//=========================================================================
// SPINE & INTER-STRATE CONNECTIONS (the (0,0) descent axis)
//=========================================================================
// Authored radius (voxels) of the guaranteed (0,0) landing room in EVERY strate, regardless
// of archetype. The §6.5 body-sized formulas turn it into a flat-floored room reserved for a
// future straight shaft of diameter 2 * this radius. At the default 28-voxel radius that is
// 56 voxels / 7 m, with a six-voxel / 0.75 m floor ledge around it. This setting does not bore the
// shaft today: lower seals remain solid until a progression passage is opened.
// 10-14 → compact landing room (default) · 20+ → broad landing room
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Spine", meta = (ClampMin = "0.0"))
float OriginSpineRadius = 28.0f;
// Auto-open the very top seal at (0,0) so the world starts with a hole to the
// surface (the entry shaft). Lower boundaries are NOT auto-opened — the player
// digs those. Disable for a fully sealed world the player must breach from above.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Spine")
bool bOpenSurfaceEntry = true;
// NOTE: inter-strate tunnel count + shape (style, width, length, placement, worming,
// spiral, cascade) are now PER-STRATE — see UVoxelStrateDefinition::PassageConfig.
// Each strate controls its own descent tunnels to the layer below it.
//=========================================================================
// CARVING (modifications du joueur)
//=========================================================================
// Nombre max d'opérations de modification par saison. 0 = illimité.
// 500 = édition légère, 2000 = mining modéré, 0 = créatif.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Carving",
meta = (ClampMin = "0"))
int32 MaxModifications = 0;
// Rayon max d'un brush (en voxels). Empêche les édits géants.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Carving",
meta = (ClampMin = "1.0"))
float MaxBrushRadius = 30.0f;
// Volume total maximum (somme des volumes de brush). 0 = illimité.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Carving",
meta = (ClampMin = "0.0"))
float MaxTotalVolume = 0.0f;
};
@@ -0,0 +1,65 @@
// Bounded, opt-in startup trace for the complete streaming path.
// The trace is deliberately independent of the shared Unreal log: callers provide an
// absolute output path and every record is one JSON object per line.
#pragma once
#include "CoreMinimal.h"
namespace VoxelForgeStartupTrace
{
struct VOXELFORGE_API FTileSample
{
int32 TileX = 0;
int32 TileY = 0;
int32 TileZ = 0;
int32 Level = -1;
int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (early path)
bool bSealedSolidProof = false;
bool bEmpty = true;
int32 Vertices = 0;
int32 Triangles = 0;
uint64 GeometryBytes = 0;
// This is worker-result -> game-thread mesh submission. Collision readiness is a later
// per-tile RMC completion event recorded separately as collision_ready.
double RequestToApplySeconds = 0.0;
double QueueWaitSeconds = 0.0;
double WorkerQueueSeconds = 0.0;
double ResultQueueSeconds = 0.0;
double GenerationSeconds = 0.0;
double ClassifySeconds = 0.0;
double MeshSeconds = 0.0;
double StreamSeconds = 0.0;
double ApplySeconds = 0.0;
};
/** Starts when -voxel.StartupTraceFile=<absolute path> is present. */
VOXELFORGE_API void BeginFromCommandLine();
VOXELFORGE_API bool IsActive();
/** FieldsJson is the contents after the type/timestamp object prefix, without braces. */
VOXELFORGE_API void RecordEvent(const TCHAR* Type, const FString& FieldsJson = FString());
VOXELFORGE_API void StageBegin(const TCHAR* Name);
VOXELFORGE_API void StageEnd(const TCHAR* Name);
VOXELFORGE_API void RecordTile(const FTileSample& Sample);
/** Writes the final bounded summary and closes the file. Safe to call more than once. */
VOXELFORGE_API void Finish(const TCHAR* Reason);
/** RAII helper for stages which have early-return failure paths. */
class VOXELFORGE_API FStageScope
{
public:
explicit FStageScope(const TCHAR* InName);
~FStageScope();
FStageScope(const FStageScope&) = delete;
FStageScope& operator=(const FStageScope&) = delete;
private:
FString Name;
bool bActive = false;
};
}
@@ -0,0 +1,27 @@
// VoxelStats.h
// Per-frame runtime counters for tile classification and meshing.
// Compteurs runtime par frame pour la classification et le meshing des tuiles.
#pragma once
#include "Stats/Stats.h"
DECLARE_STATS_GROUP(TEXT("VoxelForge"), STATGROUP_VoxelForge, STATCAT_Advanced);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Classified"), STAT_VoxelForgeTilesClassified, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Skipped All Solid"), STAT_VoxelForgeTilesSkippedAllSolid, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Skipped All Air"), STAT_VoxelForgeTilesSkippedAllAir, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Meshed"), STAT_VoxelForgeTilesMeshed, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Solid"), STAT_VoxelForgeTilesOpStackSolid, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Air"), STAT_VoxelForgeTilesOpStackAir, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Out Of Layout"), STAT_VoxelForgeCaveMixOutOfLayout, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Gap"), STAT_VoxelForgeCaveMixGap, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Surface World"), STAT_VoxelForgeCaveMixSurfaceWorld, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Two Cave Slots"), STAT_VoxelForgeCaveBailTwoCaveSlots, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Params"), STAT_VoxelForgeCaveBailParams, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Stack Verdict"), STAT_VoxelForgeCaveBailStackVerdict, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Fold Conflict"), STAT_VoxelForgeCaveBailFoldConflict, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Disturbance"), STAT_VoxelForgeCaveBailDisturbance, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail No Stack"), STAT_VoxelForgeCaveBailNoStack, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Column Memo Hits"), STAT_VoxelForgeColumnMemoHit, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Column Memo Misses"), STAT_VoxelForgeColumnMemoMiss, STATGROUP_VoxelForge, VOXELFORGE_API);
@@ -0,0 +1,821 @@
// Offline strate corpus and parameter composer.
//
// This is deliberately not part of the runtime generation path. The composer is a build-box
// tool/test API: it loads authored vectors, measures their spread, and makes deterministic
// candidates by blending those vectors before applying spread-relative (or season-zero bootstrap)
// jitter.
#pragma once
#include "CoreMinimal.h"
#include "VoxelStrateTypes.h"
#include "VoxelDensityOpStack.h"
#include "VoxelStrateMeasure.h"
#include "VoxelStrateComposer.generated.h"
class UVoxelSettings;
class UVoxelStrateDefinition;
class UVoxelStrateManager;
#if WITH_EDITOR
// The complete definitions live in VoxelSeasonManifest.h. Keeping this forward declaration here
// makes the Tier 4c entry point discoverable from the composer header without creating a circular
// include (the manifest header includes this one for the existing vector/recipe types).
struct FVoxelSeasonManifest;
struct FVoxelSeasonCompositionSettings;
VOXELFORGE_API FVoxelSeasonManifest VF_ComposeSeason(
int32 SeasonSeed, const FVoxelSeasonCompositionSettings& Settings);
#endif
/** The one-bit identity choice at the root of a rolled structure. */
UENUM(BlueprintType)
enum class EVoxelStrateRootPolarity : uint8
{
RockCarve = 0,
VoidFill = 1,
};
/** Native parameter family an op reads when a recipe is materialised. */
UENUM(BlueprintType)
enum class EVoxelStrateParamBlock : uint8
{
None = 0,
TunnelNetwork = 1,
Slab = 2,
Maze = 3,
Surface = 4,
VerticalShaft = 5,
FloatingIsland = 6,
};
/** Stable manifest id for every op the offline structure roller may place. */
UENUM(BlueprintType)
enum class EVoxelStrateOpClass : uint8
{
ConstantRockSource = 0,
ConstantVoidSource = 1,
RoomGraphSource = 2,
LatticeCorridorSource = 3,
ShaftFieldSource = 4,
IslandBlobSource = 5,
NoiseRibbonSource = 6,
SdfRoughnessMod = 7,
SdfCarve = 8,
SdfFill = 9,
GridColumnMod = 10,
CaveRoughnessMod = 11,
CaveTerraceMod = 12,
LayerLineMod = 13,
RibbingMod = 14,
CaveOverhangMod = 15,
CaveCliffMod = 16,
ScallopMod = 17,
CaveArchMod = 18,
RoomColumnMod = 19,
DomeMod = 20,
PinchMod = 21,
FloorBiasMod = 22,
WormFieldSource = 23,
ShaftLedgeMod = 24,
DensityNoiseCarveMod = 25,
DensityNoiseFillMod = 26,
};
/** One creative op in the serialisable structure manifest. */
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelOpRecipeEntry
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
EVoxelStrateOpClass OpClass = EVoxelStrateOpClass::ConstantRockSource;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
EVoxelStrateParamBlock ParamBlock = EVoxelStrateParamBlock::None;
};
/**
* Serialisable creative structure. Structural posts are intentionally absent: the recipe builder
* appends spine, vertical seal, passage carve, and XY edge seal in that fixed order every time.
*/
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelOpStackRecipe
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
EVoxelStrateRootPolarity RootPolarity = EVoxelStrateRootPolarity::RockCarve;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
FVoxelOpRecipeEntry Root;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
FVoxelOpRecipeEntry ShapeSource;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
FVoxelOpRecipeEntry Conversion;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
TArray<FVoxelOpRecipeEntry> Modifiers;
// Which native block supplies the mandatory structural post parameters.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
EVoxelStrateParamBlock StructuralParamBlock = EVoxelStrateParamBlock::None;
};
/** The kind of value represented by one entry of VF_STRATE_PARAM_FIELDS. */
enum class EVoxelStrateFieldKind : uint8
{
Continuous,
Integer,
Boolean,
Enum,
};
/** A field deliberately kept out of the parameter roll after the measured liveness audit. */
struct VOXELFORGE_API FVoxelStrateFieldExclusion
{
FString FieldName;
FString Reason;
};
/** Measured statistics for one field in one archetype's corpus group. */
struct VOXELFORGE_API FVoxelStrateFieldSpread
{
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FString ParamStructName;
FString FieldName;
EVoxelStrateFieldKind Kind = EVoxelStrateFieldKind::Continuous;
bool bExcluded = false;
bool bReflected = false;
// Actual authored corpus samples and synthetic per-operation default samples are kept
// separate in the report. SampleCount is their effective total used for the statistics.
int32 AuthoredSampleCount = 0;
int32 PromotedSampleCount = 0;
int32 DefaultSeedCount = 0;
int32 SampleCount = 0;
double Min = 0.0;
double Max = 0.0;
double Mean = 0.0;
double StdDev = 0.0;
// These are copied from UPROPERTY ClampMin/ClampMax when reflection metadata is available in
// the current build. They are safety limits, never the source of a roll range.
bool bHasClampMin = false;
bool bHasClampMax = false;
double ClampMin = 0.0;
double ClampMax = 0.0;
};
/** Where a member of the offline corpus came from. */
enum class EVoxelStrateCorpusProvenance : uint8
{
Project,
Default,
Promoted,
};
/** The finite, diffable metric summary stored with a promoted strate. */
struct VOXELFORGE_API FVoxelStrateMeasuredMetrics
{
bool bValid = false;
int64 NumSampled = 0;
int64 NumAir = 0;
int64 NumSolid = 0;
float AirFraction = 0.0f;
int32 NumAirComponents = 0;
float LargestComponentShare = 0.0f;
FVector LargestComponentPoint = FVector::ZeroVector;
int64 LargestComponentCells = 0;
int32 NumComponentsAtLeast1Pct = 0;
float WalkableFraction = 0.0f;
int64 WalkableFloorColumns = 0;
float WalkableFloorAreaFraction = 0.0f;
int32 NumWalkableSurfaceComponents = 0;
int64 LargestWalkableSurfaceColumns = 0;
float LargestWalkableSurfaceShare = 0.0f;
float MedianFeatureScale = 0.0f;
int32 MedianVerticalClearance = 0;
bool bPlayerFitResolved = false;
FString PlayerFitRefusalReason;
int64 NumPlayerFitCells = 0;
float PlayerFitFraction = 0.0f;
int32 NumTraversableComponents = 0;
int64 LargestTraversableComponentCells = 0;
float TraversableComponentShare = 0.0f;
float MinimumPlayerClearanceVoxels = 0.0f;
int32 ResolvedMarginVoxels = 0;
int32 SampledMinZ = 0;
int32 SampledMaxZ = 0;
int32 SampledNumX = 0;
int32 SampledNumY = 0;
int32 SampledNumZ = 0;
float SampledMinX = 0.0f;
float SampledMaxX = 0.0f;
float SampledMinY = 0.0f;
float SampledMaxY = 0.0f;
// This is the component-size audit trail, not a retained density grid. It is bounded by the
// number of discovered air components and is useful when reviewing a promoted record.
TArray<int64> AirComponentCells;
bool IsUsable() const;
};
/** Copy the measured facts that are safe and useful to persist; no sampler or grid is retained. */
VOXELFORGE_API FVoxelStrateMeasuredMetrics VF_SummarizeStrateMetrics(
const FVoxelStrateMetrics& Metrics);
/** Native parameter storage for every currently supported strate archetype. */
struct VOXELFORGE_API FVoxelStrateArchetypeParams
{
FStrateGenerationParams TunnelNetworkParams;
FSlabGenerationParams SlabParams;
FMazeGenerationParams MazeParams;
FSurfaceGenerationParams SurfaceParams;
FVerticalShaftParams VerticalShaftParams;
FFloatingIslandParams FloatingIslandParams;
};
/**
* One lateral region in a strate.
*
* This is deliberately a plain manifest record rather than a label on the strate. The recipe
* and the native vector belong to the region, and the runtime combines the resulting density
* fields. Structural posts are not region-owned.
*/
struct VOXELFORGE_API FVoxelStrateRegion
{
int32 RegionIndex = 0;
int32 Seed = 0;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams ArchetypeParams;
bool bUsesRecipe = false;
FVoxelOpStackRecipe Recipe;
};
/**
* Offline-composed lateral region manifest for one strate.
*
* `StrateIndex` is part of the partition identity. A candidate can be moved to another live
* slot, so the editor hand-off must call VF_RekeyStrateRegionManifest before installing it.
* `PartitionSeed` is retained in the manifest so the runtime never has to recreate an RNG stream.
*/
struct VOXELFORGE_API FVoxelStrateRegionManifest
{
bool bValid = false;
FString FailureReason;
int32 Seed = 0;
int32 StrateIndex = 0;
int32 RegionCount = 1;
uint32 PartitionSeed = 0;
// A 32-voxel chunk sees a tiny portion of this lattice. The values are manifest data, not
// hidden runtime tuning knobs; changing them changes the authored world and its hash.
float LatticeCellSize = 256.0f;
float BlendWidth = 24.0f;
// One global structural post configuration. Region recipes may use different native blocks,
// but no region is allowed to choose the spine/seal/passage/edge-seal values independently.
bool bHasGlobalStructuralParams = false;
EVoxelStrateParamBlock StructuralParamBlock = EVoxelStrateParamBlock::None;
float StrateTopWorldZ = 0.0f;
float StrateBottomWorldZ = 0.0f;
float BoundarySealThickness = 0.0f;
float BaseDensity = 8.0f;
TArray<FVoxelStrateRegion> Regions;
bool IsSingleRegion() const { return RegionCount == 1 && Regions.Num() == 1; }
bool IsValid() const
{
return bValid && RegionCount >= 1 && RegionCount <= 3
&& Regions.Num() == RegionCount
&& FMath::IsFinite(LatticeCellSize) && LatticeCellSize > 0.0f
&& FMath::IsFinite(BlendWidth) && BlendWidth >= 0.0f;
}
};
/** Result of the positional Voronoi query used by the lateral density combiner. */
struct VOXELFORGE_API FVoxelStrateRegionQuery
{
int32 PrimaryRegion = 0;
int32 NeighborRegion = INDEX_NONE;
float NeighborWeight = 0.0f;
float NearestDifferentRegionGap = FLT_MAX;
// False means the bounded lattice neighborhood could not prove that its different-region
// candidate is the nearest one. Density evaluation may still use a zero blend weight, but a
// ClassifyBox proof must refuse to call that an infinite boundary distance.
bool bNearestDifferentRegionKnown = false;
bool bInBlendBand = false;
};
/** Conservative XY proof used before a tile may be skipped. */
struct VOXELFORGE_API FVoxelStrateRegionBoxProof
{
bool bProvablySingleRegion = false;
bool bTouchesRegionBoundary = true;
bool bTouchesBlendBand = true;
};
/** One hash-lattice site retained by the per-chunk partition cache. */
struct VOXELFORGE_API FVoxelStrateRegionSite
{
int32 CellX = 0;
int32 CellY = 0;
int32 Region = 0;
float WorldX = 0.0f;
float WorldY = 0.0f;
};
/**
* Worker-local cache for the lateral partition.
*
* Hash/site discovery and the integer XY grid are prepared once for a chunk. The voxel path
* then does an O(1) lookup for the ordinary integer density samples; only fractional gradient
* probes use the small prepared site list. No seed lattice is searched or allocated per voxel.
*/
struct VOXELFORGE_API FVoxelStrateRegionPartitionCache
{
void PrepareForChunk(const FVoxelStrateRegionManifest& Manifest,
const FIntVector& ChunkCoord);
FVoxelStrateRegionQuery Query(float WorldX, float WorldY) const;
private:
int32 RegionCount = 1;
uint32 PartitionSeed = 0;
float LatticeCellSize = 256.0f;
float BlendWidth = 24.0f;
int32 BaseX = 0;
int32 BaseY = 0;
int32 Dim = 0;
TArray<FVoxelStrateRegionSite> Sites;
TArray<FVoxelStrateRegionQuery> IntegerSamples;
};
/** Set every native family’s runtime Z bounds to the same strate interval. */
VOXELFORGE_API void VF_SetStrateArchetypeRuntimeBounds(
FVoxelStrateArchetypeParams& Params, float TopWorldZ, float BottomWorldZ);
/** Stable identity helpers for the deterministic lateral partition. */
#if WITH_EDITOR
VOXELFORGE_API int32 VF_RollStrateRegionCount(int32 Seed, int32 StrateIndex);
#endif
VOXELFORGE_API uint32 VF_GetStrateRegionPartitionSeed(int32 Seed, int32 StrateIndex);
VOXELFORGE_API void VF_RekeyStrateRegionManifest(
FVoxelStrateRegionManifest& Manifest, int32 Seed, int32 StrateIndex);
/** Pure positional query; no retained state and no ordering-dependent RNG. */
VOXELFORGE_API FVoxelStrateRegionQuery VF_QueryStrateRegion(
const FVoxelStrateRegionManifest& Manifest, float WorldX, float WorldY);
/** Conservative proof for an XY projection of a voxel box. */
VOXELFORGE_API FVoxelStrateRegionBoxProof VF_AnalyzeStrateRegionBox(
const FVoxelStrateRegionManifest& Manifest, const FBox& VoxelBox);
/** One known-good authored vector and the archetype that gives it meaning. */
struct VOXELFORGE_API FVoxelStrateCorpusEntry
{
FString SourcePath;
FString SourceName;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
// Native storage is selected by Archetype. Keeping every family here makes the corpus
// type-safe without changing any runtime generation parameter struct.
FVoxelStrateArchetypeParams ArchetypeParams;
// Compatibility view for callers of the original Tier 4a API. It is meaningful for
// TunnelNetwork/Underwater entries and is the same value as ArchetypeParams.TunnelNetworkParams.
FStrateGenerationParams Params;
// The current asset format has no corpus weight field. Loaded hand-authored entries therefore
// use 1.0. The field remains explicit so a future offline corpus can assign weights without
// changing the roll algorithm.
float Weight = 1.0f;
EVoxelStrateCorpusProvenance Provenance = EVoxelStrateCorpusProvenance::Project;
// Promoted entries retain their recipe and re-measured facts so the next policy decision can
// compare measured geometry, not only parameter-space distance. Project/default entries leave
// these fields unset unless an offline simulation explicitly measures them.
bool bHasRecipe = false;
FVoxelOpStackRecipe Recipe;
bool bHasMeasuredMetrics = false;
FVoxelStrateMeasuredMetrics MeasuredMetrics;
FString PromotionRecordId;
int32 PromotionSeason = INDEX_NONE;
int32 PromotionSeed = 0;
int32 PromotionCandidateIndex = INDEX_NONE;
uint32 PromotionInputCorpusHash = 0;
};
/** A candidate record that is eligible to be considered for corpus promotion. */
struct VOXELFORGE_API FVoxelStratePromotableRecord
{
FString RecordId;
int32 Season = 0;
int32 Seed = 0;
int32 CandidateIndex = INDEX_NONE;
uint32 InputCorpusHash = 0;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams Params;
FVoxelOpStackRecipe Recipe;
FVoxelStrateMeasuredMetrics MeasuredMetrics;
// These are recorded evidence for review. The loader deliberately ignores the stored metric
// and gate values and asks its verifier to rebuild/re-measure the recipe instead.
bool bPassedNonVacuous = false;
bool bPassedLargestComponent = false;
bool bPassedPrimordialLaw = false;
};
/** Fresh validation output used by the safe promoted-record loader. */
struct VOXELFORGE_API FVoxelStratePromotionVerification
{
bool bPassedNonVacuous = false;
bool bPassedLargestComponent = false;
bool bPassedPrimordialLaw = false;
FVoxelStrateMeasuredMetrics MeasuredMetrics;
FString FailureReason;
bool PassedAllGates() const
{
return bPassedNonVacuous && bPassedLargestComponent && bPassedPrimordialLaw
&& MeasuredMetrics.IsUsable();
}
};
/**
* World-specific re-measurement seam for promoted records.
*
* A corpus store cannot know the active fixture, mouths, or validation window. Its caller must
* provide this verifier; a null verifier means promoted records are skipped rather than trusted.
*/
class VOXELFORGE_API IVoxelStratePromotionVerifier
{
public:
virtual ~IVoxelStratePromotionVerifier() = default;
virtual bool Verify(const FVoxelStratePromotableRecord& Record,
FVoxelStratePromotionVerification& OutVerification) const = 0;
// Optional companion audit for project/default members. A policy pass can then compare a
// candidate against the measured corpus rather than falling back to parameter identity. A
// world-specific verifier may return false when that fixture cannot measure the member.
virtual bool MeasureCorpusEntry(const FVoxelStrateCorpusEntry& Entry,
FVoxelStrateMeasuredMetrics& OutMetrics) const
{
(void)Entry;
(void)OutMetrics;
return false;
}
};
/** Explicit, provisional Tier 5 policy. The owner can change these after seeing the preview. */
struct VOXELFORGE_API FVoxelStratePromotionPolicy
{
// Five normalized measured dimensions are compared with Euclidean distance. 0.20 is large
// enough to reject close copies while still admitting genuinely different geometry; it is a
// provisional review threshold, not a claim that this is the owner's final definition of good.
double MinimumNormalizedMeasuredMetricDistance = 0.20;
int32 MaxPromotionsPerSeason = 6;
// Physical normalization for the two unbounded-in-principle metric dimensions. Air fraction,
// largest share, and walkable fraction already live in [0,1].
float FeatureScaleNormalizationVoxels = 256.0f;
float ClearanceNormalizationVoxels = 64.0f;
};
/** Counters and accepted records from one deterministic policy pass. */
struct VOXELFORGE_API FVoxelStratePromotionBatchResult
{
bool bPolicyValid = false;
TArray<FVoxelStratePromotableRecord> Promoted;
int32 NumCandidates = 0;
int32 NumValidationRejected = 0;
int32 NumCorpusHashRejected = 0;
int32 NumNearDuplicateRejected = 0;
int32 NumCapRejected = 0;
double MinimumAcceptedDistance = 0.0;
};
/** One line of the diffable season manifest written beside the cumulative record store. */
struct VOXELFORGE_API FVoxelStrateSeasonManifest
{
int32 Season = 0;
int32 Seed = 0;
uint32 InputCorpusHash = 0;
int32 CandidateCount = 0;
int32 SurvivorCount = 0;
int32 PromotedCount = 0;
int32 CorpusSize = 0;
int32 ProjectCount = 0;
int32 DefaultCount = 0;
int32 PromotedCorpusCount = 0;
float SurvivalRate = 0.0f;
double MeasuredSpread = 0.0;
};
/** Result of one deterministic roll, including provenance for the offline report. */
struct VOXELFORGE_API FVoxelStrateRollInfo
{
bool bValid = false;
FString FailureReason;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams ArchetypeParams;
// Compatibility view for the original tunnel-only API. For a non-tunnel roll, the native
// result is in ArchetypeParams and this member is not the active candidate vector.
FStrateGenerationParams Params;
TArray<int32> ParentEntryIndices;
TArray<float> ParentWeights;
int32 DominantParentPosition = INDEX_NONE;
// Provenance for the starved-corpus fallback. Names are keyed as "archetype:field" so a
// report can distinguish field descriptors from the number of times they were applied.
int32 BootstrapJitterFieldCount = 0;
TArray<FString> BootstrapJitterFieldNames;
};
#if WITH_EDITOR
/**
* One candidate materialised by the same deterministic calls used by the offline composer tests.
* This is an editor-only hand-off object: it is never a reflected asset and never part of the
* shipping generation contract.
*/
struct VOXELFORGE_API FVoxelStrateComposerCandidate
{
bool bValid = false;
FString FailureReason;
int32 Seed = 0;
int32 Index = 0;
bool bStructureRoll = false;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams ArchetypeParams;
FVoxelOpStackRecipe Recipe;
FVoxelStrateRegionManifest Regions;
// Kept so automation/editor callers can audit the exact six independent structure blocks
// without re-rolling them through a second implementation.
FVoxelStrateRollInfo ParameterRoll;
TArray<FVoxelStrateRollInfo> StructureBlockRolls;
};
#endif
/**
* Known-good strate vectors plus their measured field spread.
*
* `LoadFromAssetRegistry` enumerates every project UVoxelStrateDefinition through the Asset
* Registry, explicitly excluding Saved/Autosaves and Saved/Cooked copies. It also adds one
* known-good default vector for every archetype/family in VoxelStrateTypes.h, then optionally
* loads the cumulative promoted-record JSON store. Promoted records enter only after the caller's
* world-specific verifier rebuilds and remeasures them; with no verifier they are skipped safely.
* Parents are selected only inside the exact archetype group; no cross-archetype blend is attempted.
*/
class VOXELFORGE_API FVoxelStrateCorpus
{
public:
void Reset();
bool LoadFromAssetRegistry(
FString& OutReport,
const FString& PromotedStorePath = FString(),
const IVoxelStratePromotionVerifier* PromotionVerifier = nullptr);
bool LoadFromDefinitions(const TArray<UVoxelStrateDefinition*>& Definitions, FString& OutReport);
/** Add an already-resolved vector, primarily for offline tools and focused tests. */
bool AddEntry(const FString& SourcePath, const FString& SourceName,
ECaveGeneratorType Archetype, const FStrateGenerationParams& Params,
float Weight = 1.0f);
bool AddEntry(const FString& SourcePath, const FString& SourceName,
ECaveGeneratorType Archetype, const FVoxelStrateArchetypeParams& Params,
float Weight = 1.0f);
/** Load cumulative promoted records, re-verifying every record before it enters Entries. */
bool LoadPromotedRecords(const FString& StorePath,
const IVoxelStratePromotionVerifier* PromotionVerifier,
FString& OutReport);
/** Attach an offline measurement to an existing member without changing corpus identity. */
bool SetMeasuredMetrics(const FString& SourcePath,
const FVoxelStrateMeasuredMetrics& Metrics);
bool IsValid() const { return bSchemaValid && Entries.Num() > 0; }
bool IsSchemaValid() const { return bSchemaValid; }
const FString& GetSchemaError() const { return SchemaError; }
int32 Num() const { return Entries.Num(); }
const TArray<FVoxelStrateCorpusEntry>& GetEntries() const { return Entries; }
const TArray<FVoxelStrateFieldSpread>& GetFieldSpreads() const { return FieldSpreads; }
int32 NumForArchetype(ECaveGeneratorType Archetype) const;
int32 NumForProvenance(EVoxelStrateCorpusProvenance Provenance) const;
const FVoxelStrateFieldSpread* FindSpread(
ECaveGeneratorType Archetype, const FString& FieldName) const;
const FVoxelStrateFieldSpread* FindSpread(const FString& FieldName) const;
/** Stable hash of source paths, archetypes, weights, and every listed scalar corpus field. */
uint32 GetContentsHash() const;
/** Explicit exclusion list retained after the fixed-lattice terrain-detail liveness audit. */
static const TArray<FVoxelStrateFieldExclusion>& GetNonTunableFields();
/** True in an editor build where UPROPERTY metadata was available while spreads were built. */
bool AreClampMetadataAvailableAtBuild() const { return bClampMetadataAvailableAtBuild; }
/** False by policy: metadata is not promised to survive into a cooked runtime. */
bool AreClampMetadataAvailableAtRuntime() const { return false; }
private:
bool AddEntryInternal(const FString& SourcePath, const FString& SourceName,
ECaveGeneratorType Archetype,
const FVoxelStrateArchetypeParams& Params,
float Weight, EVoxelStrateCorpusProvenance Provenance,
const FVoxelOpStackRecipe* Recipe,
const FVoxelStrateMeasuredMetrics* Metrics,
const FString& PromotionRecordId,
int32 PromotionSeason,
int32 PromotionSeed,
int32 PromotionCandidateIndex,
uint32 PromotionInputCorpusHash,
bool bRebuild);
void RebuildSpreads();
TArray<FVoxelStrateCorpusEntry> Entries;
TArray<FVoxelStrateFieldSpread> FieldSpreads;
TArray<FString> SkippedDefinitions;
FString SchemaError;
bool bSchemaValid = true;
bool bClampMetadataAvailableAtBuild = false;
};
/** Human-readable name used by the offline report. */
VOXELFORGE_API const TCHAR* VF_GetStrateArchetypeName(ECaveGeneratorType Archetype);
VOXELFORGE_API const TCHAR* VF_GetStrateCorpusProvenanceName(
EVoxelStrateCorpusProvenance Provenance);
/** Full provenance result. Pure with respect to the corpus: no global or retained RNG state. */
VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailed(
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 Index);
/** Roll one exact native family, used to feed the independent blocks of a novel recipe. */
VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailedForArchetype(
const FVoxelStrateCorpus& Corpus, ECaveGeneratorType Archetype, int32 Seed, int32 Index);
/** Pure structure roll: no retained RNG state and no corpus/runtime dependency. */
VOXELFORGE_API FVoxelOpStackRecipe VF_RollStrateStructure(int32 Seed, int32 Index);
/** Roll one-to-three independent region recipes/vectors plus their deterministic partition. */
VOXELFORGE_API FVoxelStrateRegionManifest VF_RollStrateRegionManifest(
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 StrateIndex, bool bRollStructure);
/**
* ⛔ THE LATERAL-REGION GATE. Returns false, and must keep returning false until the seam law holds.
*
* Lateral regions (archetypes mixing inside one strate — "tunnel network leading to a big chamber")
* are BUILT and MEASURED but NOT SHIPPABLE. Measured 2026-09-05: `arrival -> departure` across a
* region seam passes **9 of 16 seeds**, against 11/16 for same-region controls in the same harness.
*
* Density blending alone does not guarantee a player can cross a seam. A strate that mixes archetypes
* and strands you at the boundary is strictly worse than one that does not mix — so this stays off.
*
* ⚠️ DO NOT flip this to true to "enable the feature". The fix is an explicit corridor/landing
* contract across seams (a design decision, deliberately not made unilaterally), after which the
* measured rate must be 16/16. `VoxelForge.Composer.LateralRegions` asserts that this gate is shut
* whenever the rate is imperfect, so flipping it without fixing the law turns the suite red.
*
* Safe today regardless: single-region output is bit-identical to legacy, and cross-boundary box
* verdicts returned 0 violations over 199,800 brute-forced samples (all correctly `Mixed`).
*/
VOXELFORGE_API bool VF_LateralRegionsAreShippable();
/**
* Provenance for a parameter block that does not read the authored corpus.
*
* NaiveUniform is the §3.3 control: every scalar is rolled independently inside the same
* finite code-declared envelope used by the constraint arm. ConstraintSampled draws the
* independent geometry quantities and derives the coupled quantities from generator equations.
* Neither mode reads a strate asset or a corpus entry.
*/
enum class EVoxelStrateCorpusFreeSamplingMode : uint8
{
NaiveUniform,
ConstraintSampled,
};
/** Roll one native parameter family without consulting FVoxelStrateCorpus. */
VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsCorpusFree(
ECaveGeneratorType Archetype,
int32 Seed,
int32 Index,
EVoxelStrateCorpusFreeSamplingMode Mode,
float StrateHeightInVoxels);
/** Validate the hard geometry relations used by ConstraintSampled and the corpus audit. */
VOXELFORGE_API bool VF_ValidateStrateCorpusFreeConstraints(
ECaveGeneratorType Archetype,
const FVoxelStrateArchetypeParams& Params,
float StrateHeightInVoxels,
FString& OutViolation);
/** Normalized Euclidean distance in the five policy metric dimensions. */
VOXELFORGE_API double VF_NormalizedMeasuredMetricDistance(
const FVoxelStrateMeasuredMetrics& A,
const FVoxelStrateMeasuredMetrics& B,
const FVoxelStratePromotionPolicy& Policy);
/** Select deterministic, validated, varied records for one season. */
VOXELFORGE_API FVoxelStratePromotionBatchResult VF_SelectStratePromotions(
const FVoxelStrateCorpus& Corpus,
const TArray<FVoxelStratePromotableRecord>& Candidates,
const FVoxelStratePromotionPolicy& Policy);
/** Write/readable cumulative JSON store; records are sorted by RecordId before writing. */
VOXELFORGE_API bool VF_SaveStratePromotedRecords(
const FString& StorePath,
const TArray<FVoxelStratePromotableRecord>& Records,
FString& OutReport);
/** Write the matching season summary beside the promoted-record store. */
VOXELFORGE_API bool VF_SaveStrateSeasonManifest(
const FString& ManifestPath,
const FVoxelStrateSeasonManifest& Manifest,
FString& OutReport);
#if WITH_EDITOR
/**
* Roll one candidate for the editor walk-through path. Parameter rolls call
* VF_RollStrateParamsDetailed; structure rolls call VF_RollStrateStructure and the exact six
* VF_RollStrateParamsDetailedForArchetype block rolls used by the structure test.
*/
VOXELFORGE_API FVoxelStrateComposerCandidate VF_RollStrateCandidate(
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 Index, bool bRollStructure);
/**
* Build a parameter-roll candidate through the same archetype-to-stack mapping used by production
* GetDensityAt/ClassifyTile. This is editor-only so the offline composer remains absent from a
* shipping build; the result is used only for the post-apply density sanity check.
*/
VOXELFORGE_API bool VF_BuildNativeStrateStackForCandidate(
ECaveGeneratorType Archetype,
const FVoxelStrateArchetypeParams& Params,
int32 Seed,
float SpineRadius,
float WorldRadiusVoxels,
float EdgeSealThickness,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext);
/** Build the persisted record from the exact editor candidate and its just-measured gate facts. */
VOXELFORGE_API FVoxelStratePromotableRecord VF_MakeStratePromotableRecord(
const FVoxelStrateComposerCandidate& Candidate,
const FVoxelStrateMetrics& Metrics,
int32 Season,
uint32 InputCorpusHash,
bool bPassedNonVacuous,
bool bPassedLargestComponent,
bool bPassedPrimordialLaw);
#endif
/** Stable compact representation and equality/hash helpers for manifests and reports. */
VOXELFORGE_API FString VF_FormatStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe);
VOXELFORGE_API uint32 VF_HashStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe);
VOXELFORGE_API bool VF_AreStrateStructureRecipesIdentical(
const FVoxelOpStackRecipe& A, const FVoxelOpStackRecipe& B);
/** Bitwise comparison over exactly the fields in FStrateGenerationParams::Lerp. */
VOXELFORGE_API bool VF_AreStrateParamsBitIdentical(
const FStrateGenerationParams& A, const FStrateGenerationParams& B);
/** Bitwise comparison of the native vector for one archetype family. */
VOXELFORGE_API bool VF_AreStrateArchetypeParamsBitIdentical(
const FVoxelStrateArchetypeParams& A,
const FVoxelStrateArchetypeParams& B,
ECaveGeneratorType Archetype);
/**
* Normalised distance from the measured centroid of one exact-archetype corpus group.
*
* This is an offline ordering aid for the preview only. Continuous/integer fields are measured
* in their corpus units; zero-spread fields use a conservative scale floor so the value remains
* finite without turning clamps into roll ranges. It never participates in generation.
*/
VOXELFORGE_API double VF_DistanceFromStrateCorpusCentroid(
const FVoxelStrateCorpus& Corpus,
const FVoxelStrateArchetypeParams& Params,
ECaveGeneratorType Archetype);
@@ -0,0 +1,394 @@
// VoxelStrateDefinition.h
// Data asset defining everything about a single strate (layer) type.
//
// HOW TO USE:
// -----------
// 1. Right-click in Content Browser → Miscellaneous → Data Asset
// 2. Pick "VoxelStrateDefinition" as the class
// 3. Fill in the fields — cave shape, visuals, content lists, audio, gameplay tags
// 4. Reference it from VoxelSettings (StratePool or FixedStrates)
//
// Each asset is a strate TYPE (e.g., "CrystalCaverns").
// Multiple strate slots can use the same definition.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "GameplayTagContainer.h"
#include "VoxelStrateTypes.h"
#include "VoxelBiomeTypes.h"
#include "Templates/SubclassOf.h" // IWYU : TSubclassOf<AActor> (Atmosphere/Ceiling/FloorLayerActor)
#include "VoxelStrateDefinition.generated.h"
class UVoxelBiomeDefinition;
// IWYU : tous en pointeur ou en paramètre de TSubclassOf ⇒ déclarations avant suffisantes.
// Le PCH partagé les fournissait ; `FPSemantics = Precise` (AUDIT §C9) nous en prive.
// All pointer-only or TSubclassOf parameters, so forward declarations suffice.
class UMaterialInterface;
class USoundBase;
class AActor;
/**
* UVoxelStrateDefinition — The content bag for a strate type.
*
* This is the central piece of the strate system. Every field here
* defines what makes a strate unique: how its caves form, what it
* looks like, what lives in it, how it sounds, and what gameplay
* rules apply.
*
* EXAMPLE STRATES:
* - "Crystal Caverns": big open caves, blue fog, crystal decorations
* - "Tight Tunnels": narrow caves, dark, spider creatures
* - "Flooded Galleries": flat floors, water, diving required
*/
UCLASS(BlueprintType)
class VOXELFORGE_API UVoxelStrateDefinition : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
virtual void PostLoad() override;
//=========================================================================
// IDENTIFICATION
//=========================================================================
// Human-readable name for this strate type (shown in editor and debug)
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Strate")
FText StrateName;
// Short description for editor tooltips
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Strate", meta = (MultiLine = true))
FText StrateDescription;
//=========================================================================
// DIMENSIONS
//=========================================================================
// How many chunks tall this strate is.
// Each strate can have a different height!
// Human-scale defaults use 16 chunks = 64 m. This gives a 20 m+ vertical budget for
// cathedral rooms, tall shafts, and the surface sky cap without changing the lateral gate.
// 1 chunk = 32 voxels x 12.5cm = 4 m; 16 chunks preserves the 64 m authored default.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (ClampMin = "1", ClampMax = "256"))
int32 StrateHeightInChunks = 16;
//=========================================================================
// BOUNDARY TRANSITION
//=========================================================================
// Controls how this strate blends with the strate BELOW it.
// Each boundary between two strates uses the UPPER strate's transition type.
//
// Gradient: Smooth param lerp — no visible boundary (default).
// Hard: Abrupt switch — creates a cliff/ledge at the boundary.
// Interleaved: 3D noise warps the boundary — fingers of one strate
// reach into the other for an organic, interlocking look.
//
// NOTE: The BOTTOM-MOST strate's transition type is unused (nothing below it).
// Which transition style to use at this strate's lower boundary
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
meta = (ToolTip = "How this strate transitions to the strate below it. Gradient = smooth blend, Hard = sharp cliff, Interleaved = noisy interlocking fingers."))
EVoxelStrateTransition TransitionType = EVoxelStrateTransition::Gradient;
// How many chunks the transition zone spans (used by Gradient and Interleaved).
// For Hard transitions this is ignored (effectively 0).
// Higher = wider, more gradual transition zone.
// 1 → very narrow transition (almost hard)
// 2 → moderate (good default)
// 4 → wide, gentle blend across many chunks
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
meta = (ClampMin = "1", ClampMax = "8", EditCondition = "TransitionType != EVoxelStrateTransition::Hard"))
int32 TransitionBlendChunks = 2;
//=========================================================================
// GENERATOR TYPE
//=========================================================================
// Controls which density strategy this strate uses.
//
// TunnelNetwork → classic rooms + tunnels + worm noise (FStrateGenerationParams below)
// FlatPlain → horizontal void between floor and ceiling (FSlabGenerationParams below)
// CrystalChamber → same as FlatPlain but ceiling has heavy downward formations
//
// Changing this shows/hides the relevant parameter sections below.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation")
ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork;
/**
* Authoritative per-strate worm switch. Existing assets default to enabled. The manager
* resolves a disabled strate to WormStrength == 0 before any fused, stack, composer, or
* interval path sees its parameters, so this is a geometry switch rather than a local CVar.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation",
meta = (DisplayName = "Enable Worms"))
bool bEnableWorms = true;
//=========================================================================
// TUNNEL NETWORK PARAMS (shown only for TunnelNetwork generator type)
//=========================================================================
// These params control how the density field is shaped for chunks in this strate.
// The generator reads these values to produce unique cave geometry per layer.
// Only relevant when GeneratorType == TunnelNetwork.
// Used by TunnelNetwork (rooms+tunnels) AND Underwater (same rock, flooded).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation",
meta = (EditCondition = "GeneratorType == ECaveGeneratorType::TunnelNetwork || GeneratorType == ECaveGeneratorType::Underwater"))
FStrateGenerationParams GenerationParams;
//=========================================================================
// SLAB PARAMS (shown only for FlatPlain and CrystalChamber generator types)
//=========================================================================
// Controls the floor height, ceiling height, roughness, and column density
// for open-void strates. Only relevant when GeneratorType is FlatPlain or
// CrystalChamber. Other generator types ignore these entirely.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation",
meta = (EditCondition = "GeneratorType == ECaveGeneratorType::FlatPlain || GeneratorType == ECaveGeneratorType::CrystalChamber"))
FSlabGenerationParams SlabParams;
//=========================================================================
// ARCHETYPE PARAMS (each shown only for its generator type)
//=========================================================================
// Tight branching corridors on a 3D lattice.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation",
meta = (EditCondition = "GeneratorType == ECaveGeneratorType::Maze"))
FMazeGenerationParams MazeParams;
// Open-sky terrain: hills/mountains/plains/beaches under a high ceiling, with water.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation",
meta = (EditCondition = "GeneratorType == ECaveGeneratorType::SurfaceWorld"))
FSurfaceGenerationParams SurfaceParams;
// Mostly-vertical shafts with ledges and sparse horizontal links.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation",
meta = (EditCondition = "GeneratorType == ECaveGeneratorType::VerticalShafts"))
FVerticalShaftParams VerticalShaftParams;
// Suspended land masses floating in a large open void.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation",
meta = (EditCondition = "GeneratorType == ECaveGeneratorType::FloatingIslands"))
FFloatingIslandParams FloatingIslandParams;
//=========================================================================
// BIOMES (vary terrain & content WITHIN this strate — any archetype)
//=========================================================================
// Optional. When empty, this strate generates exactly as before (no biome field,
// bit-identical output). When populated, a deterministic world-XY biome field
// (warped Voronoi + climate, see FBiomeMapParams) assigns regions; each biome can
// supply its OWN archetype params (a mini-strate-variant, output-blended for surface)
// plus its own decorations / atmosphere / water. Surface terrain wired today;
// content/atmosphere work for any archetype.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Biomes")
TArray<UVoxelBiomeDefinition*> Biomes;
// World-XY biome field tuning (cell size, border warp/blend, climate fields).
// Only relevant when Biomes is non-empty. Bake AVoxelWorld::BakeBiomePreview to
// see the resulting map before regenerating the world.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Biomes")
FBiomeMapParams BiomeMapParams;
//=========================================================================
// DISTURBANCES (the "wow" layer — applies on top of ANY archetype)
//=========================================================================
// Chasms, natural bridges and rock ridges layered onto whatever the archetype
// produces, for surprise and variety. All features default to disabled.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Disturbances")
FStrateDisturbanceParams Disturbances;
//=========================================================================
// INTER-STRATE PASSAGES (how THIS strate connects DOWN to the next)
//=========================================================================
// Auto-carved passages from this strate to the one below it: count, style
// (walkable sloped tunnel / worm / spiral / cascade), tapered width, length, placement.
// Every strate has its own finite (0,0) landing room; the passage opens the next room through
// progression. The non-default styles are retained as exotic, non-walkability-guaranteed
// descriptors.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Passages")
FStratePassageConfig PassageConfig;
//=========================================================================
// WATER RENDERING (self-contained, aesthetic — no swim/flow simulation)
//=========================================================================
// The water table HEIGHT comes from the active generator params
// (FSurfaceGenerationParams::WaterLevelRelative or FStrateGenerationParams::
// WaterLevelRelative). These fields control whether/how it is rendered.
// Master switch for the water surface in this strate.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Water")
bool bHasWater = false;
// Translucent material applied to the generated water surface planes.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Water",
meta = (EditCondition = "bHasWater"))
UMaterialInterface* WaterMaterial = nullptr;
// Water tint/colour, available to the material as a parameter if it reads it.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Water",
meta = (EditCondition = "bHasWater"))
FLinearColor WaterColor = FLinearColor(0.02f, 0.08f, 0.12f, 0.8f);
//=========================================================================
// TERRAIN OPERATIONS
//=========================================================================
// Weighted list of terrain operation data assets to apply in this strate.
//
// HOW TO USE:
// 1. Create terrain op assets (Content → Data Asset → VoxelTerrainOpDefinition)
// 2. Add entries here — pick an asset and set its Weight
// 3. Weight scales the op's intensity: 1.0 = as configured, 0.5 = half, 2.0 = double
//
// EXAMPLE:
// "Crystal Caverns" strate might reference:
// - DA_Op_TallColumns (Weight 1.0) → floor-to-ceiling pillars
// - DA_Op_WideDomes (Weight 0.8) → cathedral ceilings
// - DA_Op_LightScallop (Weight 0.5) → subtle erosion texture
//
// The same op asset can be shared across multiple strate definitions.
// At generation time, these are merged into FStrateGenerationParams
// before being passed to the density pipeline.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Terrain Operations")
TArray<FStrateTerrainOpEntry> TerrainOperations;
//=========================================================================
// VISUALS
//=========================================================================
// Material override for this strate (null = use the default VoxelMaterial)
// Allows each strate to have its own rock/ground look
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Visuals")
UMaterialInterface* OverrideMaterial = nullptr;
// Material for the SKY-CAP CEILING tiles of a surface-like strate (the rocky "night sky" overhead).
// Null = ceiling uses the same material as the ground (OverrideMaterial / default VoxelMaterial).
// The ceiling casts no shadow, so under it the ground material reads as flat/bright — give the
// ceiling its own darker/tinted material (or a tinted instance of the terrain material) to sell the
// "lit from within, dark rock overhead" look. Applies only to tiles classified as ceiling.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Visuals")
UMaterialInterface* CeilingMaterial = nullptr;
// Fog color for this strate (used by a future fog system or post-process)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Visuals")
FLinearColor FogColor = FLinearColor(0.05f, 0.05f, 0.1f, 1.0f);
// Fog density (0 = no fog, 1 = very thick)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Visuals", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float FogDensity = 0.3f;
// Ambient light tint (color of the "base" underground light)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Visuals")
FLinearColor AmbientLightColor = FLinearColor(0.1f, 0.1f, 0.15f, 1.0f);
// Ambient light brightness
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Visuals", meta = (ClampMin = "0.0"))
float AmbientLightIntensity = 0.5f;
// Render the managed height fog volumetrically (softer, light-shaft-friendly, costlier).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Visuals")
bool bVolumetricFog = false;
//=========================================================================
// ATMOSPHERE LAYERS (persistent ceiling / floor actors — e.g. seas of clouds)
//=========================================================================
// These spawn ONCE when the player enters this strate (NOT per chunk, so they
// don't churn with streaming) and follow the player in XY while hugging the
// strate's ceiling / floor. Perfect for the "sky island" look: open space
// between two cloud seas. Author the look as a Blueprint (a large plane with a
// cloud/fog material, or a Niagara system). Leave null to disable.
// This is also how you get "fog emitting from ceiling and floor".
// FULL atmosphere override: a Blueprint you author with your own ExponentialHeightFog,
// SkyLight, PostProcessVolume, SkyAtmosphere, etc. — tuned exactly how you want.
// When set, this REPLACES the simple Fog/Ambient knobs above for this strate (the
// plugin's managed fog + skylight are disabled so there's no double-up). The plugin
// spawns one instance when you enter the strate and keeps it anchored to the player.
// Leave null to use the simple knobs instead.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Atmosphere")
TSubclassOf<AActor> AtmosphereActor;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Atmosphere")
TSubclassOf<AActor> CeilingLayerActor;
// Vertical offset (Unreal units) from the strate ceiling. Negative = below the ceiling.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Atmosphere",
meta = (EditCondition = "CeilingLayerActor != nullptr"))
float CeilingLayerZOffset = -200.0f;
// Rotation applied to the ceiling layer actor each update.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Atmosphere",
meta = (EditCondition = "CeilingLayerActor != nullptr"))
FRotator CeilingLayerRotation = FRotator::ZeroRotator;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Atmosphere")
TSubclassOf<AActor> FloorLayerActor;
// Vertical offset (Unreal units) from the strate floor. Positive = above the floor.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Atmosphere",
meta = (EditCondition = "FloorLayerActor != nullptr"))
float FloorLayerZOffset = 200.0f;
// Rotation applied to the floor layer actor each update.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Atmosphere",
meta = (EditCondition = "FloorLayerActor != nullptr"))
FRotator FloorLayerRotation = FRotator::ZeroRotator;
//=========================================================================
// CONTENT LISTS
//=========================================================================
// These arrays define what gets spawned in this strate.
// Future systems (decoration placer, creature spawner) consume these lists.
// Decorations: actors placed ON cave surfaces (stalactites, mushrooms, crystals)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Content")
TArray<FStrateDecoration> Decorations;
// 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.
// 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")
TArray<FStrateLandmark> Landmarks;
// Ambient actors: things floating in cave space (fog volumes, particles, lights)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Content")
TArray<FStrateAmbientActor> AmbientActors;
// Creatures: enemies/NPCs that spawn in this strate
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Content")
TArray<FStrateCreature> Creatures;
//=========================================================================
// AUDIO
//=========================================================================
// Ambient sound loop for this strate (dripping water, wind, etc.)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Audio")
USoundBase* AmbientSound = nullptr;
// Background music for this strate
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Audio")
USoundBase* Music = nullptr;
// Music volume multiplier
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Audio", meta = (ClampMin = "0.0", ClampMax = "2.0"))
float MusicVolume = 1.0f;
//=========================================================================
// GAMEPLAY TAGS
//=========================================================================
// Extensible tag system for gameplay rules.
// Game code checks these tags to apply effects (damage, movement restrictions, etc.)
//
// Examples:
// "Strate.Hazard.Flooded" → water everywhere, need diving gear
// "Strate.Hazard.Toxic" → air damages without mask
// "Strate.Lighting.Dark" → almost no ambient light
// "Strate.Rule.NeedEquipment.Rope" → climbing areas
//
// The plugin doesn't act on these — it just stores them.
// Your game code reads them and applies the appropriate effects.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Gameplay")
FGameplayTagContainer GameplayTags;
};
@@ -0,0 +1,556 @@
// VoxelStrateManager.h
// Runtime system that maps world Z coordinates to strate definitions.
//
// HOW IT WORKS:
// -------------
// 1. At world startup, Initialize() builds the strate layout:
// - Fixed strates go into their assigned slots
// - Random strates are shuffled from a pool using the world seed
// - Each strate's height (in chunks) is accumulated to compute Z ranges
//
// 2. During gameplay, GetStrateAt(WorldZ) returns which strate definition
// applies at a given depth. GetGenerationParams() returns blended params
// (with smooth transitions at strate boundaries).
//
// 3. The generator, world manager, and future systems (decorations, creatures,
// audio) all query this manager to know "what goes here."
#pragma once
#include "CoreMinimal.h"
#include "VoxelStrateTypes.h"
#include "VoxelStrateDefinition.h"
#include "VoxelSeasonManifest.h"
#include "VoxelCaveMorphology.h"
#include "VoxelStrateManager.generated.h"
class UVoxelSettings;
#if WITH_EDITOR
struct FVoxelStrateRegionManifest;
struct FVoxelStrateComposerSlotOverride;
#endif
/**
* FVoxelPassage — A navigable connection between two strates.
*
* Generated deterministically from the world seed during Initialize().
* Each passage is a path from a point in strate A down through the
* boundary seal to a point in strate B. It's carved as a series of
* capsule SDFs in the density function.
*
* Passages are the PRIMARY progression path — players find these
* through exploration. The elevator is a FAST TRAVEL shortcut unlocked later.
*/
USTRUCT()
struct FVoxelPassage
{
GENERATED_BODY()
// Which strates this passage connects (indices into StrateLayout)
int32 UpperStrateIndex = 0; // The strate above
int32 LowerStrateIndex = 0; // The strate below
// World-space positions of the passage endpoints
FVector UpperPoint = FVector::ZeroVector; // Entry in upper strate
FVector LowerPoint = FVector::ZeroVector; // Exit in lower strate
// The deterministic upper-mouth request before source landing-site adjustment. Kept for
// diagnostics/tests so the bounded lateral snap can be measured against the original spine-
// relative placement; not used by the SDF evaluator.
FVector RequestedUpperPoint = FVector::ZeroVector;
// The deterministic lower-mouth request before destination landing-site adjustment. Kept for
// diagnostics/tests so the bounded lateral snap can be measured against the original spine-
// relative placement; not used by the SDF evaluator.
FVector RequestedLowerPoint = FVector::ZeroVector;
// Passage dimensions — how wide the carved tunnel is (in voxels).
// The default SlopedTunnel is a single-file 2.5 m clear bore at its narrowest point;
// authored exotic styles retain their own widths and are not covered by the walkable law.
float Radius = 5.0f;
// The shape/style of this passage. Determines how control points are generated
// and how the passage feels to navigate (shaft, spiral, ledges, crack, etc.).
EVoxelPassageType PassageType = EVoxelPassageType::SlopedTunnel;
// Multi-segment control points for passage shapes. The default SlopedTunnel contains the two
// gentle ramp legs of its deterministic switchback; exotic styles retain their authored chain.
// When non-empty, the SDF evaluator walks this array as a capsule chain.
TArray<FVector> ControlPoints;
// Per-control-point tube radius (parallel to ControlPoints) for tapered tunnels —
// wider chambers at the mouths, a squeeze in the middle, etc. If shorter than
// ControlPoints, the uniform Radius is used as a fallback.
TArray<float> ControlRadii;
// Body-sized, flat-floored chambers at both tube ends. UpperPoint/LowerPoint remain the
// standing anchors returned by the source query; the tube enters each room at DoorPoint.
FVoxelPassageLanding UpperLanding;
FVoxelPassageLanding LowerLanding;
// Construction-time walkability facts for diagnostics and the commandlet report. These are
// metadata, not a second runtime decision: the final-density sampler remains authoritative.
bool bWalkableTunnelContract = false;
// Passage floors are authored once, after the final control chain and both landing floors are
// known. The evaluator uses this immutable profile to turn each tapered capsule into a D
// section; it never invents a support plane from the current voxel.
bool bNativeFloorEnabled = false;
TArray<float> NativeFloorProfileZ;
TArray<float> NativeFloorReliefScales;
float NativeFloorReliefStrength = 0.0f;
float NativeFloorReliefFrequency = 0.015f;
uint32 NativeFloorSeed = 0;
float TunnelMaxGradientDegrees = 0.0f;
float TunnelVerticalDropVoxels = 0.0f;
float TunnelRequiredHorizontalRunVoxels = 0.0f;
float TunnelHorizontalPathLengthVoxels = 0.0f;
float TunnelMinimumClearWidthVoxels = 0.0f;
float TunnelClearHeightVoxels = 0.0f;
// Bounding sphere enclosing the whole passage (+ radius + blend), in voxel coords.
// Computed once in GeneratePassages; lets EvaluateModifierSDF reject far voxels with
// a single squared-distance test instead of walking every segment per voxel.
// BoundRadius (linear) feeds the per-chunk shortlist reach; BoundRadiusSq the per-voxel test.
FVector BoundCenter = FVector::ZeroVector;
float BoundRadius = 0.0f;
float BoundRadiusSq = 0.0f;
};
/**
* FStrateSlot — Runtime info for one strate in the layout.
*
* Created during Initialize() and stored in the StrateLayout array.
* Each slot knows its definition, its Z-range in chunk coordinates,
* and its index in the sequence.
*/
USTRUCT()
struct FStrateSlot
{
GENERATED_BODY()
// The strate definition asset (content bag)
UPROPERTY()
UVoxelStrateDefinition* Definition = nullptr;
// Strate index (0 = topmost, increases downward)
int32 StrateIndex = 0;
// Z chunk range (inclusive). TopZ > BottomZ since Z decreases downward.
// Example: TopZ = 0, BottomZ = -3 means this strate spans chunks Z=0 to Z=-3
int32 TopChunkZ = 0;
int32 BottomChunkZ = 0;
// Height in chunks (from the definition)
int32 HeightInChunks = 4;
};
/**
* UVoxelStrateManager — Maps depth to strate definitions at runtime.
*/
UCLASS(BlueprintType)
class VOXELFORGE_API UVoxelStrateManager : public UObject
{
GENERATED_BODY()
public:
UVoxelStrateManager();
//=========================================================================
// INITIALIZATION
//=========================================================================
/**
* Build the strate layout from settings and seed.
*
* STEPS:
* 1. For each strate index (0 to TotalStrates-1):
* - If it's in FixedStrates → use that definition
* - Else → pick from the shuffled pool (seeded random)
* 2. Stack strates top-to-bottom, accumulating heights
* 3. Store the layout in StrateLayout array
*
* @param Settings - VoxelSettings with pool/fixed strate config
* @param WorldSeed - Seed for randomizing non-fixed strates
*/
/** Returns false without a usable layout; an assigned invalid/stale season never falls back. */
bool Initialize(UVoxelSettings* Settings, int32 WorldSeed);
/**
* Copy the immutable recipe payload for this chunk. In packaged builds this comes only from
* the cooked season; editor candidate overrides use the same read-only worker hand-off.
*/
bool GetRecipeForChunk(
const FIntVector& ChunkCoord, int32& OutRecipeSeed,
ECaveGeneratorType& OutArchetype, FVoxelStrateArchetypeParams& OutParams,
FVoxelOpStackRecipe& OutRecipe) const;
bool IsUsingSeason() const { return !ActiveSeasonContentHash.IsEmpty(); }
const FString& GetSeasonContentHash() const { return ActiveSeasonContentHash; }
int32 GetWorldSeed() const { return CachedSeed; }
/**
* Monotonic identity of this manager UObject instance.
*
* PassagesVersion intentionally starts over for every layout, so it cannot distinguish a
* cached stack built by an older PIE manager that happened to have the same layout version.
* Worker-local generator caches use this identity as their manager-lifetime boundary.
*/
uint64 GetCacheLifetimeId() const { return CacheLifetimeId; }
#if WITH_EDITOR
/**
* Install an editor-only candidate over one already-built slot. The slot's definition,
* dimensions, content, and passages remain intact; only the density archetype/parameters
* (or the materialised recipe) are replaced. The caller must hold the world's generation pause.
*/
bool SetComposerOverrideForStrate(
int32 StrateIndex, int32 CandidateSeed, ECaveGeneratorType Archetype,
const FVoxelStrateArchetypeParams& Params, bool bUseRecipe,
const FVoxelOpStackRecipe* Recipe, FString& OutError,
const FVoxelStrateRegionManifest* Regions = nullptr);
/** Read the immutable editor override for a chunk into worker-local storage. */
bool GetComposerOverrideForChunk(
const FIntVector& ChunkCoord, int32& OutCandidateSeed,
ECaveGeneratorType& OutArchetype, FVoxelStrateArchetypeParams& OutParams,
bool& bOutUseRecipe, FVoxelOpStackRecipe& OutRecipe) const;
/** Read the immutable editor region override into worker-local storage. */
bool GetComposerRegionOverrideForChunk(
const FIntVector& ChunkCoord, FVoxelStrateRegionManifest& OutRegions) const;
#endif
//=========================================================================
// QUERIES
//=========================================================================
/**
* Get the strate definition at a world Z coordinate.
*
* @param WorldZ - Z position in world space (Unreal units)
* @return The strate definition, or nullptr if above/below all strates
*/
UFUNCTION(BlueprintCallable, Category = "Strate")
UVoxelStrateDefinition* GetStrateAt(float WorldZ) const;
/**
* Get the strate index at a world Z coordinate.
*
* @param WorldZ - Z position in world space
* @return Strate index (0 = topmost), or -1 if outside strate range
*/
UFUNCTION(BlueprintCallable, Category = "Strate")
int32 GetStrateIndex(float WorldZ) const;
/** 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; }
/** Diagnostic-only switch for isolating the inter-strate support writer. */
void SetPassageSupportFloorWritesEnabledForDiagnostics(bool bEnabled)
{
bPassageSupportFloorWritesEnabled = bEnabled;
}
bool ArePassageSupportFloorWritesEnabledForDiagnostics() const
{
return bPassageSupportFloorWritesEnabled;
}
/**
* Get the strate definition for a specific chunk coordinate.
*
* @param ChunkCoord - The chunk position
* @return The strate definition, or nullptr if outside strate range
*/
UVoxelStrateDefinition* GetStrateForChunk(const FIntVector& ChunkCoord) const;
/**
* Strate-aware vertical streaming: fills the chunk-Z span of the strate containing
* ChunkZ. Returns false if ChunkZ is in the inter-strate bedrock gap (or outside the
* layout) — the caller then leaves the vertical view unclamped (the gap is a brief
* see-both-sides descent transition). TopChunkZ > BottomChunkZ (Z decreases downward).
*/
bool GetStrateChunkZBounds(int32 ChunkZ, int32& OutTopChunkZ, int32& OutBottomChunkZ) const;
/**
* Get generation params for a chunk, with boundary blending.
*
* BLENDING CONCEPT:
* When a chunk is near a strate boundary (within the definition's TransitionBlendChunks of the edge),
* the params are lerped between the two adjacent strates. This prevents
* hard visual seams where one strate ends and another begins.
*
* @param ChunkCoord - The chunk position
* @return Blended generation params for this chunk
*/
FStrateGenerationParams GetGenerationParams(const FIntVector& ChunkCoord) const;
/**
* Get the generator type for a specific chunk.
*
* Returns TunnelNetwork if the chunk is outside all strates or if the
* strate definition is null. The generator uses this to pick which
* operator stack to build for the chunk.
*
* @param ChunkCoord - The chunk position
* @return The ECaveGeneratorType for the strate containing this chunk
*/
ECaveGeneratorType GetGeneratorTypeForChunk(const FIntVector& ChunkCoord) const;
/**
* True when this chunk lies in a strate slot whose archetype has an operator stack (all eight
* do). False outside the layout or for a slot without a definition. This predicate is the ONLY
* place the archetype list is written down.
*/
bool UsesOperatorStackForChunk(const FIntVector& ChunkCoord) const;
/**
* True if this chunk is in the solid-bedrock GAP between two strates (inside the
* overall stack's Z range but not in any strate slot). Chunks above the top strate
* or below the bottom strate are NOT gaps (they're open air). Driven by
* VoxelSettings::InterStrateGapChunks.
*/
bool IsGapChunk(const FIntVector& ChunkCoord) const;
/**
* Get slab generation params for a chunk, with runtime Z bounds filled in.
*
* Used when GetGeneratorTypeForChunk returns FlatPlain or CrystalChamber.
* Does NOT blend between adjacent strates — slab strates use Hard transition
* at their boundaries (blending between fundamentally different generator types
* is not meaningful).
*
* @param ChunkCoord - The chunk position
* @return FSlabGenerationParams with StrateTopWorldZ / StrateBottomWorldZ set
*/
FSlabGenerationParams GetSlabParamsForChunk(const FIntVector& ChunkCoord) const;
/**
* Per-archetype param getters. Each copies the designer params from the strate
* definition and fills in the runtime Z bounds (voxel coords). Like the slab
* getter, these do NOT blend across boundaries — fundamentally different
* archetypes meet at Hard boundaries.
*/
FMazeGenerationParams GetMazeParamsForChunk(const FIntVector& ChunkCoord) const;
FSurfaceGenerationParams GetSurfaceParamsForChunk(const FIntVector& ChunkCoord) const;
FVerticalShaftParams GetVerticalShaftParamsForChunk(const FIntVector& ChunkCoord) const;
FFloatingIslandParams GetFloatingIslandParamsForChunk(const FIntVector& ChunkCoord) const;
/**
* Flatten the strate's Biomes[] + BiomeMapParams into a POD FBiomeContext for the
* biome field. Returns an empty (invalid) context when the strate has no biomes —
* callers treat that as "biomes disabled" and fall back to base params. The result
* is window-invariant (depends only on the strate at this Z, not the chunk window).
*/
FBiomeContext GetBiomeContextForChunk(const FIntVector& ChunkCoord) const;
/**
* World-space Z (voxel coords) of this strate's water surface, or -FLT_MAX if the
* strate has no water. Derived from the active archetype's WaterLevelRelative and
* the strate's Z range. Used by the water render system.
*/
float GetWaterLevelWorldZForChunk(const FIntVector& ChunkCoord) const;
/**
* Unreal-unit (cm) world Z range of the strate containing WorldZ (Unreal units).
* OutTopZ = ceiling, OutBottomZ = floor. Returns false if WorldZ is outside all strates.
* Used by the atmosphere system to glue ceiling/floor layers to the strate.
*/
bool GetStrateUnrealZRange(float WorldZ, float& OutTopZ, float& OutBottomZ) const;
/**
* Disturbance params for a chunk (chasms/bridges/ridges), with runtime Z bounds
* filled in. Returns an all-disabled default when outside the strate range.
*/
FStrateDisturbanceParams GetDisturbanceParamsForChunk(const FIntVector& ChunkCoord) const;
/**
* Get the full strate layout (for debug display or UI).
*/
const TArray<FStrateSlot>& GetLayout() const { return StrateLayout; }
/**
* Get the total number of strates in the layout.
*/
int32 GetNumStrates() const { return StrateLayout.Num(); }
//=========================================================================
// PASSAGES & ELEVATOR
//=========================================================================
/**
* Evaluate the SDF for all passages and the elevator shaft at a world position.
* Called by the density function to carve these modifiers into the terrain.
*
* Returns: negative = inside a passage/shaft, positive = solid rock.
* FLT_MAX = no modifier nearby.
*/
float EvaluateModifierSDF(float WorldX, float WorldY, float WorldZ) const;
/** Apply the passage tube/landing carve and the guaranteed landing floor in internal density. */
void ApplyPassageModifier(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Apply only the passage SDF carve. The shared MC post owns landing/tunnel support writes. */
void ApplyPassageCarvingOnly(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Reapply the landing-only air carve in internal density (used after visual disturbances). */
void ApplyPassageLandingAir(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Reassert the default walkable tunnel's clear air in internal density after any post. */
void ApplyPassageTunnelAir(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Apply the landing/tunnel MC post in one cached passage scan after disturbances. */
void ApplyPassageStructuralPostsMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/**
* Compose the build-authored passage D-floor after generic MC writers. This is the native
* passage shape's final ownership step, not the diagnostic legacy support slab controlled by
* bPassageSupportFloorWritesEnabled.
*/
void ApplyPassageNativeFloorMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity) const;
/** Re-assert a landing floor after the landing air pass, still before the final XY seal. */
void ApplyPassageLandingFloorMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity) const;
/** Re-assert only the flat room floors after the tunnel-air post; never writes tunnel floors. */
void ApplyPassageLandingRoomFloorMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity) 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;
/** Conservative box guard for the floor fill; unlike a tube it can affect the all-air proof. */
bool AnyPassageLandingFloorNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
/** Exact-lattice counterparts used by ClassifyTile's MC-grid proof. */
bool AnyPassageNearLattice(const FBox& VoxelBox,
const FIntVector& LatticeOrigin, int32 Step) const;
/** Exact maximum of the passage carve factor on the mesher lattice; 0 means no tube/landing
* carve reaches a sampled point. Invalid geometry returns 1 conservatively. */
float MaxPassageCarveFactorNearLattice(const FBox& VoxelBox,
const FIntVector& LatticeOrigin, int32 Step) const;
/** Exact conservative candidate for the post-stack passage air writers on the MC lattice.
* This deliberately excludes the passage modifier already folded by FPassageCarveOp. */
bool AnyPassageAirPostNearLattice(const FBox& VoxelBox,
const FIntVector& LatticeOrigin, int32 Step,
float BaseDensity, float SealThickness) const;
bool AnyPassageLandingFloorNearLattice(const FBox& VoxelBox,
const FIntVector& LatticeOrigin, int32 Step) const;
/** Conservative landing-room plus floor reach for the exact MC lattice and its halo. */
bool AnyPassageLandingNearLattice(const FBox& VoxelBox,
const FIntVector& LatticeOrigin, int32 Step,
float ReachScale = 1.0f) const;
/** Per-tile reach for the modifier itself: capsule chain, landing terms, and carve blend. */
bool AnyPassageCarvingNearLattice(const FBox& VoxelBox,
const FIntVector& LatticeOrigin, int32 Step,
float ReachScale = 1.0f) const;
/** Conservative sphere guard for all passage-owned structural posts and native D-floors. */
bool AnyPassageStructuralPostNearLattice(const FBox& VoxelBox,
const FIntVector& LatticeOrigin, int32 Step,
float ReachScale = 1.0f) const;
/** Exact presence test for a structural floor write on the MC lattice. */
bool AnyLandingFloorAtLattice(const FBox& VoxelBox,
const FIntVector& LatticeOrigin, int32 Step) const;
/** Conservative box guards for the per-strate (0,0) landing rooms and their floor slabs. */
bool AnyOriginLandingNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
bool AnyOriginLandingFloorNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
bool AnyOriginLandingNearLattice(const FBox& VoxelBox,
const FIntVector& LatticeOrigin, int32 Step,
float ReachScale = 1.0f) const;
/** Exact conservative candidate for the post-disturbance origin-room air reassertion. */
bool AnyOriginLandingAirNearLattice(const FBox& VoxelBox,
const FIntVector& LatticeOrigin, int32 Step,
float BaseDensity, float SealThickness) const;
bool AnyOriginLandingFloorNearLattice(const FBox& VoxelBox,
const FIntVector& LatticeOrigin, int32 Step) const;
/** Get all generated passages (for debug display). */
const TArray<FVoxelPassage>& GetPassages() const { return Passages; }
protected:
//=========================================================================
// INTERNAL DATA
//=========================================================================
// The stacked strate layout (index 0 = topmost strate)
UPROPERTY()
TArray<FStrateSlot> StrateLayout;
// Parallel to StrateLayout only while a cooked season is active. Plain immutable data is
// copied into worker-local caches on the existing layout-versioned refetch path.
TArray<FVoxelSeasonStrate> SeasonStrates;
FString ActiveSeasonContentHash;
// Passages connecting consecutive strates
TArray<FVoxelPassage> Passages;
// Bumped every time Passages is rebuilt (GeneratePassages), and for an editor composer
// override. EvaluateModifierSDF and all generator thread_local memos use this to invalidate
// cached passage/strate data after either kind of live change.
uint32 PassagesVersion = 0;
// Never reused for the lifetime of the process. This is deliberately not a UObject pointer:
// allocator address reuse must not make a stale worker-local stack look current.
uint64 CacheLifetimeId = 0;
// World seed (stored for passage generation)
int32 CachedSeed = 0;
// Whether to auto-open the (0,0) origin landing room from above in strate 0.
// Copied from VoxelSettings::bOpenSurfaceEntry during Initialize().
bool bOpenSurfaceEntry = true;
// Authored radius (voxels) of the origin landing room and its optional surface opening.
// Copied from VoxelSettings::OriginSpineRadius.
float OriginSpineRadius = 28.0f;
// Solid-bedrock gap between consecutive strates, in chunks. Copied from
// VoxelSettings::InterStrateGapChunks during Initialize().
int32 InterStrateGapChunks = 0;
// Production keeps the legacy writer available only as a fail-closed fallback for a
// non-native/malformed passage; native D-floors bypass it. This switch never changes the
// passage carve or air contract.
bool bPassageSupportFloorWritesEnabled = true;
// Per-passage tunnel shape now lives on each UVoxelStrateDefinition::PassageConfig
// (the upper strate of each boundary controls its own descent tunnels).
//=========================================================================
// HELPERS
//=========================================================================
// Find which slot a chunk Z coordinate falls into.
// Returns INDEX into StrateLayout, or -1 if not found.
int32 FindSlotIndexForChunkZ(int32 ChunkZ) const;
#if WITH_EDITOR
const FVoxelStrateComposerSlotOverride* FindComposerOverride(int32 StrateIndex) const;
TMap<int32, TSharedPtr<FVoxelStrateComposerSlotOverride>> ComposerOverrides;
#endif
// Build FStrateGenerationParams from a strate definition:
// copies base GenerationParams, then applies all referenced terrain op assets.
// This is the single point where terrain op data assets are merged into params.
static FStrateGenerationParams BuildParamsFromDefinition(const UVoxelStrateDefinition* Definition);
// Generate passages between consecutive strates. Called from Initialize().
void GeneratePassages();
};
@@ -0,0 +1,540 @@
// Read-only, headless measurements over a coarse sample of one strate.
#pragma once
#include "CoreMinimal.h"
#include "VoxelPassageGeometry.h"
class UVoxelGenerator;
class UVoxelStrateManager;
/**
* The pinned metric capsule dimensions in voxel-space.
*
* Capsule measurements are authored in centimetres and converted through VOXEL_SIZE. At
* 12.5 cm/voxel the radius is 2.72 voxels, half-height is 7.04 voxels, and total height is
* 14.08 voxels.
* The measure settings copy these values so a future character can change the query without
* changing generation. The diagnosis test separately reads movement limits from the project's
* character CDO.
*/
struct VOXELFORGE_API FVoxelPlayerCapsuleConstants
{
static constexpr float VoxelSizeCentimeters = VOXEL_SIZE;
static constexpr float VoxelSizeMeters = VoxelPassageGeometry::VoxelSizeMeters;
static constexpr float RadiusCentimeters = 34.0f;
static constexpr float HalfHeightCentimeters = 88.0f;
// UCharacterMovementComponent's UE 5.7 default. Callers that have a character CDO should
// copy its authored value into PlayerMaxStepHeightMeters.
static constexpr float MaxStepHeightCentimeters = 45.0f;
static constexpr float MaxStepHeightMeters = MaxStepHeightCentimeters / 100.0f;
// UE 5.7's WalkableFloorZ=0.71 resolves to about 44.8 degrees; a character CDO may expose
// the precise angle. The diagnosis test copies that precise project value when available.
static constexpr float WalkableFloorAngleDegrees = 44.8f;
static constexpr float RadiusVoxels = RadiusCentimeters / VoxelSizeCentimeters;
static constexpr float HalfHeightVoxels = HalfHeightCentimeters / VoxelSizeCentimeters;
static constexpr float HeightVoxels = VoxelPassageGeometry::PlayerHeightVoxels;
};
/**
* Read-only density source used by the offline Tier 2 pass for a materialised custom stack.
*
* SampleDensity is called concurrently during grid sampling; implementations must be const,
* re-entrant, and safe for concurrent read-only calls, just like UVoxelGenerator::GetDensityAt.
*/
class VOXELFORGE_API IVoxelStrateDensitySampler
{
public:
virtual ~IVoxelStrateDensitySampler() = default;
virtual float SampleDensity(float WorldX, float WorldY, float WorldZ) const = 0;
};
/** Explicit outcome of a coarse connectivity query plus its full-resolution route re-check. */
enum class EVoxelConnectivityResult : uint8
{
Connected,
NotConnectedAtThisResolution, // Evidence only at this grid's resolution; corridors thinner than SampleStep are invisible.
StartCellSolid,
GoalCellSolid,
OutOfWindow,
CoarseLiedBudgetExhausted, // Unknown: retries are exhausted, or the refutation has no coarse edge to exclude.
// Appended to preserve the underlying values of the legacy result codes above.
StartCellNotPlayerFit,
GoalCellNotPlayerFit
};
/** Settings for the deterministic coarse grid used by the strate measurement pass. */
struct VOXELFORGE_API FVoxelStrateMeasureSettings
{
int32 SampleStep = 8; // Voxels between 1 m samples at 12.5 cm/voxel.
int32 RadiusInVoxels = 512; // Half-extent in X and Y around CenterXY (64 m).
FVector2D CenterXY = FVector2D::ZeroVector; // Actor-space voxel coordinates.
// When both points are set, replace the square centered on CenterXY with the XY AABB of the
// two points expanded by CoverMarginVoxels. This keeps a mouth-to-mouth measurement sized by
// the mouths' separation instead of by their distance from the world origin.
TOptional<FVector2D> CoverPointA;
TOptional<FVector2D> CoverPointB;
float CoverMarginVoxels = 96.0f; // 12 m.
// Origin-rooted topologies can route from one mouth to the other through the finite (0,0)
// landing room and its local room/join geometry. There is no hidden radial connector network.
// When true, include (0,0) in the fitted AABB before applying MaxCells. A caller must keep
// this enabled for that topology; an over-cap route window is then refused honestly instead
// of being cropped back to a mouth-only box.
bool bIncludeOriginInCoverWindow = false;
// Diagnostic-only strengthening of the preceding policy: ensure the sampled box contains an
// actual one-voxel column around (0,0), rather than ending exactly at the exclusive upper
// bound when both mouths lie on one side of the origin.
bool bForceOriginColumnInCoverWindow = false;
int32 MaxCells = 8000000; // Refuse a grid larger than this many cells.
int32 MaxRouteRetries = 16; // Alternate coarse routes to full-resolution-check after the first route.
int32 HeadroomCells = 2; // Legacy walkable-column metric only.
int32 InteriorMarginVoxels = -1; // <0 = derive from BoundarySealThickness (2x, clamped); explicit 0 includes the seal.
// Base ACharacter dimensions are authored in centimetres and converted by the voxel scale.
// These are parameters, not
// generation values, so another playable capsule can be measured without a code change.
float PlayerCapsuleRadiusVoxels = FVoxelPlayerCapsuleConstants::RadiusVoxels;
float PlayerCapsuleHalfHeightVoxels = FVoxelPlayerCapsuleConstants::HalfHeightVoxels;
// Character-derived fit parameters. MaxStepHeight is deliberately named and stored in
// metres, matching UCharacterMovementComponent's authored unit. The measurement converts it
// to voxels only for the bounded downward support search.
float PlayerMaxStepHeightMeters = FVoxelPlayerCapsuleConstants::MaxStepHeightMeters;
float PlayerWalkableFloorAngleDegrees = FVoxelPlayerCapsuleConstants::WalkableFloorAngleDegrees;
// A support patch is sampled at the integer columns covered by the capsule radius. 0.75 with
// the default 1.36-voxel radius requires 4 of 5 columns, including the centre column.
float PlayerSupportPatchMinCoverageFraction = 0.75f;
// Traversal is still constrained by the exact player-fit mask, but a planned climbing
// mechanic may cross a higher supported neighbouring column. This is a measurement policy,
// not a runtime movement change. The surface remains required to be supported; the extra
// height only changes whether that supported edge is traversable.
bool bPlayerClimbingEnabled = true;
float PlayerMaxClimbHeightMeters = 3.0f;
};
/**
* Optional capture of the exact grid used by one measurement.
*
* Air is deliberately stored as a polarity bit: 1 means density > 0 (air), 0 means solid.
* Density retains the scalar sample that produced that bit, so an editor preview can draw a
* density=0 contour without inventing geometry by blurring the binary view. Both arrays are
* opt-in so ordinary metrics callers do not retain a multi-million-cell buffer.
* It is an editor/automation hand-off for tools such as the composer preview, not a runtime
* generation cache.
*/
struct VOXELFORGE_API FVoxelStrateSampleGrid
{
bool bValid = false;
int32 NumX = 0;
int32 NumY = 0;
int32 NumZ = 0;
int32 SampleStep = 1;
int32 CellCount = 0;
float MinX = 0.0f;
float MinY = 0.0f;
float MinZ = 0.0f;
float MaxX = 0.0f;
float MaxY = 0.0f;
float MaxZ = 0.0f;
int32 ResolvedMarginVoxels = 0;
int32 SampledMinZ = 0;
int32 SampledMaxZ = 0;
// 1 = air (density > 0), 0 = solid (density <= 0), matching the measurement polarity.
TArray<uint8> Air;
// Exact scalar density at the same cell centre as Air. Present on measurement captures;
// empty on legacy/binary-only callers. This is never used by generation.
TArray<float> Density;
// 1 = the exact player-fit pose predicate accepted this cell, 0 = rejected. This is
// populated when a player-fit walk explicitly captures its shared grid. It is kept beside
// Air so render, walk, and diagnostics can consume one sampled field and one fit mask.
TArray<uint8> PlayerFitMask;
int64 PlayerFitCellCount = 0;
FORCEINLINE int32 Index(int32 X, int32 Y, int32 Z) const
{
return static_cast<int32>(
static_cast<int64>(X)
+ static_cast<int64>(NumX) * (static_cast<int64>(Y)
+ static_cast<int64>(NumY) * static_cast<int64>(Z)));
}
bool IsValid() const
{
return bValid && NumX > 0 && NumY > 0 && NumZ > 0
&& static_cast<int64>(CellCount)
== static_cast<int64>(NumX) * static_cast<int64>(NumY)
* static_cast<int64>(NumZ)
&& Air.Num() == CellCount;
}
bool HasScalarDensity() const
{
return IsValid() && Density.Num() == CellCount;
}
bool HasPlayerFitMask() const
{
return IsValid() && PlayerFitMask.Num() == CellCount;
}
bool ContainsPoint(const FVector& Point) const
{
return IsValid()
&& Point.X >= MinX && Point.X < MaxX
&& Point.Y >= MinY && Point.Y < MaxY
&& Point.Z >= MinZ && Point.Z < MaxZ;
}
};
/** Metrics derived from one coarse grid and its single air flood fill. */
struct VOXELFORGE_API FVoxelStrateMetrics
{
bool bValid = false;
FString RefusalReason;
int64 NumSampled = 0;
int64 NumAir = 0;
int64 NumSolid = 0;
float AirFraction = 0.f;
int32 NumAirComponents = 0;
float LargestComponentShare = 0.f;
FVector LargestComponentPoint = FVector::ZeroVector; // Actor-space voxel coords; valid iff NumAir > 0.
int64 LargestComponentCells = 0;
int32 NumComponentsAtLeast1Pct = 0;
float WalkableFraction = 0.f;
// Distinct sampled XY columns that contain at least one walkable cell, divided by the
// sampled XY footprint. This is a floor-area proxy, not a volume ratio: a large chamber
// can have a small WalkableFraction while retaining a substantial walkable floor.
int64 WalkableFloorColumns = 0;
float WalkableFloorAreaFraction = 0.f;
// Four-neighbour connected components of the projected walkable columns. This deliberately
// remains a 2D surface indicator; it does not prove that floors at different Z values are
// traversable in 3D. The separate arrival/departure route check is the traversal gate.
int32 NumWalkableSurfaceComponents = 0;
int64 LargestWalkableSurfaceColumns = 0;
float LargestWalkableSurfaceShare = 0.f;
float MedianFeatureScale = 0.f;
int32 MedianVerticalClearance = 0;
// Player-fit metrics are answered only by a one-voxel grid. A coarse grid refuses to infer
// support or clearance because SampleStep=4 cannot resolve a 1.36-voxel radius capsule.
bool bPlayerFitResolved = false;
FString PlayerFitRefusalReason;
int64 NumPlayerFitCells = 0;
float PlayerFitFraction = 0.0f; // Accepted free supported poses / all air cells.
int32 NumTraversableComponents = 0;
int64 LargestTraversableComponentCells = 0;
float TraversableComponentShare = 0.0f; // Largest player-fit component / player-fit cells.
// Minimum horizontal Chebyshev clearance, in voxels, among accepted supported poses. This is
// a diagnostic only; player occupancy itself uses a direct capsule stencil, never the old
// Manhattan distance transform.
float MinimumPlayerClearanceVoxels = 0.0f;
// The exact vertical window used by the measurement: [SampledMinZ, SampledMaxZ).
int32 ResolvedMarginVoxels = 0;
int32 SampledMinZ = 0;
int32 SampledMaxZ = 0;
// The exact sampled XY box and dimensions. For a fitted window these are the AABB-derived
// dimensions; for the legacy window they describe CenterXY +/- RadiusInVoxels.
int32 SampledNumX = 0;
int32 SampledNumY = 0;
int32 SampledNumZ = 0;
float SampledMinX = 0.0f;
float SampledMaxX = 0.0f;
float SampledMinY = 0.0f;
float SampledMaxY = 0.0f;
// Air-component sizes in deterministic flood-fill discovery order. Keeping these facts from
// the one required flood fill lets diagnostics classify small non-largest components without
// sampling the source a second time.
TArray<int64> AirComponentCells;
};
/**
* Component facts for one coarse mouth-to-mouth query.
*
* The component shares are fractions of the sampled air cells. The two component distances are
* exact spatial Euclidean distances in coarse-cell coordinates to the nearest cell of the other
* mouth's component. Solid cells do not participate in that proximity calculation because it
* measures geometric separation between air components, not an alternate route. A distance of
* zero means both mouths resolve to the same component. Negative distances mean that the
* requested grid or endpoint could not be analyzed.
*/
struct VOXELFORGE_API FVoxelConnectivityDiagnostics
{
bool bValid = false;
EVoxelConnectivityResult Result = EVoxelConnectivityResult::OutOfWindow;
bool bPlayerFitRestricted = false;
bool bPlayerFitResolved = false;
FString RefusalReason;
bool bStartSnapped = false;
bool bGoalSnapped = false;
int32 NumRouteRetries = 0; // Alternate routes full-resolution-checked after the first route.
int64 NumAirCells = 0;
int64 StartComponentCells = 0;
int64 GoalComponentCells = 0;
int64 LargestComponentCells = 0;
float StartComponentShare = 0.0f;
float GoalComponentShare = 0.0f;
float LargestComponentShare = 0.0f;
bool bStartComponentIsLargest = false;
bool bGoalComponentIsLargest = false;
float StartToGoalComponentDistanceCells = -1.0f;
float GoalToStartComponentDistanceCells = -1.0f;
};
/**
* Experiential traversal facts over the exact VF player-fit mask.
*
* This is intentionally a report, not a new movement implementation. The mask is built by the
* same direct capsule stencil used by VF_DiagnosePlayerFitConnectivityWithSampler; the explorer's
* deterministic graph walk only instruments that already-resolved six-neighbour pose graph.
* Distances are metres so the result can be read beside a human-scale screenshot.
*/
struct VOXELFORGE_API FVoxelPlayerFitWalkReport
{
bool bValid = false;
FString RefusalReason;
EVoxelConnectivityResult Result = EVoxelConnectivityResult::OutOfWindow;
bool bCanReachDeparture = false;
bool bStartSnapped = false;
bool bGoalSnapped = false;
int32 NumRouteRetries = 0;
bool bClimbingEnabled = false;
float MaxClimbHeightMeters = 0.0f;
// The primordial capability contract is walk-only. The existing reachable fields below
// therefore remain walk-only; climb is reported as a separate tier and never makes the hard
// connectivity verdict pass.
bool bWalkOnlyCanReachDeparture = false;
bool bClimbCanReachDeparture = false;
int64 PlayerFitVolumeCells = 0;
int64 ReachablePlayerFitCells = 0;
int64 WalkOnlyReachablePlayerFitCells = 0;
int64 ClimbReachablePlayerFitCells = 0;
int64 ClimbEdges = 0;
// Fit poses outside the walk+climb reachable set need an edit/mining action to become
// reachable. This is a reporting remainder, not a promise that arbitrary mining can reach
// every such pose without a separate excavation model.
int64 MineRequiredPlayerFitCells = 0;
float ReachablePlayerFitFraction = 0.0f;
float ClimbReachablePlayerFitFraction = 0.0f;
int32 SampledNumX = 0;
int32 SampledNumY = 0;
int32 SampledNumZ = 0;
int32 SampledMinZ = 0;
int32 SampledMaxZ = 0;
float SampledMinX = 0.0f;
float SampledMaxX = 0.0f;
float SampledMinY = 0.0f;
float SampledMaxY = 0.0f;
// The agent follows a fixed-order depth-first walk over the already-resolved fit graph. A
// branch edge is counted twice when the agent backtracks, which makes this an instrument of
// traversal experience rather than a shortest-path length.
int64 AgentGraphTraversals = 0;
int64 ClimbTraversals = 0;
int64 DeadEndsEncountered = 0;
float DeadEndsPer100m = 0.0f;
float DistanceTravelledMeters = 0.0f;
float StraightLineMeters = 0.0f;
float Tortuosity = 0.0f;
// A narrow-gap event is a traversed graph edge touching a route cell whose widest
// axis-aligned horizontal air span is below the threshold. This deliberately reports the
// operational proxy instead of pretending the voxel mask contains an oriented corridor width.
int64 NarrowGapTraversals = 0;
float NarrowGapFraction = 0.0f;
float NarrowGapThresholdMeters = 0.0f;
float NarrowGapEventsPer100m = 0.0f;
float CapsuleRadiusMeters = 0.0f;
float CapsuleWidthMeters = 0.0f;
float CapsuleHeightMeters = 0.0f;
FString NarrowGapDefinition;
// Component facts are retained from the same fit-mask flood fill that drives the route
// verdict. They are exposed here so an editor diagnostic does not need to sample the field
// a second time just to explain a failed mouth-to-mouth walk.
int32 PlayerFitComponents = 0;
int64 LargestPlayerFitComponentCells = 0;
int64 ArrivalComponentCells = 0;
int64 DepartureComponentCells = 0;
float MouthComponentGapVoxels = -1.0f;
// Optional room probes evaluated against this same grid and start component. The arrays are
// empty for ordinary callers; when supplied, each entry is 1 only when the probe is both a
// player-fit cell and in the arrival/start component.
TArray<uint8> ComponentProbePlayerFit;
TArray<uint8> ComponentProbeInStartComponent;
// The DFS returns to its root when the target component is unreachable. Keep both the
// physical end of that walk and the last newly reached pose: the latter is the useful
// failure-frontier point for a render, while the former is the literal final pose.
bool bHasAgentFinalPosition = false;
FVector AgentFinalVoxels = FVector::ZeroVector;
bool bHasLastReachedPosition = false;
FVector LastReachedVoxels = FVector::ZeroVector;
bool bHasTargetComponentNearestCell = false;
FVector TargetComponentNearestVoxels = FVector::ZeroVector;
float AgentToTargetComponentGapVoxels = -1.0f;
// Origin checks are facts about the sampled fit mask. An origin-inclusive secondary probe
// can populate these even when the primary mouth-sized window deliberately excludes (0,0).
bool bOriginColumnInSampledWindow = false;
bool bOriginColumnHasPlayerFit = false;
int64 OriginColumnPlayerFitCells = 0;
bool bOriginColumnReachable = false;
float ReachableSetToOriginColumnVoxels = -1.0f;
};
/**
* Measure one strate in actor-space voxel coordinates.
*
* The pass samples the requested strate between a resolved margin inside its top and bottom
* boundaries, flood-fills its air once, and derives all returned metrics from that same grid.
* It never mutates either input object.
*/
VOXELFORGE_API FVoxelStrateMetrics VF_MeasureStrate(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateSampleGrid* OutSampleGrid = nullptr);
/** The same Tier 2 pass over an explicit voxel Z window and a custom read-only sampler. */
VOXELFORGE_API FVoxelStrateMetrics VF_MeasureStrateWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateSampleGrid* OutSampleGrid = nullptr);
/**
* Test coarse connectivity and re-check recovered coarse routes at full resolution.
*
* If an endpoint's own coarse cell is solid, the query searches the 26 neighbouring cells and
* uses the nearest air cell when one exists. The snap flags report those repairs. Solid-cell
* results are returned only when no adjacent air cell exists. When a route fails its
* full-resolution re-check, the failed coarse edge is excluded from a deterministic local
* blocklist and BFS is retried up to MaxRouteRetries. A missing route after exclusions returns
* NotConnectedAtThisResolution. If the retry budget is exhausted while routes are still being
* refuted, the result is CoarseLiedBudgetExhausted: unknown, not disconnected. The same unknown
* result is used when a refutation is in an endpoint-to-cell segment with no coarse edge to
* exclude. Every Connected result has a fully re-walked air route.
*/
VOXELFORGE_API EVoxelConnectivityResult VF_AreConnected(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped);
/** The same query as the simple overload, with the number of alternate routes actually tried. */
VOXELFORGE_API EVoxelConnectivityResult VF_AreConnected(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
int32& OutNumRouteRetries);
/**
* The same connectivity query with component facts returned from the same sampled grid. This
* overload exists for diagnostics that need both the required route verdict and the component
* containing one endpoint without sampling the strate a second time.
*/
VOXELFORGE_API EVoxelConnectivityResult VF_AreConnected(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
FVoxelConnectivityDiagnostics& OutDiagnostics);
/**
* Return the component sizes and spatial separation facts for the same query performed by
* VF_AreConnected. The returned Result uses the same retrying full-resolution route re-check,
* and NumRouteRetries reports the number of alternate routes actually checked. bValid means the
* sample grid was built and both endpoint coordinates were in its bounds; component fields
* remain zero/negative when an endpoint has no air cell to analyze. Largest-component membership
* treats equal-sized components as tied for largest.
*/
VOXELFORGE_API FVoxelConnectivityDiagnostics VF_DiagnoseConnectivity(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings);
/** Connectivity variant for the explicit custom-stack measurement window. */
VOXELFORGE_API EVoxelConnectivityResult VF_AreConnectedWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
FVoxelConnectivityDiagnostics* OutDiagnostics = nullptr);
/** Full diagnostics variant for a custom stack. */
VOXELFORGE_API FVoxelConnectivityDiagnostics VF_DiagnoseConnectivityWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings);
/** Full diagnostics variant for the player-fit custom-stack route. */
VOXELFORGE_API FVoxelConnectivityDiagnostics VF_DiagnosePlayerFitConnectivityWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateMetrics* OutPlayerMetrics = nullptr);
/**
* Instrument a deterministic agent over the same exact player-fit graph used by the route gate.
* The return value is false only when the bounded fine grid/stencil could not be built; a valid
* report may still say NotConnectedAtThisResolution or another explicit connectivity result.
*/
VOXELFORGE_API bool VF_MeasurePlayerFitWalkWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
FVoxelPlayerFitWalkReport& OutReport,
const TArray<FVector>* ComponentProbePoints = nullptr,
FVoxelStrateSampleGrid* OutSampleGrid = nullptr);
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,350 @@
// VoxelTerrainOpDefinition.h
// Individual terrain operation data asset — one per operation variant.
//
// DESIGN:
// ========
// Each terrain operation (pit, arch, terrace, etc.) is its own asset.
// The strate definition holds a weighted array of references to these.
// This allows:
// - Reuse: "DA_Op_DeepPit" can be referenced by multiple strates
// - Composition: mix different ops per strate by drag-and-drop
// - Tuning: tweak one asset, all strates using it update
//
// HOW PARAMS FLOW:
// At generation time, the strate manager builds FStrateGenerationParams by:
// 1. Starting from the strate definition's base params (terrain op fields = 0)
// 2. For each terrain op reference: calling ApplyTo() which copies the op's
// fields into the params, scaled by the entry's Weight.
// 3. The generator reads the final params as usual — no code change needed.
//
// EDITOR UX:
// The Type enum controls which param group is visible via EditCondition.
// When you select "Pit", only pit-related fields appear in the Details panel.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "VoxelStrateTypes.h"
#include "VoxelTerrainOpDefinition.generated.h"
/**
* EVoxelTerrainOpType — Which terrain operation this asset configures.
* Each type maps to a specific density modification in the generation pipeline.
*/
UENUM(BlueprintType)
enum class EVoxelTerrainOpType : uint8
{
// Surface treatments (modify cave wall character)
Terrace UMETA(DisplayName = "Terrace / Ledges"),
LayerLines UMETA(DisplayName = "Layer Lines"),
Ribbing UMETA(DisplayName = "Ribbing"),
Cliff UMETA(DisplayName = "Cliff Sharpening"),
Scallop UMETA(DisplayName = "Scallop / Erosion"),
Overhang UMETA(DisplayName = "Overhang / Shelf"),
// Structural features (place geometry in caves)
Arch UMETA(DisplayName = "Arch / Bridge"),
Column UMETA(DisplayName = "Column / Pillar"),
Pit UMETA(DisplayName = "Pit / Shaft (downward)"),
Chimney UMETA(DisplayName = "Chimney / Shaft (upward)"),
Dome UMETA(DisplayName = "Dome (cathedral ceiling)"),
Pinch UMETA(DisplayName = "Pinch / Bottleneck"),
};
/**
* UVoxelTerrainOpDefinition — A single terrain operation, configured as a data asset.
*
* Create these in your Content folder (e.g., Content/TerrainOps/DA_Op_DeepPit).
* Then reference them from your strate definitions with a weight.
*
* USAGE EXAMPLE:
* 1. Create: Right-click Content → Miscellaneous → Data Asset → VoxelTerrainOpDefinition
* 2. Set Type to "Pit", configure depth/radius/taper
* 3. In your strate definition, add this asset to TerrainOperations with Weight=1.0
*/
UCLASS(BlueprintType)
class VOXELFORGE_API UVoxelTerrainOpDefinition : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
//=========================================================================
// OPERATION TYPE
//=========================================================================
// Which terrain operation this asset configures.
// Changing this shows/hides the relevant parameter group below.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Terrain Operation")
EVoxelTerrainOpType Type = EVoxelTerrainOpType::Terrace;
//=========================================================================
// TERRACE PARAMS (visible when Type == Terrace)
//=========================================================================
// Vertical spacing between terrace steps (in voxels). 0 = disabled.
// 3-5 → tight steps (staircase feel)
// 8-12 → wide ledges (platforming scale)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Terrace",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Terrace"))
float TerraceStepHeight = 5.0f;
// Edge sharpness: 0 = rounded steps, 1 = razor sharp edges.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Terrace",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Terrace",
ClampMin = "0.0", ClampMax = "1.0"))
float TerraceHardness = 0.5f;
// Noise displacement on step edges (organic variation).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Terrace",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Terrace",
ClampMin = "0.0", ClampMax = "2.0"))
float TerraceNoiseDisplacement = 0.5f;
//=========================================================================
// LAYER LINES PARAMS (visible when Type == LayerLines)
//=========================================================================
// Spacing between horizontal geological lines (in voxels). 0 = disabled.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Layer Lines",
meta = (EditCondition = "Type == EVoxelTerrainOpType::LayerLines"))
float LayerLineSpacing = 4.0f;
// Depth of the grooves (how much density is removed).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Layer Lines",
meta = (EditCondition = "Type == EVoxelTerrainOpType::LayerLines",
ClampMin = "0.0", ClampMax = "2.0"))
float LayerLineDepth = 0.3f;
//=========================================================================
// RIBBING PARAMS (visible when Type == Ribbing)
//=========================================================================
// Spacing between parallel ribs along Z (in voxels). 0 = disabled.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ribbing",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Ribbing"))
float RibbingSpacing = 3.0f;
// Depth of the ridges.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ribbing",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Ribbing",
ClampMin = "0.0", ClampMax = "3.0"))
float RibbingDepth = 0.4f;
//=========================================================================
// CLIFF PARAMS (visible when Type == Cliff)
//=========================================================================
// How much to amplify vertical gradients near cave surfaces.
// 0 = no effect, 1 = maximum sheer cliff amplification.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cliff",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Cliff",
ClampMin = "0.0", ClampMax = "1.0"))
float CliffStrength = 0.5f;
//=========================================================================
// SCALLOP PARAMS (visible when Type == Scallop)
//=========================================================================
// Strength of the erosion bowl patterns (cellular noise subtraction).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Scallop",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Scallop",
ClampMin = "0.0", ClampMax = "2.0"))
float ScallopStrength = 0.5f;
// Frequency of the cellular noise (smaller = larger bowls).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Scallop",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Scallop"))
float ScallopFrequency = 0.1f;
//=========================================================================
// OVERHANG PARAMS (visible when Type == Overhang)
//=========================================================================
// Probability/strength of overhang generation (0 = none, 1 = max).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Overhang",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Overhang",
ClampMin = "0.0", ClampMax = "1.0"))
float OverhangStrength = 0.5f;
// How far the overhang protrudes from the wall (in voxels).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Overhang",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Overhang"))
float OverhangDepth = 5.0f;
// Frequency of the noise that creates overhangs (lower = broader shelves).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Overhang",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Overhang"))
float OverhangFrequency = 0.06f;
//=========================================================================
// ARCH PARAMS (visible when Type == Arch)
//=========================================================================
// Probability per grid cell (0 = none, 0.3 = max recommended).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arch",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Arch",
ClampMin = "0.0", ClampMax = "0.3"))
float ArchDensity = 0.1f;
// Minimum arch thickness (radius of the capsule SDF).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arch",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Arch",
ClampMin = "1.0"))
float ArchMinRadius = 3.0f;
// Maximum arch thickness.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arch",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Arch",
ClampMin = "1.0"))
float ArchMaxRadius = 6.0f;
//=========================================================================
// COLUMN PARAMS (visible when Type == Column)
//=========================================================================
// Probability per grid cell (0 = none, 1 = every cell).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Column",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Column",
ClampMin = "0.0", ClampMax = "1.0"))
float ColumnDensity = 0.15f;
// Minimum column radius.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Column",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Column",
ClampMin = "0.5"))
float ColumnMinRadius = 2.0f;
// Maximum column radius.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Column",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Column",
ClampMin = "1.0"))
float ColumnMaxRadius = 5.0f;
//=========================================================================
// PIT PARAMS (visible when Type == Pit)
//=========================================================================
// Probability per grid cell.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pit",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Pit",
ClampMin = "0.0", ClampMax = "0.5"))
float PitDensity = 0.1f;
// Minimum pit radius at the top.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pit",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Pit",
ClampMin = "1.0"))
float PitMinRadius = 4.0f;
// Maximum pit radius at the top.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pit",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Pit",
ClampMin = "2.0"))
float PitMaxRadius = 10.0f;
// How deep the pit extends downward (in voxels).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pit",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Pit",
ClampMin = "5.0"))
float PitDepth = 25.0f;
//=========================================================================
// CHIMNEY PARAMS (visible when Type == Chimney)
//=========================================================================
// Probability per grid cell.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Chimney",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Chimney",
ClampMin = "0.0", ClampMax = "1.0"))
float ChimneyDensity = 0.1f;
// Minimum chimney radius.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Chimney",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Chimney",
ClampMin = "0.5"))
float ChimneyMinRadius = 2.0f;
// Maximum chimney radius.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Chimney",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Chimney",
ClampMin = "1.0"))
float ChimneyMaxRadius = 5.0f;
// How high the chimney extends upward (in voxels).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Chimney",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Chimney",
ClampMin = "5.0"))
float ChimneyHeight = 20.0f;
//=========================================================================
// DOME PARAMS (visible when Type == Dome)
//=========================================================================
// Probability per grid cell.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dome",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Dome",
ClampMin = "0.0", ClampMax = "1.0"))
float DomeDensity = 0.15f;
// Minimum dome radius.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dome",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Dome",
ClampMin = "3.0"))
float DomeMinRadius = 8.0f;
// Maximum dome radius.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dome",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Dome",
ClampMin = "4.0"))
float DomeMaxRadius = 15.0f;
// Height-to-radius ratio (0.5 = flat, 1.0 = hemisphere, 1.5 = tall/gothic).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dome",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Dome",
ClampMin = "0.3", ClampMax = "2.0"))
float DomeHeightRatio = 0.8f;
//=========================================================================
// PINCH PARAMS (visible when Type == Pinch)
//=========================================================================
// Probability per grid cell.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pinch",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Pinch",
ClampMin = "0.0", ClampMax = "1.0"))
float PinchDensity = 0.15f;
// How much the passage narrows (voxels of added density from each side).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pinch",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Pinch",
ClampMin = "1.0"))
float PinchStrength = 5.0f;
// Length of the narrowed section along the passage.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pinch",
meta = (EditCondition = "Type == EVoxelTerrainOpType::Pinch",
ClampMin = "3.0"))
float PinchLength = 12.0f;
//=========================================================================
// APPLY TO GENERATION PARAMS
//=========================================================================
/**
* Copy this operation's parameters into a FStrateGenerationParams struct.
* Only the fields matching this operation's Type are written.
* Weight scales the primary "strength" or "density" field for intensity control.
*
* @param OutParams - The generation params to write into
* @param Weight - Multiplier for the op's intensity (1.0 = full strength)
*/
void ApplyTo(FStrateGenerationParams& OutParams, float Weight = 1.0f) const;
};
@@ -0,0 +1,414 @@
// Per-tile reach state for density posts.
//
// The marching-cubes worker installs this state once for the tile's lattice plus its one-point
// halo. Density helpers only read the bit that owns them. The inline TLS is intentional: the
// runtime hot path must not cross a DLL boundary for a single boolean load.
#pragma once
#include "CoreMinimal.h"
#include "HAL/PlatformTime.h"
#include <atomic>
namespace VoxelGenLOD
{
enum ETilePostReach : uint8
{
OriginLandingReachable = 1u << 0,
PassageLandingReachable = 1u << 1,
PassageStructuralReachable= 1u << 2,
TunnelCoreReachable = 1u << 3,
PassageCarvingReachable = 1u << 4,
AllTilePostReach = OriginLandingReachable
| PassageLandingReachable
| PassageStructuralReachable
| TunnelCoreReachable
| PassageCarvingReachable,
AllTileReach = AllTilePostReach,
};
enum class ETilePostComparisonKind : uint8
{
Other = 0,
OriginAir,
OriginFloor,
PassageLandingSDF,
PassageLandingAir,
PassageTunnelAir,
PassageLandingFloor,
PassageStructuralPosts,
PassageNativeFloor,
PassageLandingRoomFloor,
TunnelCore,
PassageCarving,
FinalField,
Count,
};
enum class ETileReachCostKind : uint8
{
TunnelCoreWorld = 0,
TunnelCoreTail,
PassageCarving,
Count,
};
constexpr int32 TileReachBlockCells = 8;
constexpr int32 TileReachMaxBlocksPerAxis = 4;
constexpr int32 TileReachMaxBlockCount =
TileReachMaxBlocksPerAxis * TileReachMaxBlocksPerAxis * TileReachMaxBlocksPerAxis;
FORCEINLINE FBox MakeTileReachBlockBox(
const FIntVector& TileOrigin, int32 Step, int32 CellsPerAxis,
int32 BlockX, int32 BlockY, int32 BlockZ)
{
const int32 SafeStep = FMath::Max(Step, 1);
const int32 MinX = BlockX * TileReachBlockCells;
const int32 MinY = BlockY * TileReachBlockCells;
const int32 MinZ = BlockZ * TileReachBlockCells;
const int32 MaxX = FMath::Min(
CellsPerAxis, MinX + TileReachBlockCells);
const int32 MaxY = FMath::Min(
CellsPerAxis, MinY + TileReachBlockCells);
const int32 MaxZ = FMath::Min(
CellsPerAxis, MinZ + TileReachBlockCells);
return FBox(
FVector(
static_cast<float>(static_cast<int64>(TileOrigin.X) + MinX * SafeStep - SafeStep),
static_cast<float>(static_cast<int64>(TileOrigin.Y) + MinY * SafeStep - SafeStep),
static_cast<float>(static_cast<int64>(TileOrigin.Z) + MinZ * SafeStep - SafeStep)),
FVector(
static_cast<float>(static_cast<int64>(TileOrigin.X) + MaxX * SafeStep),
static_cast<float>(static_cast<int64>(TileOrigin.Y) + MaxY * SafeStep),
static_cast<float>(static_cast<int64>(TileOrigin.Z) + MaxZ * SafeStep)));
}
// A tile that is not being meshed, or a point query outside the mesher, must retain the
// canonical behaviour. GenerateMesh scopes this to its computed mask.
inline thread_local uint8 TilePostReachFlags = AllTileReach;
inline thread_local bool bTilePostReachDebug = false;
inline thread_local bool bTilePostReachBypass = false;
inline thread_local bool bTileReachDiagnosticTile = false;
inline thread_local bool bTileCoreReachDecisionRecorded = false;
inline thread_local float TileReachScale = 1.0f;
inline thread_local int32 TileReachBlocksPerAxis = 0;
inline thread_local int32 TileReachBlockIndex = INDEX_NONE;
inline thread_local bool bTileBlockReachValid = false;
inline thread_local uint8 TileBlockReachFlags[TileReachMaxBlockCount]{};
// A false result is installed only after a cache was built with the whole mesher tile as its
// graph window. Chunk-local caches leave this true (the conservative no-skip fallback).
inline thread_local bool bTileCoreReachProofEnabled = false;
// These are diagnostics only. They never participate in a field decision.
inline std::atomic<bool> GTilePostReachDebugEnabled { false };
inline std::atomic<uint64> GTilePostReachTileCount { 0 };
inline std::atomic<uint64> GOriginLandingReachableTiles { 0 };
inline std::atomic<uint64> GPassageLandingReachableTiles { 0 };
inline std::atomic<uint64> GPassageStructuralReachableTiles { 0 };
inline std::atomic<uint64> GOriginLandingSkippedTiles { 0 };
inline std::atomic<uint64> GPassageLandingSkippedTiles { 0 };
inline std::atomic<uint64> GPassageStructuralSkippedTiles { 0 };
inline std::atomic<uint64> GTunnelCoreReachableTiles { 0 };
inline std::atomic<uint64> GTunnelCoreSkippedTiles { 0 };
inline std::atomic<uint64> GPassageCarvingReachableTiles { 0 };
inline std::atomic<uint64> GPassageCarvingSkippedTiles { 0 };
inline std::atomic<uint64> GSkippedPostComparisons { 0 };
inline std::atomic<uint64> GSkippedPostDifferences { 0 };
inline std::atomic<uint64> GSkippedPostComparisonsByKind[
static_cast<uint8>(ETilePostComparisonKind::Count)]{};
inline std::atomic<uint64> GSkippedPostDifferencesByKind[
static_cast<uint8>(ETilePostComparisonKind::Count)]{};
inline std::atomic<bool> GTileReachCostDiagnosticsEnabled { false };
inline std::atomic<uint64> GTileReachCostCalls[
static_cast<uint8>(ETileReachCostKind::Count)][2]{};
inline std::atomic<uint64> GTileReachCostCycles[
static_cast<uint8>(ETileReachCostKind::Count)][2]{};
inline std::atomic<uint64> GTileReachBlockCostCalls[
static_cast<uint8>(ETileReachCostKind::Count)][2]{};
inline std::atomic<uint64> GTileReachBlockCostCycles[
static_cast<uint8>(ETileReachCostKind::Count)][2]{};
// Automation tests deliberately run the real mesher twice (full reach and a shrunken reach)
// in one process. Keep the reset in the reach owner so a test cannot accidentally omit one
// of the per-kind counters and accept a vacuous proof.
FORCEINLINE void ResetTilePostReachDiagnostics()
{
GTilePostReachDebugEnabled.store(false, std::memory_order_relaxed);
GTilePostReachTileCount.store(0, std::memory_order_relaxed);
GOriginLandingReachableTiles.store(0, std::memory_order_relaxed);
GPassageLandingReachableTiles.store(0, std::memory_order_relaxed);
GPassageStructuralReachableTiles.store(0, std::memory_order_relaxed);
GOriginLandingSkippedTiles.store(0, std::memory_order_relaxed);
GPassageLandingSkippedTiles.store(0, std::memory_order_relaxed);
GPassageStructuralSkippedTiles.store(0, std::memory_order_relaxed);
GTunnelCoreReachableTiles.store(0, std::memory_order_relaxed);
GTunnelCoreSkippedTiles.store(0, std::memory_order_relaxed);
GPassageCarvingReachableTiles.store(0, std::memory_order_relaxed);
GPassageCarvingSkippedTiles.store(0, std::memory_order_relaxed);
GSkippedPostComparisons.store(0, std::memory_order_relaxed);
GSkippedPostDifferences.store(0, std::memory_order_relaxed);
for (uint8 Index = 0;
Index < static_cast<uint8>(ETilePostComparisonKind::Count); ++Index)
{
GSkippedPostComparisonsByKind[Index].store(0, std::memory_order_relaxed);
GSkippedPostDifferencesByKind[Index].store(0, std::memory_order_relaxed);
}
GTileReachCostDiagnosticsEnabled.store(false, std::memory_order_relaxed);
for (uint8 Kind = 0; Kind < static_cast<uint8>(ETileReachCostKind::Count); ++Kind)
{
for (uint8 Reach = 0; Reach < 2; ++Reach)
{
GTileReachCostCalls[Kind][Reach].store(0, std::memory_order_relaxed);
GTileReachCostCycles[Kind][Reach].store(0, std::memory_order_relaxed);
GTileReachBlockCostCalls[Kind][Reach].store(0, std::memory_order_relaxed);
GTileReachBlockCostCycles[Kind][Reach].store(0, std::memory_order_relaxed);
}
}
}
FORCEINLINE bool IsTilePostReachable(uint8 Bit)
{
return (TilePostReachFlags & Bit) != 0;
}
FORCEINLINE bool IsOriginLandingReachable()
{
return IsTilePostReachable(OriginLandingReachable);
}
FORCEINLINE bool IsPassageLandingReachable()
{
return IsTilePostReachable(PassageLandingReachable);
}
FORCEINLINE bool IsPassageStructuralReachable()
{
return IsTilePostReachable(PassageStructuralReachable);
}
FORCEINLINE bool IsTunnelCoreReachable()
{
// Core's measured far share is negligible; keep its exact tile decision and avoid a
// second, weaker block geometry approximation. Passage carving is the only block-gated
// work because its tight chain produced a material far share in the pre-gate measure.
return IsTilePostReachable(TunnelCoreReachable);
}
FORCEINLINE bool IsPassageCarvingReachable()
{
if (bTileBlockReachValid
&& TileReachBlockIndex >= 0
&& TileReachBlockIndex < TileReachMaxBlockCount)
{
return (TileBlockReachFlags[TileReachBlockIndex] & PassageCarvingReachable) != 0;
}
return IsTilePostReachable(PassageCarvingReachable);
}
FORCEINLINE bool IsTilePostReachBypassActive()
{
return bTilePostReachBypass;
}
FORCEINLINE void RecordTileReachMask(uint8 Flags)
{
GTilePostReachTileCount.fetch_add(1, std::memory_order_relaxed);
if ((Flags & OriginLandingReachable) != 0)
{
GOriginLandingReachableTiles.fetch_add(1, std::memory_order_relaxed);
}
else
{
GOriginLandingSkippedTiles.fetch_add(1, std::memory_order_relaxed);
}
if ((Flags & PassageLandingReachable) != 0)
{
GPassageLandingReachableTiles.fetch_add(1, std::memory_order_relaxed);
}
else
{
GPassageLandingSkippedTiles.fetch_add(1, std::memory_order_relaxed);
}
if ((Flags & PassageStructuralReachable) != 0)
{
GPassageStructuralReachableTiles.fetch_add(1, std::memory_order_relaxed);
}
else
{
GPassageStructuralSkippedTiles.fetch_add(1, std::memory_order_relaxed);
}
if ((Flags & PassageCarvingReachable) != 0)
{
GPassageCarvingReachableTiles.fetch_add(1, std::memory_order_relaxed);
}
else
{
GPassageCarvingSkippedTiles.fetch_add(1, std::memory_order_relaxed);
}
}
FORCEINLINE void RecordTunnelCoreReachDecision(bool bReachable)
{
if (bTileCoreReachDecisionRecorded)
{
return;
}
bTileCoreReachDecisionRecorded = true;
if (bReachable)
{
GTunnelCoreReachableTiles.fetch_add(1, std::memory_order_relaxed);
}
else
{
GTunnelCoreSkippedTiles.fetch_add(1, std::memory_order_relaxed);
}
}
FORCEINLINE uint8 TileReachCostIndex(ETileReachCostKind Kind)
{
switch (Kind)
{
case ETileReachCostKind::TunnelCoreWorld:
case ETileReachCostKind::TunnelCoreTail:
return IsTilePostReachable(TunnelCoreReachable) ? 1u : 0u;
case ETileReachCostKind::PassageCarving:
return IsTilePostReachable(PassageCarvingReachable) ? 1u : 0u;
default:
return 1u;
}
}
FORCEINLINE uint8 BlockReachCostIndex(ETileReachCostKind Kind)
{
switch (Kind)
{
case ETileReachCostKind::TunnelCoreWorld:
case ETileReachCostKind::TunnelCoreTail:
return IsTunnelCoreReachable() ? 1u : 0u;
case ETileReachCostKind::PassageCarving:
return IsPassageCarvingReachable() ? 1u : 0u;
default:
return 1u;
}
}
FORCEINLINE void RecordTileReachBlockCost(
ETileReachCostKind Kind, uint8 ReachIndex, uint64 Cycles)
{
const uint8 KindIndex = static_cast<uint8>(Kind);
if (KindIndex < static_cast<uint8>(ETileReachCostKind::Count))
{
GTileReachBlockCostCalls[KindIndex][ReachIndex].fetch_add(
1, std::memory_order_relaxed);
GTileReachBlockCostCycles[KindIndex][ReachIndex].fetch_add(
Cycles, std::memory_order_relaxed);
}
}
/** Inclusive timing split used for the pre-gate near/far measurement only. */
struct FScopedReachCost
{
ETileReachCostKind Kind;
uint8 TileReachIndex = 1;
uint8 BlockReachIndex = 1;
uint64 StartCycles = 0;
bool bActive = false;
explicit FScopedReachCost(ETileReachCostKind InKind)
: Kind(InKind)
{
if (!bTileReachDiagnosticTile
|| !GTileReachCostDiagnosticsEnabled.load(std::memory_order_relaxed))
{
return;
}
TileReachIndex = TileReachCostIndex(Kind);
BlockReachIndex = BlockReachCostIndex(Kind);
StartCycles = FPlatformTime::Cycles64();
bActive = true;
}
~FScopedReachCost()
{
if (!bActive)
{
return;
}
const uint8 KindIndex = static_cast<uint8>(Kind);
if (KindIndex < static_cast<uint8>(ETileReachCostKind::Count))
{
const uint64 Cycles = FPlatformTime::Cycles64() - StartCycles;
GTileReachCostCalls[KindIndex][TileReachIndex].fetch_add(
1, std::memory_order_relaxed);
GTileReachCostCycles[KindIndex][TileReachIndex].fetch_add(
Cycles,
std::memory_order_relaxed);
if (bTileBlockReachValid
&& TileReachBlockIndex >= 0
&& TileReachBlockIndex < TileReachMaxBlockCount)
{
RecordTileReachBlockCost(Kind, BlockReachIndex, Cycles);
}
}
}
};
FORCEINLINE bool SameFloatBits(float A, float B)
{
uint32 ABits = 0;
uint32 BBits = 0;
FMemory::Memcpy(&ABits, &A, sizeof(float));
FMemory::Memcpy(&BBits, &B, sizeof(float));
return ABits == BBits;
}
FORCEINLINE void RecordSkippedPostComparison(
ETilePostComparisonKind Kind, bool bDifferent)
{
GSkippedPostComparisons.fetch_add(1, std::memory_order_relaxed);
const uint8 KindIndex = static_cast<uint8>(Kind);
if (KindIndex < static_cast<uint8>(ETilePostComparisonKind::Count))
{
GSkippedPostComparisonsByKind[KindIndex].fetch_add(
1, std::memory_order_relaxed);
}
if (bDifferent)
{
GSkippedPostDifferences.fetch_add(1, std::memory_order_relaxed);
if (KindIndex < static_cast<uint8>(ETilePostComparisonKind::Count))
{
GSkippedPostDifferencesByKind[KindIndex].fetch_add(
1, std::memory_order_relaxed);
}
}
}
FORCEINLINE void RecordSkippedPostComparison(bool bDifferent)
{
RecordSkippedPostComparison(ETilePostComparisonKind::Other, bDifferent);
}
/** Run one skipped float post through its canonical body, then restore the fast-path value. */
template <typename CallableType>
FORCEINLINE void CompareSkippedPost(
float& Density, CallableType Callable,
ETilePostComparisonKind Kind = ETilePostComparisonKind::Other)
{
if (!bTilePostReachDebug || bTilePostReachBypass)
{
return;
}
const float Before = Density;
{
const bool PreviousBypass = bTilePostReachBypass;
bTilePostReachBypass = true;
Callable();
bTilePostReachBypass = PreviousBypass;
}
RecordSkippedPostComparison(Kind, !SameFloatBits(Before, Density));
Density = Before;
}
}
@@ -0,0 +1,401 @@
// VoxelTypes.h
// Core type definitions, coordinate conversions, and mesh data.
//
// Ce header est le socle du plugin — tout le monde l'inclut.
// Pas de dépendance sur des UClasses ici (on reste léger).
#pragma once
#include "CoreMinimal.h"
//=============================================================================
// EXACT FINITE PREDICATE / PREDICAT FINI EXACT
//=============================================================================
// The CRT-backed FMath::IsFinite is an out-of-line call on the MSVC game path. A finite
// IEEE-754 value is identified entirely by its exponent bits, so this helper has the same
// boolean result for every bit pattern without doing a floating-point comparison.
namespace VoxelMath
{
// Runtime policy switch. The fast helper below is deliberately independent of this value so
// the equivalence test can compare it directly with FMath::IsFinite. IsFinite() is the A/B
// entry point used by generation code.
VOXELFORGE_API extern int32 GFastIsFinite;
FORCEINLINE bool IsFiniteFast(float Value)
{
static_assert(sizeof(float) == sizeof(uint32), "VoxelMath::IsFiniteFast requires IEEE float");
uint32 Bits = 0;
FMemory::Memcpy(&Bits, &Value, sizeof(Bits));
constexpr uint32 ExponentMask = 0x7F800000u;
return (Bits & ExponentMask) != ExponentMask;
}
FORCEINLINE bool IsFiniteFast(double Value)
{
static_assert(sizeof(double) == sizeof(uint64), "VoxelMath::IsFiniteFast requires IEEE double");
uint64 Bits = 0;
FMemory::Memcpy(&Bits, &Value, sizeof(Bits));
constexpr uint64 ExponentMask = 0x7FF0000000000000ull;
return (Bits & ExponentMask) != ExponentMask;
}
// Keep the switch at the call site while keeping the exact bit test separate and directly
// testable. In the default-on game path this inlines to the bit load/mask/branch; the 0 path
// is retained for within-round performance and sampler A/B runs.
FORCEINLINE bool IsFinite(float Value)
{
return GFastIsFinite != 0 ? IsFiniteFast(Value) : FMath::IsFinite(Value);
}
FORCEINLINE bool IsFinite(double Value)
{
return GFastIsFinite != 0 ? IsFiniteFast(Value) : FMath::IsFinite(Value);
}
// Deterministic trigonometry for world decisions.
//
// The implementation deliberately owns both the range reduction and the polynomial. It
// does not call the CRT, FMath, or a platform vector intrinsic: the same finite float is
// reduced by the same constants and evaluated by the same scalar arithmetic on every host.
// The reduction is done in double so world-coordinate phases do not lose all low bits before
// the period is removed. The polynomial itself is float arithmetic, matching the values
// consumed by the density field and keeping the hot path small.
FORCEINLINE void DetSinCos(float& OutSin, float& OutCos, float Value)
{
if (!IsFiniteFast(Value))
{
// Non-finite values are outside the world contract. Preserve the input rather than
// entering an undefined integer conversion during range reduction.
OutSin = Value;
OutCos = Value;
return;
}
constexpr double TwoPi = 6.283185307179586476925286766559;
constexpr double HalfPi = 1.57079632679489661923132169164;
// Round the number of complete periods toward the nearest integer. All measured world
// phases are many orders of magnitude below INT64_MAX. For a pathological finite input
// whose period count does not fit in int64, use the exact integer significand from the
// float representation and reduce it by fixed binary doubling instead of converting an
// out-of-range quotient.
const double PeriodsReal = static_cast<double>(Value) / TwoPi;
constexpr double MaxSafeInt64 = 9223372036854774784.0;
double Reduced = 0.0;
if (PeriodsReal >= MaxSafeInt64 || PeriodsReal <= -MaxSafeInt64)
{
uint32 ValueBits = 0;
FMemory::Memcpy(&ValueBits, &Value, sizeof(ValueBits));
const uint32 MagnitudeBits = ValueBits & 0x7FFFFFFFu;
const uint32 ExponentBits = (MagnitudeBits >> 23) & 0xFFu;
const uint32 Significand = (MagnitudeBits & 0x007FFFFFu) | 0x00800000u;
const int32 BinaryExponent = static_cast<int32>(ExponentBits) - 127 - 23;
double ReducedInteger = 0.0;
for (int32 Bit = 23; Bit >= 0; --Bit)
{
ReducedInteger *= 2.0;
ReducedInteger += static_cast<double>((Significand >> Bit) & 1u);
if (ReducedInteger >= TwoPi)
{
ReducedInteger -= TwoPi;
}
}
for (int32 Shift = 0; Shift < BinaryExponent; ++Shift)
{
ReducedInteger *= 2.0;
if (ReducedInteger >= TwoPi)
{
ReducedInteger -= TwoPi;
}
}
Reduced = (ValueBits & 0x80000000u) != 0u ? -ReducedInteger : ReducedInteger;
}
else
{
int64 Periods = 0;
if (PeriodsReal >= 0.0)
{
Periods = static_cast<int64>(PeriodsReal + 0.5);
}
else
{
Periods = static_cast<int64>(PeriodsReal - 0.5);
}
Reduced = static_cast<double>(Value)
- static_cast<double>(Periods) * TwoPi;
}
// Reduce the remaining angle to [-pi/4, pi/4]. Both reduction branches above keep
// Reduced bounded by one period, so this quadrant conversion is always in int32 range.
const double QuadrantReal = Reduced / HalfPi;
int32 Quadrant = 0;
if (QuadrantReal >= 0.0)
{
Quadrant = static_cast<int32>(QuadrantReal + 0.5);
}
else
{
Quadrant = static_cast<int32>(QuadrantReal - 0.5);
}
Reduced -= static_cast<double>(Quadrant) * HalfPi;
const float Y = static_cast<float>(Reduced);
const float Y2 = Y * Y;
// Fixed minimax polynomials, the same degree as UE's scalar SinCos implementation. The
// quarter-period reduction gives them a smaller interval than UE's [-pi/2, pi/2] path.
const float SinP = (((((-2.3889859e-08f * Y2 + 2.7525562e-06f) * Y2
- 0.00019840874f) * Y2 + 0.0083333310f) * Y2
- 0.16666667f) * Y2 + 1.0f) * Y;
const float CosP = (((((-2.6051615e-07f * Y2 + 2.4760495e-05f) * Y2
- 0.0013888378f) * Y2 + 0.041666638f) * Y2
- 0.5f) * Y2 + 1.0f);
int32 QuadrantMod = Quadrant % 4;
if (QuadrantMod < 0)
{
QuadrantMod += 4;
}
switch (QuadrantMod)
{
case 0:
OutSin = SinP;
OutCos = CosP;
break;
case 1:
OutSin = CosP;
OutCos = -SinP;
break;
case 2:
OutSin = -SinP;
OutCos = -CosP;
break;
default:
OutSin = -CosP;
OutCos = SinP;
break;
}
}
FORCEINLINE float DetSin(float Value)
{
float SinValue = 0.0f;
float CosValue = 0.0f;
DetSinCos(SinValue, CosValue, Value);
return SinValue;
}
FORCEINLINE float DetCos(float Value)
{
float SinValue = 0.0f;
float CosValue = 0.0f;
DetSinCos(SinValue, CosValue, Value);
return CosValue;
}
}
//=============================================================================
// CHUNK CONSTANTS
//=============================================================================
//
// Taille de chunk: 32^3 = 32 768 voxels.
// Pourquoi 32 ? Puissance de 2 → astuces bit à bit + bon alignement GPU.
// VOXEL_SIZE = 12.5 cm/voxel (Unreal travaille en centimètres). 1 chunk = 4 m.
//
// NOTE: 64³ coupe les draw calls (8× moins de chunks) mais rend le streaming trop
// saccadé (briques 8× plus lourdes → applies + spawns de contenu en gros à-coups sur
// le game thread). Le fps se règle côté RENDU (ombres off sur LOD lointain, voir
// ApplyMeshToChunk), pas via la taille de chunk. La taille de chunk reste le levier streaming-vs-draws si besoin.
constexpr int32 CHUNK_SIZE = 32;
constexpr float VOXEL_SIZE = 25.0f;
//=============================================================================
// TRIVIAL-TILE CLASSIFICATION (T1.d)
//=============================================================================
// Verdict de ClassifyTile pour une tuile AVANT le pré-échantillonnage 33³+ :
// AllSolid / AllAir guarantee every core cell vertex of the mesher (g=0..Cells;
// the ±1 halo is normal-only) is on the same side of the iso ⇒ empty mesh, GenerateMesh is
// 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).
//
// Vit ici plutôt que dans VoxelGenerator.h pour que VoxelDensityOp.h (le contrat
// de la pile d'opérateurs) puisse s'en servir sans tirer un header UCLASS.
// Lives here rather than in VoxelGenerator.h so VoxelDensityOp.h (the operator-stack
// contract) can use it without pulling in a UCLASS header.
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
};
//=============================================================================
// DENSITY → R8 QUANTIZATION (density clipmap / mini-sun shadows)
//=============================================================================
//
// MC convention: NÉGATIF = solide, POSITIF = air, 0 = isosurface. On encode la densité
// dans un R8 où SOLIDE = HAUT, AIR = BAS, iso ≈ 0.5 (128), clampé ±1 autour de la surface
// (loin de la surface ⇒ sature plein solide / plein air). Le trilinear garde la traversée
// iso sub-voxel nette.
//
// SHARED entre deux producteurs qui DOIVENT rester bit-identiques :
// 1. UVoxelDensityVolume (fill worker re-évaluant GetDensityAt),
// 2. UVoxelMarchingCubesMesher (capture-during-meshing : réutilise la grille déjà
// échantillonnée par le mesher au lieu de re-sampler — voir GenerateMesh OutCaptureGrid).
// Même densité d'entrée ⇒ même octet. Ne PAS dupliquer cette formule ailleurs.
FORCEINLINE uint8 VF_QuantizeDensity(float MCDensity)
{
const float S = FMath::Clamp(0.5f - 0.5f * MCDensity, 0.0f, 1.0f);
return (uint8)FMath::RoundToInt(S * 255.0f);
}
//=============================================================================
// FACE DIRECTIONS
//=============================================================================
//
// Les 6 faces d'un cube. Gardé ici car le mesher peut encore en avoir besoin
// (ex. normales orientées), et les strates pourraient s'en servir pour des
// décorations "sur le sol" vs "au plafond".
enum class EVoxelFace : uint8
{
PositiveX, // East (+X)
NegativeX, // West (-X)
PositiveY, // North (+Y)
NegativeY, // South (-Y)
PositiveZ, // Up (+Z)
NegativeZ, // Down (-Z)
};
//=============================================================================
// COORDINATE CONVERSION
//=============================================================================
//
// Trois espaces:
// WORLD (FVector, cm) → position Unreal
// CHUNK (FIntVector) → quel chunk (32 voxels)
// LOCAL (FIntVector 0-31) → position dans le chunk
//
// Relation: WorldPos = (ChunkCoord * CHUNK_SIZE + LocalPos) * VOXEL_SIZE
inline FIntVector WorldToChunkCoord(const FVector& WorldPos)
{
// FloorToInt (pas division int) pour gérer proprement les coords négatives:
// -5 / 32 = 0 (faux — on veut -1)
// floor(-5 / 32) = floor(-0.156) = -1 (correct)
return FIntVector(
FMath::FloorToInt((WorldPos.X / VOXEL_SIZE) / CHUNK_SIZE),
FMath::FloorToInt((WorldPos.Y / VOXEL_SIZE) / CHUNK_SIZE),
FMath::FloorToInt((WorldPos.Z / VOXEL_SIZE) / CHUNK_SIZE)
);
}
//=============================================================================
// MATH HELPERS (partagés entre générateur, morphologie, manager)
//=============================================================================
// Smoothstep classique: 3x² - 2x³. Maps 0→0, 1→1, avec pente nulle aux bornes.
// Utilisé partout pour adoucir les transitions de densité (blend SDF, seal boundary).
// Entrée: x doit être dans [0, 1] (clamp avant si besoin).
inline float SmoothStep01(float x)
{
return x * x * (3.0f - 2.0f * x);
}
// Le coeur de bruit (VoxelNoise::Perlin3D, T2.a) renvoie ~[-0.8, 0.8] comme l'ancien
// FMath::PerlinNoise3D — ce facteur remet à ~[-1, 1] pour les formules de densité.
constexpr float VOXEL_NOISE_SCALE = 1.25f;
//=============================================================================
// CLIPMAP TILE KEY
//=============================================================================
//
// A "tile" generalises a chunk for the chunked-LOD clipmap. A level-L tile spans
// (CHUNK_SIZE << Level) voxels per axis and is meshed at step (1 << Level), so it always
// produces a constant CHUNK_SIZE³-cell mesh (one component, one draw) but covers 8^Level ×
// the volume. Level 0 = a full-resolution chunk; each level up doubles linear size.
// Streaming loads concentric shells: level 0 near the player, coarser levels farther out,
// so chunk/draw count stays ~flat regardless of view distance.
struct FVoxelTileKey
{
FIntVector Coord = FIntVector::ZeroValue; // in units of (CHUNK_SIZE << Level) voxels
int32 Level = 0; // 0 = full res
FVoxelTileKey() = default;
FVoxelTileKey(const FIntVector& InCoord, int32 InLevel) : Coord(InCoord), Level(InLevel) {}
// Cell size (sampling step) in voxels, and tile extent in voxels per axis.
int32 StepVoxels() const { return 1 << Level; }
int32 ExtentVoxels() const { return CHUNK_SIZE << Level; }
// Min-corner of the tile in VOXEL coords (what the mesher takes).
FIntVector OriginVoxels() const { return Coord * (CHUNK_SIZE << Level); }
// Tile centre in world cm (for distance sorting / LOD selection).
FVector CenterCm() const
{
const double Ext = (double)(CHUNK_SIZE << Level) * (double)VOXEL_SIZE;
return FVector((Coord.X + 0.5) * Ext, (Coord.Y + 0.5) * Ext, (Coord.Z + 0.5) * Ext);
}
bool operator==(const FVoxelTileKey& O) const { return Level == O.Level && Coord == O.Coord; }
bool operator!=(const FVoxelTileKey& O) const { return !(*this == O); }
};
FORCEINLINE uint32 GetTypeHash(const FVoxelTileKey& K)
{
return HashCombine(GetTypeHash(K.Coord), ::GetTypeHash(K.Level));
}
//=============================================================================
// MESH DATA
//=============================================================================
//
// Sortie du mesher: géométrie prête pour l'upload GPU.
// Struct plain C++ (pas de USTRUCT) — uniquement utilisé entre tâches C++.
// Si lighting est ajouté plus tard, ré-ajouter un champ Colors dédié.
struct FVoxelMeshData
{
TArray<FVector> Vertices; // Positions monde
TArray<int32> Triangles; // Indices, 3 par triangle
TArray<FVector2D> UVs; // Coords de texture (une par vertex)
TArray<FVector> Normals; // Normale lissée (gradient de densité)
TArray<FColor> Colors; // Masques matériau F6 (R=palette biome dominant, G=pente,
// B=poids de fondu de bordure, A=palette biome voisin)
// Number of vertices on the actual density isosurface. LOD seam skirts are appended after
// this prefix and must not be used as witnesses for terrain visibility audits.
int32 NumSurfaceVertices = 0;
// Diagnostic-only count: skirt triangles are render geometry and are included in the RMC
// sections that currently request collision. This lets collision profiling quantify their
// share without changing the emitted indices.
int32 NumSkirtTriangles = 0;
// 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()
{
Vertices.Empty();
Triangles.Empty();
UVs.Empty();
Normals.Empty();
Colors.Empty();
NumSurfaceVertices = 0;
NumSkirtTriangles = 0;
NumCeilingTriangles = 0;
}
bool IsEmpty() const { return Vertices.Num() == 0; }
};
@@ -0,0 +1,26 @@
// VoxelWormField.h
// Shared deterministic evaluation for the TunnelNetwork worm field.
#pragma once
#include "CoreMinimal.h"
namespace VoxelWormField
{
/** The scalar inputs used by both the fused and operator-stack evaluators. */
struct FParameters
{
float Frequency = 0.0f;
float HorizontalBias = 0.0f;
float VerticalScale = 1.0f;
uint32 Seed = 0;
};
/** Evaluate the exact two-noise field, retaining the N1 threshold short-circuit. */
VOXELFORGE_API float Evaluate(
float WorldX,
float WorldY,
float WorldZ,
float Threshold,
const FParameters& Parameters);
}
+179
View File
@@ -0,0 +1,179 @@
#pragma once
#include "OwnerWorldLoader.h"
#include "VoxelClipmapDesiredTiles.h"
#include "VoxelMarchingCubesMesher.h"
#include <atomic>
#include <chrono>
#include <thread>
struct FStandaloneCpuVertex { float Position[3]; float Normal[3]; };
struct FStandaloneCpuLevel
{
std::vector<FStandaloneCpuVertex> Vertices;
std::vector<uint32> Indices;
uint64 VertexCount = 0;
uint64 IndexCount = 0;
uint64 TileCount = 0;
uint64 NonEmptyTiles = 0;
uint64 DensitySamples = 0;
double Seconds = 0.0;
};
struct FStandaloneCpuClipmap
{
std::array<FStandaloneCpuLevel,9> Levels;
FIntVector CenterChunk = FIntVector::ZeroValue;
int32 MinimumChunkZ = MIN_int32;
int32 MaximumChunkZ = MAX_int32;
int32 WorkerCount = 6;
double TotalSeconds = 0.0;
bool bCancelled = false;
};
namespace StandaloneWorldMesher
{
using FClock = std::chrono::steady_clock;
inline FIntVector CameraChunk(const FVector& PositionVoxels)
{
return FIntVector(
FMath::FloorToInt(PositionVoxels.X / CHUNK_SIZE),
FMath::FloorToInt(PositionVoxels.Y / CHUNK_SIZE),
FMath::FloorToInt(PositionVoxels.Z / CHUNK_SIZE));
}
inline FStandaloneCpuClipmap Build(
FStandaloneOwnerWorld& World, const FVector& CameraPositionVoxels, int32 WorkerCount = 6,
const std::function<void(int32,FStandaloneCpuLevel&&)>& OnLevelReady = {},
bool bRetainMeshData = true, std::atomic<bool>* Cancel = nullptr)
{
FStandaloneCpuClipmap Output;
Output.WorkerCount = FMath::Clamp(WorkerCount,1,32);
Output.CenterChunk = CameraChunk(CameraPositionVoxels);
bool bHasStrate = false;
int32 TopChunkZ = 0, BottomChunkZ = 0;
if (World.Settings->bClampViewToStrate)
bHasStrate = World.Manager->GetStrateChunkZBounds(
Output.CenterChunk.Z,TopChunkZ,BottomChunkZ);
VoxelClipmapDesiredTiles::ResolveVerticalBounds(
Output.CenterChunk.Z,World.Settings->ViewDistanceUp,World.Settings->ViewDistanceDown,
World.Settings->StrateViewMarginChunks,World.Settings->bClampViewToStrate,bHasStrate,
TopChunkZ,BottomChunkZ,Output.MinimumChunkZ,Output.MaximumChunkZ);
VoxelClipmapDesiredTiles::FParameters Parameters;
Parameters.ClipRadius = World.ClipRadius;
Parameters.MaxClipLevel = FMath::Clamp(World.MaxClipLevel,0,8);
Parameters.RenderDistanceChunks = World.RenderDistanceChunks;
TArray<FVoxelTileKey> AllTiles;
VoxelClipmapDesiredTiles::Build(
Output.CenterChunk,Output.MinimumChunkZ,Output.MaximumChunkZ,Parameters,AllTiles);
std::array<std::vector<FVoxelTileKey>,9> TilesByLevel;
for (const FVoxelTileKey& Tile : AllTiles)
TilesByLevel[static_cast<size_t>(Tile.Level)].push_back(Tile);
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
const auto TotalStart = FClock::now();
for (int32 Level = 0; Level <= Parameters.MaxClipLevel; ++Level)
{
if (Cancel && Cancel->load(std::memory_order_relaxed)) { Output.bCancelled = true; break; }
const auto LevelStart = FClock::now();
const std::vector<FVoxelTileKey>& Tiles = TilesByLevel[static_cast<size_t>(Level)];
FStandaloneCpuLevel& LevelOutput = Output.Levels[static_cast<size_t>(Level)];
LevelOutput.TileCount = Tiles.size();
struct FTileResult { FVoxelMeshData Mesh; uint64 Samples = 0; };
std::vector<FTileResult> Results(Tiles.size());
std::atomic<size_t> Next{0};
const int32 ThreadCount = FMath::Min<int32>(Output.WorkerCount,
FMath::Max<int32>(1,static_cast<int32>(Tiles.size())));
std::vector<std::thread> Threads;
Threads.reserve(static_cast<size_t>(ThreadCount));
for (int32 Worker = 0; Worker < ThreadCount; ++Worker)
{
Threads.emplace_back([&]()
{
UVoxelMarchingCubesMesher Mesher;
Mesher.SetGenerator(World.Generator.get());
Mesher.bGenerateSkirts = World.bGenerateSkirts;
Mesher.SkirtCells = static_cast<float>(World.SkirtCells);
Mesher.LODOctaveDrop = World.LODOctaveDrop;
for (;;)
{
if (Cancel && Cancel->load(std::memory_order_relaxed)) break;
const size_t Index = Next.fetch_add(1,std::memory_order_relaxed);
if (Index >= Tiles.size()) break;
const FVoxelTileKey& Tile = Tiles[Index];
const int32 Extent = Tile.ExtentVoxels();
const int32 Cells = Level < World.FullResClipLevels
? CHUNK_SIZE : FMath::Max(1,World.CoarseTileCells);
const int32 Step = FMath::Max(1,Extent / Cells);
int32 BandMin = MIN_int32, BandMax = MAX_int32;
if (World.StrateContentCutMinLevel < 9
&& Level >= World.StrateContentCutMinLevel && bHasStrate)
{
BandMin = BottomChunkZ * CHUNK_SIZE;
BandMax = (TopChunkZ + 1) * CHUNK_SIZE;
}
int64 Samples = 0;
Results[Index].Mesh = Mesher.GenerateMesh(
Tile.OriginVoxels(),Step,Cells,nullptr,BandMin,BandMax,&Samples);
Results[Index].Samples = static_cast<uint64>(FMath::Max<int64>(0,Samples));
}
});
}
for (std::thread& Thread : Threads) Thread.join();
if (Cancel && Cancel->load(std::memory_order_relaxed)) { Output.bCancelled = true; break; }
size_t VertexCount = 0, IndexCount = 0;
for (const FTileResult& Result : Results)
{
if (!Result.Mesh.Triangles.IsEmpty()) ++LevelOutput.NonEmptyTiles;
VertexCount += static_cast<size_t>(Result.Mesh.Vertices.Num());
IndexCount += static_cast<size_t>(Result.Mesh.Triangles.Num());
LevelOutput.DensitySamples += Result.Samples;
}
LevelOutput.Vertices.reserve(VertexCount);
LevelOutput.Indices.reserve(IndexCount);
for (FTileResult& Result : Results)
{
const uint32 BaseVertex = static_cast<uint32>(LevelOutput.Vertices.size());
for (int32 I = 0; I < Result.Mesh.Vertices.Num(); ++I)
{
const FVector& P = Result.Mesh.Vertices[I];
const FVector& N = Result.Mesh.Normals[I];
LevelOutput.Vertices.push_back({
{static_cast<float>(P.X*0.01),static_cast<float>(P.Y*0.01),static_cast<float>(P.Z*0.01)},
{static_cast<float>(N.X),static_cast<float>(N.Y),static_cast<float>(N.Z)}});
}
for (int32 Index : Result.Mesh.Triangles)
LevelOutput.Indices.push_back(BaseVertex + static_cast<uint32>(Index));
}
LevelOutput.Seconds = std::chrono::duration<double>(FClock::now()-LevelStart).count();
LevelOutput.VertexCount = LevelOutput.Vertices.size();
LevelOutput.IndexCount = LevelOutput.Indices.size();
std::printf("clip level %d: %.3f s, %llu tiles (%llu nonempty), %llu vertices, %.2f M density samples\n",
Level,LevelOutput.Seconds,
static_cast<unsigned long long>(LevelOutput.TileCount),
static_cast<unsigned long long>(LevelOutput.NonEmptyTiles),
static_cast<unsigned long long>(LevelOutput.VertexCount),
static_cast<double>(LevelOutput.DensitySamples)/1.0e6);
std::fflush(stdout);
if (OnLevelReady) OnLevelReady(Level,std::move(LevelOutput));
if (!bRetainMeshData)
{
FStandaloneCpuLevel& Metadata = Output.Levels[static_cast<size_t>(Level)];
Metadata.Vertices.clear();
Metadata.Indices.clear();
Metadata.Vertices.shrink_to_fit();
Metadata.Indices.shrink_to_fit();
}
}
Output.TotalSeconds = std::chrono::duration<double>(FClock::now()-TotalStart).count();
std::printf("clipmap total: %.3f s on %d workers, %zu tiles\n",Output.TotalSeconds,
Output.WorkerCount,static_cast<size_t>(AllTiles.Num()));
return Output;
}
}
+56
View File
@@ -0,0 +1,56 @@
#include "OwnerWorldLoader.h"
#include "WorldMesher.h"
#include <bit>
#include <iostream>
#include <map>
int main(int argc, char** argv)
{
const std::string DataPath = argc > 1 ? argv[1] : "Standalone/data/owner_world.json";
FStandaloneOwnerWorld World;
std::string Error;
if (!StandaloneOwnerLoader::Load(DataPath,World,Error))
{
std::cerr << "LOAD FAILED: " << Error << '\n';
return 2;
}
std::cout << "Loaded owner world: seed=" << World.Seed
<< " strates=" << World.Manager->GetNumStrates()
<< " passages=" << World.Manager->GetPassages().Num()
<< " samples=" << World.DensitySamples.size() << '\n';
struct FMismatch { size_t Index; float Actual; };
std::vector<FMismatch> Mismatches;
Mismatches.reserve(World.DensitySamples.size());
std::map<std::string,size_t> SampleKinds;
for (size_t I = 0; I < World.DensitySamples.size(); ++I)
{
const FStandaloneParitySample& Sample = World.DensitySamples[I];
const float Actual = World.Generator->GetDensityAt(
static_cast<float>(Sample.PositionVoxels.X),
static_cast<float>(Sample.PositionVoxels.Y),
static_cast<float>(Sample.PositionVoxels.Z));
++SampleKinds[Sample.Kind];
if (std::bit_cast<uint32>(Actual) != std::bit_cast<uint32>(Sample.PluginDensity))
Mismatches.push_back({I,Actual});
}
std::cout << "Parity samples by kind:\n";
for (const auto& [Kind,Count] : SampleKinds) std::cout << " " << Kind << ": " << Count << '\n';
for (const FMismatch& M : Mismatches)
{
const FStandaloneParitySample& S = World.DensitySamples[M.Index];
std::cerr << "MISMATCH index=" << M.Index << " kind=" << S.Kind
<< " strate=" << S.StrateIndex << " at_voxels=("
<< S.PositionVoxels.X << ',' << S.PositionVoxels.Y << ',' << S.PositionVoxels.Z
<< ") plugin=" << S.PluginDensity << " standalone=" << M.Actual
<< " plugin_bits=0x" << std::hex << std::bit_cast<uint32>(S.PluginDensity)
<< " standalone_bits=0x" << std::bit_cast<uint32>(M.Actual) << std::dec << '\n';
}
std::cout << "Parity: " << (World.DensitySamples.size()-Mismatches.size()) << '/'
<< World.DensitySamples.size() << " bit-identical; mismatches=" << Mismatches.size() << '\n';
const FStandaloneCpuClipmap Clipmap = StandaloneWorldMesher::Build(
World,World.ViewerStartVoxels,6);
std::cout << "Clipmap build finished in " << Clipmap.TotalSeconds << " seconds.\n";
return Mismatches.empty() ? 0 : 1;
}