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:
1 parent
a443eb8546
commit
05f62cff36
114 files changed
+68206
-3
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user