#include "VoxelForgeExploreCommandlet.h" #include "VoxelStackOfflineSymbolizer.h" #include "CoreMinimal.h" #include "Async/ParallelFor.h" #include "Commandlets/Commandlet.h" #include "Containers/StringConv.h" #include "HAL/FileManager.h" #include "HAL/PlatformTime.h" #include "HAL/PlatformTLS.h" #include "IImageWrapper.h" #include "IImageWrapperModule.h" #include "Misc/CommandLine.h" #include "Misc/Crc.h" #include "Misc/FileHelper.h" #include "Misc/Parse.h" #include "Misc/Paths.h" #include "Modules/ModuleManager.h" #include "ProfilingDebugging/CpuProfilerTrace.h" #include "Dom/JsonObject.h" #include "Policies/CondensedJsonPrintPolicy.h" #include "Serialization/Archive.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" #include "Serialization/JsonWriter.h" #include "UObject/Package.h" #include "UObject/StrongObjectPtr.h" #include "UObject/UObjectGlobals.h" #include "VoxelCaveMorphology.h" #include "VoxelDiffLayer.h" #include "VoxelDensityOpStack.h" #include "VoxelDensityAblation.h" #include "VoxelDensityProfile.h" #include "VoxelGenerator.h" #include "VoxelMarchingCubesMesher.h" #include "VoxelNoise.h" #include "VoxelPassageGeometry.h" #include "VoxelSettings.h" #include "VoxelStackSampler.h" #include "VoxelStrateDefinition.h" #include "VoxelStrateManager.h" #include "VoxelStrateMeasure.h" #include "VoxelTypes.h" #include #include #include namespace { using FExploreJsonWriter = TJsonWriter>; constexpr int32 DefaultRenderWidth = 512; constexpr int32 DefaultRenderHeight = 288; constexpr float DefaultRenderStepVoxels = 0.25f; constexpr float DefaultRenderMaxDistanceVoxels = 160.0f; constexpr int32 DefaultExportSize = 128; constexpr int32 DefaultExportStep = 1; constexpr int32 DefaultMaxWalkCells = 32000000; constexpr float DefaultBudgetMinutes = 25.0f; constexpr float MaxBudgetMinutes = 30.0f; constexpr int32 MaxSyntheticStrates = 64; constexpr int32 MaxRenderPixels = 1048576; constexpr int64 MaxRenderDensitySamples = 400000000ll; constexpr int32 MaxExportSize = 128; constexpr int64 MaxWalkWorkingBytes = 768ll * 1024ll * 1024ll; // The exact fine-grid walk retains Air + Density + player-fit bits + non-finite flags, and can // transiently hold Parent + Queue + Path or Visited + NarrowCell + DFS stacks. 21 bytes/cell is a // conservative preflight figure for those arrays, including allocator slack/headroom. constexpr int64 EstimatedWalkBytesPerCell = 21ll; constexpr int64 MaxExportWorkingBytes = 1024ll * 1024ll * 1024ll; FString VoxelForgeSavedRoot() { FString Root = FPaths::ConvertRelativePathToFull( FPaths::Combine(FPaths::ProjectDir(), TEXT("Plugins/VoxelForge/Saved"))); FPaths::NormalizeDirectoryName(Root); return Root; } bool IsUnderVoxelForgeSaved(const FString& InPath) { FString Candidate = FPaths::ConvertRelativePathToFull(InPath); FString Root = VoxelForgeSavedRoot(); FPaths::NormalizeDirectoryName(Candidate); FPaths::NormalizeDirectoryName(Root); return Candidate.Equals(Root, ESearchCase::IgnoreCase) || Candidate.StartsWith(Root + TEXT("/"), ESearchCase::IgnoreCase) || Candidate.StartsWith(Root + TEXT("\\"), ESearchCase::IgnoreCase); } const TCHAR* ArchetypeName(ECaveGeneratorType Archetype) { switch (Archetype) { case ECaveGeneratorType::TunnelNetwork: return TEXT("TunnelNetwork"); case ECaveGeneratorType::FlatPlain: return TEXT("FlatPlain"); case ECaveGeneratorType::CrystalChamber: return TEXT("CrystalChamber"); case ECaveGeneratorType::Maze: return TEXT("Maze"); case ECaveGeneratorType::SurfaceWorld: return TEXT("SurfaceWorld"); case ECaveGeneratorType::VerticalShafts: return TEXT("VerticalShafts"); case ECaveGeneratorType::FloatingIslands: return TEXT("FloatingIslands"); case ECaveGeneratorType::Underwater: return TEXT("Underwater"); default: return TEXT("Unknown"); } } const TCHAR* ConnectivityResultName(EVoxelConnectivityResult Result) { switch (Result) { case EVoxelConnectivityResult::Connected: return TEXT("Connected"); case EVoxelConnectivityResult::NotConnectedAtThisResolution: return TEXT("NotConnectedAtThisResolution"); case EVoxelConnectivityResult::StartCellSolid: return TEXT("StartCellSolid"); case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GoalCellSolid"); case EVoxelConnectivityResult::OutOfWindow: return TEXT("OutOfWindow"); case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return TEXT("CoarseLiedBudgetExhausted"); case EVoxelConnectivityResult::StartCellNotPlayerFit: return TEXT("StartCellNotPlayerFit"); case EVoxelConnectivityResult::GoalCellNotPlayerFit: return TEXT("GoalCellNotPlayerFit"); default: return TEXT("Unknown"); } } bool ParseArchetype(const FString& Text, ECaveGeneratorType& OutArchetype) { FString Normalized = Text; Normalized.TrimStartAndEndInline(); Normalized.ToLowerInline(); Normalized.ReplaceInline(TEXT("_"), TEXT("")); Normalized.ReplaceInline(TEXT("-"), TEXT("")); const TPair Names[] = { { TEXT("tunnelnetwork"), ECaveGeneratorType::TunnelNetwork }, { TEXT("flatplain"), ECaveGeneratorType::FlatPlain }, { TEXT("crystalchamber"), ECaveGeneratorType::CrystalChamber }, { TEXT("maze"), ECaveGeneratorType::Maze }, { TEXT("surfaceworld"), ECaveGeneratorType::SurfaceWorld }, { TEXT("verticalshafts"), ECaveGeneratorType::VerticalShafts }, { TEXT("floatingislands"), ECaveGeneratorType::FloatingIslands }, { TEXT("floatingisland"), ECaveGeneratorType::FloatingIslands }, { TEXT("underwater"), ECaveGeneratorType::Underwater }, }; for (const auto& Pair : Names) { if (Normalized == Pair.Key) { OutArchetype = Pair.Value; return true; } } return false; } const TCHAR* NoiseTypeName(EVoxelNoiseType NoiseType) { switch (NoiseType) { case EVoxelNoiseType::FBM: return TEXT("FBM"); case EVoxelNoiseType::Ridged: return TEXT("Ridged"); case EVoxelNoiseType::Mixed: return TEXT("Mixed"); case EVoxelNoiseType::Cellular: return TEXT("Cellular"); default: return TEXT("Unknown"); } } bool ParseNoiseType(const FString& Text, EVoxelNoiseType& OutNoiseType) { FString Normalized = Text; Normalized.TrimStartAndEndInline(); Normalized.ToLowerInline(); Normalized.ReplaceInline(TEXT("_"), TEXT("")); Normalized.ReplaceInline(TEXT("-"), TEXT("")); if (Normalized == TEXT("fbm") || Normalized == TEXT("fractal")) { OutNoiseType = EVoxelNoiseType::FBM; return true; } if (Normalized == TEXT("ridged") || Normalized == TEXT("ridge")) { OutNoiseType = EVoxelNoiseType::Ridged; return true; } if (Normalized == TEXT("mixed")) { OutNoiseType = EVoxelNoiseType::Mixed; return true; } if (Normalized == TEXT("cellular") || Normalized == TEXT("worley")) { OutNoiseType = EVoxelNoiseType::Cellular; return true; } return false; } bool ParseRenderCamera(const FString& Text, FVector& OutCamera) { TArray Components; Text.ParseIntoArray(Components, TEXT(","), true); if (Components.Num() != 3) { return false; } const float X = FCString::Atof(*Components[0]); const float Y = FCString::Atof(*Components[1]); const float Z = FCString::Atof(*Components[2]); if (!FMath::IsFinite(X) || !FMath::IsFinite(Y) || !FMath::IsFinite(Z)) { return false; } OutCamera = FVector(X, Y, Z); return true; } bool ParseDensitySlice( const FString& Text, FVector& OutOrigin, FVector& OutDirection, int32& OutSamples, float& OutStep) { TArray Components; Text.ParseIntoArray(Components, TEXT(","), true); if (Components.Num() != 8) { return false; } const float OriginX = FCString::Atof(*Components[0]); const float OriginY = FCString::Atof(*Components[1]); const float OriginZ = FCString::Atof(*Components[2]); const float DirectionX = FCString::Atof(*Components[3]); const float DirectionY = FCString::Atof(*Components[4]); const float DirectionZ = FCString::Atof(*Components[5]); const int32 Samples = FCString::Atoi(*Components[6]); const float Step = FCString::Atof(*Components[7]); if (!FMath::IsFinite(OriginX) || !FMath::IsFinite(OriginY) || !FMath::IsFinite(OriginZ) || !FMath::IsFinite(DirectionX) || !FMath::IsFinite(DirectionY) || !FMath::IsFinite(DirectionZ) || !FMath::IsFinite(Step) || Samples < 2 || Samples > 4096 || Step <= 0.0f) { return false; } FVector Direction(DirectionX, DirectionY, DirectionZ); if (!Direction.Normalize()) { return false; } OutOrigin = FVector(OriginX, OriginY, OriginZ); OutDirection = Direction; OutSamples = Samples; OutStep = Step; return true; } bool NeedsOriginInWindow(ECaveGeneratorType Archetype) { // This is the same topology policy as VoxelForgePlayerFitWindow.h. That header is test-only, // so the editor commandlet keeps the policy here while still delegating the actual fit test to // VoxelStrateMeasure's shared VF_ stencil. switch (Archetype) { case ECaveGeneratorType::Maze: case ECaveGeneratorType::VerticalShafts: case ECaveGeneratorType::TunnelNetwork: case ECaveGeneratorType::Underwater: return true; case ECaveGeneratorType::FlatPlain: case ECaveGeneratorType::CrystalChamber: case ECaveGeneratorType::SurfaceWorld: case ECaveGeneratorType::FloatingIslands: default: return false; } } float BoundarySealForArchetype( const UVoxelStrateDefinition& Definition, ECaveGeneratorType Archetype) { switch (Archetype) { case ECaveGeneratorType::FlatPlain: case ECaveGeneratorType::CrystalChamber: return Definition.SlabParams.BoundarySealThickness; case ECaveGeneratorType::Maze: return Definition.MazeParams.BoundarySealThickness; case ECaveGeneratorType::SurfaceWorld: return Definition.SurfaceParams.BoundarySealThickness; case ECaveGeneratorType::VerticalShafts: return Definition.VerticalShaftParams.BoundarySealThickness; case ECaveGeneratorType::FloatingIslands: return Definition.FloatingIslandParams.BoundarySealThickness; case ECaveGeneratorType::TunnelNetwork: case ECaveGeneratorType::Underwater: default: return Definition.GenerationParams.BoundarySealThickness; } } struct FExploreArguments { int32 Seed = 0; bool bSeedOverride = false; ECaveGeneratorType Archetype = ECaveGeneratorType::Maze; int32 Slot = 4; FString StrateReference; FString SettingsReference; bool bRender = true; bool bWalk = true; bool bExport = true; bool bUseOperatorStack = true; bool bWormsEnabledOverride = false; bool bWormsEnabled = true; bool bWormStrengthOverride = false; float WormStrength = 0.0f; bool bProfileDensity = false; bool bProfileDensityFull = false; bool bPerfAttribution = false; bool bSampleStacks = false; uint32 SampleStacksIntervalUs = FVoxelStackSampler::DefaultIntervalUs; bool bProfileLod = false; bool bOpBounds = false; bool bFailureFocusRender = false; FString OutDirectory; int32 RenderWidth = DefaultRenderWidth; int32 RenderHeight = DefaultRenderHeight; float RenderStepVoxels = DefaultRenderStepVoxels; float RenderMaxDistanceVoxels = DefaultRenderMaxDistanceVoxels; int32 ExportSize = DefaultExportSize; int32 ExportStep = DefaultExportStep; // The canonical export owns one immutable lattice for the whole run. Batch cases can pass // density_grid_reuse=false to produce a matched wall-clock control. bool bReuseDensityGrid = true; bool bBlockEarlyOut = false; int32 MeshMinBatchSize = 1; int32 MaxWalkCells = DefaultMaxWalkCells; float BudgetMinutes = DefaultBudgetMinutes; bool bSurfaceRoughnessOverride = false; float SurfaceRoughness = 0.0f; bool bTunnelWarpStrengthOverride = false; float TunnelWarpStrength = 0.0f; bool bRoughnessNoiseTypeOverride = false; EVoxelNoiseType RoughnessNoiseType = EVoxelNoiseType::FBM; bool bRoughnessFrequencyOverride = false; float RoughnessFrequency = 0.0f; bool bTunnelFloorEnabledOverride = false; int32 TunnelFloorEnabled = 1; bool bTunnelFloorTerracingOverride = false; int32 TunnelFloorTerracing = 1; bool bTunnelFloorStepOverride = false; float TunnelFloorStep = 0.0f; bool bTunnelFloorMaxLedgeOverride = false; float TunnelFloorMaxLedge = 0.0f; bool bTunnelFloorGentleOverride = false; float TunnelFloorGentle = 0.0f; bool bTunnelFloorLedgesOverride = false; int32 TunnelFloorLedges = 0; bool bTunnelFloorMaxLedgesOverride = false; int32 TunnelFloorMaxLedges = 0; bool bRoomMouthRiseOverride = false; float RoomMouthRise = 0.0f; bool bRoomMouthRiseBlendOverride = false; float RoomMouthRiseBlend = 0.0f; bool bPassageFloorWritesOverride = false; int32 PassageFloorWrites = 1; bool bRenderCameraOverride = false; FVector RenderCamera = FVector::ZeroVector; bool bRenderTargetOverride = false; FVector RenderTarget = FVector::ZeroVector; bool bDensitySlice = false; FVector DensitySliceOrigin = FVector::ZeroVector; FVector DensitySliceDirection = FVector::ForwardVector; int32 DensitySliceSamples = 257; float DensitySliceStep = 0.5f; bool bTunnelCoreProbe = false; bool IsDensityProfilingEnabled() const { return bProfileDensity || bPerfAttribution; } FString CanonicalModes() const { FString Result; if (bRender) Result += TEXT("render,"); if (bWalk) Result += TEXT("walk,"); if (bExport) Result += TEXT("export,"); if (bOpBounds) Result += TEXT("opbounds,"); if (bTunnelCoreProbe) Result += TEXT("tunnelcoreprobe,"); if (Result.EndsWith(TEXT(","))) { Result.LeftChopInline(1); } return Result; } }; bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FString& OutError) { FString ArchetypeText = TEXT("Maze"); FString ModesText = TEXT("render,walk,export"); FString OutText; OutArguments.bSeedOverride = FParse::Value(*Params, TEXT("seed="), OutArguments.Seed); FParse::Value(*Params, TEXT("archetype="), ArchetypeText); FParse::Value(*Params, TEXT("slot="), OutArguments.Slot); FParse::Value(*Params, TEXT("strateref="), OutArguments.StrateReference); FParse::Value(*Params, TEXT("settingsref="), OutArguments.SettingsReference); FParse::Value(*Params, TEXT("modes="), ModesText); // FParse::Value treats a comma as a command-line separator in some UE commandlet // launch paths. Recover the complete token so both -modes=walk,export and the // quoted -modes="walk,export" form have the same meaning. const int32 ModesOffset = Params.Find(TEXT("modes="), ESearchCase::IgnoreCase); if (ModesOffset != INDEX_NONE) { ModesText = Params.Mid(ModesOffset + 6); const int32 ModesEnd = ModesText.Find(TEXT(" ")); if (ModesEnd != INDEX_NONE) { ModesText.LeftInline(ModesEnd); } ModesText.TrimQuotesInline(); } int32 UseOperatorStack = 1; FParse::Value(*Params, TEXT("opstack="), UseOperatorStack); OutArguments.bUseOperatorStack = UseOperatorStack != 0; int32 WormsEnabled = 1; if (FParse::Value(*Params, TEXT("wormsenabled="), WormsEnabled)) { OutArguments.bWormsEnabledOverride = true; OutArguments.bWormsEnabled = WormsEnabled != 0; } if (Params.Contains(TEXT("wormstrength="), ESearchCase::IgnoreCase) && FParse::Value(*Params, TEXT("wormstrength="), OutArguments.WormStrength)) { OutArguments.bWormStrengthOverride = true; } OutArguments.bProfileDensity = FParse::Param(*Params, TEXT("profiledensity")); OutArguments.bProfileDensityFull = FParse::Param(*Params, TEXT("profiledensityfull")); OutArguments.bProfileDensity |= OutArguments.bProfileDensityFull; OutArguments.bPerfAttribution = FParse::Param(*Params, TEXT("perfattribution")); OutArguments.bSampleStacks = FParse::Param(*Params, TEXT("samplestacks")); int32 SampleStacksIntervalUs = static_cast( FVoxelStackSampler::DefaultIntervalUs); if (FParse::Value(*Params, TEXT("samplestacks="), SampleStacksIntervalUs)) { OutArguments.bSampleStacks = true; OutArguments.SampleStacksIntervalUs = static_cast(FMath::Max(0, SampleStacksIntervalUs)); } OutArguments.bProfileLod = FParse::Param(*Params, TEXT("profilelod")); OutArguments.bOpBounds = FParse::Param(*Params, TEXT("opbounds")); OutArguments.bTunnelCoreProbe = FParse::Param(*Params, TEXT("tunnelcoreprobe")); FParse::Value(*Params, TEXT("out="), OutText); FParse::Value(*Params, TEXT("renderwidth="), OutArguments.RenderWidth); FParse::Value(*Params, TEXT("renderheight="), OutArguments.RenderHeight); FParse::Value(*Params, TEXT("renderstep="), OutArguments.RenderStepVoxels); FParse::Value(*Params, TEXT("rendermaxdistance="), OutArguments.RenderMaxDistanceVoxels); FParse::Value(*Params, TEXT("exportsize="), OutArguments.ExportSize); FParse::Value(*Params, TEXT("exportstep="), OutArguments.ExportStep); int32 DensityGridReuse = OutArguments.bReuseDensityGrid ? 1 : 0; FParse::Value(*Params, TEXT("densitygridreuse="), DensityGridReuse); OutArguments.bReuseDensityGrid = DensityGridReuse != 0; int32 BlockEarlyOut = OutArguments.bBlockEarlyOut ? 1 : 0; FParse::Value(*Params, TEXT("blockearlyout="), BlockEarlyOut); OutArguments.bBlockEarlyOut = BlockEarlyOut != 0; FParse::Value(*Params, TEXT("meshminbatch="), OutArguments.MeshMinBatchSize); OutArguments.MeshMinBatchSize = FMath::Clamp(OutArguments.MeshMinBatchSize, 1, 64); int32 Lod = 0; if (FParse::Value(*Params, TEXT("lod="), Lod)) { if (Lod < 0 || Lod > 3) { OutError = TEXT("lod must be an integer in [0,3]."); return false; } OutArguments.ExportStep = 1 << Lod; } FParse::Value(*Params, TEXT("maxwalkcells="), OutArguments.MaxWalkCells); FParse::Value(*Params, TEXT("budget="), OutArguments.BudgetMinutes); const bool bSurfaceRoughnessSpecified = Params.Contains( TEXT("surfaceroughness="), ESearchCase::IgnoreCase); const bool bSurfaceRoughnessParsed = FParse::Value( *Params, TEXT("surfaceroughness="), OutArguments.SurfaceRoughness); OutArguments.bSurfaceRoughnessOverride = bSurfaceRoughnessSpecified; const bool bTunnelWarpStrengthSpecified = Params.Contains( TEXT("tunnelwarp="), ESearchCase::IgnoreCase); const bool bTunnelWarpStrengthParsed = FParse::Value( *Params, TEXT("tunnelwarp="), OutArguments.TunnelWarpStrength); OutArguments.bTunnelWarpStrengthOverride = bTunnelWarpStrengthSpecified; FString RoughnessNoiseTypeText; const bool bRoughnessNoiseTypeSpecified = Params.Contains( TEXT("roughnesstype="), ESearchCase::IgnoreCase); const bool bRoughnessNoiseTypeParsed = FParse::Value( *Params, TEXT("roughnesstype="), RoughnessNoiseTypeText) && ParseNoiseType(RoughnessNoiseTypeText, OutArguments.RoughnessNoiseType); OutArguments.bRoughnessNoiseTypeOverride = bRoughnessNoiseTypeSpecified; const bool bRoughnessFrequencySpecified = Params.Contains( TEXT("roughnessfrequency="), ESearchCase::IgnoreCase); const bool bRoughnessFrequencyParsed = FParse::Value( *Params, TEXT("roughnessfrequency="), OutArguments.RoughnessFrequency); OutArguments.bRoughnessFrequencyOverride = bRoughnessFrequencySpecified; const bool bTunnelFloorEnabledSpecified = Params.Contains( TEXT("tunnelfloor="), ESearchCase::IgnoreCase); const bool bTunnelFloorEnabledParsed = FParse::Value( *Params, TEXT("tunnelfloor="), OutArguments.TunnelFloorEnabled); OutArguments.bTunnelFloorEnabledOverride = bTunnelFloorEnabledSpecified; const bool bTunnelFloorTerracingSpecified = Params.Contains( TEXT("tunnelfloorterracing="), ESearchCase::IgnoreCase); const bool bTunnelFloorTerracingParsed = FParse::Value( *Params, TEXT("tunnelfloorterracing="), OutArguments.TunnelFloorTerracing); OutArguments.bTunnelFloorTerracingOverride = bTunnelFloorTerracingSpecified; const bool bTunnelFloorStepSpecified = Params.Contains( TEXT("tunnelfloorstep="), ESearchCase::IgnoreCase); const bool bTunnelFloorStepParsed = FParse::Value( *Params, TEXT("tunnelfloorstep="), OutArguments.TunnelFloorStep); OutArguments.bTunnelFloorStepOverride = bTunnelFloorStepSpecified; const bool bTunnelFloorMaxLedgeSpecified = Params.Contains( TEXT("tunnelfloormaxledge="), ESearchCase::IgnoreCase); const bool bTunnelFloorMaxLedgeParsed = FParse::Value( *Params, TEXT("tunnelfloormaxledge="), OutArguments.TunnelFloorMaxLedge); OutArguments.bTunnelFloorMaxLedgeOverride = bTunnelFloorMaxLedgeSpecified; const bool bTunnelFloorGentleSpecified = Params.Contains( TEXT("tunnelfloorgentle="), ESearchCase::IgnoreCase); const bool bTunnelFloorGentleParsed = FParse::Value( *Params, TEXT("tunnelfloorgentle="), OutArguments.TunnelFloorGentle); OutArguments.bTunnelFloorGentleOverride = bTunnelFloorGentleSpecified; const bool bTunnelFloorLedgesSpecified = Params.Contains( TEXT("tunnelfloorledges="), ESearchCase::IgnoreCase); const bool bTunnelFloorLedgesParsed = FParse::Value( *Params, TEXT("tunnelfloorledges="), OutArguments.TunnelFloorLedges); OutArguments.bTunnelFloorLedgesOverride = bTunnelFloorLedgesSpecified; const bool bTunnelFloorMaxLedgesSpecified = Params.Contains( TEXT("tunnelfloormaxledges="), ESearchCase::IgnoreCase); const bool bTunnelFloorMaxLedgesParsed = FParse::Value( *Params, TEXT("tunnelfloormaxledges="), OutArguments.TunnelFloorMaxLedges); OutArguments.bTunnelFloorMaxLedgesOverride = bTunnelFloorMaxLedgesSpecified; const bool bRoomMouthRiseSpecified = Params.Contains( TEXT("roommouthrise="), ESearchCase::IgnoreCase); const bool bRoomMouthRiseParsed = FParse::Value( *Params, TEXT("roommouthrise="), OutArguments.RoomMouthRise); OutArguments.bRoomMouthRiseOverride = bRoomMouthRiseSpecified; const bool bRoomMouthRiseBlendSpecified = Params.Contains( TEXT("roommouthriseblend="), ESearchCase::IgnoreCase); const bool bRoomMouthRiseBlendParsed = FParse::Value( *Params, TEXT("roommouthriseblend="), OutArguments.RoomMouthRiseBlend); OutArguments.bRoomMouthRiseBlendOverride = bRoomMouthRiseBlendSpecified; const bool bPassageFloorWritesSpecified = Params.Contains( TEXT("passagefloorwrites="), ESearchCase::IgnoreCase); const bool bPassageFloorWritesParsed = FParse::Value( *Params, TEXT("passagefloorwrites="), OutArguments.PassageFloorWrites); OutArguments.bPassageFloorWritesOverride = bPassageFloorWritesSpecified; FString RenderCameraText; const bool bRenderCameraSpecified = Params.Contains( TEXT("rendercamera="), ESearchCase::IgnoreCase); bool bRenderCameraParsed = false; if (bRenderCameraSpecified) { const int32 CameraOffset = Params.Find(TEXT("rendercamera="), ESearchCase::IgnoreCase); if (CameraOffset != INDEX_NONE) { RenderCameraText = Params.Mid(CameraOffset + 13); const int32 CameraEnd = RenderCameraText.Find(TEXT(" ")); if (CameraEnd != INDEX_NONE) { RenderCameraText.LeftInline(CameraEnd); } RenderCameraText.TrimQuotesInline(); bRenderCameraParsed = ParseRenderCamera(RenderCameraText, OutArguments.RenderCamera); } } OutArguments.bRenderCameraOverride = bRenderCameraSpecified; FString RenderTargetText; const bool bRenderTargetSpecified = Params.Contains( TEXT("rendertarget="), ESearchCase::IgnoreCase); bool bRenderTargetParsed = false; if (bRenderTargetSpecified) { const int32 TargetOffset = Params.Find(TEXT("rendertarget="), ESearchCase::IgnoreCase); if (TargetOffset != INDEX_NONE) { RenderTargetText = Params.Mid(TargetOffset + 13); const int32 TargetEnd = RenderTargetText.Find(TEXT(" ")); if (TargetEnd != INDEX_NONE) { RenderTargetText.LeftInline(TargetEnd); } RenderTargetText.TrimQuotesInline(); bRenderTargetParsed = ParseRenderCamera(RenderTargetText, OutArguments.RenderTarget); } } OutArguments.bRenderTargetOverride = bRenderTargetSpecified; FString DensitySliceText; const bool bDensitySliceSpecified = Params.Contains( TEXT("densityslice="), ESearchCase::IgnoreCase); bool bDensitySliceParsed = false; if (bDensitySliceSpecified) { const int32 SliceOffset = Params.Find(TEXT("densityslice="), ESearchCase::IgnoreCase); if (SliceOffset != INDEX_NONE) { DensitySliceText = Params.Mid(SliceOffset + 13); const int32 SliceEnd = DensitySliceText.Find(TEXT(" ")); if (SliceEnd != INDEX_NONE) { DensitySliceText.LeftInline(SliceEnd); } DensitySliceText.TrimQuotesInline(); bDensitySliceParsed = ParseDensitySlice( DensitySliceText, OutArguments.DensitySliceOrigin, OutArguments.DensitySliceDirection, OutArguments.DensitySliceSamples, OutArguments.DensitySliceStep); } } OutArguments.bDensitySlice = bDensitySliceSpecified; int32 FailureFocusRender = 0; FParse::Value(*Params, TEXT("failurefocus="), FailureFocusRender); OutArguments.bFailureFocusRender = FailureFocusRender != 0; if (!ParseArchetype(ArchetypeText, OutArguments.Archetype)) { OutError = FString::Printf( TEXT("Unknown archetype '%s'. Expected Maze, TunnelNetwork, FlatPlain, " "CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, or Underwater."), *ArchetypeText); return false; } OutArguments.bRender = false; OutArguments.bWalk = false; OutArguments.bExport = false; TArray Modes; ModesText.ParseIntoArray(Modes, TEXT(","), true); for (FString Mode : Modes) { Mode.TrimStartAndEndInline(); Mode.ToLowerInline(); if (Mode == TEXT("all")) { OutArguments.bRender = true; OutArguments.bWalk = true; OutArguments.bExport = true; } else if (Mode == TEXT("opbounds")) { OutArguments.bOpBounds = true; } else if (Mode == TEXT("tunnelcoreprobe")) { OutArguments.bTunnelCoreProbe = true; } else if (Mode == TEXT("render")) { OutArguments.bRender = true; } else if (Mode == TEXT("walk")) { OutArguments.bWalk = true; } else if (Mode == TEXT("export")) { OutArguments.bExport = true; } else { OutError = FString::Printf( TEXT("Unknown mode '%s'. Expected render, walk, export, opbounds, tunnelcoreprobe, or all."), *Mode); return false; } } if (!OutArguments.bRender && !OutArguments.bWalk && !OutArguments.bExport && !OutArguments.bOpBounds && !OutArguments.bTunnelCoreProbe) { OutError = TEXT("At least one mode must be selected."); return false; } if (OutArguments.Slot < 1 || OutArguments.Slot > MaxSyntheticStrates - 2) { OutError = FString::Printf( TEXT("slot must be an interior slot in [1,%d] so arrival and departure mouths exist."), MaxSyntheticStrates - 2); return false; } if (OutArguments.RenderWidth < 16 || OutArguments.RenderHeight < 16 || static_cast(OutArguments.RenderWidth) * static_cast(OutArguments.RenderHeight) > MaxRenderPixels) { OutError = FString::Printf( TEXT("renderwidth*renderheight must be between 256 pixels and %d pixels."), MaxRenderPixels); return false; } if (!FMath::IsFinite(OutArguments.RenderStepVoxels) || OutArguments.RenderStepVoxels < 0.125f || OutArguments.RenderStepVoxels > 8.0f) { OutError = TEXT("renderstep must be finite and in [0.125,8] voxels."); return false; } if (!FMath::IsFinite(OutArguments.RenderMaxDistanceVoxels) || OutArguments.RenderMaxDistanceVoxels <= 0.0f || OutArguments.RenderMaxDistanceVoxels > 2048.0f) { OutError = TEXT("rendermaxdistance must be finite and in (0,2048] voxels."); return false; } // Render no longer budgets density samples: pixels are rasterised from the one canonical // mesh. Keep the legacy renderstep/maxdistance arguments for compatible invocations and use // only maxdistance as the raster depth clip. if (OutArguments.ExportSize < CHUNK_SIZE || OutArguments.ExportSize > MaxExportSize || OutArguments.ExportSize % CHUNK_SIZE != 0) { OutError = FString::Printf( TEXT("exportsize must be a multiple of CHUNK_SIZE (%d) in [%d,%d] cells per axis."), CHUNK_SIZE, CHUNK_SIZE, MaxExportSize); return false; } if (OutArguments.ExportStep < 1 || OutArguments.ExportStep > 8 || (OutArguments.ExportStep & (OutArguments.ExportStep - 1)) != 0 || CHUNK_SIZE % OutArguments.ExportStep != 0) { OutError = TEXT("exportstep must be a power of two in [1,8] and divide CHUNK_SIZE."); return false; } if (OutArguments.MaxWalkCells <= 0) { OutError = TEXT("maxwalkcells must be greater than zero."); return false; } if (!FMath::IsFinite(OutArguments.BudgetMinutes) || OutArguments.BudgetMinutes <= 0.0f || OutArguments.BudgetMinutes > MaxBudgetMinutes) { OutError = FString::Printf( TEXT("budget must be finite and in (0,%g] minutes."), MaxBudgetMinutes); return false; } if (OutArguments.bSampleStacks && (OutArguments.SampleStacksIntervalUs < FVoxelStackSampler::MinIntervalUs || OutArguments.SampleStacksIntervalUs > FVoxelStackSampler::MaxIntervalUs)) { OutError = FString::Printf( TEXT("samplestacks interval must be in [%u,%u] microseconds."), FVoxelStackSampler::MinIntervalUs, FVoxelStackSampler::MaxIntervalUs); return false; } if (OutArguments.bSurfaceRoughnessOverride && !bSurfaceRoughnessParsed) { OutError = TEXT("surfaceroughness must be a finite number greater than or equal to zero."); return false; } if (OutArguments.bSurfaceRoughnessOverride && (!FMath::IsFinite(OutArguments.SurfaceRoughness) || OutArguments.SurfaceRoughness < 0.0f)) { OutError = TEXT("surfaceroughness must be finite and greater than or equal to zero."); return false; } if (OutArguments.bTunnelWarpStrengthOverride && (!bTunnelWarpStrengthParsed || !FMath::IsFinite(OutArguments.TunnelWarpStrength) || OutArguments.TunnelWarpStrength < 0.0f)) { OutError = TEXT("tunnelwarp must be finite and greater than or equal to zero."); return false; } if (OutArguments.bRoughnessNoiseTypeOverride && !bRoughnessNoiseTypeParsed) { OutError = TEXT("roughnesstype must be FBM, Ridged, Mixed, or Cellular."); return false; } if (OutArguments.bRoughnessFrequencyOverride && (!bRoughnessFrequencyParsed || !FMath::IsFinite(OutArguments.RoughnessFrequency) || OutArguments.RoughnessFrequency <= 0.0f)) { OutError = TEXT("roughnessfrequency must be finite and greater than zero."); return false; } if (OutArguments.bTunnelFloorEnabledOverride && (!bTunnelFloorEnabledParsed || (OutArguments.TunnelFloorEnabled != 0 && OutArguments.TunnelFloorEnabled != 1))) { OutError = TEXT("tunnelfloor must be 0 or 1."); return false; } if (OutArguments.bTunnelFloorTerracingOverride && (!bTunnelFloorTerracingParsed || (OutArguments.TunnelFloorTerracing != 0 && OutArguments.TunnelFloorTerracing != 1))) { OutError = TEXT("tunnelfloorterracing must be 0 or 1."); return false; } if (OutArguments.bTunnelFloorStepOverride && (!bTunnelFloorStepParsed || !FMath::IsFinite(OutArguments.TunnelFloorStep) || OutArguments.TunnelFloorStep < 0.0f)) { OutError = TEXT("tunnelfloorstep must be finite and non-negative."); return false; } if (OutArguments.bTunnelFloorMaxLedgeOverride && (!bTunnelFloorMaxLedgeParsed || !FMath::IsFinite(OutArguments.TunnelFloorMaxLedge) || OutArguments.TunnelFloorMaxLedge <= 0.0f)) { OutError = TEXT("tunnelfloormaxledge must be finite and greater than zero."); return false; } if (OutArguments.bTunnelFloorGentleOverride && (!bTunnelFloorGentleParsed || !FMath::IsFinite(OutArguments.TunnelFloorGentle) || OutArguments.TunnelFloorGentle < 0.0f || OutArguments.TunnelFloorGentle > VoxelPassageGeometry::TunnelFloorGentleSlopeMaximumDegrees)) { OutError = TEXT("tunnelfloorgentle must be finite degrees in [0,90]."); return false; } if (OutArguments.bTunnelFloorLedgesOverride && (!bTunnelFloorLedgesParsed || OutArguments.TunnelFloorLedges < 0 || OutArguments.TunnelFloorLedges > 4096)) { OutError = TEXT("tunnelfloorledges must be an integer in [0,4096]."); return false; } if (OutArguments.bTunnelFloorMaxLedgesOverride && (!bTunnelFloorMaxLedgesParsed || OutArguments.TunnelFloorMaxLedges < 1 || OutArguments.TunnelFloorMaxLedges > 4096)) { OutError = TEXT("tunnelfloormaxledges must be an integer in [1,4096]."); return false; } if (OutArguments.bRoomMouthRiseOverride && (!bRoomMouthRiseParsed || !FMath::IsFinite(OutArguments.RoomMouthRise) || OutArguments.RoomMouthRise < 0.0f || OutArguments.RoomMouthRise > 1.0f)) { OutError = TEXT("roommouthrise must be finite and in [0,1]."); return false; } if (OutArguments.bRoomMouthRiseBlendOverride && (!bRoomMouthRiseBlendParsed || !FMath::IsFinite(OutArguments.RoomMouthRiseBlend) || OutArguments.RoomMouthRiseBlend < 8.0f || OutArguments.RoomMouthRiseBlend > 128.0f)) { OutError = TEXT("roommouthriseblend must be finite and in [8,128]."); return false; } if (OutArguments.bPassageFloorWritesOverride && (!bPassageFloorWritesParsed || (OutArguments.PassageFloorWrites != 0 && OutArguments.PassageFloorWrites != 1))) { OutError = TEXT("passagefloorwrites must be 0 or 1."); return false; } if (OutArguments.bRenderCameraOverride && !bRenderCameraParsed) { OutError = TEXT("rendercamera must be three finite comma-separated voxel coordinates."); return false; } if (OutArguments.bRenderTargetOverride && !bRenderTargetParsed) { OutError = TEXT("rendertarget must be three finite comma-separated voxel coordinates."); return false; } if (OutArguments.bDensitySlice && !bDensitySliceParsed) { OutError = TEXT( "densityslice must be origin x,y,z, direction x,y,z, sample count, and positive step."); return false; } if (OutText.IsEmpty()) { OutText = FPaths::Combine( VoxelForgeSavedRoot(), FString::Printf(TEXT("Explore/seed%d_%s_slot%d"), OutArguments.Seed, ArchetypeName(OutArguments.Archetype), OutArguments.Slot)); } if (FPaths::IsRelative(OutText)) { OutError = FString::Printf( TEXT("out must be an absolute path under '%s'."), *VoxelForgeSavedRoot()); return false; } OutArguments.OutDirectory = FPaths::ConvertRelativePathToFull(OutText); FPaths::NormalizeDirectoryName(OutArguments.OutDirectory); if (!IsUnderVoxelForgeSaved(OutArguments.OutDirectory)) { OutError = FString::Printf( TEXT("out must be an absolute path under '%s' (got '%s')."), *VoxelForgeSavedRoot(), *OutArguments.OutDirectory); return false; } return true; } VoxelDensityProfile::EMode ExploreProfileMode(const FExploreArguments& Arguments) { if (Arguments.bPerfAttribution) { return VoxelDensityProfile::EMode::Attribution; } return Arguments.bProfileDensityFull ? VoxelDensityProfile::EMode::Full : VoxelDensityProfile::EMode::Sampled; } struct FExploreBudget { double StartSeconds = 0.0; double LimitSeconds = 0.0; bool bTruncated = false; FString TruncatedDuring; TArray CompletedModes; FExploreBudget(double InStartSeconds, double InLimitSeconds) : StartSeconds(InStartSeconds) , LimitSeconds(InLimitSeconds) { } double ElapsedSeconds() const { return FPlatformTime::Seconds() - StartSeconds; } double RemainingSeconds() const { return LimitSeconds - ElapsedSeconds(); } bool ShouldStop(const TCHAR* Phase) { if (bTruncated) { return true; } // Leave a small deterministic cleanup window for the current phase to close files and // emit the report. The owner-facing hard ceiling is still enforced by the argument cap at // 30 minutes; the default 25-minute budget leaves substantially more headroom. constexpr double CleanupReserveSeconds = 0.25; if (RemainingSeconds() <= CleanupReserveSeconds) { bTruncated = true; TruncatedDuring = Phase != nullptr ? FString(Phase) : TEXT("unknown"); UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeExplore] budget reached during %s; keeping completed artifacts."), *TruncatedDuring); return true; } return false; } void CompleteMode(const TCHAR* Mode) { if (Mode == nullptr || bTruncated) { return; } CompletedModes.AddUnique(FString(Mode)); } }; struct FExploreMeshTriangle { FVector A = FVector::ZeroVector; FVector B = FVector::ZeroVector; FVector C = FVector::ZeroVector; FVector NormalA = FVector::UpVector; FVector NormalB = FVector::UpVector; FVector NormalC = FVector::UpVector; FVector Min = FVector::ZeroVector; FVector Max = FVector::ZeroVector; FVector Centre = FVector::ZeroVector; FVector Normal = FVector::UpVector; }; struct FExploreMeshBvhNode { FVector Min = FVector::ZeroVector; FVector Max = FVector::ZeroVector; int32 Left = INDEX_NONE; int32 Right = INDEX_NONE; int32 FirstTriangle = 0; int32 TriangleCount = 0; bool IsLeaf() const { return Left == INDEX_NONE && Right == INDEX_NONE; } }; struct FExploreWorld { TStrongObjectPtr Settings; TStrongObjectPtr Manager; TStrongObjectPtr DiffLayer; TStrongObjectPtr Generator; TStrongObjectPtr Mesher; TArray> Definitions; // One canonical mesh cache for this commandlet run. Render and export consume these exact // triangles; neither path is allowed to ask the density field once per pixel. FVoxelMeshData ExploreMesh; FVoxelSharedDensityGrid ExploreSharedDensityGrid; FIntVector ExploreMeshOrigin = FIntVector::ZeroValue; int32 ExploreMeshSize = 0; int32 ExploreMeshTileCount = 0; int32 ExploreMeshTilesCompleted = 0; // Per-tile geometry identity is retained in canonical Z/Y/X order so an optimization can be // compared without treating one aggregate hash or triangle total as a shape measurement. TArray ExploreTileTriangleCounts; TArray ExploreTileGeometryHashes; double ExploreMeshSeconds = 0.0; // Exact wall-clock partition of ExploreMeshSeconds. The first three fields end at the // parallel tile launch; merge closes the mesh clock. Remainder is intentionally retained // instead of rounding the ledger to 100%. double ExploreMeshPreparationSeconds = 0.0; double ExploreMeshPreTileSetupSeconds = 0.0; double ExploreMeshMergeSeconds = 0.0; double ExploreMeshAccountingRemainderSeconds = 0.0; double ExploreMeshHashSeconds = 0.0; double ExploreDensityGridSeconds = 0.0; int64 ExploreDensityGridTotalSamples = 0; int64 ExploreDensityGridUniqueSamples = 0; int64 ExploreDensityGridDuplicateSamplesBefore = 0; int64 ExploreDensityGridDuplicateSamplesAfter = 0; bool bExploreDensityGridReused = false; int32 ExploreMeshTaskWorkerCount = 0; int32 ExploreMeshTaskJobCount = 0; int32 ExploreMeshTaskMinBatchSize = 1; double ExploreMeshTaskMinSeconds = 0.0; double ExploreMeshTaskMeanSeconds = 0.0; double ExploreMeshTaskMaxSeconds = 0.0; double ExploreMeshTaskSumSeconds = 0.0; double ExploreMeshTaskWallSeconds = 0.0; double ExploreMeshTaskLaunchOverheadSeconds = 0.0; bool bExploreMeshBuilt = false; bool bExploreMeshComplete = false; FString ExploreMeshError; FString ExploreGeometryHash; // A deterministic CPU acceleration structure over the canonical triangles. It is built once // after meshing and reused by every viewpoint; no density query is reachable from raster. TArray ExploreTriangles; TArray ExploreTriangleOrder; TArray ExploreBvhNodes; int32 ExploreBvhRoot = INDEX_NONE; double ExploreAccelerationSeconds = 0.0; bool bExploreAccelerationBuilt = false; int32 TargetBottomWorldZ = 0; int32 TargetTopWorldZ = 0; float TargetBoundarySealThickness = 4.0f; bool Build(const FExploreArguments& Arguments, FString& OutError) { const bool bUseAuthoredSettings = !Arguments.SettingsReference.IsEmpty(); const bool bUseAuthoredStrate = !Arguments.StrateReference.IsEmpty(); UVoxelStrateDefinition* AuthoredDefinition = nullptr; if (bUseAuthoredStrate) { AuthoredDefinition = LoadObject( nullptr, *Arguments.StrateReference); if (AuthoredDefinition == nullptr) { OutError = FString::Printf( TEXT("Could not load staged strate asset '%s'."), *Arguments.StrateReference); return false; } } if (bUseAuthoredSettings) { UVoxelSettings* AuthoredSettings = LoadObject( nullptr, *Arguments.SettingsReference); if (AuthoredSettings == nullptr) { OutError = FString::Printf( TEXT("Could not load staged settings asset '%s'."), *Arguments.SettingsReference); return false; } Settings = TStrongObjectPtr( DuplicateObject(AuthoredSettings, GetTransientPackage())); if (!Settings.IsValid()) { OutError = TEXT("Could not duplicate the staged settings asset into the transient package."); return false; } } else { Settings = TStrongObjectPtr( NewObject(GetTransientPackage(), NAME_None, RF_Transient)); } const int32 NumStrates = bUseAuthoredSettings ? FMath::Max(Settings->TotalStrates, Arguments.Slot + 2) : Arguments.Slot + 2; if (NumStrates > MaxSyntheticStrates) { OutError = TEXT("The synthetic layout would exceed the commandlet strate cap."); return false; } if (!bUseAuthoredSettings) { Settings->Seed = Arguments.Seed; Settings->TotalStrates = NumStrates; Settings->InterStrateGapChunks = 0; Settings->WorldRadiusVoxels = 0.0f; Settings->EdgeSealThickness = 64.0f; Settings->OriginSpineRadius = 14.0f; Settings->bOpenSurfaceEntry = true; } else if (Arguments.bSeedOverride) { Settings->Seed = Arguments.Seed; } // The transient fixed-definition layout may need to extend an authored settings asset // for a requested interior slot. Keep that duplicate's advertised count consistent; the // loaded package remains untouched. Settings->TotalStrates = NumStrates; if (bUseAuthoredStrate) { // The owner’s snapshot states that every slot uses DA_Strate3. Clone it per slot so // commandlet overrides never mutate a loaded package or leak between slots. Settings->StratePool.Reset(); Settings->FixedStrates.Reset(); } Definitions.Reserve(NumStrates); for (int32 Index = 0; Index < NumStrates; ++Index) { UVoxelStrateDefinition* Definition = bUseAuthoredStrate ? DuplicateObject( AuthoredDefinition, GetTransientPackage()) : NewObject( GetTransientPackage(), NAME_None, RF_Transient); if (Definition == nullptr) { OutError = FString::Printf( TEXT("Could not create transient strate definition for slot %d."), Index); return false; } if (!bUseAuthoredStrate) { Definition->StrateName = FText::FromString(FString::Printf( TEXT("VoxelForgeExplore_%s_slot%d"), ArchetypeName(Index == Arguments.Slot ? Arguments.Archetype : ECaveGeneratorType::TunnelNetwork), Index)); Definition->StrateDescription = FText::FromString( TEXT("Transient, deterministic commandlet definition.")); // The commandlet's acceptance sweep uses the owner-facing base spacing: // 4 chunks * 32 voxels * 0.25 m = 32 m per strate. Production assets may be taller; // this keeps the measured tunnel arithmetic tied to the stated ~32 m descent. Definition->StrateHeightInChunks = 4; Definition->TransitionType = EVoxelStrateTransition::Hard; Definition->GeneratorType = Index == Arguments.Slot ? Arguments.Archetype : ECaveGeneratorType::TunnelNetwork; // Explicitly select the production switch so the same synthetic layout, seed and // mesher invocation can be measured through both density branches. Definition->bUseOperatorStack = Arguments.bUseOperatorStack; if (Arguments.bWormsEnabledOverride) { Definition->bEnableWorms = Arguments.bWormsEnabled; } if (Arguments.bWormStrengthOverride && (Definition->GeneratorType == ECaveGeneratorType::TunnelNetwork || Definition->GeneratorType == ECaveGeneratorType::Underwater)) { Definition->GenerationParams.WormStrength = Arguments.WormStrength; } } const TSoftObjectPtr SoftDefinition(Definition); Settings->FixedStrates.Add(Index, SoftDefinition); if (!bUseAuthoredStrate) { Settings->StratePool.Add(SoftDefinition); } Definitions.Add(TStrongObjectPtr(Definition)); } auto ApplyTargetOverrides = [&Arguments](UVoxelStrateDefinition* Definition) -> bool { if (Definition == nullptr) { return false; } const ECaveGeneratorType TargetArchetype = Definition->GeneratorType; if (Arguments.bSurfaceRoughnessOverride) { switch (TargetArchetype) { case ECaveGeneratorType::FlatPlain: case ECaveGeneratorType::CrystalChamber: Definition->SlabParams.FloorRoughness = Arguments.SurfaceRoughness; Definition->SlabParams.CeilingRoughness = Arguments.SurfaceRoughness; break; case ECaveGeneratorType::Maze: Definition->MazeParams.SurfaceRoughness = Arguments.SurfaceRoughness; break; case ECaveGeneratorType::SurfaceWorld: Definition->SurfaceParams.SurfaceRoughness = Arguments.SurfaceRoughness; break; case ECaveGeneratorType::VerticalShafts: Definition->VerticalShaftParams.SurfaceRoughness = Arguments.SurfaceRoughness; break; case ECaveGeneratorType::FloatingIslands: Definition->FloatingIslandParams.SurfaceRoughness = Arguments.SurfaceRoughness; break; case ECaveGeneratorType::TunnelNetwork: case ECaveGeneratorType::Underwater: default: Definition->GenerationParams.SurfaceRoughness = Arguments.SurfaceRoughness; break; } } if (Arguments.bRoughnessNoiseTypeOverride || Arguments.bRoughnessFrequencyOverride) { if (TargetArchetype != ECaveGeneratorType::TunnelNetwork && TargetArchetype != ECaveGeneratorType::Underwater) { return false; } if (Arguments.bRoughnessNoiseTypeOverride) { Definition->GenerationParams.RoughnessNoiseType = Arguments.RoughnessNoiseType; } if (Arguments.bRoughnessFrequencyOverride) { Definition->GenerationParams.RoughnessFrequency = Arguments.RoughnessFrequency; } } FStrateGenerationParams& TunnelParams = Definition->GenerationParams; if (Arguments.bTunnelWarpStrengthOverride) { if (TargetArchetype != ECaveGeneratorType::TunnelNetwork && TargetArchetype != ECaveGeneratorType::Underwater) { return false; } TunnelParams.TunnelWarpStrength = Arguments.TunnelWarpStrength; } if (Arguments.bTunnelFloorEnabledOverride) { // Retain the command-line spelling for old probes, but do not feed the removed // floor switch into generation. The serialized property has the same // compatibility-only treatment in UVoxelStrateDefinition::PostLoad. } if (Arguments.bTunnelFloorTerracingOverride) { TunnelParams.bTunnelFloorTerracingEnabled = Arguments.TunnelFloorTerracing != 0; } if (Arguments.bTunnelFloorStepOverride) { TunnelParams.TunnelFloorTerraceStepHeight = Arguments.TunnelFloorStep; } if (Arguments.bTunnelFloorMaxLedgeOverride) { TunnelParams.TunnelFloorMaxLedgeHeight = Arguments.TunnelFloorMaxLedge; } if (Arguments.bTunnelFloorGentleOverride) { TunnelParams.TunnelFloorGentleSlopeThreshold = Arguments.TunnelFloorGentle; } if (Arguments.bTunnelFloorLedgesOverride) { TunnelParams.TunnelFloorLedgeCountPreference = Arguments.TunnelFloorLedges; } if (Arguments.bTunnelFloorMaxLedgesOverride) { TunnelParams.TunnelFloorMaxLedges = Arguments.TunnelFloorMaxLedges; } if (Arguments.bRoomMouthRiseOverride) { TunnelParams.RoomMouthRiseStrength = Arguments.RoomMouthRise; } if (Arguments.bRoomMouthRiseBlendOverride) { TunnelParams.RoomMouthRiseBlendVoxels = Arguments.RoomMouthRiseBlend; } return true; }; if (!Definitions.IsValidIndex(Arguments.Slot) || !ApplyTargetOverrides(Definitions[Arguments.Slot].Get())) { OutError = TEXT("The target definition rejected a commandlet override."); return false; } Manager = TStrongObjectPtr( NewObject(GetTransientPackage(), NAME_None, RF_Transient)); if (!Manager->Initialize(Settings.Get(), Settings->Seed)) { OutError = TEXT("UVoxelStrateManager::Initialize refused the transient layout."); return false; } if (Arguments.bPassageFloorWritesOverride) { Manager->SetPassageSupportFloorWritesEnabledForDiagnostics( Arguments.PassageFloorWrites != 0); } if (!Manager->GetLayout().IsValidIndex(Arguments.Slot)) { OutError = TEXT("The requested target slot did not enter the manager layout."); return false; } const FStrateSlot& Target = Manager->GetLayout()[Arguments.Slot]; if (Target.Definition == nullptr) { OutError = TEXT("The requested target slot has no resolved definition."); return false; } const ECaveGeneratorType TargetArchetype = Target.Definition->GeneratorType; const FStrateGenerationParams& ReportParams = Target.Definition->GenerationParams; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore][OwnerAssets] strate=%s settings=%s slot=%d " "seed=%d height_chunks=%d archetype=%s opstack=%d " "floor_switch_ignored=1 floor_terracing=%d floor_step=%.9g max_ledge=%.9g " "gentle_slope_degrees=%.9g gentle_gradient=%.9g route_cap=%.9g " "wind_wave_cap=%d " "ledge_preference=%d max_ledges=%d tunnel_warp=%.9g terrain_ops=%d terrace_step=%.9g " "layer_spacing=%.9g rib_spacing=%.9g"), bUseAuthoredStrate ? *Arguments.StrateReference : TEXT("synthetic"), bUseAuthoredSettings ? *Arguments.SettingsReference : TEXT("synthetic"), Arguments.Slot, Settings->Seed, Target.Definition->StrateHeightInChunks, ArchetypeName(TargetArchetype), Target.Definition->bUseOperatorStack ? 1 : 0, ReportParams.bTunnelFloorTerracingEnabled ? 1 : 0, ReportParams.TunnelFloorTerraceStepHeight, ReportParams.TunnelFloorMaxLedgeHeight, ReportParams.TunnelFloorGentleSlopeThreshold, VoxelPassageGeometry::TunnelFloorGradientFromDegrees( ReportParams.TunnelFloorGentleSlopeThreshold), VoxelPassageGeometry::TunnelFloorMaximumRouteLengthVoxels, VoxelPassageGeometry::TunnelFloorMaximumWindWaves, ReportParams.TunnelFloorLedgeCountPreference, ReportParams.TunnelFloorMaxLedges, ReportParams.TunnelWarpStrength, Target.Definition->TerrainOperations.Num(), ReportParams.TerraceStepHeight, ReportParams.LayerLineSpacing, ReportParams.RibbingSpacing); TargetBottomWorldZ = Target.BottomChunkZ * CHUNK_SIZE; TargetTopWorldZ = (Target.TopChunkZ + 1) * CHUNK_SIZE; TargetBoundarySealThickness = BoundarySealForArchetype( *Target.Definition, TargetArchetype); if (TargetTopWorldZ <= TargetBottomWorldZ) { OutError = TEXT("The target strate has no positive voxel-space height."); return false; } DiffLayer = TStrongObjectPtr( NewObject(GetTransientPackage(), NAME_None, RF_Transient)); Generator = TStrongObjectPtr( NewObject(GetTransientPackage(), NAME_None, RF_Transient)); Generator->InitializeSettings(Settings.Get()); Generator->SetStrateManager(Manager.Get()); Generator->SetDiffLayer(DiffLayer.Get()); Mesher = TStrongObjectPtr( NewObject(GetTransientPackage(), NAME_None, RF_Transient)); Mesher->SetGenerator(Generator.Get()); Mesher->bUseBlockEarlyOut = Arguments.bBlockEarlyOut; if (Settings->WorldRadiusVoxels != 0.0f || Generator->WorldRadiusVoxels != 0.0f || Settings->InterStrateGapChunks != 0) { OutError = TEXT( "Explorer invariant failed: WorldRadiusVoxels must stay 0 and lateral regions must stay gated off."); return false; } return true; } }; class FGeneratorDensitySampler final : public IVoxelStrateDensitySampler { public: explicit FGeneratorDensitySampler(const UVoxelGenerator& InGenerator) : Generator(InGenerator) { } virtual float SampleDensity(float WorldX, float WorldY, float WorldZ) const override { return Generator.GetDensityAt(WorldX, WorldY, WorldZ); } private: const UVoxelGenerator& Generator; }; struct FExploreRenderFrame { FString FileName; FString Purpose = TEXT("overview"); FString CameraSeedSource = TEXT("player_fit"); FVector CameraSeedPoseVoxels = FVector::ZeroVector; FVector CameraVoxels = FVector::ZeroVector; FVector TargetVoxels = FVector::ZeroVector; int32 HitPixels = 0; bool bScaleMarkerProjected = false; bool bFailureMarkersProjected = false; bool bPartial = false; }; struct FExploreRenderOutput { FString Status; FString RefusalReason; int32 Width = 0; int32 Height = 0; float FixedStepVoxels = 0.0f; float FixedStepMeters = 0.0f; int32 BisectionIterations = 0; float MaxDistanceVoxels = 0.0f; int64 EstimatedFixedDensitySamples = 0; int64 LegacyTwoViewEstimatedDensitySamples = 0; FString StepRationale; FString CameraSeedPolicy; int32 CameraSeedCount = 0; bool bAllCamerasPlayerFit = false; double MeshSeconds = 0.0; double RasterSeconds = 0.0; double FirstViewRasterSeconds = 0.0; double AdditionalViewRasterSeconds = 0.0; int32 RequestedViewpointCount = 8; bool bMeshComplete = false; double MeshAccelerationSeconds = 0.0; bool bTruncated = false; TArray Frames; }; struct FExploreWalkOutput { FString Status; FString RefusalReason; bool bHasArrival = false; bool bHasDeparture = false; int32 ArrivalPassageCount = 0; int32 DeparturePassageCount = 0; FVector ArrivalVoxels = FVector::ZeroVector; FVector DepartureVoxels = FVector::ZeroVector; FString WindowPolicy; FVoxelPlayerFitWalkReport Report; bool bOriginCheckAttempted = false; bool bOriginCheckAvailable = false; FString OriginCheckRefusalReason; FVoxelPlayerFitWalkReport OriginCheckReport; int32 ArrivalLandingRoomProbeCount = 0; int32 ArrivalLandingRoomPlayerFitProbeCount = 0; int32 ArrivalLandingRoomArrivalComponentProbeCount = 0; int32 DepartureLandingRoomProbeCount = 0; int32 DepartureLandingRoomPlayerFitProbeCount = 0; int32 DepartureLandingRoomArrivalComponentProbeCount = 0; // Captured by the one walk pass and consumed by render camera selection. These are internal // hand-off buffers, not a second source of truth for the field. FVoxelStrateSampleGrid SharedSampleGrid; TArray CameraSeedPoses; TArray CameraSeedSources; }; struct FExploreMeshMetrics { int32 VertexCount = 0; int32 TriangleCount = 0; int32 SharedEdgeCount = 0; int32 DihedralSampleCount = 0; int32 NormalComparisonTriangleCount = 0; float EdgeLengthP50Meters = 0.0f; float EdgeLengthP99Meters = 0.0f; float EdgeLengthMaxMeters = 0.0f; float DihedralP50Degrees = 0.0f; float DihedralP75Degrees = 0.0f; float DihedralP90Degrees = 0.0f; float DihedralP95Degrees = 0.0f; float DihedralP99Degrees = 0.0f; float DihedralFractionOver20Degrees = 0.0f; float DihedralFractionOver40Degrees = 0.0f; float FaceNormalVsMeanVertexNormalP50Degrees = 0.0f; float FaceNormalVsMeanVertexNormalP90Degrees = 0.0f; }; struct FExploreMeshEdgeKey { FIntVector First = FIntVector::ZeroValue; FIntVector Second = FIntVector::ZeroValue; bool operator==(const FExploreMeshEdgeKey& Other) const { return First == Other.First && Second == Other.Second; } }; FORCEINLINE uint32 GetTypeHash(const FExploreMeshEdgeKey& Key) { return HashCombine(GetTypeHash(Key.First), GetTypeHash(Key.Second)); } struct FExploreMeshEdgeIncident { int32 IncidentCount = 0; int32 FirstTriangle = INDEX_NONE; int32 SecondTriangle = INDEX_NONE; }; struct FExploreExportOutput { FString Status; FString RefusalReason; FString MeshFileName; FString ManifestFileName; FIntVector RegionOrigin = FIntVector::ZeroValue; int32 RegionSize = 0; int32 MesherTileCount = 0; int32 MesherTilesCompleted = 0; int64 EstimatedWorkingBytes = 0; int64 WorkingMemoryCapBytes = MaxExportWorkingBytes; int64 MeshArrayBytes = 0; int64 MeshFileSizeBytes = 0; int32 VertexCount = 0; int32 TriangleCount = 0; FExploreMeshMetrics SurfaceMetrics; double MeshSeconds = 0.0; double MeshUsPerVoxel = 0.0; double MeasureSeconds = 0.0; double ObjWriteSeconds = 0.0; double ManifestSeconds = 0.0; double ExportWallSeconds = 0.0; double ExportAccountingRemainderSeconds = 0.0; FString GeometryHash; bool bTruncated = false; }; struct FExploreDensitySliceOutput { FString Status; FString RefusalReason; FString FileName; int32 SampleCount = 0; int32 FiniteSampleCount = 0; int32 NonFiniteSampleCount = 0; int32 ZeroCrossingCount = 0; int32 SaturatedSampleCount = 0; float MinDensity = 0.0f; float MaxDensity = 0.0f; float MaxAbsDensity = 0.0f; double Seconds = 0.0; }; struct FExploreTunnelCoreProbeAxis { FString Name; int32 ValidCoreCrossings = 0; int32 ValidFinalCrossings = 0; float CoreMeanCrossingVoxels = 0.0f; float FinalMeanCrossingVoxels = 0.0f; float CoreMinCrossingVoxels = 0.0f; float CoreMaxCrossingVoxels = 0.0f; float FinalMinCrossingVoxels = 0.0f; float FinalMaxCrossingVoxels = 0.0f; float CorePeriodicAmplitudeVoxels = 0.0f; float CorePeriodicPeriodVoxels = 0.0f; float FinalPeriodicAmplitudeVoxels = 0.0f; float FinalPeriodicPeriodVoxels = 0.0f; int32 CorePeriodicMeasuredSamples = 0; float CorePeriodicMeasuredLengthVoxels = 0.0f; int32 FinalPeriodicMeasuredSamples = 0; float FinalPeriodicMeasuredLengthVoxels = 0.0f; TArray CoreCrossingsVoxels; TArray FinalCrossingsVoxels; }; struct FExploreTunnelMouthProbeAxis { FString Name; int32 ValidCrossings = 0; float MeanCrossingVoxels = 0.0f; float MinCrossingVoxels = 0.0f; float MaxCrossingVoxels = 0.0f; float PeriodicAmplitudeVoxels = 0.0f; float PeriodicPeriodVoxels = 0.0f; int32 PeriodicMeasuredSamples = 0; float PeriodicMeasuredLengthVoxels = 0.0f; // Audit value from the core probe's local-median detrend. Mouth surfaces can carry a broad // room-floor bowl over the +/-2R window, so the accepted mouth value below uses a degree-six // slow trend while retaining this legacy value for before/after diagnosis. float LocalMedianPeriodicAmplitudeVoxels = 0.0f; float LocalMedianPeriodicPeriodVoxels = 0.0f; TArray CrossingsVoxels; }; struct FExploreTunnelMouthProbeEndpoint { FString Name; uint32 RoomHash = 0; FVector Mouth = FVector::ZeroVector; float RadiusVoxels = 0.0f; float TunnelFloorZ = -FLT_MAX; float RoomFloorCrossingZ = -FLT_MAX; float FloorJoinDeltaVoxels = 0.0f; FExploreTunnelMouthProbeAxis Wall; FExploreTunnelMouthProbeAxis Floor; }; struct FExploreTunnelMouthProbeOutput { FString Status; FString RefusalReason; FString FileName; int32 EndpointCount = 0; int32 LateralSampleCount = 0; float LateralStepVoxels = 0.25f; float LateralWindowVoxels = 0.0f; int32 RoomSampleStepVoxels = 2; int64 RoomVolumeSampleCount = 0; int64 RoomAirSampleCount = 0; int64 CoreBottomSampleCount = 0; int64 FinalSolidSampleCount = 0; int64 CoreInducedSolidSampleCount = 0; float MaxWallPeriodicAmplitudeVoxels = 0.0f; float MaxFloorPeriodicAmplitudeVoxels = 0.0f; float MaxLocalMedianWallPeriodicAmplitudeVoxels = 0.0f; float MaxLocalMedianFloorPeriodicAmplitudeVoxels = 0.0f; double Seconds = 0.0; TArray Endpoints; }; struct FExploreTunnelCoreProbeOutput { FString Status; FString RefusalReason; FString FileName; int32 TunnelIndex = INDEX_NONE; int32 SampleCount = 0; int32 CoreCenterFiniteSampleCount = 0; int32 CoreCenterZeroCrossingCount = 0; int32 FinalCenterFiniteSampleCount = 0; int32 FinalCenterZeroCrossingCount = 0; int32 FinalCenterSaturatedSampleCount = 0; float CoreCenterMin = 0.0f; float CoreCenterMax = 0.0f; float FinalCenterMin = 0.0f; float FinalCenterMax = 0.0f; float FinalCenterMaxAbs = 0.0f; float StepVoxels = 0.25f; float LengthVoxels = 0.0f; FVector AxisStart = FVector::ZeroVector; FVector AxisEnd = FVector::ZeroVector; FVector AxisDirection = FVector::ForwardVector; FVector WallDirection = FVector::RightVector; FExploreTunnelCoreProbeAxis Floor; FExploreTunnelCoreProbeAxis Wall; FExploreTunnelCoreProbeAxis Ceiling; FExploreTunnelMouthProbeOutput Mouth; double Seconds = 0.0; }; struct FExploreProfilerComparison { bool bAvailable = false; FString Scope; double ProfiledSeconds = 0.0; double UnprofiledSeconds = 0.0; double DiscrepancySeconds = 0.0; double DiscrepancyPercent = 0.0; double Ratio = 0.0; bool bGeometryEqual = false; FString ProfiledGeometryHash; FString UnprofiledGeometryHash; FString SelfCheck = TEXT("not_run"); }; struct FExploreRunOutput { FExploreRenderOutput Render; FExploreWalkOutput Walk; FExploreExportOutput Export; FExploreDensitySliceOutput DensitySlice; FExploreTunnelCoreProbeOutput TunnelCoreProbe; double BudgetSeconds = 0.0; double ElapsedSeconds = 0.0; double WalkSeconds = 0.0; bool bTruncated = false; FString TruncatedDuring; TArray CompletedModes; VoxelDensityProfile::FSnapshot Profile; bool bHasProfile = false; FExploreProfilerComparison ProfilerComparison; }; struct FExploreStackSamplerSession { bool bStarted = false; ~FExploreStackSamplerSession() { Stop(); } bool Start(uint32 IntervalUs, const FString& OutputDirectory) { if (bStarted) { return true; } bStarted = FVoxelStackSampler::Get().Start( IntervalUs, OutputDirectory, TEXT("commandlet")); return bStarted; } void Stop() { if (bStarted) { FVoxelStackSampler::Get().StopAndWrite(); bStarted = false; } } }; bool ValidateCanonicalTile(const FVoxelMeshData& MeshData, FString& OutError); bool MeasureExploreMesh( const FVoxelMeshData& MeshData, FExploreMeshMetrics& OutMetrics, FString& OutError); bool FindMouths( const UVoxelStrateManager& Manager, int32 TargetSlot, FVector& OutArrival, FVector& OutDeparture, int32& OutArrivalCount, int32& OutDepartureCount, const FVoxelPassage*& OutArrivalPassage, const FVoxelPassage*& OutDeparturePassage) { OutArrival = FVector::ZeroVector; OutDeparture = FVector::ZeroVector; OutArrivalCount = 0; OutDepartureCount = 0; OutArrivalPassage = nullptr; OutDeparturePassage = nullptr; for (const FVoxelPassage& Passage : Manager.GetPassages()) { if (Passage.LowerStrateIndex == TargetSlot && Passage.UpperStrateIndex + 1 == TargetSlot) { if (OutArrivalCount == 0) { OutArrival = Passage.LowerPoint; OutArrivalPassage = &Passage; } ++OutArrivalCount; } if (Passage.UpperStrateIndex == TargetSlot && Passage.LowerStrateIndex == TargetSlot + 1) { if (OutDepartureCount == 0) { OutDeparture = Passage.UpperPoint; OutDeparturePassage = &Passage; } ++OutDepartureCount; } } return OutArrivalCount > 0 && OutDepartureCount > 0; } struct FExploreCameraBasis { FVector Forward = FVector::ForwardVector; FVector Right = FVector::RightVector; FVector Up = FVector::UpVector; float Aspect = 1.0f; float TanHalfFov = 1.0f; }; bool BuildCameraBasis( const FVector& Position, const FVector& Target, float FieldOfViewDegrees, int32 Width, int32 Height, FExploreCameraBasis& OutBasis) { OutBasis.Forward = (Target - Position).GetSafeNormal(); if (OutBasis.Forward.IsNearlyZero()) { return false; } OutBasis.Right = FVector::CrossProduct(OutBasis.Forward, FVector::UpVector).GetSafeNormal(); if (OutBasis.Right.IsNearlyZero()) { OutBasis.Right = FVector::CrossProduct(OutBasis.Forward, FVector::RightVector).GetSafeNormal(); } OutBasis.Up = FVector::CrossProduct(OutBasis.Right, OutBasis.Forward).GetSafeNormal(); OutBasis.Aspect = static_cast(Width) / static_cast(Height); OutBasis.TanHalfFov = FMath::Tan(FMath::DegreesToRadians(FieldOfViewDegrees * 0.5f)); return OutBasis.Aspect > 0.0f && OutBasis.TanHalfFov > 0.0f; } void PutPixel(TArray& Pixels, int32 Width, int32 Height, int32 X, int32 Y, const FColor& Color) { if (X >= 0 && X < Width && Y >= 0 && Y < Height) { Pixels[X + Width * Y] = Color; } } void DrawLine( TArray& Pixels, int32 Width, int32 Height, int32 X0, int32 Y0, int32 X1, int32 Y1, const FColor& Color) { int32 DX = FMath::Abs(X1 - X0); const int32 SX = X0 < X1 ? 1 : -1; int32 DY = -FMath::Abs(Y1 - Y0); const int32 SY = Y0 < Y1 ? 1 : -1; int32 Error = DX + DY; for (;;) { PutPixel(Pixels, Width, Height, X0, Y0, Color); if (X0 == X1 && Y0 == Y1) { break; } const int32 DoubleError = 2 * Error; if (DoubleError >= DY) { Error += DY; X0 += SX; } if (DoubleError <= DX) { Error += DX; Y0 += SY; } } } bool ProjectPoint( const FVector& Point, const FVector& CameraPosition, const FExploreCameraBasis& Basis, int32 Width, int32 Height, FIntPoint& OutPixel) { const FVector Relative = Point - CameraPosition; const float Depth = FVector::DotProduct(Relative, Basis.Forward); if (Depth <= KINDA_SMALL_NUMBER) { return false; } const float NdcX = FVector::DotProduct(Relative, Basis.Right) / (Depth * Basis.Aspect * Basis.TanHalfFov); const float NdcY = FVector::DotProduct(Relative, Basis.Up) / (Depth * Basis.TanHalfFov); OutPixel.X = FMath::RoundToInt((NdcX * 0.5f + 0.5f) * static_cast(Width - 1)); OutPixel.Y = FMath::RoundToInt((1.0f - (NdcY * 0.5f + 0.5f)) * static_cast(Height - 1)); return NdcX >= -1.0f && NdcX <= 1.0f && NdcY >= -1.0f && NdcY <= 1.0f; } void DrawScaleMarker( TArray& Pixels, int32 Width, int32 Height, const FVector& CameraPosition, const FVector& Target, const FExploreCameraBasis& Basis, bool& bOutProjected) { constexpr float HumanHeightVoxels = 2.0f * FVoxelPlayerCapsuleConstants::HalfHeightVoxels; const FVector MarkerBase = FVector( Target.X, Target.Y, Target.Z - FVoxelPlayerCapsuleConstants::HalfHeightVoxels) - Basis.Right * 18.0f + Basis.Forward * 48.0f; const FVector MarkerTop = MarkerBase + FVector(0.0f, 0.0f, HumanHeightVoxels); FIntPoint BasePixel = FIntPoint::ZeroValue; FIntPoint TopPixel = FIntPoint::ZeroValue; bOutProjected = ProjectPoint( MarkerBase, CameraPosition, Basis, Width, Height, BasePixel) && ProjectPoint(MarkerTop, CameraPosition, Basis, Width, Height, TopPixel); if (!bOutProjected) { // The world-space marker is normally in frame by construction. Keep a visible fallback // for unusual camera/FOV overrides, while the JSON records that it was not projected. BasePixel = FIntPoint(12, Height - 12); TopPixel = FIntPoint(12, FMath::Max(12, Height - 12 - Height / 5)); } const FColor MarkerColor(255, 220, 32, 255); DrawLine(Pixels, Width, Height, BasePixel.X, BasePixel.Y, TopPixel.X, TopPixel.Y, MarkerColor); const int32 TickHalfWidth = 5; for (const FIntPoint& Tick : { BasePixel, TopPixel }) { DrawLine( Pixels, Width, Height, Tick.X - TickHalfWidth, Tick.Y, Tick.X + TickHalfWidth, Tick.Y, MarkerColor); } } bool SavePng( const FString& FilePath, const TArray& Pixels, int32 Width, int32 Height, FString& OutError) { IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked( FName(TEXT("ImageWrapper"))); TSharedPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper( EImageFormat::PNG); if (!ImageWrapper.IsValid() || !ImageWrapper->SetRaw( Pixels.GetData(), static_cast(Pixels.Num()) * sizeof(FColor), Width, Height, ERGBFormat::BGRA, 8)) { OutError = TEXT("ImageWrapper could not accept the BGRA render buffer."); return false; } const TArray64& Compressed = ImageWrapper->GetCompressed(100); if (!FFileHelper::SaveArrayToFile(Compressed, *FilePath)) { OutError = FString::Printf(TEXT("Could not write PNG '%s'."), *FilePath); return false; } return true; } FIntVector ChooseExploreMeshOrigin( const FExploreArguments& Arguments, const FExploreWalkOutput* CameraSeedWalk, int32 MeshSize, int32 TargetMiddleWorldZ) { FVector Centre(0.0f, 0.0f, static_cast(TargetMiddleWorldZ)); if (CameraSeedWalk != nullptr && CameraSeedWalk->CameraSeedPoses.Num() > 0) { Centre = CameraSeedWalk->CameraSeedPoses[0]; } const auto AlignTileOrigin = [MeshSize](float CentreCoordinate) { return FMath::FloorToInt( (CentreCoordinate - 0.5f * static_cast(MeshSize)) / static_cast(CHUNK_SIZE)) * CHUNK_SIZE; }; return FIntVector( AlignTileOrigin(Centre.X), AlignTileOrigin(Centre.Y), AlignTileOrigin(Centre.Z)); } bool AppendCanonicalMesh( FVoxelMeshData& Destination, const FVoxelMeshData& Source, FString& OutError) { if (!ValidateCanonicalTile(Source, OutError)) { return false; } const int32 VertexOffset = Destination.Vertices.Num(); for (const int32 Index : Source.Triangles) { if (Index < 0 || Index >= Source.Vertices.Num()) { OutError = TEXT("Canonical mesher returned an out-of-range triangle index."); return false; } } Destination.Vertices.Append(Source.Vertices); Destination.Normals.Append(Source.Normals); Destination.UVs.Append(Source.UVs); Destination.Colors.Append(Source.Colors); Destination.Triangles.Reserve(Destination.Triangles.Num() + Source.Triangles.Num()); for (const int32 Index : Source.Triangles) { Destination.Triangles.Add(Index + VertexOffset); } // A multi-tile aggregate interleaves each tile's ground/cap runs. The CPU rasterizer uses // flat geometry and the OBJ path is material-neutral, so there is deliberately no invented // global ceiling run here. Destination.NumCeilingTriangles = 0; return true; } FString ComputeGeometryHash(const FVoxelMeshData& MeshData) { uint32 Crc = 0; const int32 Counts[] = { MeshData.Vertices.Num(), MeshData.Normals.Num(), MeshData.UVs.Num(), MeshData.Colors.Num(), MeshData.Triangles.Num(), MeshData.NumCeilingTriangles, }; Crc = FCrc::MemCrc32(Counts, sizeof(Counts), Crc); if (MeshData.Vertices.Num() > 0) { Crc = FCrc::MemCrc32( MeshData.Vertices.GetData(), MeshData.Vertices.Num() * sizeof(FVector), Crc); } if (MeshData.Normals.Num() > 0) { Crc = FCrc::MemCrc32( MeshData.Normals.GetData(), MeshData.Normals.Num() * sizeof(FVector), Crc); } if (MeshData.UVs.Num() > 0) { Crc = FCrc::MemCrc32( MeshData.UVs.GetData(), MeshData.UVs.Num() * sizeof(FVector2D), Crc); } if (MeshData.Colors.Num() > 0) { Crc = FCrc::MemCrc32( MeshData.Colors.GetData(), MeshData.Colors.Num() * sizeof(FColor), Crc); } if (MeshData.Triangles.Num() > 0) { Crc = FCrc::MemCrc32( MeshData.Triangles.GetData(), MeshData.Triangles.Num() * sizeof(int32), Crc); } return FString::Printf(TEXT("%08X"), Crc); } bool EnsureExploreMesh( const FExploreArguments& Arguments, FExploreWorld& World, const FExploreWalkOutput* CameraSeedWalk, FExploreBudget& Budget, FString& OutError) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ExploreMesh); if (World.bExploreMeshBuilt) { OutError = World.ExploreMeshError; return World.bExploreMeshComplete; } World.bExploreMeshBuilt = true; World.bExploreMeshComplete = false; World.ExploreMeshError.Reset(); World.ExploreMesh.Clear(); World.ExploreMeshSize = Arguments.ExportSize; const int32 TilesPerAxis = Arguments.ExportSize / CHUNK_SIZE; World.ExploreMeshTileCount = TilesPerAxis * TilesPerAxis * TilesPerAxis; World.ExploreMeshTilesCompleted = 0; World.ExploreTileTriangleCounts.Init(0, World.ExploreMeshTileCount); World.ExploreTileGeometryHashes.SetNum(World.ExploreMeshTileCount); World.ExploreMeshOrigin = ChooseExploreMeshOrigin( Arguments, CameraSeedWalk, World.ExploreMeshSize, FMath::FloorToInt(0.5f * static_cast( World.TargetBottomWorldZ + World.TargetTopWorldZ))); World.ExploreMeshSeconds = 0.0; World.ExploreMeshPreparationSeconds = 0.0; World.ExploreMeshPreTileSetupSeconds = 0.0; World.ExploreMeshMergeSeconds = 0.0; World.ExploreMeshAccountingRemainderSeconds = 0.0; World.ExploreMeshHashSeconds = 0.0; World.ExploreDensityGridSeconds = 0.0; World.ExploreDensityGridTotalSamples = 0; World.ExploreDensityGridUniqueSamples = 0; World.ExploreDensityGridDuplicateSamplesBefore = 0; World.ExploreDensityGridDuplicateSamplesAfter = 0; World.bExploreDensityGridReused = false; World.ExploreMeshTaskWorkerCount = 0; World.ExploreMeshTaskJobCount = 0; World.ExploreMeshTaskMinBatchSize = FMath::Clamp(Arguments.MeshMinBatchSize, 1, 64); World.ExploreMeshTaskMinSeconds = 0.0; World.ExploreMeshTaskMeanSeconds = 0.0; World.ExploreMeshTaskMaxSeconds = 0.0; World.ExploreMeshTaskSumSeconds = 0.0; World.ExploreMeshTaskWallSeconds = 0.0; World.ExploreMeshTaskLaunchOverheadSeconds = 0.0; World.Mesher->SetSharedDensityGrid(nullptr); World.ExploreSharedDensityGrid.Reset(); if (Budget.ShouldStop(TEXT("mesh"))) { World.ExploreMeshError = TEXT("The wall-clock budget elapsed before meshing began."); OutError = World.ExploreMeshError; return false; } // This is the same canonical UVoxelMarchingCubesMesher used by export and runtime chunk // generation. Skirts are presentation geometry for LOD seams and would extend outside the // bounded explorer region, so the shared mesh uses the export setting. World.Mesher->bGenerateSkirts = false; const double MeshStartSeconds = FPlatformTime::Seconds(); if (Budget.ShouldStop(TEXT("mesh"))) { World.ExploreMeshError = TEXT("The wall-clock budget elapsed before mesh tiles began."); OutError = World.ExploreMeshError; World.ExploreMeshSeconds = FPlatformTime::Seconds() - MeshStartSeconds; return false; } // Adjacent tiles ask for the same one-point-halo planes. Build one immutable lattice for // the whole export when requested, then let each tile copy its local window from it. The // precomputation is inside MeshStartSeconds on purpose: this is an end-to-end export number, // not a profiled bucket hiding the cost of producing the cache. const int32 CellsPerTile = CHUNK_SIZE / Arguments.ExportStep; const int32 PointsPerTile = CellsPerTile + 3; const int64 TileGridSamples = static_cast(PointsPerTile) * static_cast(PointsPerTile) * static_cast(PointsPerTile); const int32 SharedGridDim = Arguments.ExportSize / Arguments.ExportStep + 3; const int64 SharedGridSamples = static_cast(SharedGridDim) * static_cast(SharedGridDim) * static_cast(SharedGridDim); World.ExploreDensityGridTotalSamples = static_cast(World.ExploreMeshTileCount) * TileGridSamples; World.ExploreDensityGridUniqueSamples = SharedGridSamples; World.ExploreDensityGridDuplicateSamplesBefore = FMath::Max( 0, World.ExploreDensityGridTotalSamples - SharedGridSamples); World.ExploreDensityGridDuplicateSamplesAfter = Arguments.bReuseDensityGrid ? 0 : World.ExploreDensityGridDuplicateSamplesBefore; const double DensityStageStartSeconds = FPlatformTime::Seconds(); if (Arguments.bReuseDensityGrid) { const double DensityGridStartSeconds = FPlatformTime::Seconds(); World.ExploreSharedDensityGrid.OriginVoxels = World.ExploreMeshOrigin - FIntVector(Arguments.ExportStep); World.ExploreSharedDensityGrid.Step = Arguments.ExportStep; World.ExploreSharedDensityGrid.Dim = SharedGridDim; World.ExploreSharedDensityGrid.Samples.SetNumUninitialized( SharedGridDim * SharedGridDim * SharedGridDim); std::atomic bGridWorkCancelled(false); const double GridWorkDeadline = Budget.StartSeconds + Budget.LimitSeconds - 0.25; const int32 GridTileCount = TilesPerAxis; ParallelFor( TEXT("VoxelForgeExploreDensityGrid"), World.ExploreMeshTileCount, World.ExploreMeshTaskMinBatchSize, [&](int32 LinearOwnerIndex) { if (bGridWorkCancelled.load(std::memory_order_relaxed) || FPlatformTime::Seconds() >= GridWorkDeadline) { bGridWorkCancelled.store(true, std::memory_order_relaxed); return; } FScopedVoxelStackRegistration StackRegistration; const int32 GridOctaveBias = (World.Mesher->LODOctaveDrop > 0 && Arguments.ExportStep > 1) ? World.Mesher->LODOctaveDrop * (int32)FMath::FloorLog2((uint32)Arguments.ExportStep) : 0; const int32 PreviousOctaveBias = VoxelGenLOD::GetThreadOctaveBias(); const int32 PreviousSampleStep = VoxelGenLOD::GetThreadSampleStep(); VoxelGenLOD::SetThreadOctaveBias(GridOctaveBias); VoxelGenLOD::SetThreadSampleStep(Arguments.ExportStep); const int32 OwnerX = LinearOwnerIndex % GridTileCount; const int32 OwnerY = (LinearOwnerIndex / GridTileCount) % GridTileCount; const int32 OwnerZ = LinearOwnerIndex / (GridTileCount * GridTileCount); FIntVector PreviousTileOrigin = FIntVector::ZeroValue; int32 IgnoredPreviousTileStep = 1; int32 PreviousTileCells = 0; VoxelGenLOD::GetThreadTileCacheWindow( PreviousTileOrigin, IgnoredPreviousTileStep, PreviousTileCells); VoxelGenLOD::SetThreadTileContext( World.ExploreMeshOrigin + FIntVector(OwnerX * CHUNK_SIZE, OwnerY * CHUNK_SIZE, OwnerZ * CHUNK_SIZE), CellsPerTile); // Partition the shared lattice into disjoint rectangular owner regions. A // boundary sample belongs to exactly one owner; tile windows may read it from // either side. This keeps the generator's spatial locality while guaranteeing // one evaluation per global lattice point. const auto OwnerMinGrid = [CellsPerTile](int32 Owner) -> int32 { return Owner == 0 ? 0 : 1 + Owner * CellsPerTile; }; const auto OwnerMaxGrid = [CellsPerTile, SharedGridDim, GridTileCount](int32 Owner) -> int32 { return Owner == GridTileCount - 1 ? SharedGridDim : 1 + (Owner + 1) * CellsPerTile; }; const int32 GxMin = OwnerMinGrid(OwnerX); const int32 GxMax = OwnerMaxGrid(OwnerX); const int32 GyMin = OwnerMinGrid(OwnerY); const int32 GyMax = OwnerMaxGrid(OwnerY); const int32 GzMin = OwnerMinGrid(OwnerZ); const int32 GzMax = OwnerMaxGrid(OwnerZ); for (int32 Gz = GzMin; Gz < GzMax; ++Gz) { for (int32 Gy = GyMin; Gy < GyMax; ++Gy) { for (int32 Gx = GxMin; Gx < GxMax; ++Gx) { const int32 Index = (Gz * SharedGridDim + Gy) * SharedGridDim + Gx; World.ExploreSharedDensityGrid.Samples[Index] = World.Generator->GetDensityAt( World.ExploreSharedDensityGrid.OriginVoxels.X + Gx * Arguments.ExportStep, World.ExploreSharedDensityGrid.OriginVoxels.Y + Gy * Arguments.ExportStep, World.ExploreSharedDensityGrid.OriginVoxels.Z + Gz * Arguments.ExportStep); } } } VoxelGenLOD::SetThreadOctaveBias(PreviousOctaveBias); VoxelGenLOD::SetThreadSampleStep(PreviousSampleStep); VoxelGenLOD::SetThreadTileContext(PreviousTileOrigin, PreviousTileCells); }, EParallelForFlags::None); World.ExploreDensityGridSeconds = FPlatformTime::Seconds() - DensityGridStartSeconds; if (bGridWorkCancelled.load(std::memory_order_relaxed)) { World.ExploreMeshError = TEXT("The wall-clock budget elapsed while building the shared density grid."); OutError = World.ExploreMeshError; World.ExploreMeshSeconds = FPlatformTime::Seconds() - MeshStartSeconds; return false; } World.Mesher->SetSharedDensityGrid(&World.ExploreSharedDensityGrid); World.bExploreDensityGridReused = true; } const double DensityStageEndSeconds = FPlatformTime::Seconds(); World.ExploreMeshPreparationSeconds = DensityStageStartSeconds - MeshStartSeconds; // Generate each tile through the canonical mesher on worker threads, then append strictly in // Z/Y/X order. The mesher's scratch buffers and the generator's hot caches are thread-local; // the fixed merge order keeps the aggregate mesh and all downstream raster/export bytes // deterministic even though tile completion order is not. TArray TileMeshes; TileMeshes.SetNum(World.ExploreMeshTileCount); TArray TileErrors; TileErrors.SetNum(World.ExploreMeshTileCount); TArray TileCompleted; TileCompleted.Init(0, World.ExploreMeshTileCount); TArray TileDurations; TileDurations.Init(0.0, World.ExploreMeshTileCount); TArray TileStartOffsets; TileStartOffsets.Init(-1.0, World.ExploreMeshTileCount); TArray TileEndOffsets; TileEndOffsets.Init(-1.0, World.ExploreMeshTileCount); TArray TileThreadIds; TileThreadIds.Init(0, World.ExploreMeshTileCount); std::atomic bTileWorkCancelled(false); const double TileWorkDeadline = Budget.StartSeconds + Budget.LimitSeconds - 0.25; const double ParallelStartSeconds = FPlatformTime::Seconds(); World.ExploreMeshPreTileSetupSeconds = ParallelStartSeconds - DensityStageEndSeconds; ParallelFor( TEXT("VoxelForgeExploreMeshTiles"), World.ExploreMeshTileCount, World.ExploreMeshTaskMinBatchSize, [&](int32 LinearTileIndex) { if (bTileWorkCancelled.load(std::memory_order_relaxed) || FPlatformTime::Seconds() >= TileWorkDeadline) { bTileWorkCancelled.store(true, std::memory_order_relaxed); return; } FScopedVoxelStackRegistration StackRegistration; const double TileStartSeconds = FPlatformTime::Seconds(); TileStartOffsets[LinearTileIndex] = TileStartSeconds - ParallelStartSeconds; TileThreadIds[LinearTileIndex] = FPlatformTLS::GetCurrentThreadId(); const int32 TileX = LinearTileIndex % TilesPerAxis; const int32 TileY = (LinearTileIndex / TilesPerAxis) % TilesPerAxis; const int32 TileZ = LinearTileIndex / (TilesPerAxis * TilesPerAxis); const FIntVector TileOrigin = World.ExploreMeshOrigin + FIntVector(TileX * CHUNK_SIZE, TileY * CHUNK_SIZE, TileZ * CHUNK_SIZE); TileMeshes[LinearTileIndex] = World.Mesher->GenerateMesh( TileOrigin, Arguments.ExportStep, CHUNK_SIZE / Arguments.ExportStep); FString TileError; if (!ValidateCanonicalTile(TileMeshes[LinearTileIndex], TileError)) { TileErrors[LinearTileIndex] = MoveTemp(TileError); bTileWorkCancelled.store(true, std::memory_order_relaxed); const double TileEndSeconds = FPlatformTime::Seconds(); TileEndOffsets[LinearTileIndex] = TileEndSeconds - ParallelStartSeconds; TileDurations[LinearTileIndex] = TileEndSeconds - TileStartSeconds; return; } TileCompleted[LinearTileIndex] = 1; const double TileEndSeconds = FPlatformTime::Seconds(); TileEndOffsets[LinearTileIndex] = TileEndSeconds - ParallelStartSeconds; TileDurations[LinearTileIndex] = TileEndSeconds - TileStartSeconds; }, EParallelForFlags::None); const double ParallelEndSeconds = FPlatformTime::Seconds(); TArray CompletedTileDurations; TSet WorkerIds; double EarliestTileStart = 1.0e30; double LatestTileEnd = 0.0; for (int32 TileIndex = 0; TileIndex < World.ExploreMeshTileCount; ++TileIndex) { if (!TileCompleted[TileIndex]) { continue; } CompletedTileDurations.Add(TileDurations[TileIndex]); WorkerIds.Add(TileThreadIds[TileIndex]); EarliestTileStart = FMath::Min(EarliestTileStart, TileStartOffsets[TileIndex]); LatestTileEnd = FMath::Max(LatestTileEnd, TileEndOffsets[TileIndex]); } if (CompletedTileDurations.Num() > 0) { CompletedTileDurations.Sort(); World.ExploreMeshTaskJobCount = CompletedTileDurations.Num(); World.ExploreMeshTaskWorkerCount = WorkerIds.Num(); World.ExploreMeshTaskMinSeconds = CompletedTileDurations[0]; World.ExploreMeshTaskMaxSeconds = CompletedTileDurations.Last(); for (const double Duration : CompletedTileDurations) { World.ExploreMeshTaskSumSeconds += Duration; } World.ExploreMeshTaskMeanSeconds = World.ExploreMeshTaskSumSeconds / static_cast(CompletedTileDurations.Num()); } World.ExploreMeshTaskWallSeconds = ParallelEndSeconds - ParallelStartSeconds; if (EarliestTileStart < 1.0e30) { World.ExploreMeshTaskLaunchOverheadSeconds = FMath::Max(0.0, EarliestTileStart) + FMath::Max(0.0, World.ExploreMeshTaskWallSeconds - LatestTileEnd); } int32 CompletedTiles = 0; FString MeshError; for (; CompletedTiles < World.ExploreMeshTileCount; ++CompletedTiles) { if (!TileCompleted[CompletedTiles]) { break; } if (!AppendCanonicalMesh( World.ExploreMesh, TileMeshes[CompletedTiles], MeshError)) { World.ExploreMeshError = MeshError; OutError = MeshError; World.ExploreMeshTilesCompleted = CompletedTiles; World.ExploreMeshSeconds = FPlatformTime::Seconds() - MeshStartSeconds; return false; } World.ExploreTileTriangleCounts[CompletedTiles] = TileMeshes[CompletedTiles].Triangles.Num() / 3; World.ExploreTileGeometryHashes[CompletedTiles] = ComputeGeometryHash(TileMeshes[CompletedTiles]); World.ExploreMeshTilesCompleted = CompletedTiles + 1; } if (CompletedTiles < World.ExploreMeshTileCount) { if (!TileErrors[CompletedTiles].IsEmpty()) { World.ExploreMeshError = TileErrors[CompletedTiles]; OutError = World.ExploreMeshError; World.ExploreMeshSeconds = FPlatformTime::Seconds() - MeshStartSeconds; return false; } Budget.ShouldStop(TEXT("mesh")); World.ExploreMeshError = FString::Printf( TEXT("Mesh was truncated after %d/%d canonical tiles."), CompletedTiles, World.ExploreMeshTileCount); OutError = World.ExploreMeshError; World.ExploreMeshSeconds = FPlatformTime::Seconds() - MeshStartSeconds; return false; } const double MeshEndSeconds = FPlatformTime::Seconds(); World.ExploreMeshSeconds = MeshEndSeconds - MeshStartSeconds; World.ExploreMeshMergeSeconds = MeshEndSeconds - ParallelEndSeconds; const double MeshStageSumSeconds = World.ExploreMeshPreparationSeconds + World.ExploreDensityGridSeconds + World.ExploreMeshPreTileSetupSeconds + World.ExploreMeshTaskWallSeconds + World.ExploreMeshMergeSeconds; World.ExploreMeshAccountingRemainderSeconds = World.ExploreMeshSeconds - MeshStageSumSeconds; World.bExploreMeshComplete = true; const double GeometryHashStartSeconds = FPlatformTime::Seconds(); World.ExploreGeometryHash = ComputeGeometryHash(World.ExploreMesh); World.ExploreMeshHashSeconds = FPlatformTime::Seconds() - GeometryHashStartSeconds; OutError.Reset(); return true; } float ExploreAxisValue(const FVector& Value, int32 Axis) { switch (Axis) { case 1: return Value.Y; case 2: return Value.Z; default: return Value.X; } } FVector ExploreTriangleMin(const FVector& A, const FVector& B, const FVector& C) { return FVector( FMath::Min3(A.X, B.X, C.X), FMath::Min3(A.Y, B.Y, C.Y), FMath::Min3(A.Z, B.Z, C.Z)); } FVector ExploreTriangleMax(const FVector& A, const FVector& B, const FVector& C) { return FVector( FMath::Max3(A.X, B.X, C.X), FMath::Max3(A.Y, B.Y, C.Y), FMath::Max3(A.Z, B.Z, C.Z)); } bool EnsureExploreAcceleration( const FExploreArguments& Arguments, FExploreWorld& World, FExploreBudget& Budget, FString& OutError) { if (World.bExploreAccelerationBuilt) { OutError.Reset(); return true; } World.ExploreTriangles.Reset(); World.ExploreTriangleOrder.Reset(); World.ExploreBvhNodes.Reset(); World.ExploreBvhRoot = INDEX_NONE; World.ExploreAccelerationSeconds = 0.0; const double AccelerationStartSeconds = FPlatformTime::Seconds(); if (!World.bExploreMeshComplete) { OutError = TEXT("Cannot build mesh acceleration before the canonical mesh is complete."); return false; } if (Budget.ShouldStop(TEXT("mesh-acceleration"))) { OutError = TEXT("The wall-clock budget elapsed before mesh acceleration began."); return false; } const FVoxelMeshData& Mesh = World.ExploreMesh; if (Mesh.Triangles.Num() % 3 != 0) { OutError = TEXT("Canonical mesh triangle index data is not divisible by three."); return false; } World.ExploreTriangles.Reserve(Mesh.Triangles.Num() / 3); for (int32 TriangleIndex = 0; TriangleIndex + 2 < Mesh.Triangles.Num(); TriangleIndex += 3) { if ((TriangleIndex & 16383) == 0 && Budget.ShouldStop(TEXT("mesh-acceleration"))) { OutError = TEXT("Mesh acceleration was truncated before its triangle cache completed."); World.ExploreTriangles.Reset(); return false; } const int32 IndexA = Mesh.Triangles[TriangleIndex]; const int32 IndexB = Mesh.Triangles[TriangleIndex + 1]; const int32 IndexC = Mesh.Triangles[TriangleIndex + 2]; if (!Mesh.Vertices.IsValidIndex(IndexA) || !Mesh.Vertices.IsValidIndex(IndexB) || !Mesh.Vertices.IsValidIndex(IndexC)) { OutError = TEXT("Canonical mesh acceleration saw an out-of-range triangle index."); World.ExploreTriangles.Reset(); return false; } const FVector A = Mesh.Vertices[IndexA] / VOXEL_SIZE; const FVector B = Mesh.Vertices[IndexB] / VOXEL_SIZE; const FVector C = Mesh.Vertices[IndexC] / VOXEL_SIZE; if (!FMath::IsFinite(A.X) || !FMath::IsFinite(A.Y) || !FMath::IsFinite(A.Z) || !FMath::IsFinite(B.X) || !FMath::IsFinite(B.Y) || !FMath::IsFinite(B.Z) || !FMath::IsFinite(C.X) || !FMath::IsFinite(C.Y) || !FMath::IsFinite(C.Z)) { OutError = TEXT("Canonical mesh acceleration saw a non-finite vertex."); World.ExploreTriangles.Reset(); return false; } const FVector Cross = FVector::CrossProduct(B - A, C - A); if (Cross.SizeSquared() <= SMALL_NUMBER) { // Degenerate canonical triangles have no raster contribution. Keep them out of the // acceleration structure without changing the source mesh or its export semantics. continue; } FExploreMeshTriangle& Triangle = World.ExploreTriangles.AddDefaulted_GetRef(); Triangle.A = A; Triangle.B = B; Triangle.C = C; Triangle.NormalA = Mesh.Normals[IndexA].GetSafeNormal(); Triangle.NormalB = Mesh.Normals[IndexB].GetSafeNormal(); Triangle.NormalC = Mesh.Normals[IndexC].GetSafeNormal(); Triangle.Min = ExploreTriangleMin(A, B, C); Triangle.Max = ExploreTriangleMax(A, B, C); Triangle.Centre = (A + B + C) / 3.0f; Triangle.Normal = Cross.GetSafeNormal(); } if (World.ExploreTriangles.Num() == 0) { // A valid empty mesh is still a complete mesh render: every pixel remains the explicit // background colour. The root sentinel lets the rasterizer handle this deterministically. World.ExploreAccelerationSeconds = FPlatformTime::Seconds() - AccelerationStartSeconds; World.bExploreAccelerationBuilt = true; OutError.Reset(); return true; } World.ExploreTriangleOrder.SetNumUninitialized(World.ExploreTriangles.Num()); for (int32 TriangleIndex = 0; TriangleIndex < World.ExploreTriangleOrder.Num(); ++TriangleIndex) { World.ExploreTriangleOrder[TriangleIndex] = TriangleIndex; } World.ExploreBvhNodes.Reserve(World.ExploreTriangles.Num() / 4 + 2); bool bBuildFailed = false; TFunction BuildNode; BuildNode = [&](int32 Start, int32 Count) -> int32 { if (bBuildFailed || Count <= 0) { return INDEX_NONE; } if (Budget.ShouldStop(TEXT("mesh-acceleration"))) { bBuildFailed = true; return INDEX_NONE; } const int32 NodeIndex = World.ExploreBvhNodes.AddDefaulted(); FExploreMeshBvhNode Node; Node.Min = FVector(FLT_MAX, FLT_MAX, FLT_MAX); Node.Max = FVector(-FLT_MAX, -FLT_MAX, -FLT_MAX); FVector CentroidMin = FVector(FLT_MAX, FLT_MAX, FLT_MAX); FVector CentroidMax = FVector(-FLT_MAX, -FLT_MAX, -FLT_MAX); for (int32 Offset = 0; Offset < Count; ++Offset) { const int32 TriangleIndex = World.ExploreTriangleOrder[Start + Offset]; const FExploreMeshTriangle& Triangle = World.ExploreTriangles[TriangleIndex]; Node.Min.X = FMath::Min(Node.Min.X, Triangle.Min.X); Node.Min.Y = FMath::Min(Node.Min.Y, Triangle.Min.Y); Node.Min.Z = FMath::Min(Node.Min.Z, Triangle.Min.Z); Node.Max.X = FMath::Max(Node.Max.X, Triangle.Max.X); Node.Max.Y = FMath::Max(Node.Max.Y, Triangle.Max.Y); Node.Max.Z = FMath::Max(Node.Max.Z, Triangle.Max.Z); CentroidMin.X = FMath::Min(CentroidMin.X, Triangle.Centre.X); CentroidMin.Y = FMath::Min(CentroidMin.Y, Triangle.Centre.Y); CentroidMin.Z = FMath::Min(CentroidMin.Z, Triangle.Centre.Z); CentroidMax.X = FMath::Max(CentroidMax.X, Triangle.Centre.X); CentroidMax.Y = FMath::Max(CentroidMax.Y, Triangle.Centre.Y); CentroidMax.Z = FMath::Max(CentroidMax.Z, Triangle.Centre.Z); } constexpr int32 LeafTriangleCount = 8; if (Count <= LeafTriangleCount) { Node.FirstTriangle = Start; Node.TriangleCount = Count; World.ExploreBvhNodes[NodeIndex] = Node; return NodeIndex; } const FVector CentroidExtent = CentroidMax - CentroidMin; int32 SplitAxis = 0; if (CentroidExtent.Y > CentroidExtent.X && CentroidExtent.Y >= CentroidExtent.Z) { SplitAxis = 1; } else if (CentroidExtent.Z > CentroidExtent.X && CentroidExtent.Z > CentroidExtent.Y) { SplitAxis = 2; } int32* OrderBegin = World.ExploreTriangleOrder.GetData() + Start; int32* OrderEnd = OrderBegin + Count; std::sort( OrderBegin, OrderEnd, [&](int32 LeftIndex, int32 RightIndex) { const float LeftValue = ExploreAxisValue( World.ExploreTriangles[LeftIndex].Centre, SplitAxis); const float RightValue = ExploreAxisValue( World.ExploreTriangles[RightIndex].Centre, SplitAxis); if (LeftValue != RightValue) { return LeftValue < RightValue; } return LeftIndex < RightIndex; }); const int32 LeftCount = Count / 2; const int32 RightCount = Count - LeftCount; const int32 LeftNode = BuildNode(Start, LeftCount); const int32 RightNode = BuildNode(Start + LeftCount, RightCount); if (LeftNode == INDEX_NONE || RightNode == INDEX_NONE) { bBuildFailed = true; return INDEX_NONE; } Node.Left = LeftNode; Node.Right = RightNode; World.ExploreBvhNodes[NodeIndex] = Node; return NodeIndex; }; World.ExploreBvhRoot = BuildNode(0, World.ExploreTriangles.Num()); if (bBuildFailed || World.ExploreBvhRoot == INDEX_NONE || Budget.bTruncated) { World.ExploreTriangles.Reset(); World.ExploreTriangleOrder.Reset(); World.ExploreBvhNodes.Reset(); World.ExploreBvhRoot = INDEX_NONE; OutError = TEXT("Mesh acceleration was truncated before its BVH completed."); return false; } World.ExploreAccelerationSeconds = FPlatformTime::Seconds() - AccelerationStartSeconds; World.bExploreAccelerationBuilt = true; OutError.Reset(); return true; } bool ExploreRayAabb( const FVector& Origin, const FVector& Direction, const FVector& BoundsMin, const FVector& BoundsMax, float MaxDistance, float& OutNear) { float NearDistance = 0.0f; float FarDistance = MaxDistance; for (int32 Axis = 0; Axis < 3; ++Axis) { const float OriginComponent = ExploreAxisValue(Origin, Axis); const float DirectionComponent = ExploreAxisValue(Direction, Axis); const float MinComponent = ExploreAxisValue(BoundsMin, Axis); const float MaxComponent = ExploreAxisValue(BoundsMax, Axis); if (FMath::Abs(DirectionComponent) <= KINDA_SMALL_NUMBER) { if (OriginComponent < MinComponent || OriginComponent > MaxComponent) { return false; } continue; } const float InverseDirection = 1.0f / DirectionComponent; float AxisNear = (MinComponent - OriginComponent) * InverseDirection; float AxisFar = (MaxComponent - OriginComponent) * InverseDirection; if (AxisNear > AxisFar) { Swap(AxisNear, AxisFar); } NearDistance = FMath::Max(NearDistance, AxisNear); FarDistance = FMath::Min(FarDistance, AxisFar); if (NearDistance > FarDistance) { return false; } } OutNear = NearDistance; return true; } bool ExploreRayTriangle( const FVector& Origin, const FVector& Direction, const FExploreMeshTriangle& Triangle, float MaxDistance, float& OutDistance, float& OutU, float& OutV) { const FVector Edge1 = Triangle.B - Triangle.A; const FVector Edge2 = Triangle.C - Triangle.A; const FVector Perpendicular = FVector::CrossProduct(Direction, Edge2); const float Determinant = FVector::DotProduct(Edge1, Perpendicular); if (FMath::Abs(Determinant) <= KINDA_SMALL_NUMBER) { return false; } const float InverseDeterminant = 1.0f / Determinant; const FVector ToOrigin = Origin - Triangle.A; const float U = FVector::DotProduct(ToOrigin, Perpendicular) * InverseDeterminant; if (U < 0.0f || U > 1.0f) { return false; } const FVector CrossOrigin = FVector::CrossProduct(ToOrigin, Edge1); const float V = FVector::DotProduct(Direction, CrossOrigin) * InverseDeterminant; if (V < 0.0f || U + V > 1.0f) { return false; } const float Distance = FVector::DotProduct(Edge2, CrossOrigin) * InverseDeterminant; if (Distance <= 0.01f || Distance >= MaxDistance) { return false; } OutDistance = Distance; OutU = U; OutV = V; return true; } bool RasterizeExploreMesh( const FExploreArguments& Arguments, FExploreWorld& World, const FVector& CameraPosition, const FVector& Target, FExploreBudget& Budget, TArray& OutPixels, int32& OutHitPixels, bool& bOutTruncated) { OutPixels.Init(FColor(12, 18, 28, 255), Arguments.RenderWidth * Arguments.RenderHeight); OutHitPixels = 0; bOutTruncated = false; if (!World.bExploreAccelerationBuilt) { FString AccelerationError; if (!EnsureExploreAcceleration(Arguments, World, Budget, AccelerationError)) { if (Budget.bTruncated) { bOutTruncated = true; return true; } return false; } } FExploreCameraBasis Basis; if (!BuildCameraBasis( CameraPosition, Target, 70.0f, Arguments.RenderWidth, Arguments.RenderHeight, Basis)) { return false; } const FVector KeyLightDirection = FVector(-0.35f, -0.45f, 0.82f).GetSafeNormal(); const FVector FillLightDirection = FVector(0.65f, 0.30f, 0.70f).GetSafeNormal(); constexpr float AmbientFill = 0.62f; constexpr float KeyLightStrength = 0.50f; constexpr float FillLightStrength = 0.24f; for (int32 PixelY = 0; PixelY < Arguments.RenderHeight; ++PixelY) { if (Budget.ShouldStop(TEXT("raster"))) { bOutTruncated = true; return true; } for (int32 PixelX = 0; PixelX < Arguments.RenderWidth; ++PixelX) { const float NormalizedX = (2.0f * (static_cast(PixelX) + 0.5f) / static_cast(Arguments.RenderWidth)) - 1.0f; const float NormalizedY = 1.0f - (2.0f * (static_cast(PixelY) + 0.5f) / static_cast(Arguments.RenderHeight)); const FVector Direction = ( Basis.Forward + Basis.Right * (NormalizedX * Basis.Aspect * Basis.TanHalfFov) + Basis.Up * (NormalizedY * Basis.TanHalfFov)).GetSafeNormal(); float ClosestDistance = Arguments.RenderMaxDistanceVoxels; int32 ClosestTriangleIndex = INDEX_NONE; float ClosestU = 0.0f; float ClosestV = 0.0f; int32 NodeStack[256]; int32 StackCount = 0; if (World.ExploreBvhRoot != INDEX_NONE) { NodeStack[StackCount++] = World.ExploreBvhRoot; } while (StackCount > 0) { const int32 NodeIndex = NodeStack[--StackCount]; if (!World.ExploreBvhNodes.IsValidIndex(NodeIndex)) { continue; } const FExploreMeshBvhNode& Node = World.ExploreBvhNodes[NodeIndex]; float NodeNear = 0.0f; if (!ExploreRayAabb( CameraPosition, Direction, Node.Min, Node.Max, ClosestDistance, NodeNear)) { continue; } if (Node.IsLeaf()) { for (int32 Offset = 0; Offset < Node.TriangleCount; ++Offset) { const int32 OrderIndex = Node.FirstTriangle + Offset; if (!World.ExploreTriangleOrder.IsValidIndex(OrderIndex)) { continue; } const int32 TriangleIndex = World.ExploreTriangleOrder[OrderIndex]; if (!World.ExploreTriangles.IsValidIndex(TriangleIndex)) { continue; } float TriangleDistance = 0.0f; float TriangleU = 0.0f; float TriangleV = 0.0f; if (ExploreRayTriangle( CameraPosition, Direction, World.ExploreTriangles[TriangleIndex], ClosestDistance, TriangleDistance, TriangleU, TriangleV)) { ClosestDistance = TriangleDistance; ClosestTriangleIndex = TriangleIndex; ClosestU = TriangleU; ClosestV = TriangleV; } } continue; } const int32 LeftNodeIndex = Node.Left; const int32 RightNodeIndex = Node.Right; float LeftNear = 0.0f; float RightNear = 0.0f; const bool bHitLeft = World.ExploreBvhNodes.IsValidIndex(LeftNodeIndex) && ExploreRayAabb( CameraPosition, Direction, World.ExploreBvhNodes[LeftNodeIndex].Min, World.ExploreBvhNodes[LeftNodeIndex].Max, ClosestDistance, LeftNear); const bool bHitRight = World.ExploreBvhNodes.IsValidIndex(RightNodeIndex) && ExploreRayAabb( CameraPosition, Direction, World.ExploreBvhNodes[RightNodeIndex].Min, World.ExploreBvhNodes[RightNodeIndex].Max, ClosestDistance, RightNear); if (bHitLeft && bHitRight) { if (LeftNear < RightNear) { if (StackCount + 2 <= UE_ARRAY_COUNT(NodeStack)) { NodeStack[StackCount++] = RightNodeIndex; NodeStack[StackCount++] = LeftNodeIndex; } } else if (StackCount + 2 <= UE_ARRAY_COUNT(NodeStack)) { NodeStack[StackCount++] = LeftNodeIndex; NodeStack[StackCount++] = RightNodeIndex; } } else if (bHitLeft && StackCount < UE_ARRAY_COUNT(NodeStack)) { NodeStack[StackCount++] = LeftNodeIndex; } else if (bHitRight && StackCount < UE_ARRAY_COUNT(NodeStack)) { NodeStack[StackCount++] = RightNodeIndex; } } if (ClosestTriangleIndex == INDEX_NONE) { continue; } const FExploreMeshTriangle& Triangle = World.ExploreTriangles[ClosestTriangleIndex]; FVector ShadingNormal = ( Triangle.NormalA * (1.0f - ClosestU - ClosestV) + Triangle.NormalB * ClosestU + Triangle.NormalC * ClosestV).GetSafeNormal(); if (ShadingNormal.IsNearlyZero()) { // The canonical mesher supplies non-zero gradient normals. Keep a defensive // fallback for malformed mesh data without reintroducing camera-facing flips. ShadingNormal = Triangle.Normal; } const float Key = FMath::Max( 0.0f, FVector::DotProduct(ShadingNormal, KeyLightDirection)); const float Fill = FMath::Max( 0.0f, FVector::DotProduct(ShadingNormal, FillLightDirection)); const float Brightness = FMath::Clamp( AmbientFill + KeyLightStrength * Key + FillLightStrength * Fill, 0.0f, 1.25f); const int32 PixelIndex = PixelX + Arguments.RenderWidth * PixelY; OutPixels[PixelIndex] = FColor( static_cast(FMath::Clamp(100.0f * Brightness, 0.0f, 255.0f)), static_cast(FMath::Clamp(128.0f * Brightness, 0.0f, 255.0f)), static_cast(FMath::Clamp(160.0f * Brightness, 0.0f, 255.0f)), 255); ++OutHitPixels; } } return true; } bool RunRender( const FExploreArguments& Arguments, FExploreWorld& World, const FExploreWalkOutput& CameraSeedWalk, FExploreRenderOutput& OutOutput, FExploreBudget& Budget) { OutOutput = FExploreRenderOutput(); OutOutput.Width = Arguments.RenderWidth; OutOutput.Height = Arguments.RenderHeight; OutOutput.FixedStepVoxels = Arguments.RenderStepVoxels; OutOutput.FixedStepMeters = Arguments.RenderStepVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters; OutOutput.BisectionIterations = 0; OutOutput.MaxDistanceVoxels = Arguments.RenderMaxDistanceVoxels; OutOutput.EstimatedFixedDensitySamples = 0; OutOutput.LegacyTwoViewEstimatedDensitySamples = static_cast(Arguments.RenderWidth) * static_cast(Arguments.RenderHeight) * 3ll * static_cast(FMath::Max( 1, FMath::CeilToInt( Arguments.RenderMaxDistanceVoxels / Arguments.RenderStepVoxels))) * 2ll; OutOutput.RequestedViewpointCount = 8; OutOutput.StepRationale = TEXT( "The renderer meshes the bounded region once with UVoxelMarchingCubesMesher, then CPU " "rasterises its canonical triangles through a deterministic BVH-assisted depth pass. " "-renderstep is retained only for legacy invocation compatibility; no render pixel samples " "the density field."); OutOutput.CameraSeedPolicy = TEXT( "Eight deterministic viewpoints are selected from the one shared player-fit mask. Their " "eye-height origins are density-checked, and their azimuths are rotated in fixed 45-degree " "increments so every image is from inside walkable space."); FString MeshError; if (!EnsureExploreMesh(Arguments, World, &CameraSeedWalk, Budget, MeshError)) { OutOutput.MeshSeconds = World.ExploreMeshSeconds; OutOutput.bMeshComplete = World.bExploreMeshComplete; if (Budget.bTruncated) { OutOutput.Status = TEXT("truncated"); OutOutput.bTruncated = true; OutOutput.RefusalReason = MeshError; } else { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = MeshError; } return false; } OutOutput.MeshSeconds = World.ExploreMeshSeconds; OutOutput.bMeshComplete = true; FString AccelerationError; if (!EnsureExploreAcceleration(Arguments, World, Budget, AccelerationError)) { OutOutput.MeshAccelerationSeconds = World.ExploreAccelerationSeconds; if (Budget.bTruncated) { OutOutput.Status = TEXT("truncated"); OutOutput.bTruncated = true; OutOutput.RefusalReason = AccelerationError; } else { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = AccelerationError; } return false; } OutOutput.MeshAccelerationSeconds = World.ExploreAccelerationSeconds; const double RasterStartSeconds = FPlatformTime::Seconds(); TArray, TInlineAllocator<8>> CameraSeeds; const auto AddCameraSeed = [&](bool bAvailable, const FVector& Position, const TCHAR* Source) { if (!bAvailable || !FMath::IsFinite(Position.X) || !FMath::IsFinite(Position.Y) || !FMath::IsFinite(Position.Z)) { return; } // The walk mask is authoritative, but this guard catches stale/corrupt report data before // a render can silently start inside rock. const float Density = World.Generator->GetDensityAt(Position.X, Position.Y, Position.Z); if (!FMath::IsFinite(Density) || !(Density > 0.0f)) { return; } for (const TPair& Existing : CameraSeeds) { if (Existing.Key.Equals(Position, 0.001f)) { return; } } CameraSeeds.Emplace(Position, FString(Source)); }; if (Arguments.bRenderCameraOverride) { AddCameraSeed(true, Arguments.RenderCamera, TEXT("render_camera_override")); } for (int32 SeedIndex = 0; SeedIndex < CameraSeedWalk.CameraSeedPoses.Num(); ++SeedIndex) { const FString Source = CameraSeedWalk.CameraSeedSources.IsValidIndex(SeedIndex) ? CameraSeedWalk.CameraSeedSources[SeedIndex] : FString::Printf(TEXT("player_fit_%02d"), SeedIndex); AddCameraSeed( true, CameraSeedWalk.CameraSeedPoses[SeedIndex], *Source); if (CameraSeeds.Num() >= OutOutput.RequestedViewpointCount) { break; } } if (CameraSeeds.Num() == 0) { OutOutput.Status = TEXT("refused"); OutOutput.RefusalReason = TEXT( "The player-fit seed pass produced no density-positive pose; refusing an inside-rock camera."); return false; } // A small room can contain fewer than eight distinct lattice cells. Reusing the same valid // player-fit origin with another deterministic angle is still a valid viewpoint and keeps // the contract (all camera origins are inside walkable space) without fabricating positions. while (CameraSeeds.Num() < OutOutput.RequestedViewpointCount) { const TPair& Anchor = CameraSeeds[0]; CameraSeeds.Emplace( Anchor.Key, FString::Printf(TEXT("%s_angle_%02d"), *Anchor.Value, CameraSeeds.Num())); } OutOutput.CameraSeedCount = CameraSeeds.Num(); OutOutput.bAllCamerasPlayerFit = true; FVector RouteFocus = FVector::ZeroVector; int32 RouteFocusCount = 0; if (Arguments.bRenderTargetOverride) { RouteFocus = Arguments.RenderTarget; RouteFocusCount = 1; } else if (CameraSeedWalk.bHasArrival) { RouteFocus += CameraSeedWalk.ArrivalVoxels; ++RouteFocusCount; } if (CameraSeedWalk.bHasDeparture) { RouteFocus += CameraSeedWalk.DepartureVoxels; ++RouteFocusCount; } if (RouteFocusCount > 0) { RouteFocus /= static_cast(RouteFocusCount); } else { RouteFocus = CameraSeeds[0].Key; } for (int32 ViewIndex = 0; ViewIndex < CameraSeeds.Num(); ++ViewIndex) { if (Budget.ShouldStop(TEXT("render"))) { OutOutput.Status = TEXT("truncated"); OutOutput.bTruncated = true; OutOutput.RefusalReason = TEXT("The wall-clock budget stopped viewpoint generation."); return false; } const FVector CameraSeedPose = CameraSeeds[ViewIndex].Key; const FVector CameraPosition = CameraSeedPose + FVector::UpVector * (FVoxelPlayerCapsuleConstants::HalfHeightVoxels - 0.5f); const float CameraDensity = World.Generator->GetDensityAt( CameraPosition.X, CameraPosition.Y, CameraPosition.Z); if (!FMath::IsFinite(CameraDensity) || !(CameraDensity > 0.0f)) { OutOutput.Status = TEXT("refused"); OutOutput.RefusalReason = TEXT( "The eye-height offset of a player-fit camera entered solid density."); return false; } FVector LookDirection = RouteFocus - CameraPosition; LookDirection.Z = 0.0f; LookDirection.Normalize(); if (LookDirection.IsNearlyZero()) { LookDirection = FVector( ViewIndex == 1 ? -0.6f : 0.8f, ViewIndex == 2 ? 0.7f : -0.4f, 0.18f).GetSafeNormal(); } // Keep the camera at the fitted pose while changing the azimuth for each viewpoint. LookDirection = LookDirection.RotateAngleAxis( 45.0f * static_cast(ViewIndex), FVector::UpVector).GetSafeNormal(); const FVector Target = CameraPosition + LookDirection * 96.0f + FVector::UpVector * 8.0f; const double ViewStartSeconds = FPlatformTime::Seconds(); TArray Pixels; int32 HitPixels = 0; bool bRasterTruncated = false; if (!RasterizeExploreMesh( Arguments, World, CameraPosition, Target, Budget, Pixels, HitPixels, bRasterTruncated)) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = TEXT("Could not construct a mesh raster camera."); return false; } FExploreCameraBasis Basis; if (!BuildCameraBasis( CameraPosition, Target, 70.0f, Arguments.RenderWidth, Arguments.RenderHeight, Basis)) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = TEXT("Could not construct a render camera basis."); return false; } bool bScaleMarkerProjected = false; DrawScaleMarker( Pixels, Arguments.RenderWidth, Arguments.RenderHeight, CameraPosition, Target, Basis, bScaleMarkerProjected); FExploreRenderFrame& Frame = OutOutput.Frames.AddDefaulted_GetRef(); Frame.FileName = FString::Printf(TEXT("render_%02d.png"), ViewIndex); Frame.Purpose = CameraSeeds[ViewIndex].Value; Frame.CameraSeedSource = CameraSeeds[ViewIndex].Value; Frame.CameraSeedPoseVoxels = CameraSeedPose; Frame.CameraVoxels = CameraPosition; Frame.TargetVoxels = Target; Frame.HitPixels = HitPixels; Frame.bScaleMarkerProjected = bScaleMarkerProjected; Frame.bPartial = bRasterTruncated; FString Error; if (!SavePng( FPaths::Combine(Arguments.OutDirectory, Frame.FileName), Pixels, Arguments.RenderWidth, Arguments.RenderHeight, Error)) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = Error; return false; } const double ViewSeconds = FPlatformTime::Seconds() - ViewStartSeconds; if (ViewIndex == 0) { OutOutput.FirstViewRasterSeconds = ViewSeconds; } if (bRasterTruncated) { OutOutput.Status = TEXT("truncated"); OutOutput.bTruncated = true; OutOutput.RefusalReason = TEXT("The wall-clock budget stopped triangle rasterisation."); break; } } OutOutput.MeshSeconds = World.ExploreMeshSeconds; OutOutput.RasterSeconds = FPlatformTime::Seconds() - RasterStartSeconds; const int32 AdditionalViewCount = FMath::Max(0, OutOutput.Frames.Num() - 1); OutOutput.AdditionalViewRasterSeconds = AdditionalViewCount > 0 ? (OutOutput.RasterSeconds - OutOutput.FirstViewRasterSeconds) / static_cast(AdditionalViewCount) : 0.0; if (!OutOutput.bTruncated) { OutOutput.Status = TEXT("ok"); } return !OutOutput.bTruncated; } void DrawPointMarker( TArray& Pixels, int32 Width, int32 Height, const FVector& Point, const FVector& CameraPosition, const FExploreCameraBasis& Basis, const FColor& Color, bool& bOutProjected) { FIntPoint Pixel = FIntPoint::ZeroValue; bOutProjected = ProjectPoint( Point, CameraPosition, Basis, Width, Height, Pixel); if (!bOutProjected) { return; } constexpr int32 MarkerRadius = 7; DrawLine( Pixels, Width, Height, Pixel.X - MarkerRadius, Pixel.Y, Pixel.X + MarkerRadius, Pixel.Y, Color); DrawLine( Pixels, Width, Height, Pixel.X, Pixel.Y - MarkerRadius, Pixel.X, Pixel.Y + MarkerRadius, Color); DrawLine( Pixels, Width, Height, Pixel.X - 4, Pixel.Y - 4, Pixel.X + 4, Pixel.Y + 4, Color); DrawLine( Pixels, Width, Height, Pixel.X - 4, Pixel.Y + 4, Pixel.X + 4, Pixel.Y - 4, Color); } bool RunFailureBoundaryRender( const FExploreArguments& Arguments, FExploreWorld& World, const FExploreWalkOutput& Walk, FExploreRenderOutput& InOutOutput, FExploreBudget& Budget) { const FVoxelPlayerFitWalkReport& Report = Walk.Report; if (!Report.bHasAgentFinalPosition || !Report.bHasTargetComponentNearestCell) { return false; } const FVector AgentFinal = Report.AgentFinalVoxels; const FVector TargetComponentCell = Report.TargetComponentNearestVoxels; const FVector LastReached = Report.LastReachedVoxels; const bool bHaveLastReached = Report.bHasLastReachedPosition; const FVector Focus = (AgentFinal + TargetComponentCell) * 0.5f; const FVector Segment = TargetComponentCell - AgentFinal; const FVector SegmentDirection = Segment.GetSafeNormal(); if (SegmentDirection.IsNearlyZero()) { return false; } FVector ViewSide = FVector::CrossProduct(SegmentDirection, FVector::UpVector).GetSafeNormal(); if (ViewSide.IsNearlyZero()) { ViewSide = FVector::CrossProduct(SegmentDirection, FVector::RightVector).GetSafeNormal(); } const float SegmentLength = Segment.Size(); const float ViewSideOffset = FMath::Min(64.0f, FMath::Max(16.0f, 0.25f * SegmentLength)); // Failure-focus views are still required to originate from the fitted graph. The old // implementation nudged the camera four voxels toward/away from the boundary and could put // the viewpoint in the very rock that this diagnostic is meant to explain. const FVector CameraCandidates[] = { AgentFinal, bHaveLastReached ? LastReached : AgentFinal, }; const FVector ViewTargets[] = { Focus + ViewSide * ViewSideOffset, Focus - ViewSide * ViewSideOffset, }; for (int32 ViewIndex = 0; ViewIndex < UE_ARRAY_COUNT(ViewTargets); ++ViewIndex) { if (Budget.ShouldStop(TEXT("failure-focus-render"))) { InOutOutput.Status = TEXT("truncated"); InOutOutput.bTruncated = true; InOutOutput.RefusalReason = TEXT( "The wall-clock budget stopped failure-focus viewpoint generation."); return false; } const FVector CameraSeedPose = CameraCandidates[ViewIndex]; const FVector CameraPosition = CameraSeedPose + FVector::UpVector * (FVoxelPlayerCapsuleConstants::HalfHeightVoxels - 0.5f); const float CameraDensity = World.Generator->GetDensityAt( CameraPosition.X, CameraPosition.Y, CameraPosition.Z); if (!FMath::IsFinite(CameraDensity) || !(CameraDensity > 0.0f)) { UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeExplore] refusing failure-focus camera %d: fitted pose is not air."), ViewIndex); return false; } const FVector Target = ViewTargets[ViewIndex]; TArray Pixels; int32 HitPixels = 0; bool bRasterTruncated = false; if (!RasterizeExploreMesh( Arguments, World, CameraPosition, Target, Budget, Pixels, HitPixels, bRasterTruncated)) { InOutOutput.Status = TEXT("error"); InOutOutput.RefusalReason = TEXT("Could not construct failure-focus mesh raster."); return false; } FExploreCameraBasis Basis; if (!BuildCameraBasis( CameraPosition, Target, 70.0f, Arguments.RenderWidth, Arguments.RenderHeight, Basis)) { return false; } bool bAgentProjected = false; bool bTargetProjected = false; bool bLastReachedProjected = false; DrawPointMarker( Pixels, Arguments.RenderWidth, Arguments.RenderHeight, AgentFinal, CameraPosition, Basis, FColor(242, 72, 72, 255), bAgentProjected); DrawPointMarker( Pixels, Arguments.RenderWidth, Arguments.RenderHeight, TargetComponentCell, CameraPosition, Basis, FColor(72, 242, 112, 255), bTargetProjected); if (bHaveLastReached) { DrawPointMarker( Pixels, Arguments.RenderWidth, Arguments.RenderHeight, LastReached, CameraPosition, Basis, FColor(242, 176, 56, 255), bLastReachedProjected); } FIntPoint AgentPixel = FIntPoint::ZeroValue; FIntPoint TargetPixel = FIntPoint::ZeroValue; if (ProjectPoint( AgentFinal, CameraPosition, Basis, Arguments.RenderWidth, Arguments.RenderHeight, AgentPixel) && ProjectPoint( TargetComponentCell, CameraPosition, Basis, Arguments.RenderWidth, Arguments.RenderHeight, TargetPixel)) { DrawLine( Pixels, Arguments.RenderWidth, Arguments.RenderHeight, AgentPixel.X, AgentPixel.Y, TargetPixel.X, TargetPixel.Y, FColor(244, 214, 72, 255)); } FExploreRenderFrame& Frame = InOutOutput.Frames.AddDefaulted_GetRef(); Frame.FileName = FString::Printf(TEXT("failure_boundary_%02d.png"), ViewIndex); Frame.Purpose = TEXT("agent_final_red_target_component_nearest_green_last_reached_orange"); Frame.CameraSeedSource = bHaveLastReached && ViewIndex == 1 ? TEXT("last_reached_player_fit") : TEXT("agent_final_player_fit"); Frame.CameraSeedPoseVoxels = CameraSeedPose; Frame.CameraVoxels = CameraPosition; Frame.TargetVoxels = Target; Frame.HitPixels = HitPixels; Frame.bFailureMarkersProjected = bAgentProjected && bTargetProjected && (!bHaveLastReached || bLastReachedProjected); Frame.bPartial = bRasterTruncated; bool bScaleMarkerProjected = false; DrawScaleMarker( Pixels, Arguments.RenderWidth, Arguments.RenderHeight, CameraPosition, Target, Basis, bScaleMarkerProjected); Frame.bScaleMarkerProjected = bScaleMarkerProjected; FString Error; if (!SavePng( FPaths::Combine(Arguments.OutDirectory, Frame.FileName), Pixels, Arguments.RenderWidth, Arguments.RenderHeight, Error)) { InOutOutput.Status = TEXT("error"); InOutOutput.RefusalReason = Error; return false; } if (bRasterTruncated) { InOutOutput.Status = TEXT("truncated"); InOutOutput.bTruncated = true; InOutOutput.RefusalReason = TEXT( "The wall-clock budget stopped failure-focus rasterisation."); return false; } } return true; } bool RunWalk( const FExploreArguments& Arguments, const FExploreWorld& World, FExploreWalkOutput& OutOutput, FExploreBudget& Budget) { OutOutput = FExploreWalkOutput(); OutOutput.WindowPolicy = NeedsOriginInWindow(Arguments.Archetype) ? TEXT("origin-inclusive target-strate passage-envelope AABB + margin") : TEXT("target-strate passage-envelope AABB + margin"); const int64 EstimatedWorkingBytes = static_cast(Arguments.MaxWalkCells) * EstimatedWalkBytesPerCell; if (EstimatedWorkingBytes > MaxWalkWorkingBytes) { OutOutput.Status = TEXT("refused"); OutOutput.RefusalReason = FString::Printf( TEXT("Refused before sampling: estimated walk memory %lld bytes exceeds cap %lld bytes."), static_cast(EstimatedWorkingBytes), static_cast(MaxWalkWorkingBytes)); return false; } if (Budget.ShouldStop(TEXT("walk"))) { OutOutput.Status = TEXT("truncated"); OutOutput.RefusalReason = TEXT("The wall-clock budget elapsed before walk sampling."); return false; } const FVoxelPassage* ArrivalPassage = nullptr; const FVoxelPassage* DeparturePassage = nullptr; if (!FindMouths( *World.Manager, Arguments.Slot, OutOutput.ArrivalVoxels, OutOutput.DepartureVoxels, OutOutput.ArrivalPassageCount, OutOutput.DeparturePassageCount, ArrivalPassage, DeparturePassage)) { OutOutput.Status = TEXT("refused"); OutOutput.RefusalReason = TEXT( "The target slot did not have both one-step chain mouths in the generated passage list."); return false; } OutOutput.bHasArrival = true; OutOutput.bHasDeparture = true; // Temporary seed-14 probe while diagnosing the fitted VerticalShafts junction. This is // intentionally narrow and will be removed after the floor/tree seam is corrected. if (Arguments.Archetype == ECaveGeneratorType::VerticalShafts && Arguments.Seed == 14) { const FVector ProbeA = OutOutput.ArrivalVoxels; const FVector ProbeB(-100.8f, -26.9f, ProbeA.Z); for (int32 Index = 0; Index <= 10; ++Index) { const float T = static_cast(Index) / 10.0f; const FVector P = FMath::Lerp(ProbeA, ProbeB, T); const float Dm704 = World.Generator->GetDensityAt(P.X, P.Y, -704.0f); const float Dm702 = World.Generator->GetDensityAt(P.X, P.Y, -702.0f); const float Dm7005 = World.Generator->GetDensityAt(P.X, P.Y, -700.5f); const float Dm700 = World.Generator->GetDensityAt(P.X, P.Y, -700.0f); const float Dm6995 = World.Generator->GetDensityAt(P.X, P.Y, -699.5f); const float Dm697 = World.Generator->GetDensityAt(P.X, P.Y, -697.0f); float MinBody = FLT_MAX; int32 NumSolidBody = 0; int32 SolidOffsetX = 0; int32 SolidOffsetY = 0; int32 SolidBodyRow = -1; for (int32 OffsetY = -1; OffsetY <= 1; ++OffsetY) { for (int32 OffsetX = -1; OffsetX <= 1; ++OffsetX) { for (int32 BodyRow = 0; BodyRow < 8; ++BodyRow) { const float BodyDensity = World.Generator->GetDensityAt( P.X + static_cast(OffsetX), P.Y + static_cast(OffsetY), -699.5f + static_cast(BodyRow)); if (BodyDensity < MinBody) { MinBody = BodyDensity; SolidOffsetX = OffsetX; SolidOffsetY = OffsetY; SolidBodyRow = BodyRow; } NumSolidBody += BodyDensity <= 0.0f ? 1 : 0; } } } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore][TemporaryShaftProbe] t=%.2f xy=(%.2f,%.2f) " "d[-704,-702,-700.5,-700,-699.5,-697]=[%.3f,%.3f,%.3f,%.3f,%.3f,%.3f] " "bodyMin=%.3f bodySolid=%d/72 firstMinOffset=(%d,%d) row=%d"), T, P.X, P.Y, Dm704, Dm702, Dm7005, Dm700, Dm6995, Dm697, MinBody, NumSolidBody, SolidOffsetX, SolidOffsetY, SolidBodyRow); } } FVoxelStrateMeasureSettings Settings; Settings.SampleStep = 1; Settings.MaxCells = Arguments.MaxWalkCells; Settings.MaxRouteRetries = 16; Settings.CoverPointA = FVector2D(OutOutput.ArrivalVoxels.X, OutOutput.ArrivalVoxels.Y); Settings.CoverPointB = FVector2D(OutOutput.DepartureVoxels.X, OutOutput.DepartureVoxels.Y); Settings.CoverMarginVoxels = FMath::CeilToFloat( Settings.PlayerCapsuleRadiusVoxels) + 2.0f; // A mouth-AABB is not a landing window: the landing door and its local apron can legitimately // leave that box before the player reaches the next room. Expand the same two-point box just // enough to contain the selected production mouth envelope. This is still a bounded, // deterministic measurement window; the central inter-strate descent belongs to the adjacent // strata and is not folded into this intra-strate walk question. const float WindowMinX = FMath::Min(OutOutput.ArrivalVoxels.X, OutOutput.DepartureVoxels.X); const float WindowMaxX = FMath::Max(OutOutput.ArrivalVoxels.X, OutOutput.DepartureVoxels.X); const float WindowMinY = FMath::Min(OutOutput.ArrivalVoxels.Y, OutOutput.DepartureVoxels.Y); const float WindowMaxY = FMath::Max(OutOutput.ArrivalVoxels.Y, OutOutput.DepartureVoxels.Y); const auto IncludeTargetPassageEnvelope = [&]( const FVoxelPassage* Passage, const FVoxelPassageLanding& Landing) { if (Passage == nullptr) { return; } const auto IncludeXY = [&](const FVector& Point, float Radius) { if (!FMath::IsFinite(Point.X) || !FMath::IsFinite(Point.Y) || !FMath::IsFinite(Point.Z) || !FMath::IsFinite(Radius)) { return; } const float AbsRadius = FMath::Abs(Radius); // Include control points whose centreline belongs to the measured strate. A control // point just outside the boundary belongs to the adjacent strate's descent; pulling // that whole switchback into this bounded walk box can multiply the sampled volume // without improving the intra-strate mouth-to-mouth question. if (Point.Z < static_cast(World.TargetBottomWorldZ) || Point.Z > static_cast(World.TargetTopWorldZ)) { return; } const float ExcessX = FMath::Max( 0.0f, FMath::Max( WindowMinX - (Point.X + AbsRadius), (Point.X - AbsRadius) - WindowMaxX)); const float ExcessY = FMath::Max( 0.0f, FMath::Max( WindowMinY - (Point.Y + AbsRadius), (Point.Y - AbsRadius) - WindowMaxY)); Settings.CoverMarginVoxels = FMath::Max( Settings.CoverMarginVoxels, FMath::Max(ExcessX, ExcessY)); }; IncludeXY( Landing.StandingPoint, Landing.HalfWidth + VoxelPassageGeometry::LandingCarveBlendVoxels); // The first/last three points are the production mouth, apron, and first slope section. // Do not pull the whole inter-strate switchback into the bounded target-strate test. const int32 NumLocalPoints = FMath::Min(3, Passage->ControlPoints.Num()); const bool bUseUpperEnd = &Landing == &Passage->UpperLanding; const int32 FirstPoint = bUseUpperEnd ? 0 : FMath::Max(0, Passage->ControlPoints.Num() - NumLocalPoints); const int32 LastPoint = bUseUpperEnd ? NumLocalPoints : Passage->ControlPoints.Num(); for (int32 PointIndex = FirstPoint; PointIndex < LastPoint; ++PointIndex) { const float Radius = Passage->ControlRadii.IsValidIndex(PointIndex) ? Passage->ControlRadii[PointIndex] : Passage->Radius; IncludeXY(Passage->ControlPoints[PointIndex], Radius + VoxelPassageGeometry::LandingCarveBlendVoxels); } }; IncludeTargetPassageEnvelope( ArrivalPassage, ArrivalPassage != nullptr ? ArrivalPassage->LowerLanding : FVoxelPassageLanding()); IncludeTargetPassageEnvelope( DeparturePassage, DeparturePassage != nullptr ? DeparturePassage->UpperLanding : FVoxelPassageLanding()); Settings.bIncludeOriginInCoverWindow = NeedsOriginInWindow(Arguments.Archetype); TArray LandingRoomProbePoints; auto AppendLandingRoomProbes = [&LandingRoomProbePoints]( const FVoxelPassageLanding& Landing) { const float ProbeOffset = FMath::Min( 2.0f, FMath::Max(0.0f, Landing.HalfWidth - 2.0f)); LandingRoomProbePoints.Add(Landing.StandingPoint); LandingRoomProbePoints.Add( Landing.StandingPoint + FVector(ProbeOffset, 0.0f, 0.0f)); LandingRoomProbePoints.Add( Landing.StandingPoint + FVector(-ProbeOffset, 0.0f, 0.0f)); LandingRoomProbePoints.Add( Landing.StandingPoint + FVector(0.0f, ProbeOffset, 0.0f)); LandingRoomProbePoints.Add( Landing.StandingPoint + FVector(0.0f, -ProbeOffset, 0.0f)); }; if (ArrivalPassage != nullptr) { AppendLandingRoomProbes(ArrivalPassage->LowerLanding); } if (DeparturePassage != nullptr) { AppendLandingRoomProbes(DeparturePassage->UpperLanding); } const FGeneratorDensitySampler Sampler(*World.Generator); if (!VF_MeasurePlayerFitWalkWithSampler( Sampler, World.TargetBottomWorldZ, World.TargetTopWorldZ, World.TargetBoundarySealThickness, OutOutput.ArrivalVoxels, OutOutput.DepartureVoxels, Settings, OutOutput.Report, &LandingRoomProbePoints, &OutOutput.SharedSampleGrid)) { OutOutput.Status = TEXT("refused"); OutOutput.RefusalReason = OutOutput.Report.RefusalReason; return false; } // Camera seeds are selected from this exact captured mask. The first valid fit cell anchors // the bounded render mesh; subsequent cells are kept local to that anchor so the mesh region // contains every requested viewpoint rather than silently rendering a distant empty box. OutOutput.CameraSeedPoses.Reset(); OutOutput.CameraSeedSources.Reset(); FVector CameraAnchor = FVector::ZeroVector; bool bHaveCameraAnchor = false; const auto AddCameraSeed = [&](const FVector& Position, const TCHAR* Source) { if (!FMath::IsFinite(Position.X) || !FMath::IsFinite(Position.Y) || !FMath::IsFinite(Position.Z) || !OutOutput.SharedSampleGrid.ContainsPoint(Position)) { return; } const float Step = static_cast(OutOutput.SharedSampleGrid.SampleStep); const int32 X = FMath::Clamp( FMath::FloorToInt((Position.X - OutOutput.SharedSampleGrid.MinX) / Step), 0, OutOutput.SharedSampleGrid.NumX - 1); const int32 Y = FMath::Clamp( FMath::FloorToInt((Position.Y - OutOutput.SharedSampleGrid.MinY) / Step), 0, OutOutput.SharedSampleGrid.NumY - 1); const int32 Z = FMath::Clamp( FMath::FloorToInt((Position.Z - OutOutput.SharedSampleGrid.MinZ) / Step), 0, OutOutput.SharedSampleGrid.NumZ - 1); const int32 Cell = OutOutput.SharedSampleGrid.Index(X, Y, Z); if (!OutOutput.SharedSampleGrid.PlayerFitMask.IsValidIndex(Cell) || OutOutput.SharedSampleGrid.PlayerFitMask[Cell] == 0u) { return; } if (!bHaveCameraAnchor) { CameraAnchor = Position; bHaveCameraAnchor = true; } if (FVector::DistSquared(Position, CameraAnchor) > FMath::Square(40.0f)) { return; } for (const FVector& Existing : OutOutput.CameraSeedPoses) { if (Existing.Equals(Position, 0.001f)) { return; } } OutOutput.CameraSeedPoses.Add(Position); OutOutput.CameraSeedSources.Add( Source != nullptr ? FString(Source) : FString::Printf(TEXT("player_fit_%02d"), OutOutput.CameraSeedPoses.Num() - 1)); }; AddCameraSeed(OutOutput.ArrivalVoxels, TEXT("arrival_player_fit")); AddCameraSeed(OutOutput.DepartureVoxels, TEXT("departure_player_fit")); if (OutOutput.Report.bHasLastReachedPosition) { AddCameraSeed( OutOutput.Report.LastReachedVoxels, TEXT("last_reached_player_fit")); } if (OutOutput.Report.bHasAgentFinalPosition) { AddCameraSeed( OutOutput.Report.AgentFinalVoxels, TEXT("agent_final_player_fit")); } if (!bHaveCameraAnchor) { for (int32 Cell = 0; Cell < OutOutput.SharedSampleGrid.PlayerFitMask.Num(); ++Cell) { if (OutOutput.SharedSampleGrid.PlayerFitMask[Cell] == 0u) { continue; } const int32 Plane = OutOutput.SharedSampleGrid.NumX * OutOutput.SharedSampleGrid.NumY; const int32 Z = Cell / Plane; const int32 InPlane = Cell - Z * Plane; const int32 Y = InPlane / OutOutput.SharedSampleGrid.NumX; const int32 X = InPlane - Y * OutOutput.SharedSampleGrid.NumX; AddCameraSeed( FVector( OutOutput.SharedSampleGrid.MinX + (static_cast(X) + 0.5f) * OutOutput.SharedSampleGrid.SampleStep, OutOutput.SharedSampleGrid.MinY + (static_cast(Y) + 0.5f) * OutOutput.SharedSampleGrid.SampleStep, OutOutput.SharedSampleGrid.MinZ + (static_cast(Z) + 0.5f) * OutOutput.SharedSampleGrid.SampleStep), TEXT("player_fit_grid")); if (bHaveCameraAnchor) { break; } } } if (bHaveCameraAnchor) { const int32 NumCandidates = OutOutput.SharedSampleGrid.PlayerFitMask.Num(); for (int32 Cell = 0; Cell < NumCandidates && OutOutput.CameraSeedPoses.Num() < 8; ++Cell) { if (OutOutput.SharedSampleGrid.PlayerFitMask[Cell] == 0u) { continue; } const int32 Plane = OutOutput.SharedSampleGrid.NumX * OutOutput.SharedSampleGrid.NumY; const int32 Z = Cell / Plane; const int32 InPlane = Cell - Z * Plane; const int32 Y = InPlane / OutOutput.SharedSampleGrid.NumX; const int32 X = InPlane - Y * OutOutput.SharedSampleGrid.NumX; AddCameraSeed( FVector( OutOutput.SharedSampleGrid.MinX + (static_cast(X) + 0.5f) * OutOutput.SharedSampleGrid.SampleStep, OutOutput.SharedSampleGrid.MinY + (static_cast(Y) + 0.5f) * OutOutput.SharedSampleGrid.SampleStep, OutOutput.SharedSampleGrid.MinZ + (static_cast(Z) + 0.5f) * OutOutput.SharedSampleGrid.SampleStep), TEXT("player_fit_grid")); } } // The origin observation is now a view over the shared primary grid. A second, expanded // walk would defeat the one-grid contract; callers that need origin coverage must request an // origin-inclusive archetype window, which is already selected before the single sample pass. OutOutput.bOriginCheckAttempted = true; if (OutOutput.Report.bOriginColumnInSampledWindow) { OutOutput.OriginCheckReport = OutOutput.Report; OutOutput.bOriginCheckAvailable = true; } else { OutOutput.OriginCheckRefusalReason = TEXT( "Origin is outside the shared primary sample window; no second grid was sampled."); } const int32 ArrivalProbeCount = ArrivalPassage != nullptr ? 5 : 0; const int32 DepartureProbeCount = DeparturePassage != nullptr ? 5 : 0; const int32 LandingProbeCount = ArrivalProbeCount + DepartureProbeCount; OutOutput.ArrivalLandingRoomProbeCount = ArrivalProbeCount; OutOutput.DepartureLandingRoomProbeCount = DepartureProbeCount; for (int32 ProbeIndex = 0; ProbeIndex < FMath::Min(LandingProbeCount, OutOutput.Report.ComponentProbePlayerFit.Num()); ++ProbeIndex) { const bool bPlayerFit = OutOutput.Report.ComponentProbePlayerFit[ProbeIndex] != 0u; const bool bInArrivalComponent = OutOutput.Report.ComponentProbeInStartComponent.IsValidIndex(ProbeIndex) && OutOutput.Report.ComponentProbeInStartComponent[ProbeIndex] != 0u; if (ProbeIndex < ArrivalProbeCount) { OutOutput.ArrivalLandingRoomPlayerFitProbeCount += bPlayerFit ? 1 : 0; OutOutput.ArrivalLandingRoomArrivalComponentProbeCount += bInArrivalComponent ? 1 : 0; } else { OutOutput.DepartureLandingRoomPlayerFitProbeCount += bPlayerFit ? 1 : 0; OutOutput.DepartureLandingRoomArrivalComponentProbeCount += bInArrivalComponent ? 1 : 0; } } if (Budget.ShouldStop(TEXT("walk"))) { OutOutput.Status = TEXT("truncated"); OutOutput.RefusalReason = TEXT("The wall-clock budget elapsed after walk sampling."); return false; } OutOutput.Status = TEXT("ok"); return true; } int64 EstimateExportWorkingBytes(int32 RegionSize) { const int64 TileCells = static_cast(CHUNK_SIZE) * static_cast(CHUNK_SIZE) * static_cast(CHUNK_SIZE); const int64 TileGridSamples = static_cast(CHUNK_SIZE + 3) * static_cast(CHUNK_SIZE + 3) * static_cast(CHUNK_SIZE + 3); // This preflight covers one canonical tile's scalar grid, edge map, mesh arrays, and temporary // triangle lists. The run-level canonical aggregate is retained separately so render and // export consume the same mesh. RegionSize is still accepted here to keep the call site's // contract explicit. (void)RegionSize; return TileGridSamples * static_cast(sizeof(float)) + TileCells * 448ll; } bool WriteUtf8(FArchive& Archive, const FString& Text) { FTCHARToUTF8 Converted(*Text); if (Converted.Length() > 0) { Archive.Serialize(const_cast(Converted.Get()), Converted.Length()); } return !Archive.IsError(); } bool ValidateCanonicalTile(const FVoxelMeshData& MeshData, FString& OutError) { if (MeshData.Vertices.Num() != MeshData.Normals.Num() || MeshData.Vertices.Num() != MeshData.UVs.Num() || MeshData.Vertices.Num() != MeshData.Colors.Num() || MeshData.Triangles.Num() % 3 != 0) { OutError = TEXT("Canonical mesher returned non-parallel mesh arrays."); return false; } return true; } bool ExploreMetricVectorIsFinite(const FVector& Value) { return FMath::IsFinite(Value.X) && FMath::IsFinite(Value.Y) && FMath::IsFinite(Value.Z); } FIntVector ExploreMetricWeldPosition(const FVector& PositionCm) { // Mesh positions are Unreal centimetres. 0.01 cm is deliberately much smaller than a // marching-cubes cell and only absorbs independent-tile floating-point round-off. constexpr float WeldQuantumCm = 0.01f; return FIntVector( FMath::RoundToInt(PositionCm.X / WeldQuantumCm), FMath::RoundToInt(PositionCm.Y / WeldQuantumCm), FMath::RoundToInt(PositionCm.Z / WeldQuantumCm)); } FExploreMeshEdgeKey ExploreMetricMakeEdgeKey( const FVector& PositionA, const FVector& PositionB) { const FIntVector A = ExploreMetricWeldPosition(PositionA); const FIntVector B = ExploreMetricWeldPosition(PositionB); const bool bAFirst = A.X < B.X || (A.X == B.X && (A.Y < B.Y || (A.Y == B.Y && A.Z <= B.Z))); return bAFirst ? FExploreMeshEdgeKey{A, B} : FExploreMeshEdgeKey{B, A}; } float ExploreMetricPercentile(const TArray& SortedValues, float Quantile) { if (SortedValues.Num() == 0) { return 0.0f; } const float Position = FMath::Clamp(Quantile, 0.0f, 1.0f) * static_cast(SortedValues.Num() - 1); const int32 Lower = FMath::FloorToInt(Position); const int32 Upper = FMath::Min(Lower + 1, SortedValues.Num() - 1); return FMath::Lerp(SortedValues[Lower], SortedValues[Upper], Position - static_cast(Lower)); } bool MeasureExploreMesh( const FVoxelMeshData& MeshData, FExploreMeshMetrics& OutMetrics, FString& OutError) { OutMetrics = FExploreMeshMetrics(); if (!ValidateCanonicalTile(MeshData, OutError)) { return false; } const int32 NumTriangles = MeshData.Triangles.Num() / 3; OutMetrics.VertexCount = MeshData.Vertices.Num(); OutMetrics.TriangleCount = NumTriangles; TArray EdgeLengthsMeters; TArray DihedralAnglesDegrees; TArray FaceNormalDivergencesDegrees; EdgeLengthsMeters.Reserve(NumTriangles * 3); DihedralAnglesDegrees.Reserve(NumTriangles * 2); FaceNormalDivergencesDegrees.Reserve(NumTriangles); TArray FaceNormals; FaceNormals.Init(FVector::ZeroVector, NumTriangles); TArray ValidFaceNormals; ValidFaceNormals.Init(0, NumTriangles); TMap EdgeIncidents; EdgeIncidents.Reserve(FMath::Max(1, NumTriangles * 2)); const auto AddEdgeIncident = [&EdgeIncidents]( const FVector& PositionA, const FVector& PositionB, int32 TriangleIndex) { const FExploreMeshEdgeKey Key = ExploreMetricMakeEdgeKey(PositionA, PositionB); if (Key.First == Key.Second) { return; } FExploreMeshEdgeIncident& Incident = EdgeIncidents.FindOrAdd(Key); if (Incident.IncidentCount == 0) { Incident.FirstTriangle = TriangleIndex; } else if (Incident.IncidentCount == 1) { Incident.SecondTriangle = TriangleIndex; } Incident.IncidentCount = FMath::Min(Incident.IncidentCount + 1, 3); }; for (int32 TriangleIndex = 0; TriangleIndex < NumTriangles; ++TriangleIndex) { const int32 Base = TriangleIndex * 3; const int32 IndexA = MeshData.Triangles[Base]; const int32 IndexB = MeshData.Triangles[Base + 1]; const int32 IndexC = MeshData.Triangles[Base + 2]; if (!MeshData.Vertices.IsValidIndex(IndexA) || !MeshData.Vertices.IsValidIndex(IndexB) || !MeshData.Vertices.IsValidIndex(IndexC) || !MeshData.Normals.IsValidIndex(IndexA) || !MeshData.Normals.IsValidIndex(IndexB) || !MeshData.Normals.IsValidIndex(IndexC)) { OutError = TEXT("Surface metric measurement saw an out-of-range mesh index."); return false; } const FVector& A = MeshData.Vertices[IndexA]; const FVector& B = MeshData.Vertices[IndexB]; const FVector& C = MeshData.Vertices[IndexC]; if (!ExploreMetricVectorIsFinite(A) || !ExploreMetricVectorIsFinite(B) || !ExploreMetricVectorIsFinite(C)) { OutError = TEXT("Surface metric measurement saw a non-finite vertex."); return false; } EdgeLengthsMeters.Add(FVector::Dist(A, B) * 0.01f); EdgeLengthsMeters.Add(FVector::Dist(B, C) * 0.01f); EdgeLengthsMeters.Add(FVector::Dist(C, A) * 0.01f); const FVector Cross = FVector::CrossProduct(B - A, C - A); if (Cross.SizeSquared() > SMALL_NUMBER) { const FVector MeanVertexNormal = ( MeshData.Normals[IndexA] + MeshData.Normals[IndexB] + MeshData.Normals[IndexC]).GetSafeNormal(); if (ExploreMetricVectorIsFinite(MeanVertexNormal) && !MeanVertexNormal.IsNearlyZero()) { // The canonical marching-cubes table is emitted with the winding required by // the runtime mesh path, while the density-gradient normals point solid-to-air. // Orient the geometric face normal to that same outward convention before // measuring the relief divergence; otherwise a winding convention change would // report ~180 degrees for an otherwise smooth surface. FVector FaceNormal = Cross.GetSafeNormal(); if (FVector::DotProduct(FaceNormal, MeanVertexNormal) < 0.0f) { FaceNormal *= -1.0f; } FaceNormals[TriangleIndex] = FaceNormal; ValidFaceNormals[TriangleIndex] = 1; const float Dot = FMath::Clamp( FVector::DotProduct(FaceNormal, MeanVertexNormal), -1.0f, 1.0f); FaceNormalDivergencesDegrees.Add( FMath::RadiansToDegrees(FMath::Acos(Dot))); ++OutMetrics.NormalComparisonTriangleCount; } } AddEdgeIncident(A, B, TriangleIndex); AddEdgeIncident(B, C, TriangleIndex); AddEdgeIncident(C, A, TriangleIndex); } for (const TPair& Pair : EdgeIncidents) { const FExploreMeshEdgeIncident& Incident = Pair.Value; if (Incident.IncidentCount != 2) { continue; } ++OutMetrics.SharedEdgeCount; if (!ValidFaceNormals.IsValidIndex(Incident.FirstTriangle) || !ValidFaceNormals.IsValidIndex(Incident.SecondTriangle) || !ValidFaceNormals[Incident.FirstTriangle] || !ValidFaceNormals[Incident.SecondTriangle]) { continue; } const float Dot = FMath::Clamp( FVector::DotProduct( FaceNormals[Incident.FirstTriangle], FaceNormals[Incident.SecondTriangle]), -1.0f, 1.0f); DihedralAnglesDegrees.Add(FMath::RadiansToDegrees(FMath::Acos(Dot))); } EdgeLengthsMeters.Sort(); DihedralAnglesDegrees.Sort(); FaceNormalDivergencesDegrees.Sort(); OutMetrics.DihedralSampleCount = DihedralAnglesDegrees.Num(); OutMetrics.EdgeLengthP50Meters = ExploreMetricPercentile(EdgeLengthsMeters, 0.50f); OutMetrics.EdgeLengthP99Meters = ExploreMetricPercentile(EdgeLengthsMeters, 0.99f); OutMetrics.EdgeLengthMaxMeters = EdgeLengthsMeters.Num() > 0 ? EdgeLengthsMeters.Last() : 0.0f; OutMetrics.DihedralP50Degrees = ExploreMetricPercentile(DihedralAnglesDegrees, 0.50f); OutMetrics.DihedralP75Degrees = ExploreMetricPercentile(DihedralAnglesDegrees, 0.75f); OutMetrics.DihedralP90Degrees = ExploreMetricPercentile(DihedralAnglesDegrees, 0.90f); OutMetrics.DihedralP95Degrees = ExploreMetricPercentile(DihedralAnglesDegrees, 0.95f); OutMetrics.DihedralP99Degrees = ExploreMetricPercentile(DihedralAnglesDegrees, 0.99f); if (DihedralAnglesDegrees.Num() > 0) { int32 Over20 = 0; int32 Over40 = 0; for (const float Angle : DihedralAnglesDegrees) { Over20 += Angle > 20.0f ? 1 : 0; Over40 += Angle > 40.0f ? 1 : 0; } OutMetrics.DihedralFractionOver20Degrees = static_cast(Over20) / static_cast(DihedralAnglesDegrees.Num()); OutMetrics.DihedralFractionOver40Degrees = static_cast(Over40) / static_cast(DihedralAnglesDegrees.Num()); } OutMetrics.FaceNormalVsMeanVertexNormalP50Degrees = ExploreMetricPercentile(FaceNormalDivergencesDegrees, 0.50f); OutMetrics.FaceNormalVsMeanVertexNormalP90Degrees = ExploreMetricPercentile(FaceNormalDivergencesDegrees, 0.90f); OutError.Reset(); return true; } bool WriteObjHeader(FArchive& Archive) { bool bOk = WriteUtf8(Archive, TEXT("# VoxelForge authoritative marching-cubes mesh\n")); bOk = bOk && WriteUtf8(Archive, TEXT("# Positions are metres; UVs retain canonical voxel-space values; source winding is preserved.\n")); bOk = bOk && WriteUtf8(Archive, TEXT("o VoxelForgeExplore\n")); return bOk && !Archive.IsError(); } bool WriteObjTile( FArchive& Archive, const FVoxelMeshData& MeshData, int32 VertexOffset, FString& OutError) { if (!ValidateCanonicalTile(MeshData, OutError)) { return false; } bool bOk = true; for (const FVector& Vertex : MeshData.Vertices) { bOk = bOk && WriteUtf8(Archive, FString::Printf( TEXT("v %.9f %.9f %.9f\n"), Vertex.X / 100.0, Vertex.Y / 100.0, Vertex.Z / 100.0)); } for (const FVector2D& UV : MeshData.UVs) { bOk = bOk && WriteUtf8(Archive, FString::Printf( TEXT("vt %.9f %.9f\n"), UV.X, UV.Y)); } for (const FVector& Normal : MeshData.Normals) { bOk = bOk && WriteUtf8(Archive, FString::Printf( TEXT("vn %.9f %.9f %.9f\n"), Normal.X, Normal.Y, Normal.Z)); } const int32 NumTriangles = MeshData.Triangles.Num() / 3; const int32 FirstCeilingTriangle = FMath::Clamp( NumTriangles - MeshData.NumCeilingTriangles, 0, NumTriangles); if (NumTriangles > 0) { bOk = bOk && WriteUtf8(Archive, TEXT("usemtl VoxelGround\n")); } for (int32 Triangle = 0; Triangle < NumTriangles; ++Triangle) { if (Triangle == FirstCeilingTriangle) { bOk = bOk && WriteUtf8(Archive, TEXT("usemtl VoxelCeiling\n")); } const int32 Base = Triangle * 3; const int32 SourceA = MeshData.Triangles[Base]; const int32 SourceB = MeshData.Triangles[Base + 1]; const int32 SourceC = MeshData.Triangles[Base + 2]; if (SourceA < 0 || SourceB < 0 || SourceC < 0 || SourceA >= MeshData.Vertices.Num() || SourceB >= MeshData.Vertices.Num() || SourceC >= MeshData.Vertices.Num()) { OutError = TEXT("Canonical mesher returned an out-of-range triangle index."); return false; } const int32 A = SourceA + 1 + VertexOffset; const int32 B = SourceB + 1 + VertexOffset; const int32 C = SourceC + 1 + VertexOffset; bOk = bOk && WriteUtf8(Archive, FString::Printf( TEXT("f %d/%d/%d %d/%d/%d %d/%d/%d\n"), A, A, A, B, B, B, C, C, C)); } if (!bOk || Archive.IsError()) { OutError = TEXT("Could not write a canonical OBJ tile."); return false; } return true; } bool WriteObjMesh( FArchive& Archive, const FVoxelMeshData& MeshData, FExploreBudget& Budget, FString& OutError, bool& bOutTruncated, int32& OutVerticesWritten, int32& OutTrianglesWritten) { bOutTruncated = false; OutVerticesWritten = 0; OutTrianglesWritten = 0; if (!ValidateCanonicalTile(MeshData, OutError)) { return false; } bool bOk = true; for (int32 VertexIndex = 0; VertexIndex < MeshData.Vertices.Num(); ++VertexIndex) { if ((VertexIndex & 255) == 0 && Budget.ShouldStop(TEXT("export-write"))) { bOutTruncated = true; return true; } const FVector& Vertex = MeshData.Vertices[VertexIndex]; bOk = bOk && WriteUtf8(Archive, FString::Printf( TEXT("v %.9f %.9f %.9f\n"), Vertex.X / 100.0, Vertex.Y / 100.0, Vertex.Z / 100.0)); ++OutVerticesWritten; } for (int32 UVIndex = 0; UVIndex < MeshData.UVs.Num(); ++UVIndex) { if ((UVIndex & 255) == 0 && Budget.ShouldStop(TEXT("export-write"))) { bOutTruncated = true; return true; } const FVector2D& UV = MeshData.UVs[UVIndex]; bOk = bOk && WriteUtf8(Archive, FString::Printf( TEXT("vt %.9f %.9f\n"), UV.X, UV.Y)); } for (int32 NormalIndex = 0; NormalIndex < MeshData.Normals.Num(); ++NormalIndex) { if ((NormalIndex & 255) == 0 && Budget.ShouldStop(TEXT("export-write"))) { bOutTruncated = true; return true; } const FVector& Normal = MeshData.Normals[NormalIndex]; bOk = bOk && WriteUtf8(Archive, FString::Printf( TEXT("vn %.9f %.9f %.9f\n"), Normal.X, Normal.Y, Normal.Z)); } const int32 NumTriangles = MeshData.Triangles.Num() / 3; if (NumTriangles > 0) { bOk = bOk && WriteUtf8(Archive, TEXT("usemtl VoxelGround\n")); } for (int32 Triangle = 0; Triangle < NumTriangles; ++Triangle) { if ((Triangle & 255) == 0 && Budget.ShouldStop(TEXT("export-write"))) { bOutTruncated = true; return true; } const int32 Base = Triangle * 3; const int32 A = MeshData.Triangles[Base]; const int32 B = MeshData.Triangles[Base + 1]; const int32 C = MeshData.Triangles[Base + 2]; if (A < 0 || B < 0 || C < 0 || A >= MeshData.Vertices.Num() || B >= MeshData.Vertices.Num() || C >= MeshData.Vertices.Num()) { OutError = TEXT("Canonical aggregate mesh returned an out-of-range triangle index."); return false; } bOk = bOk && WriteUtf8(Archive, FString::Printf( TEXT("f %d/%d/%d %d/%d/%d %d/%d/%d\n"), A + 1, A + 1, A + 1, B + 1, B + 1, B + 1, C + 1, C + 1, C + 1)); ++OutTrianglesWritten; } if (!bOk || Archive.IsError()) { OutError = TEXT("Could not write the canonical aggregate OBJ."); return false; } return true; } void WriteJsonVector(FExploreJsonWriter& Writer, const TCHAR* Key, const FVector& Value, float Scale) { Writer.WriteObjectStart(Key); Writer.WriteValue(TEXT("x"), static_cast(Value.X) * Scale); Writer.WriteValue(TEXT("y"), static_cast(Value.Y) * Scale); Writer.WriteValue(TEXT("z"), static_cast(Value.Z) * Scale); Writer.WriteObjectEnd(); } void WriteJsonIntVector(FExploreJsonWriter& Writer, const TCHAR* Key, const FIntVector& Value) { Writer.WriteObjectStart(Key); Writer.WriteValue(TEXT("x"), Value.X); Writer.WriteValue(TEXT("y"), Value.Y); Writer.WriteValue(TEXT("z"), Value.Z); Writer.WriteObjectEnd(); } void WriteJsonBounds( FExploreJsonWriter& Writer, const TCHAR* Key, const FIntVector& Origin, int32 Size, float VoxelToMeters) { Writer.WriteObjectStart(Key); Writer.WriteObjectStart(TEXT("min")); Writer.WriteValue(TEXT("x"), Origin.X); Writer.WriteValue(TEXT("y"), Origin.Y); Writer.WriteValue(TEXT("z"), Origin.Z); Writer.WriteObjectEnd(); Writer.WriteObjectStart(TEXT("max_exclusive")); Writer.WriteValue(TEXT("x"), Origin.X + Size); Writer.WriteValue(TEXT("y"), Origin.Y + Size); Writer.WriteValue(TEXT("z"), Origin.Z + Size); Writer.WriteObjectEnd(); Writer.WriteValue(TEXT("units"), TEXT("voxels")); Writer.WriteValue(TEXT("voxel_size_m"), static_cast(VoxelToMeters)); Writer.WriteObjectEnd(); } void WriteSurfaceReliefMetrics( FExploreJsonWriter& Writer, const FExploreMeshMetrics& Metrics) { Writer.WriteObjectStart(TEXT("surface_relief_metrics")); Writer.WriteValue(TEXT("vertex_count"), Metrics.VertexCount); Writer.WriteValue(TEXT("triangle_count"), Metrics.TriangleCount); Writer.WriteValue(TEXT("shared_edge_count"), Metrics.SharedEdgeCount); Writer.WriteValue(TEXT("dihedral_sample_count"), Metrics.DihedralSampleCount); Writer.WriteValue(TEXT("normal_comparison_triangle_count"), Metrics.NormalComparisonTriangleCount); Writer.WriteValue(TEXT("vertex_weld_quantum_cm"), 0.01); Writer.WriteObjectStart(TEXT("triangle_edge_length_m")); Writer.WriteValue(TEXT("p50"), static_cast(Metrics.EdgeLengthP50Meters)); Writer.WriteValue(TEXT("p99"), static_cast(Metrics.EdgeLengthP99Meters)); Writer.WriteValue(TEXT("max"), static_cast(Metrics.EdgeLengthMaxMeters)); Writer.WriteObjectEnd(); Writer.WriteObjectStart(TEXT("dihedral_angle_degrees")); Writer.WriteValue(TEXT("p50"), static_cast(Metrics.DihedralP50Degrees)); Writer.WriteValue(TEXT("p75"), static_cast(Metrics.DihedralP75Degrees)); Writer.WriteValue(TEXT("p90"), static_cast(Metrics.DihedralP90Degrees)); Writer.WriteValue(TEXT("p95"), static_cast(Metrics.DihedralP95Degrees)); Writer.WriteValue(TEXT("p99"), static_cast(Metrics.DihedralP99Degrees)); Writer.WriteValue(TEXT("fraction_over_20_degrees"), static_cast(Metrics.DihedralFractionOver20Degrees)); Writer.WriteValue(TEXT("fraction_over_40_degrees"), static_cast(Metrics.DihedralFractionOver40Degrees)); Writer.WriteObjectEnd(); Writer.WriteObjectStart(TEXT("face_normal_vs_mean_vertex_normal_degrees")); Writer.WriteValue(TEXT("p50"), static_cast(Metrics.FaceNormalVsMeanVertexNormalP50Degrees)); Writer.WriteValue(TEXT("p90"), static_cast(Metrics.FaceNormalVsMeanVertexNormalP90Degrees)); Writer.WriteObjectEnd(); Writer.WriteObjectEnd(); } void WriteTunnelFloorOverrides( FExploreJsonWriter& Writer, const FExploreArguments& Arguments) { Writer.WriteObjectStart(TEXT("tunnel_floor_overrides")); Writer.WriteValue(TEXT("legacy_enabled_override"), Arguments.bTunnelFloorEnabledOverride); if (Arguments.bTunnelFloorEnabledOverride) { Writer.WriteValue(TEXT("legacy_enabled_value"), Arguments.TunnelFloorEnabled != 0); } Writer.WriteValue(TEXT("enabled_ignored"), true); Writer.WriteValue(TEXT("terracing_override"), Arguments.bTunnelFloorTerracingOverride); if (Arguments.bTunnelFloorTerracingOverride) { Writer.WriteValue(TEXT("terracing"), Arguments.TunnelFloorTerracing != 0); } Writer.WriteValue(TEXT("step_height_override"), Arguments.bTunnelFloorStepOverride); if (Arguments.bTunnelFloorStepOverride) { Writer.WriteValue(TEXT("step_height_voxels"), static_cast(Arguments.TunnelFloorStep)); } Writer.WriteValue(TEXT("max_ledge_height_override"), Arguments.bTunnelFloorMaxLedgeOverride); if (Arguments.bTunnelFloorMaxLedgeOverride) { Writer.WriteValue(TEXT("max_ledge_height_voxels"), static_cast(Arguments.TunnelFloorMaxLedge)); } Writer.WriteValue(TEXT("gentle_slope_override"), Arguments.bTunnelFloorGentleOverride); if (Arguments.bTunnelFloorGentleOverride) { Writer.WriteValue(TEXT("gentle_slope"), static_cast(Arguments.TunnelFloorGentle)); Writer.WriteValue(TEXT("gentle_slope_units"), TEXT("degrees")); } Writer.WriteValue(TEXT("ledge_count_override"), Arguments.bTunnelFloorLedgesOverride); if (Arguments.bTunnelFloorLedgesOverride) { Writer.WriteValue(TEXT("ledge_count"), Arguments.TunnelFloorLedges); } Writer.WriteValue(TEXT("max_ledges_override"), Arguments.bTunnelFloorMaxLedgesOverride); if (Arguments.bTunnelFloorMaxLedgesOverride) { Writer.WriteValue(TEXT("max_ledges"), Arguments.TunnelFloorMaxLedges); } Writer.WriteValue(TEXT("passage_support_floor_writes_override"), Arguments.bPassageFloorWritesOverride); if (Arguments.bPassageFloorWritesOverride) { Writer.WriteValue(TEXT("passage_support_floor_writes"), Arguments.PassageFloorWrites != 0); } Writer.WriteObjectEnd(); } FString BuildManifestJson( const FExploreArguments& Arguments, const FExploreExportOutput& Export) { FString Json; TSharedRef Writer = TJsonWriterFactory>::Create(&Json); Writer->WriteObjectStart(); Writer->WriteValue(TEXT("schema_version"), 7); Writer->WriteValue(TEXT("format"), TEXT("OBJ")); Writer->WriteValue(TEXT("mesh_file"), Export.MeshFileName); Writer->WriteValue(TEXT("mesh_positions_units"), TEXT("metres")); Writer->WriteValue(TEXT("mesh_uv_units"), TEXT("voxels")); Writer->WriteValue(TEXT("load_notes"), TEXT( "Load geometry.obj directly in a browser OBJ loader; do not regenerate density. " "The manifest bounds are max-exclusive voxel bounds, OBJ positions are metres, and OBJ UVs " "are the canonical voxel-space UVs.")); Writer->WriteValue(TEXT("skirts"), false); Writer->WriteValue(TEXT("vertex_count"), Export.VertexCount); Writer->WriteValue(TEXT("triangle_count"), Export.TriangleCount); Writer->WriteValue(TEXT("geometry_hash"), Export.GeometryHash); WriteSurfaceReliefMetrics(*Writer, Export.SurfaceMetrics); Writer->WriteValue(TEXT("mesh_file_size_bytes"), Export.MeshFileSizeBytes); Writer->WriteValue(TEXT("mesh_array_bytes_sum_over_tiles"), Export.MeshArrayBytes); Writer->WriteValue(TEXT("canonical_mesher_tile_cells"), CHUNK_SIZE); Writer->WriteValue(TEXT("canonical_mesher_tile_count"), Export.MesherTileCount); Writer->WriteValue(TEXT("canonical_mesher_tiles_completed"), Export.MesherTilesCompleted); Writer->WriteValue(TEXT("mesh_seconds"), Export.MeshSeconds); Writer->WriteValue(TEXT("mesh_us_per_voxel"), Export.MeshUsPerVoxel); Writer->WriteValue(TEXT("export_step"), Arguments.ExportStep); Writer->WriteValue(TEXT("truncated"), Export.bTruncated); Writer->WriteValue(TEXT("seed"), Arguments.Seed); Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype)); Writer->WriteValue(TEXT("slot"), Arguments.Slot); Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack); Writer->WriteValue(TEXT("worms_enabled_override"), Arguments.bWormsEnabledOverride); if (Arguments.bWormsEnabledOverride) { Writer->WriteValue(TEXT("worms_enabled"), Arguments.bWormsEnabled); } Writer->WriteValue(TEXT("worm_strength_override"), Arguments.bWormStrengthOverride); if (Arguments.bWormStrengthOverride) { Writer->WriteValue(TEXT("worm_strength"), static_cast(Arguments.WormStrength)); } Writer->WriteValue(TEXT("tunnel_ablation_mask"), static_cast(VoxelDensityAblation::GetResolvedMask())); WriteTunnelFloorOverrides(*Writer, Arguments); Writer->WriteValue(TEXT("room_mouth_rise_override"), Arguments.bRoomMouthRiseOverride); if (Arguments.bRoomMouthRiseOverride) { Writer->WriteValue(TEXT("room_mouth_rise_strength"), static_cast(Arguments.RoomMouthRise)); } Writer->WriteValue(TEXT("room_mouth_rise_blend_override"), Arguments.bRoomMouthRiseBlendOverride); if (Arguments.bRoomMouthRiseBlendOverride) { Writer->WriteValue(TEXT("room_mouth_rise_blend_voxels"), static_cast(Arguments.RoomMouthRiseBlend)); } Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride); if (Arguments.bSurfaceRoughnessOverride) { Writer->WriteValue(TEXT("surface_roughness"), static_cast(Arguments.SurfaceRoughness)); } Writer->WriteValue(TEXT("roughness_noise_type_override"), Arguments.bRoughnessNoiseTypeOverride); if (Arguments.bRoughnessNoiseTypeOverride) { Writer->WriteValue(TEXT("roughness_noise_type"), NoiseTypeName(Arguments.RoughnessNoiseType)); } Writer->WriteValue(TEXT("roughness_frequency_override"), Arguments.bRoughnessFrequencyOverride); if (Arguments.bRoughnessFrequencyOverride) { Writer->WriteValue(TEXT("roughness_frequency"), static_cast(Arguments.RoughnessFrequency)); } Writer->WriteValue(TEXT("render_camera_override"), Arguments.bRenderCameraOverride); if (Arguments.bRenderCameraOverride) { WriteJsonVector(*Writer, TEXT("render_camera_voxels"), Arguments.RenderCamera, 1.0f); } WriteJsonIntVector(*Writer, TEXT("region_origin_voxels"), Export.RegionOrigin); WriteJsonBounds(*Writer, TEXT("region_bounds_voxels"), Export.RegionOrigin, Export.RegionSize, 0.25f); Writer->WriteObjectStart(TEXT("region_bounds_metres")); Writer->WriteObjectStart(TEXT("min")); Writer->WriteValue(TEXT("x"), static_cast(Export.RegionOrigin.X) * 0.25); Writer->WriteValue(TEXT("y"), static_cast(Export.RegionOrigin.Y) * 0.25); Writer->WriteValue(TEXT("z"), static_cast(Export.RegionOrigin.Z) * 0.25); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("max_exclusive")); Writer->WriteValue(TEXT("x"), static_cast(Export.RegionOrigin.X + Export.RegionSize) * 0.25); Writer->WriteValue(TEXT("y"), static_cast(Export.RegionOrigin.Y + Export.RegionSize) * 0.25); Writer->WriteValue(TEXT("z"), static_cast(Export.RegionOrigin.Z + Export.RegionSize) * 0.25); Writer->WriteObjectEnd(); Writer->WriteValue(TEXT("units"), TEXT("metres")); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("player_dimensions")); Writer->WriteValue(TEXT("capsule_radius_m"), static_cast(FVoxelPlayerCapsuleConstants::RadiusCentimeters) / 100.0); Writer->WriteValue(TEXT("capsule_width_m"), static_cast(2.0f * FVoxelPlayerCapsuleConstants::RadiusCentimeters) / 100.0); Writer->WriteValue(TEXT("capsule_half_height_m"), static_cast(FVoxelPlayerCapsuleConstants::HalfHeightCentimeters) / 100.0); Writer->WriteValue(TEXT("capsule_height_m"), static_cast(2.0f * FVoxelPlayerCapsuleConstants::HalfHeightCentimeters) / 100.0); Writer->WriteValue(TEXT("max_step_height_m"), static_cast(FVoxelPlayerCapsuleConstants::MaxStepHeightMeters)); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("world_invariants")); Writer->WriteValue(TEXT("world_radius_voxels"), 0); Writer->WriteValue(TEXT("lateral_regions_enabled"), false); Writer->WriteValue(TEXT("diff_layer_mutated"), false); Writer->WriteObjectEnd(); Writer->WriteObjectEnd(); return Writer->Close() ? Json : FString(); } bool RunExport( const FExploreArguments& Arguments, FExploreWorld& World, const FExploreWalkOutput* SharedSampling, FExploreExportOutput& OutOutput, FExploreBudget& Budget) { OutOutput = FExploreExportOutput(); const double ExportStartSeconds = FPlatformTime::Seconds(); OutOutput.RegionSize = Arguments.ExportSize; OutOutput.EstimatedWorkingBytes = EstimateExportWorkingBytes(Arguments.ExportSize); if (OutOutput.EstimatedWorkingBytes > OutOutput.WorkingMemoryCapBytes) { OutOutput.Status = TEXT("refused"); OutOutput.RefusalReason = FString::Printf( TEXT("Refused before meshing: estimated working memory %lld bytes exceeds cap %lld bytes."), static_cast(OutOutput.EstimatedWorkingBytes), static_cast(OutOutput.WorkingMemoryCapBytes)); return false; } FString MeshError; const bool bMeshComplete = EnsureExploreMesh( Arguments, World, SharedSampling, Budget, MeshError); if (!bMeshComplete && !Budget.bTruncated) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = MeshError; return false; } OutOutput.RegionSize = World.ExploreMeshSize; OutOutput.MesherTileCount = World.ExploreMeshTileCount; OutOutput.MesherTilesCompleted = World.ExploreMeshTilesCompleted; OutOutput.RegionOrigin = World.ExploreMeshOrigin; FString Error; OutOutput.MeshFileName = TEXT("geometry.obj"); OutOutput.ManifestFileName = TEXT("manifest.json"); const FString MeshPath = FPaths::Combine(Arguments.OutDirectory, OutOutput.MeshFileName); TUniquePtr Archive(IFileManager::Get().CreateFileWriter(*MeshPath)); if (!Archive.IsValid()) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = FString::Printf(TEXT("Could not create OBJ '%s'."), *MeshPath); return false; } const double ObjWriteStartSeconds = FPlatformTime::Seconds(); if (!WriteObjHeader(*Archive)) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = TEXT("Could not write the OBJ header."); return false; } const double MeasureStartSeconds = FPlatformTime::Seconds(); if (!MeasureExploreMesh(World.ExploreMesh, OutOutput.SurfaceMetrics, Error)) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = Error; return false; } OutOutput.MeasureSeconds = FPlatformTime::Seconds() - MeasureStartSeconds; OutOutput.MeshArrayBytes = static_cast(World.ExploreMesh.Vertices.Num()) * sizeof(FVector) + static_cast(World.ExploreMesh.Normals.Num()) * sizeof(FVector) + static_cast(World.ExploreMesh.UVs.Num()) * sizeof(FVector2D) + static_cast(World.ExploreMesh.Colors.Num()) * sizeof(FColor) + static_cast(World.ExploreMesh.Triangles.Num()) * sizeof(int32); OutOutput.VertexCount = World.ExploreMesh.Vertices.Num(); OutOutput.TriangleCount = World.ExploreMesh.Triangles.Num() / 3; OutOutput.MeshSeconds = World.ExploreMeshSeconds; OutOutput.MeshUsPerVoxel = Arguments.ExportSize > 0 ? OutOutput.MeshSeconds * 1.0e6 / static_cast(Arguments.ExportSize) / static_cast(Arguments.ExportSize) / static_cast(Arguments.ExportSize) : 0.0; OutOutput.GeometryHash = World.ExploreGeometryHash; bool bObjWriteTruncated = false; int32 VerticesWritten = 0; int32 TrianglesWritten = 0; if (!WriteObjMesh( *Archive, World.ExploreMesh, Budget, Error, bObjWriteTruncated, VerticesWritten, TrianglesWritten)) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = Error; return false; } if (Archive->IsError()) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = TEXT("Could not finish the streamed OBJ."); return false; } Archive.Reset(); OutOutput.MeshFileSizeBytes = IFileManager::Get().FileSize(*MeshPath); if (OutOutput.MeshFileSizeBytes < 0) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = TEXT("OBJ was written but its file size could not be read."); return false; } OutOutput.ObjWriteSeconds = FPlatformTime::Seconds() - ObjWriteStartSeconds; OutOutput.bTruncated = Budget.bTruncated || !World.bExploreMeshComplete || bObjWriteTruncated; if (OutOutput.bTruncated) { OutOutput.VertexCount = VerticesWritten; OutOutput.TriangleCount = TrianglesWritten; OutOutput.Status = TEXT("truncated"); OutOutput.RefusalReason = !World.bExploreMeshComplete ? MeshError : TEXT("The wall-clock budget stopped export after writing a partial mesh."); } const double ManifestStartSeconds = FPlatformTime::Seconds(); const FString ManifestJson = BuildManifestJson(Arguments, OutOutput); const FString ManifestJsonRepeat = BuildManifestJson(Arguments, OutOutput); if (ManifestJson.IsEmpty() || ManifestJsonRepeat.IsEmpty()) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = TEXT("The export manifest JSON writer failed."); return false; } if (ManifestJson != ManifestJsonRepeat) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = TEXT("The export manifest failed its deterministic repeat assertion."); return false; } if (!FFileHelper::SaveStringToFile( ManifestJson, *FPaths::Combine(Arguments.OutDirectory, OutOutput.ManifestFileName), FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = TEXT("Could not write export manifest.json."); return false; } OutOutput.ManifestSeconds = FPlatformTime::Seconds() - ManifestStartSeconds; OutOutput.ExportWallSeconds = FPlatformTime::Seconds() - ExportStartSeconds; OutOutput.ExportAccountingRemainderSeconds = OutOutput.ExportWallSeconds - (World.ExploreMeshSeconds + World.ExploreMeshHashSeconds + OutOutput.ObjWriteSeconds + OutOutput.ManifestSeconds); if (!OutOutput.bTruncated) { OutOutput.Status = TEXT("ok"); } return !OutOutput.bTruncated; } bool RunDensitySlice( const FExploreArguments& Arguments, FExploreWorld& World, FExploreDensitySliceOutput& OutOutput, FExploreBudget& Budget) { OutOutput = FExploreDensitySliceOutput(); OutOutput.FileName = TEXT("density_slice.csv"); OutOutput.SampleCount = Arguments.DensitySliceSamples; const double StartSeconds = FPlatformTime::Seconds(); FString Csv; Csv.Reserve(Arguments.DensitySliceSamples * 96); Csv += TEXT("index,x_voxels,y_voxels,z_voxels,density\r\n"); float PreviousDensity = 0.0f; bool bHavePreviousDensity = false; float MinDensity = TNumericLimits::Max(); float MaxDensity = -TNumericLimits::Max(); float MaxAbsDensity = 0.0f; for (int32 Index = 0; Index < Arguments.DensitySliceSamples; ++Index) { if ((Index & 127) == 0 && Budget.ShouldStop(TEXT("density-slice"))) { OutOutput.Status = TEXT("truncated"); OutOutput.RefusalReason = TEXT("The wall-clock budget stopped the density slice."); OutOutput.Seconds = FPlatformTime::Seconds() - StartSeconds; return false; } const FVector Position = Arguments.DensitySliceOrigin + Arguments.DensitySliceDirection * (Arguments.DensitySliceStep * static_cast(Index)); const float Density = World.Generator->GetDensityAt( Position.X, Position.Y, Position.Z); Csv += FString::Printf( TEXT("%d,%.9g,%.9g,%.9g,%.9g\r\n"), Index, Position.X, Position.Y, Position.Z, Density); if (!FMath::IsFinite(Density)) { ++OutOutput.NonFiniteSampleCount; bHavePreviousDensity = false; continue; } ++OutOutput.FiniteSampleCount; MinDensity = FMath::Min(MinDensity, Density); MaxDensity = FMath::Max(MaxDensity, Density); MaxAbsDensity = FMath::Max(MaxAbsDensity, FMath::Abs(Density)); if (FMath::Abs(Density) >= 0.999f) { ++OutOutput.SaturatedSampleCount; } if (bHavePreviousDensity && ((PreviousDensity < 0.0f && Density > 0.0f) || (PreviousDensity > 0.0f && Density < 0.0f))) { ++OutOutput.ZeroCrossingCount; } PreviousDensity = Density; bHavePreviousDensity = true; } if (OutOutput.FiniteSampleCount > 0) { OutOutput.MinDensity = MinDensity; OutOutput.MaxDensity = MaxDensity; OutOutput.MaxAbsDensity = MaxAbsDensity; } const FString Path = FPaths::Combine(Arguments.OutDirectory, OutOutput.FileName); if (!FFileHelper::SaveStringToFile( Csv, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = TEXT("Could not write density_slice.csv."); OutOutput.Seconds = FPlatformTime::Seconds() - StartSeconds; return false; } OutOutput.Status = TEXT("ok"); OutOutput.Seconds = FPlatformTime::Seconds() - StartSeconds; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDensitySlice] file=%s samples=%d finite=%d non_finite=%d " "zero_crossings=%d saturated=%d min=%.9g max=%.9g max_abs=%.9g " "origin=(%.3f,%.3f,%.3f) direction=(%.6f,%.6f,%.6f) step=%.6g"), *Path, OutOutput.SampleCount, OutOutput.FiniteSampleCount, OutOutput.NonFiniteSampleCount, OutOutput.ZeroCrossingCount, OutOutput.SaturatedSampleCount, OutOutput.MinDensity, OutOutput.MaxDensity, OutOutput.MaxAbsDensity, Arguments.DensitySliceOrigin.X, Arguments.DensitySliceOrigin.Y, Arguments.DensitySliceOrigin.Z, Arguments.DensitySliceDirection.X, Arguments.DensitySliceDirection.Y, Arguments.DensitySliceDirection.Z, Arguments.DensitySliceStep); return true; } void ComputeTunnelCoreProbeMetrics( const TArray& Samples, float StepVoxels, int32& OutValidCount, float& OutMean, float& OutMin, float& OutMax, float& OutPeriodicAmplitudeVoxels, float& OutPeriodicPeriodVoxels, int32& OutPeriodicMeasuredSamples, float& OutPeriodicMeasuredLengthVoxels, bool bPolynomialTrend = false) { OutValidCount = 0; OutMean = 0.0f; OutMin = 0.0f; OutMax = 0.0f; OutPeriodicAmplitudeVoxels = 0.0f; OutPeriodicPeriodVoxels = 0.0f; OutPeriodicMeasuredSamples = 0; OutPeriodicMeasuredLengthVoxels = 0.0f; if (Samples.Num() < 4 || !FMath::IsFinite(StepVoxels) || StepVoxels <= 0.0f) { return; } double Sum = 0.0; for (int32 Index = 0; Index < Samples.Num(); ++Index) { const float Value = Samples[Index]; if (!FMath::IsFinite(Value)) { continue; } ++OutValidCount; Sum += Value; if (OutValidCount == 1) { OutMin = Value; OutMax = Value; } else { OutMin = FMath::Min(OutMin, Value); OutMax = FMath::Max(OutMax, Value); } } if (OutValidCount < 4) { return; } const double Count = static_cast(OutValidCount); OutMean = static_cast(Sum / Count); // Find the longest contiguous surface run. A missing radial crossing is not a zero-valued // sample: it is a hole in the measurement and must not be bridged before the spectral pass. int32 BestRunStart = INDEX_NONE; int32 BestRunEnd = INDEX_NONE; int32 RunStart = INDEX_NONE; for (int32 Index = 0; Index <= Samples.Num(); ++Index) { const bool bFinite = Index < Samples.Num() && FMath::IsFinite(Samples[Index]); if (bFinite) { if (RunStart == INDEX_NONE) { RunStart = Index; } continue; } if (RunStart != INDEX_NONE && (BestRunStart == INDEX_NONE || Index - RunStart > BestRunEnd - BestRunStart + 1)) { BestRunStart = RunStart; BestRunEnd = Index - 1; } RunStart = INDEX_NONE; } const int32 BestRunLength = BestRunStart != INDEX_NONE ? BestRunEnd - BestRunStart + 1 : 0; // The requested acceptance window is at least 40 voxels. Keep the raw crossing statistics // above for shorter runs, but explicitly mark their periodic result as unmeasured. constexpr float RequiredLengthVoxels = 40.0f; const int32 RequiredSampleCount = FMath::CeilToInt(RequiredLengthVoxels / StepVoxels) + 1; if (BestRunLength < FMath::Max(4, RequiredSampleCount)) { return; } // Remove only the slow authored floor/radius trend. A 32-voxel median window is wider than // the 1..16 voxel rib band, so a genuine rib train survives while a single tunnel transition // or route slope follows the local baseline. The mouth probe uses a degree-six fit instead: the // room-side +/-2R window can contain one broad floor bowl, which a local median would turn // into a false short-period peak when only the middle 21.5 voxels remain. constexpr float TrendWindowVoxels = 32.0f; const int32 HalfWindowSamples = FMath::Max( 1, FMath::RoundToInt(0.5f * TrendWindowVoxels / StepVoxels)); const int32 AnalysisStart = bPolynomialTrend ? BestRunStart : BestRunStart + HalfWindowSamples; const int32 AnalysisEnd = bPolynomialTrend ? BestRunEnd : BestRunEnd - HalfWindowSamples; const int32 AnalysisCount = AnalysisEnd - AnalysisStart + 1; if (AnalysisCount < 4) { return; } OutPeriodicMeasuredSamples = AnalysisCount; OutPeriodicMeasuredLengthVoxels = static_cast(AnalysisCount - 1) * StepVoxels; TArray> Residuals; Residuals.SetNumZeroed(AnalysisCount); bool bHavePolynomialTrend = false; if (bPolynomialTrend) { // Fit a normalized degree-six smooth trend over the complete contiguous surface run. The // room-side +/-2R sample can contain a broad, asymmetric floor bowl; a cubic leaves that // endpoint curvature in the rib-band DFT. Degree six follows that slow authored shape but // cannot reproduce a repeated <=16-voxel train across the full run. Keep the legacy // local-median measurement separately as an audit of the conservative raw instrument. constexpr int32 PolynomialDegree = 6; double Matrix[PolynomialDegree + 1][PolynomialDegree + 1] = {}; double RightHandSide[PolynomialDegree + 1] = {}; const double Denominator = static_cast(FMath::Max( BestRunLength - 1, 1)); for (int32 Index = BestRunStart; Index <= BestRunEnd; ++Index) { const double NormalizedX = -1.0 + 2.0 * static_cast(Index - BestRunStart) / Denominator; double Basis[PolynomialDegree + 1]; Basis[0] = 1.0; for (int32 Power = 1; Power <= PolynomialDegree; ++Power) { Basis[Power] = Basis[Power - 1] * NormalizedX; } const double Value = static_cast(Samples[Index]); for (int32 Row = 0; Row <= PolynomialDegree; ++Row) { RightHandSide[Row] += Basis[Row] * Value; for (int32 Column = 0; Column <= PolynomialDegree; ++Column) { Matrix[Row][Column] += Basis[Row] * Basis[Column]; } } } double Coefficients[PolynomialDegree + 1] = {}; bHavePolynomialTrend = true; for (int32 PivotIndex = 0; PivotIndex <= PolynomialDegree; ++PivotIndex) { int32 PivotRow = PivotIndex; for (int32 Row = PivotIndex + 1; Row <= PolynomialDegree; ++Row) { if (FMath::Abs(Matrix[Row][PivotIndex]) > FMath::Abs(Matrix[PivotRow][PivotIndex])) { PivotRow = Row; } } if (FMath::Abs(Matrix[PivotRow][PivotIndex]) <= 1.0e-12) { bHavePolynomialTrend = false; break; } if (PivotRow != PivotIndex) { for (int32 Column = PivotIndex; Column <= PolynomialDegree; ++Column) { Swap(Matrix[PivotIndex][Column], Matrix[PivotRow][Column]); } Swap(RightHandSide[PivotIndex], RightHandSide[PivotRow]); } for (int32 Row = PivotIndex + 1; Row <= PolynomialDegree; ++Row) { const double Factor = Matrix[Row][PivotIndex] / Matrix[PivotIndex][PivotIndex]; for (int32 Column = PivotIndex; Column <= PolynomialDegree; ++Column) { Matrix[Row][Column] -= Factor * Matrix[PivotIndex][Column]; } RightHandSide[Row] -= Factor * RightHandSide[PivotIndex]; } } if (bHavePolynomialTrend) { for (int32 Row = PolynomialDegree; Row >= 0; --Row) { double Value = RightHandSide[Row]; for (int32 Column = Row + 1; Column <= PolynomialDegree; ++Column) { Value -= Matrix[Row][Column] * Coefficients[Column]; } Coefficients[Row] = Value / Matrix[Row][Row]; } for (int32 Index = AnalysisStart; Index <= AnalysisEnd; ++Index) { const double NormalizedX = -1.0 + 2.0 * static_cast(Index - BestRunStart) / Denominator; double Basis = 1.0; double Trend = 0.0; for (int32 Power = 0; Power <= PolynomialDegree; ++Power) { Trend += Coefficients[Power] * Basis; Basis *= NormalizedX; } Residuals[Index - AnalysisStart] = static_cast(static_cast(Samples[Index]) - Trend); } } } constexpr float MaxRibPeriodVoxels = 16.0f; const int32 MinRibFrequency = FMath::Max( 1, FMath::CeilToInt(StepVoxels * static_cast(AnalysisCount) / MaxRibPeriodVoxels)); const int32 MaxFrequency = AnalysisCount / 2; double BestAmplitude = 0.0; int32 BestFrequency = 0; for (int32 Frequency = MinRibFrequency; Frequency <= MaxFrequency; ++Frequency) { double Real = 0.0; double Imaginary = 0.0; for (int32 Index = AnalysisStart; Index <= AnalysisEnd; ++Index) { const int32 AnalysisIndex = Index - AnalysisStart; if (!bHavePolynomialTrend) { TArray> Window; const int32 WindowStart = FMath::Max(BestRunStart, Index - HalfWindowSamples); const int32 WindowEnd = FMath::Min(BestRunEnd, Index + HalfWindowSamples); Window.Reserve(WindowEnd - WindowStart + 1); for (int32 WindowIndex = WindowStart; WindowIndex <= WindowEnd; ++WindowIndex) { Window.Add(Samples[WindowIndex]); } Window.Sort(); const int32 Middle = Window.Num() / 2; const double Median = (Window.Num() & 1) != 0 ? static_cast(Window[Middle]) : 0.5 * (static_cast(Window[Middle - 1]) + static_cast(Window[Middle])); Residuals[AnalysisIndex] = static_cast( static_cast(Samples[Index]) - Median); } const double Residual = static_cast(Residuals[AnalysisIndex]); const double Angle = 2.0 * PI * static_cast(Frequency) * static_cast(AnalysisIndex) / static_cast(AnalysisCount); Real += Residual * FMath::Cos(static_cast(Angle)); Imaginary -= Residual * FMath::Sin(static_cast(Angle)); } const double Amplitude = 2.0 * FMath::Sqrt(Real * Real + Imaginary * Imaginary) / static_cast(AnalysisCount); if (Amplitude > BestAmplitude) { BestAmplitude = Amplitude; BestFrequency = Frequency; } } OutPeriodicAmplitudeVoxels = static_cast(BestAmplitude); OutPeriodicPeriodVoxels = BestFrequency > 0 ? StepVoxels * static_cast(AnalysisCount) / static_cast(BestFrequency) : 0.0f; } void ComputeTunnelMouthProbeMetrics( FExploreTunnelMouthProbeAxis& Axis, float StepVoxels) { int32 LegacyValid = 0; float LegacyMean = 0.0f; float LegacyMin = 0.0f; float LegacyMax = 0.0f; int32 LegacyMeasuredSamples = 0; float LegacyMeasuredLength = 0.0f; ComputeTunnelCoreProbeMetrics( Axis.CrossingsVoxels, StepVoxels, Axis.ValidCrossings, Axis.MeanCrossingVoxels, Axis.MinCrossingVoxels, Axis.MaxCrossingVoxels, Axis.LocalMedianPeriodicAmplitudeVoxels, Axis.LocalMedianPeriodicPeriodVoxels, LegacyMeasuredSamples, LegacyMeasuredLength); ComputeTunnelCoreProbeMetrics( Axis.CrossingsVoxels, StepVoxels, LegacyValid, LegacyMean, LegacyMin, LegacyMax, Axis.PeriodicAmplitudeVoxels, Axis.PeriodicPeriodVoxels, Axis.PeriodicMeasuredSamples, Axis.PeriodicMeasuredLengthVoxels, /*bPolynomialTrend=*/true); } bool RunTunnelCoreProbe( const FExploreArguments& Arguments, FExploreWorld& World, FExploreTunnelCoreProbeOutput& OutOutput, FExploreBudget& Budget) { OutOutput = FExploreTunnelCoreProbeOutput(); OutOutput.FileName = TEXT("tunnel_core_probe.csv"); OutOutput.StepVoxels = 0.25f; const double StartSeconds = FPlatformTime::Seconds(); if (!World.Manager.IsValid() || !World.Generator.IsValid() || !World.Settings.IsValid() || !World.Definitions.IsValidIndex(Arguments.Slot)) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = TEXT("The owner-value probe has no complete transient world."); return false; } const int32 TargetBottomChunkZ = FMath::FloorToInt( static_cast(World.TargetBottomWorldZ) / static_cast(CHUNK_SIZE)); const int32 TargetTopChunkZ = FMath::FloorToInt( (static_cast(World.TargetTopWorldZ) - KINDA_SMALL_NUMBER) / static_cast(CHUNK_SIZE)); const float TargetMinZ = static_cast(World.TargetBottomWorldZ); const float TargetMaxZ = static_cast(World.TargetTopWorldZ); constexpr float TargetBandPadding = 8.0f; constexpr float RouteParameterCheckStep = 4.0f; // The owner path asks GetGenerationParams for the exact (x,y,z) chunk of every sample. A // cache built from an arbitrary interior Z can therefore be a different graph in a boundary // blend band; that was the source of the earlier probe's FLT_MAX/core-only result. Build the // small set of candidate graphs used by the target Z chunks, and retain only a complete chain // whose route and radial measurement band resolve to the same parameter fingerprint that was // used to bake the graph. The field measured below is then the same field the game queries. auto GenerationParamsFingerprint = [](const FStrateGenerationParams& InParams) -> uint32 { return FCrc::MemCrc32(&InParams, sizeof(InParams)); }; FChunkSDFCache Cache; FStrateGenerationParams Params; int32 StrateIndex = INDEX_NONE; int32 QueryChunkZ = INDEX_NONE; const FCachedTunnel* BestTunnel = nullptr; float BestLength = 0.0f; int32 BestTunnelIndex = INDEX_NONE; bool bBestTunnelFullyInTargetStrate = false; int32 BestTargetBandPointCount = 0; uint32 BestParamsFingerprint = 0xFFFFFFFFu; const uint32 WorldSeed = static_cast(World.Settings->Seed); for (int32 CandidateChunkZ = TargetBottomChunkZ; CandidateChunkZ <= TargetTopChunkZ; ++CandidateChunkZ) { if (Budget.ShouldStop(TEXT("tunnel-core-probe-cache"))) { OutOutput.Status = TEXT("truncated"); OutOutput.RefusalReason = TEXT("The wall-clock budget stopped candidate cache construction."); OutOutput.Seconds = FPlatformTime::Seconds() - StartSeconds; return false; } const FIntVector CandidateChunk(0, 0, CandidateChunkZ); const FStrateGenerationParams CandidateParams = World.Manager->GetGenerationParams(CandidateChunk); const int32 CandidateStrateIndex = World.Manager->GetStrateIndex( (static_cast(CandidateChunk.Z) + 0.5f) * static_cast(CHUNK_SIZE) * VOXEL_SIZE); if (CandidateStrateIndex < 0 || !(CandidateParams.RoomDensity > 0.0f) || !(CandidateParams.RoomSpacing > 0.0f)) { continue; } const UVoxelStrateDefinition* CandidateDefinition = World.Manager->GetStrateForChunk(CandidateChunk); const TArray* CandidateTerrainOps = CandidateDefinition != nullptr ? &CandidateDefinition->TerrainOperations : nullptr; const uint32 CandidateFingerprint = GenerationParamsFingerprint(CandidateParams); // The owner render region is centred near the origin. Keep each candidate cache broad // enough for the local graph, exactly like the ordinary point-query cache window. FChunkSDFCache CandidateCache; VoxelCaveMorphology::BuildChunkCache( CandidateCache, -256.0f, -256.0f, 256.0f, 256.0f, CandidateParams, WorldSeed, CandidateStrateIndex, CandidateTerrainOps, ERoomGraphBuildSite::GeneratorTunnelCore); int32 CandidateBestTunnelIndex = INDEX_NONE; float CandidateBestLength = 0.0f; int32 CandidateBestTargetSampleCount = 0; for (int32 TunnelIndex = 0; TunnelIndex < CandidateCache.Tunnels.Num(); ++TunnelIndex) { const FCachedTunnel& Tunnel = CandidateCache.Tunnels[TunnelIndex]; const TArray& Points = Tunnel.WorldControlPoints.Num() >= 2 && Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num() ? Tunnel.WorldControlPoints : Tunnel.ControlPoints; const TArray& Radii = Tunnel.WorldControlRadii.Num() == Points.Num() ? Tunnel.WorldControlRadii : Tunnel.ControlRadii; if (Points.Num() < 2 || Radii.Num() != Points.Num()) { continue; } TArray CandidateCumulativeLengths; CandidateCumulativeLengths.SetNumZeroed(Points.Num()); bool bFinite = true; float Length = 0.0f; float MaxRadius = 0.0f; for (int32 PointIndex = 0; PointIndex + 1 < Points.Num(); ++PointIndex) { const float SegmentLength = FVector::Dist( Points[PointIndex], Points[PointIndex + 1]); if (!FMath::IsFinite(SegmentLength)) { bFinite = false; break; } Length += SegmentLength; CandidateCumulativeLengths[PointIndex + 1] = Length; MaxRadius = FMath::Max(MaxRadius, FMath::Abs(Radii[PointIndex])); } if (Points.Num() > 0) { MaxRadius = FMath::Max(MaxRadius, FMath::Abs(Radii.Last())); } if (!bFinite || Length < 40.0f) { continue; } auto PointAtCandidateDistance = [&](float Distance) -> FVector { const float ClampedDistance = FMath::Clamp(Distance, 0.0f, Length); for (int32 SegmentIndex = 0; SegmentIndex + 1 < Points.Num(); ++SegmentIndex) { if (ClampedDistance <= CandidateCumulativeLengths[SegmentIndex + 1] || SegmentIndex + 2 == Points.Num()) { const float SegmentLength = CandidateCumulativeLengths[SegmentIndex + 1] - CandidateCumulativeLengths[SegmentIndex]; const float Alpha = SegmentLength > KINDA_SMALL_NUMBER ? (ClampedDistance - CandidateCumulativeLengths[SegmentIndex]) / SegmentLength : 0.0f; return FMath::Lerp(Points[SegmentIndex], Points[SegmentIndex + 1], Alpha); } } return Points.Last(); }; const float RadialParameterReach = FMath::Clamp( MaxRadius + 12.0f, 12.0f, 64.0f); bool bHasTargetSample = false; bool bRouteMatchesParams = true; int32 TargetSampleCount = 0; const int32 ParameterSampleCount = FMath::CeilToInt( Length / RouteParameterCheckStep); for (int32 SampleIndex = 0; SampleIndex <= ParameterSampleCount; ++SampleIndex) { const float Distance = FMath::Min( Length, static_cast(SampleIndex) * RouteParameterCheckStep); const FVector Point = PointAtCandidateDistance(Distance); const bool bInTargetBand = Point.Z >= TargetMinZ - TargetBandPadding && Point.Z <= TargetMaxZ + TargetBandPadding; bHasTargetSample |= bInTargetBand; TargetSampleCount += bInTargetBand ? 1 : 0; if (!bInTargetBand) { bRouteMatchesParams = false; break; } const FIntVector PointChunk( FMath::FloorToInt(Point.X / static_cast(CHUNK_SIZE)), FMath::FloorToInt(Point.Y / static_cast(CHUNK_SIZE)), FMath::FloorToInt(Point.Z / static_cast(CHUNK_SIZE))); if (GenerationParamsFingerprint( World.Manager->GetGenerationParams(PointChunk)) != CandidateFingerprint) { bRouteMatchesParams = false; break; } // The floor, wall and ceiling roots are measured around this same centerline. // Reject a route whose radial band crosses into a different parameter blend; a // single cache cannot represent that discontinuous query context. for (float RadialOffset = -RadialParameterReach; RadialOffset <= RadialParameterReach + KINDA_SMALL_NUMBER; RadialOffset += 8.0f) { const FVector RadialPoint = Point + FVector(0.0f, 0.0f, RadialOffset); const FIntVector RadialChunk( FMath::FloorToInt(RadialPoint.X / static_cast(CHUNK_SIZE)), FMath::FloorToInt(RadialPoint.Y / static_cast(CHUNK_SIZE)), FMath::FloorToInt(RadialPoint.Z / static_cast(CHUNK_SIZE))); if (GenerationParamsFingerprint( World.Manager->GetGenerationParams(RadialChunk)) != CandidateFingerprint) { bRouteMatchesParams = false; break; } } if (!bRouteMatchesParams) { break; } } if (bHasTargetSample && bRouteMatchesParams && (TargetSampleCount > CandidateBestTargetSampleCount || (TargetSampleCount == CandidateBestTargetSampleCount && Length > CandidateBestLength))) { CandidateBestTunnelIndex = TunnelIndex; CandidateBestLength = Length; CandidateBestTargetSampleCount = TargetSampleCount; } } if (CandidateBestTunnelIndex != INDEX_NONE && CandidateBestLength > BestLength) { Cache = MoveTemp(CandidateCache); Params = CandidateParams; StrateIndex = CandidateStrateIndex; QueryChunkZ = CandidateChunkZ; BestLength = CandidateBestLength; BestTunnelIndex = CandidateBestTunnelIndex; BestTargetBandPointCount = CandidateBestTargetSampleCount; bBestTunnelFullyInTargetStrate = true; BestParamsFingerprint = CandidateFingerprint; } } BestTunnel = Cache.Tunnels.IsValidIndex(BestTunnelIndex) ? &Cache.Tunnels[BestTunnelIndex] : nullptr; if (BestTunnel == nullptr || BestLength < 40.0f) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = FString::Printf( TEXT("No cached owner tunnel had a 40-voxel span with stable game params " "and a stable radial band (best=%.3f)."), BestLength); OutOutput.Seconds = FPlatformTime::Seconds() - StartSeconds; return false; } const TArray& Points = BestTunnel->WorldControlPoints.Num() >= 2 && BestTunnel->WorldControlRadii.Num() == BestTunnel->WorldControlPoints.Num() ? BestTunnel->WorldControlPoints : BestTunnel->ControlPoints; TArray CumulativeLengths; CumulativeLengths.SetNumZeroed(Points.Num()); for (int32 PointIndex = 0; PointIndex + 1 < Points.Num(); ++PointIndex) { CumulativeLengths[PointIndex + 1] = CumulativeLengths[PointIndex] + FVector::Dist(Points[PointIndex], Points[PointIndex + 1]); } const float ProbeStep = OutOutput.StepVoxels; const int32 MaxProbeSamples = 4097; float ProbeStart = 0.0f; float ProbeLength = FMath::Min( BestLength, static_cast(MaxProbeSamples - 1) * ProbeStep); auto PointAtDistance = [&](float Distance) -> FVector { const float ClampedDistance = FMath::Clamp(Distance, 0.0f, BestLength); for (int32 SegmentIndex = 0; SegmentIndex + 1 < Points.Num(); ++SegmentIndex) { if (ClampedDistance <= CumulativeLengths[SegmentIndex + 1] || SegmentIndex + 2 == Points.Num()) { const float SegmentLength = CumulativeLengths[SegmentIndex + 1] - CumulativeLengths[SegmentIndex]; const float Alpha = SegmentLength > KINDA_SMALL_NUMBER ? (ClampedDistance - CumulativeLengths[SegmentIndex]) / SegmentLength : 0.0f; return FMath::Lerp(Points[SegmentIndex], Points[SegmentIndex + 1], Alpha); } } return Points.Last(); }; // Keep a long, endpoint-inset window on the selected tunnel chain. The DFT is intended to // expose a repeated radial surface displacement, so using the entire wandering chain would // mix mouth overlap and changes in the local frame into that signal. The center window stays // on the same owner-parameter band already validated above and is at least 40 voxels long. const float MaximumWindowLength = static_cast(MaxProbeSamples - 1) * ProbeStep; if (BestLength >= 56.0f) { ProbeLength = FMath::Min(96.0f, FMath::Min(BestLength - 16.0f, MaximumWindowLength)); if (ProbeLength >= 40.0f) { ProbeStart = FMath::Clamp( (BestLength - ProbeLength) * 0.5f, 8.0f, BestLength - ProbeLength - 8.0f); } } if (ProbeStart + ProbeLength > BestLength) { ProbeStart = 0.0f; ProbeLength = FMath::Min(BestLength, MaximumWindowLength); } const int32 SampleCount = FMath::FloorToInt(ProbeLength / ProbeStep) + 1; ProbeLength = static_cast(SampleCount - 1) * ProbeStep; OutOutput.TunnelIndex = BestTunnelIndex; OutOutput.SampleCount = SampleCount; OutOutput.LengthVoxels = ProbeLength; FVector AxisDirection = PointAtDistance(ProbeStart + ProbeLength) - PointAtDistance(ProbeStart); AxisDirection.Z = 0.0f; if (!AxisDirection.Normalize()) { float LongestSegment = 0.0f; for (int32 SegmentIndex = 0; SegmentIndex + 1 < Points.Num(); ++SegmentIndex) { FVector Candidate = Points[SegmentIndex + 1] - Points[SegmentIndex]; Candidate.Z = 0.0f; const float SegmentLength = Candidate.Size(); if (SegmentLength > LongestSegment) { LongestSegment = SegmentLength; AxisDirection = Candidate / SegmentLength; } } } if (!AxisDirection.Normalize()) { AxisDirection = FVector::ForwardVector; } const FVector WallDirection(-AxisDirection.Y, AxisDirection.X, 0.0f); OutOutput.AxisStart = PointAtDistance(ProbeStart); OutOutput.AxisEnd = PointAtDistance(ProbeStart + ProbeLength); OutOutput.AxisDirection = AxisDirection; OutOutput.WallDirection = WallDirection; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTunnelCoreProbe] cache_tunnels=%d strate_index=%d " "query_chunk_z=%d target_z=[%.3f,%.3f] params_z=[%.3f,%.3f] selected=%d " "params_fp=0x%08X window_start=%.3f window_length=%.3f " "selected_fully_in_target=%d target_points=%d"), Cache.Tunnels.Num(), StrateIndex, QueryChunkZ, TargetMinZ, TargetMaxZ, Params.StrateBottomWorldZ, Params.StrateTopWorldZ, BestTunnelIndex, BestParamsFingerprint, ProbeStart, ProbeLength, bBestTunnelFullyInTargetStrate ? 1 : 0, BestTargetBandPointCount); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTunnelCoreProbe] selected_endpoints a=(%.3f,%.3f,%.3f) " "b=(%.3f,%.3f,%.3f) world_min=(%.3f,%.3f,%.3f) " "world_max=(%.3f,%.3f,%.3f) world_radius=%.3f"), BestTunnel->EndpointA.X, BestTunnel->EndpointA.Y, BestTunnel->EndpointA.Z, BestTunnel->EndpointB.X, BestTunnel->EndpointB.Y, BestTunnel->EndpointB.Z, BestTunnel->WorldCenterlineMin.X, BestTunnel->WorldCenterlineMin.Y, BestTunnel->WorldCenterlineMin.Z, BestTunnel->WorldCenterlineMax.X, BestTunnel->WorldCenterlineMax.Y, BestTunnel->WorldCenterlineMax.Z, FMath::Sqrt(FMath::Max(BestTunnel->WorldBoundRadiusSq, 0.0f))); float MaxRadius = 0.0f; const TArray& Radii = BestTunnel->WorldControlRadii.Num() == Points.Num() ? BestTunnel->WorldControlRadii : BestTunnel->ControlRadii; for (const float Radius : Radii) { MaxRadius = FMath::Max(MaxRadius, FMath::Abs(Radius)); } const float MaxRadialDistance = FMath::Clamp(MaxRadius + 12.0f, 12.0f, 64.0f); auto FindZeroCrossing = [&](const FVector& Origin, const FVector& Direction, bool bCoreSDF, float& OutDistance) -> bool { auto Evaluate = [&](float Distance) -> float { const FVector Position = Origin + Direction * Distance; return bCoreSDF ? VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF( Position.X, Position.Y, Position.Z, Cache, true) : World.Generator->GetDensityAt(Position.X, Position.Y, Position.Z); }; float PreviousDistance = 0.0f; float PreviousValue = Evaluate(0.0f); bool bHavePrevious = FMath::IsFinite(PreviousValue); if (bHavePrevious && FMath::Abs(PreviousValue) <= 1.0e-4f) { OutDistance = 0.0f; return true; } const int32 RadialSteps = FMath::CeilToInt(MaxRadialDistance / ProbeStep); for (int32 StepIndex = 1; StepIndex <= RadialSteps; ++StepIndex) { const float CurrentDistance = FMath::Min( MaxRadialDistance, static_cast(StepIndex) * ProbeStep); const float CurrentValue = Evaluate(CurrentDistance); if (!FMath::IsFinite(CurrentValue)) { // A transient invalid query must split the finite run, not poison every // subsequent sample. The floor ray can cross a chunk/parameter seam where // one exact fractional query is unavailable; later finite samples still form a // valid zero-crossing interval and are deliberately allowed to restart it. bHavePrevious = false; continue; } if (!bHavePrevious) { PreviousDistance = CurrentDistance; PreviousValue = CurrentValue; bHavePrevious = true; if (FMath::Abs(CurrentValue) <= 1.0e-4f) { OutDistance = CurrentDistance; return true; } continue; } const bool bCrossed = (PreviousValue < 0.0f && CurrentValue >= 0.0f) || (PreviousValue > 0.0f && CurrentValue <= 0.0f); if (bCrossed) { float Low = PreviousDistance; float High = CurrentDistance; float LowValue = PreviousValue; for (int32 Iteration = 0; Iteration < 24; ++Iteration) { const float Mid = (Low + High) * 0.5f; const float MidValue = Evaluate(Mid); if (!FMath::IsFinite(MidValue)) { break; } if (FMath::Abs(MidValue) <= 1.0e-5f) { Low = Mid; High = Mid; break; } if ((LowValue < 0.0f && MidValue < 0.0f) || (LowValue > 0.0f && MidValue > 0.0f)) { Low = Mid; LowValue = MidValue; } else { High = Mid; } } OutDistance = (Low + High) * 0.5f; return true; } PreviousDistance = CurrentDistance; PreviousValue = CurrentValue; } return false; }; OutOutput.Floor.Name = TEXT("floor"); OutOutput.Wall.Name = TEXT("wall"); OutOutput.Ceiling.Name = TEXT("ceiling"); OutOutput.Floor.CoreCrossingsVoxels.Reserve(SampleCount); OutOutput.Floor.FinalCrossingsVoxels.Reserve(SampleCount); OutOutput.Wall.CoreCrossingsVoxels.Reserve(SampleCount); OutOutput.Wall.FinalCrossingsVoxels.Reserve(SampleCount); OutOutput.Ceiling.CoreCrossingsVoxels.Reserve(SampleCount); OutOutput.Ceiling.FinalCrossingsVoxels.Reserve(SampleCount); FString Csv; Csv.Reserve(SampleCount * 180); Csv += TEXT("sample,axis_distance_voxels,center_x,center_y,center_z,core_center_sdf,final_center_density," "floor_core_zero,floor_final_zero,wall_core_zero,wall_final_zero," "ceiling_core_zero,ceiling_final_zero\r\n"); float PreviousCoreCenter = 0.0f; float PreviousFinalCenter = 0.0f; bool bHavePreviousCoreCenter = false; bool bHavePreviousFinalCenter = false; float MaxGeneratorCoreDelta = 0.0f; int32 GeneratorCoreMismatchCount = 0; const uint32 ProbeLayoutVersion = World.Manager->GetLayoutVersion(); for (int32 SampleIndex = 0; SampleIndex < SampleCount; ++SampleIndex) { if ((SampleIndex & 127) == 0 && Budget.ShouldStop(TEXT("tunnel-core-probe"))) { OutOutput.Status = TEXT("truncated"); OutOutput.RefusalReason = TEXT("The wall-clock budget stopped the tunnel-core probe."); OutOutput.Seconds = FPlatformTime::Seconds() - StartSeconds; return false; } const float AxisDistance = static_cast(SampleIndex) * ProbeStep; const FVector Center = PointAtDistance(ProbeStart + AxisDistance); const float CoreCenter = VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF( Center.X, Center.Y, Center.Z, Cache, true); const float FinalCenter = World.Generator->GetDensityAt(Center.X, Center.Y, Center.Z); FTunnelCoreWorldEvaluation GeneratorCoreEvaluation; World.Generator->GetDensityWithParams( Center.X, Center.Y, Center.Z, Params, BestParamsFingerprint, ProbeLayoutVersion, /*bApplyLegacyStructuralPosts=*/false, &GeneratorCoreEvaluation, /*bCollectFusedDiagnostics=*/false); if (FMath::IsFinite(CoreCenter) && FMath::IsFinite(GeneratorCoreEvaluation.SDF)) { const float CoreDelta = FMath::Abs(CoreCenter - GeneratorCoreEvaluation.SDF); MaxGeneratorCoreDelta = FMath::Max(MaxGeneratorCoreDelta, CoreDelta); if (CoreDelta > 1.0e-3f) { ++GeneratorCoreMismatchCount; } } if (SampleIndex == 0 || SampleIndex == SampleCount / 2 || SampleIndex + 1 == SampleCount) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTunnelCoreProbe] generator_core_compare sample=%d " "probe_sdf=%.6g generator_sdf=%.6g generator_mc=%.6g final_mc=%.6g"), SampleIndex, CoreCenter, GeneratorCoreEvaluation.SDF, -GeneratorCoreEvaluation.SDF, FinalCenter); } if (FMath::IsFinite(CoreCenter)) { ++OutOutput.CoreCenterFiniteSampleCount; OutOutput.CoreCenterMin = OutOutput.CoreCenterFiniteSampleCount == 1 ? CoreCenter : FMath::Min(OutOutput.CoreCenterMin, CoreCenter); OutOutput.CoreCenterMax = OutOutput.CoreCenterFiniteSampleCount == 1 ? CoreCenter : FMath::Max(OutOutput.CoreCenterMax, CoreCenter); if (bHavePreviousCoreCenter && ((PreviousCoreCenter < 0.0f && CoreCenter > 0.0f) || (PreviousCoreCenter > 0.0f && CoreCenter < 0.0f))) { ++OutOutput.CoreCenterZeroCrossingCount; } PreviousCoreCenter = CoreCenter; bHavePreviousCoreCenter = true; } if (FMath::IsFinite(FinalCenter)) { ++OutOutput.FinalCenterFiniteSampleCount; OutOutput.FinalCenterMin = OutOutput.FinalCenterFiniteSampleCount == 1 ? FinalCenter : FMath::Min(OutOutput.FinalCenterMin, FinalCenter); OutOutput.FinalCenterMax = OutOutput.FinalCenterFiniteSampleCount == 1 ? FinalCenter : FMath::Max(OutOutput.FinalCenterMax, FinalCenter); OutOutput.FinalCenterMaxAbs = FMath::Max( OutOutput.FinalCenterMaxAbs, FMath::Abs(FinalCenter)); // The old hand-off wrote a fixed MC-air value (normally +16, with the reciprocal // solid plateau at -8 in other definitions). Do not call every ordinary SDF sample // "saturated"; count only the known clamp plateaus so the probe distinguishes the // old hard switch from the continuous signed field. const bool bKnownClampPlateau = FMath::Abs(FinalCenter - 8.0f) <= 1.0e-3f || FMath::Abs(FinalCenter + 8.0f) <= 1.0e-3f || FMath::Abs(FinalCenter - 16.0f) <= 1.0e-3f || FMath::Abs(FinalCenter + 16.0f) <= 1.0e-3f; if (bKnownClampPlateau) { ++OutOutput.FinalCenterSaturatedSampleCount; } if (bHavePreviousFinalCenter && ((PreviousFinalCenter < 0.0f && FinalCenter > 0.0f) || (PreviousFinalCenter > 0.0f && FinalCenter < 0.0f))) { ++OutOutput.FinalCenterZeroCrossingCount; } PreviousFinalCenter = FinalCenter; bHavePreviousFinalCenter = true; } const FVector RadialDirections[3] = { FVector::DownVector, WallDirection, FVector::UpVector }; FExploreTunnelCoreProbeAxis* Axes[3] = { &OutOutput.Floor, &OutOutput.Wall, &OutOutput.Ceiling }; float Roots[3][2]; for (int32 AxisIndex = 0; AxisIndex < 3; ++AxisIndex) { Roots[AxisIndex][0] = std::numeric_limits::quiet_NaN(); Roots[AxisIndex][1] = std::numeric_limits::quiet_NaN(); FindZeroCrossing(Center, RadialDirections[AxisIndex], true, Roots[AxisIndex][0]); FindZeroCrossing(Center, RadialDirections[AxisIndex], false, Roots[AxisIndex][1]); Axes[AxisIndex]->CoreCrossingsVoxels.Add(Roots[AxisIndex][0]); Axes[AxisIndex]->FinalCrossingsVoxels.Add(Roots[AxisIndex][1]); } if (SampleIndex == 50 || SampleIndex == 302) { const float DiagnosticDistances[] = { 0.0f, 4.0f, 8.0f, 10.0f, 11.0f, 12.0f, 13.0f, 16.0f, 20.0f, 24.0f, 28.0f, 32.0f }; for (int32 AxisIndex = 0; AxisIndex < 3; ++AxisIndex) { FString Values; for (const float Distance : DiagnosticDistances) { const FVector Position = Center + RadialDirections[AxisIndex] * Distance; const float CoreValue = VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF( Position.X, Position.Y, Position.Z, Cache, true); FTunnelCoreWorldEvaluation GenericCoreEvaluation; const float GenericValue = World.Generator->GetDensityWithParams( Position.X, Position.Y, Position.Z, Params, BestParamsFingerprint, ProbeLayoutVersion, /*bApplyLegacyStructuralPosts=*/false, &GenericCoreEvaluation, /*bCollectFusedDiagnostics=*/false); const float FinalValue = World.Generator->GetDensityAt( Position.X, Position.Y, Position.Z); Values += FString::Printf( TEXT(" %.0f:(%.3g,%.3g,%.3g)"), Distance, CoreValue, GenericValue, FinalValue); } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTunnelCoreProbe] radial_diagnostic sample=%d axis=%s " "distance:(core_sdf,generic_mc,final_mc)%s"), SampleIndex, *Axes[AxisIndex]->Name, *Values); } } Csv += FString::Printf( TEXT("%d,%.9g,%.9g,%.9g,%.9g,%.9g,%.9g,%.9g,%.9g,%.9g,%.9g,%.9g,%.9g\r\n"), SampleIndex, AxisDistance, Center.X, Center.Y, Center.Z, CoreCenter, FinalCenter, Roots[0][0], Roots[0][1], Roots[1][0], Roots[1][1], Roots[2][0], Roots[2][1]); } FExploreTunnelCoreProbeAxis* Axes[3] = { &OutOutput.Floor, &OutOutput.Wall, &OutOutput.Ceiling }; for (FExploreTunnelCoreProbeAxis* Axis : Axes) { ComputeTunnelCoreProbeMetrics( Axis->CoreCrossingsVoxels, ProbeStep, Axis->ValidCoreCrossings, Axis->CoreMeanCrossingVoxels, Axis->CoreMinCrossingVoxels, Axis->CoreMaxCrossingVoxels, Axis->CorePeriodicAmplitudeVoxels, Axis->CorePeriodicPeriodVoxels, Axis->CorePeriodicMeasuredSamples, Axis->CorePeriodicMeasuredLengthVoxels); ComputeTunnelCoreProbeMetrics( Axis->FinalCrossingsVoxels, ProbeStep, Axis->ValidFinalCrossings, Axis->FinalMeanCrossingVoxels, Axis->FinalMinCrossingVoxels, Axis->FinalMaxCrossingVoxels, Axis->FinalPeriodicAmplitudeVoxels, Axis->FinalPeriodicPeriodVoxels, Axis->FinalPeriodicMeasuredSamples, Axis->FinalPeriodicMeasuredLengthVoxels); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTunnelCoreProbe] axis=%s samples=%d final_valid=%d " "rib_periodic_amplitude=%.6g_voxels=%.6g_cm period=%.6g_voxels " "measured=%.6g_voxels/%d core_valid=%d core_rib_periodic_amplitude=%.6g_voxels=%.6g_cm period=%.6g_voxels " "measured=%.6g_voxels/%d"), *Axis->Name, SampleCount, Axis->ValidFinalCrossings, Axis->FinalPeriodicAmplitudeVoxels, Axis->FinalPeriodicAmplitudeVoxels * 25.0f, Axis->FinalPeriodicPeriodVoxels, Axis->FinalPeriodicMeasuredLengthVoxels, Axis->FinalPeriodicMeasuredSamples, Axis->ValidCoreCrossings, Axis->CorePeriodicAmplitudeVoxels, Axis->CorePeriodicAmplitudeVoxels * 25.0f, Axis->CorePeriodicPeriodVoxels, Axis->CorePeriodicMeasuredLengthVoxels, Axis->CorePeriodicMeasuredSamples); } // Mouth probe: measure the two surfaces that meet at each room endpoint over a lateral // window of approximately +/-2 tunnel radii. The wall ray starts in room air and points // toward the tunnel; the floor ray starts below the same room-side patch and points up. // Both use the final MC-facing density, so this catches a ring/fillet handoff rather than // merely comparing two independent source SDFs. Keep this in the same owner-value process // and cache selection as the core probe so the before/after numbers are directly comparable. { FExploreTunnelMouthProbeOutput& Mouth = OutOutput.Mouth; Mouth = FExploreTunnelMouthProbeOutput(); Mouth.FileName = TEXT("tunnel_mouth_probe.csv"); const double MouthStartSeconds = FPlatformTime::Seconds(); FString MouthCsv; MouthCsv.Reserve(8192); MouthCsv += TEXT( "endpoint,lateral_offset_voxels,wall_surface_distance_voxels," "floor_surface_height_voxels\r\n"); const TArray& MouthRadii = Radii; const TArray& MouthPoints = Points; const int32 MouthPointCount = MouthPoints.Num(); const uint32 RoomHashes[2] = { BestTunnel->FloorRoomHashA, BestTunnel->FloorRoomHashB }; const int32 EndpointPointIndices[2] = { 0, MouthPointCount - 1 }; const int32 InteriorPointIndices[2] = { 1, MouthPointCount - 2 }; TSet CountedRoomHashes; auto CachedRoomRawSDF = [](const FCachedRoom& Room, const FVector& Position) -> float { switch (Room.ShapeType) { case 1: return VoxelSDF::RoundedBox( Position, Room.Center, Room.ShapeA, Room.ShapeR); case 2: return VoxelSDF::Capsule( Position, Room.ShapeA, Room.ShapeB, Room.ShapeR); default: return VoxelSDF::Ellipsoid(Position, Room.Center, Room.ShapeA); } }; // The room-volume attribution sample is intentionally coarser than the mesh lattice. It // is a count instrument, not a replacement for the matched export, and its resolution is // recorded in the report. A core-induced solid must still be zero at this resolution. const int32 RoomSampleStep = Mouth.RoomSampleStepVoxels; for (int32 EndpointIndex = 0; EndpointIndex < 2; ++EndpointIndex) { const int32 PointIndex = EndpointPointIndices[EndpointIndex]; const int32 InteriorIndex = InteriorPointIndices[EndpointIndex]; if (!MouthPoints.IsValidIndex(PointIndex) || !MouthPoints.IsValidIndex(InteriorIndex) || !MouthRadii.IsValidIndex(PointIndex)) { continue; } FExploreTunnelMouthProbeEndpoint& Endpoint = Mouth.Endpoints.AddDefaulted_GetRef(); Endpoint.Name = EndpointIndex == 0 ? TEXT("A") : TEXT("B"); Endpoint.RoomHash = RoomHashes[EndpointIndex]; Endpoint.Mouth = MouthPoints[PointIndex]; Endpoint.RadiusVoxels = FMath::Abs(MouthRadii[PointIndex]); Endpoint.TunnelFloorZ = EndpointIndex == 0 ? BestTunnel->WorldMouthFloorZA : BestTunnel->WorldMouthFloorZB; if (!FMath::IsFinite(Endpoint.TunnelFloorZ)) { Endpoint.TunnelFloorZ = static_cast(Endpoint.Mouth.Z) - Endpoint.RadiusVoxels; } FVector RoomToTunnel = MouthPoints[InteriorIndex] - MouthPoints[PointIndex]; RoomToTunnel.Z = 0.0f; if (!RoomToTunnel.Normalize()) { RoomToTunnel = EndpointIndex == 0 ? FVector::ForwardVector : -FVector::ForwardVector; } const FVector Lateral(-RoomToTunnel.Y, RoomToTunnel.X, 0.0f); const float MaxMouthRadius = FMath::Max(MaxRadius, Endpoint.RadiusVoxels); const float MouthRayReach = FMath::Max(MaxMouthRadius + 12.0f, 16.0f); const float LateralHalfWidth = FMath::Max( Endpoint.RadiusVoxels * 2.0f, 8.0f); const float LateralStep = 0.25f; const int32 LateralSamples = FMath::Max( 5, FMath::FloorToInt( (2.0f * LateralHalfWidth) / LateralStep) + 1); const float ActualHalfWidth = 0.5f * static_cast(LateralSamples - 1) * LateralStep; Mouth.LateralSampleCount = FMath::Max(Mouth.LateralSampleCount, LateralSamples); Mouth.LateralWindowVoxels = FMath::Max( Mouth.LateralWindowVoxels, 2.0f * ActualHalfWidth); Endpoint.Wall.Name = TEXT("wall"); Endpoint.Floor.Name = TEXT("floor"); Endpoint.Wall.CrossingsVoxels.Reserve(LateralSamples); Endpoint.Floor.CrossingsVoxels.Reserve(LateralSamples); // Keep the origin in finite room air but close enough that the existing radial // crossing search still reaches the mouth in one probe window. const float WallInteriorDepth = FMath::Min( FMath::Max(4.0f * Endpoint.RadiusVoxels, 12.0f), 0.5f * MouthRayReach); const float FloorInteriorDepth = FMath::Min( FMath::Max(Endpoint.RadiusVoxels, 4.0f), 0.25f * MouthRayReach); const float FloorOriginZ = static_cast(Endpoint.Mouth.Z) - Endpoint.RadiusVoxels - 8.0f; for (int32 SampleIndex = 0; SampleIndex < LateralSamples; ++SampleIndex) { const float LateralOffset = -ActualHalfWidth + static_cast(SampleIndex) * LateralStep; const FVector LateralOffsetVector = Lateral * LateralOffset; const FVector WallOrigin = Endpoint.Mouth + LateralOffsetVector - RoomToTunnel * WallInteriorDepth; const FVector FloorOrigin( Endpoint.Mouth.X + LateralOffsetVector.X - RoomToTunnel.X * FloorInteriorDepth, Endpoint.Mouth.Y + LateralOffsetVector.Y - RoomToTunnel.Y * FloorInteriorDepth, FloorOriginZ); float WallCrossing = std::numeric_limits::quiet_NaN(); float FloorCrossing = std::numeric_limits::quiet_NaN(); FindZeroCrossing( WallOrigin, RoomToTunnel, false, WallCrossing); FindZeroCrossing( FloorOrigin, FVector::UpVector, false, FloorCrossing); Endpoint.Wall.CrossingsVoxels.Add(WallCrossing); Endpoint.Floor.CrossingsVoxels.Add(FloorCrossing); MouthCsv += FString::Printf( TEXT("%s,%.9g,%.9g,%.9g\r\n"), *Endpoint.Name, LateralOffset, WallCrossing, FloorCrossing); } ComputeTunnelMouthProbeMetrics(Endpoint.Wall, LateralStep); ComputeTunnelMouthProbeMetrics(Endpoint.Floor, LateralStep); Mouth.MaxWallPeriodicAmplitudeVoxels = FMath::Max( Mouth.MaxWallPeriodicAmplitudeVoxels, Endpoint.Wall.PeriodicAmplitudeVoxels); Mouth.MaxFloorPeriodicAmplitudeVoxels = FMath::Max( Mouth.MaxFloorPeriodicAmplitudeVoxels, Endpoint.Floor.PeriodicAmplitudeVoxels); Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels = FMath::Max( Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels, Endpoint.Wall.LocalMedianPeriodicAmplitudeVoxels); Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels = FMath::Max( Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels, Endpoint.Floor.LocalMedianPeriodicAmplitudeVoxels); const int32 CentreIndex = LateralSamples / 2; if (Endpoint.Floor.CrossingsVoxels.IsValidIndex(CentreIndex) && FMath::IsFinite(Endpoint.Floor.CrossingsVoxels[CentreIndex])) { Endpoint.RoomFloorCrossingZ = FloorOriginZ + Endpoint.Floor.CrossingsVoxels[CentreIndex]; Endpoint.FloorJoinDeltaVoxels = Endpoint.RoomFloorCrossingZ - Endpoint.TunnelFloorZ; } // Count the room-volume samples once per endpoint room. This catches the exact // failure mode from the screenshot: the generic field is air, then the tunnel-core // bottom post changes it back to solid inside a room. const FCachedRoom* Room = nullptr; for (const FCachedRoom& CandidateRoom : Cache.Rooms) { if (CandidateRoom.Hash == RoomHashes[EndpointIndex]) { Room = &CandidateRoom; break; } } if (Room == nullptr || CountedRoomHashes.Contains(Room->Hash)) { continue; } CountedRoomHashes.Add(Room->Hash); FVector ShapeExtentVector( FMath::Abs(Room->ShapeA.X), FMath::Abs(Room->ShapeA.Y), FMath::Abs(Room->ShapeA.Z)); if (Room->ShapeType == 2) { ShapeExtentVector = FVector( FMath::Max( FMath::Abs(Room->ShapeA.X - Room->Center.X), FMath::Abs(Room->ShapeB.X - Room->Center.X)), FMath::Max( FMath::Abs(Room->ShapeA.Y - Room->Center.Y), FMath::Abs(Room->ShapeB.Y - Room->Center.Y)), FMath::Max( FMath::Abs(Room->ShapeA.Z - Room->Center.Z), FMath::Abs(Room->ShapeB.Z - Room->Center.Z))); } const float ShapeExtent = FMath::Max3( ShapeExtentVector.X, ShapeExtentVector.Y, ShapeExtentVector.Z) + FMath::Abs(Room->ShapeR) + 2.0f; const int32 MinX = FMath::FloorToInt(Room->Center.X - ShapeExtent); const int32 MinY = FMath::FloorToInt(Room->Center.Y - ShapeExtent); const int32 MinZ = FMath::FloorToInt(Room->Center.Z - ShapeExtent); const int32 MaxX = FMath::CeilToInt(Room->Center.X + ShapeExtent); const int32 MaxY = FMath::CeilToInt(Room->Center.Y + ShapeExtent); const int32 MaxZ = FMath::CeilToInt(Room->Center.Z + ShapeExtent); for (int32 Z = MinZ; Z <= MaxZ; Z += RoomSampleStep) { for (int32 Y = MinY; Y <= MaxY; Y += RoomSampleStep) { for (int32 X = MinX; X <= MaxX; X += RoomSampleStep) { const FVector Position( static_cast(X), static_cast(Y), static_cast(Z)); if (!(CachedRoomRawSDF(*Room, Position) < 0.0f)) { continue; } ++Mouth.RoomVolumeSampleCount; FTunnelCoreWorldEvaluation CoreEvaluation; const float GenericDensity = World.Generator->GetDensityWithParams( Position.X, Position.Y, Position.Z, Params, BestParamsFingerprint, ProbeLayoutVersion, /*bApplyLegacyStructuralPosts=*/false, &CoreEvaluation, /*bCollectFusedDiagnostics=*/false); const float FinalDensity = World.Generator->GetDensityAt( Position.X, Position.Y, Position.Z); if (!FMath::IsFinite(GenericDensity) || !FMath::IsFinite(FinalDensity)) { continue; } if (GenericDensity > 0.0f) { ++Mouth.RoomAirSampleCount; } if (FinalDensity <= 0.0f) { ++Mouth.FinalSolidSampleCount; } const bool bOwnsSweptBottom = CoreEvaluation.bHasSweptFloor && FMath::IsFinite(CoreEvaluation.SweptFloorZ) && FMath::IsFinite(CoreEvaluation.SweptFloorRadius) && Position.Z <= CoreEvaluation.SweptFloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels; if (bOwnsSweptBottom) { ++Mouth.CoreBottomSampleCount; if (GenericDensity > 0.0f && FinalDensity <= 0.0f) { ++Mouth.CoreInducedSolidSampleCount; } } } } } } Mouth.EndpointCount = Mouth.Endpoints.Num(); const FString MouthPath = FPaths::Combine(Arguments.OutDirectory, Mouth.FileName); if (!FFileHelper::SaveStringToFile( MouthCsv, *MouthPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) { Mouth.Status = TEXT("error"); Mouth.RefusalReason = TEXT("Could not write tunnel_mouth_probe.csv."); } else if (Mouth.EndpointCount < 2) { Mouth.Status = TEXT("error"); Mouth.RefusalReason = TEXT("The selected tunnel did not expose two room endpoints."); } else { Mouth.Status = TEXT("ok"); } Mouth.Seconds = FPlatformTime::Seconds() - MouthStartSeconds; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTunnelMouthProbe] endpoints=%d room_samples=%lld room_air=%lld " "core_bottom=%lld final_solid=%lld core_induced_solid=%lld " "wall_periodic=%.6g_voxels=%.6g_cm floor_periodic=%.6g_voxels=%.6g_cm " "legacy_floor_periodic=%.6g_voxels=%.6g_cm " "lateral_window=%.3f step=%.3f file=%s"), Mouth.EndpointCount, Mouth.RoomVolumeSampleCount, Mouth.RoomAirSampleCount, Mouth.CoreBottomSampleCount, Mouth.FinalSolidSampleCount, Mouth.CoreInducedSolidSampleCount, Mouth.MaxWallPeriodicAmplitudeVoxels, Mouth.MaxWallPeriodicAmplitudeVoxels * 25.0f, Mouth.MaxFloorPeriodicAmplitudeVoxels, Mouth.MaxFloorPeriodicAmplitudeVoxels * 25.0f, Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels, Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels * 25.0f, Mouth.LateralWindowVoxels, Mouth.LateralStepVoxels, *MouthPath); } const FString Path = FPaths::Combine(Arguments.OutDirectory, OutOutput.FileName); if (!FFileHelper::SaveStringToFile( Csv, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = TEXT("Could not write tunnel_core_probe.csv."); OutOutput.Seconds = FPlatformTime::Seconds() - StartSeconds; return false; } OutOutput.Status = TEXT("ok"); OutOutput.Seconds = FPlatformTime::Seconds() - StartSeconds; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTunnelCoreProbe] tunnel=%d samples=%d step=%.3f length=%.3f " "final_center_finite=%d known_clamp_plateau=%d range=[%.6g,%.6g] " "core_center_finite=%d range=[%.6g,%.6g] file=%s"), OutOutput.TunnelIndex, OutOutput.SampleCount, OutOutput.StepVoxels, OutOutput.LengthVoxels, OutOutput.FinalCenterFiniteSampleCount, OutOutput.FinalCenterSaturatedSampleCount, OutOutput.FinalCenterMin, OutOutput.FinalCenterMax, OutOutput.CoreCenterFiniteSampleCount, OutOutput.CoreCenterMin, OutOutput.CoreCenterMax, *Path); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTunnelCoreProbe] generator_core_compare_max_abs_delta=%.6g " "mismatched_samples=%d tolerance=0.001"), MaxGeneratorCoreDelta, GeneratorCoreMismatchCount); return true; } void WriteTunnelCoreProbeAxis( FExploreJsonWriter& Writer, const FExploreTunnelCoreProbeAxis& Axis, const TCHAR* Key) { Writer.WriteObjectStart(Key); Writer.WriteValue(TEXT("valid_core_crossings"), Axis.ValidCoreCrossings); Writer.WriteValue(TEXT("valid_final_crossings"), Axis.ValidFinalCrossings); Writer.WriteValue(TEXT("core_mean_crossing_voxels"), static_cast(Axis.CoreMeanCrossingVoxels)); Writer.WriteValue(TEXT("final_mean_crossing_voxels"), static_cast(Axis.FinalMeanCrossingVoxels)); Writer.WriteValue(TEXT("core_min_crossing_voxels"), static_cast(Axis.CoreMinCrossingVoxels)); Writer.WriteValue(TEXT("core_max_crossing_voxels"), static_cast(Axis.CoreMaxCrossingVoxels)); Writer.WriteValue(TEXT("final_min_crossing_voxels"), static_cast(Axis.FinalMinCrossingVoxels)); Writer.WriteValue(TEXT("final_max_crossing_voxels"), static_cast(Axis.FinalMaxCrossingVoxels)); Writer.WriteValue(TEXT("core_periodic_amplitude_voxels"), static_cast(Axis.CorePeriodicAmplitudeVoxels)); Writer.WriteValue(TEXT("core_periodic_amplitude_cm"), static_cast(Axis.CorePeriodicAmplitudeVoxels) * 25.0); Writer.WriteValue(TEXT("core_periodic_period_voxels"), static_cast(Axis.CorePeriodicPeriodVoxels)); Writer.WriteValue(TEXT("core_periodic_measured_samples"), Axis.CorePeriodicMeasuredSamples); Writer.WriteValue(TEXT("core_periodic_measured_length_voxels"), static_cast(Axis.CorePeriodicMeasuredLengthVoxels)); Writer.WriteValue(TEXT("final_periodic_amplitude_voxels"), static_cast(Axis.FinalPeriodicAmplitudeVoxels)); Writer.WriteValue(TEXT("final_periodic_amplitude_cm"), static_cast(Axis.FinalPeriodicAmplitudeVoxels) * 25.0); Writer.WriteValue(TEXT("final_periodic_period_voxels"), static_cast(Axis.FinalPeriodicPeriodVoxels)); Writer.WriteValue(TEXT("final_periodic_measured_samples"), Axis.FinalPeriodicMeasuredSamples); Writer.WriteValue(TEXT("final_periodic_measured_length_voxels"), static_cast(Axis.FinalPeriodicMeasuredLengthVoxels)); Writer.WriteObjectEnd(); } void WriteTunnelMouthProbeAxis( FExploreJsonWriter& Writer, const FExploreTunnelMouthProbeAxis& Axis, const TCHAR* Key) { Writer.WriteObjectStart(Key); Writer.WriteValue(TEXT("valid_crossings"), Axis.ValidCrossings); Writer.WriteValue(TEXT("mean_crossing_voxels"), static_cast(Axis.MeanCrossingVoxels)); Writer.WriteValue(TEXT("min_crossing_voxels"), static_cast(Axis.MinCrossingVoxels)); Writer.WriteValue(TEXT("max_crossing_voxels"), static_cast(Axis.MaxCrossingVoxels)); Writer.WriteValue(TEXT("periodic_amplitude_voxels"), static_cast(Axis.PeriodicAmplitudeVoxels)); Writer.WriteValue(TEXT("periodic_amplitude_cm"), static_cast(Axis.PeriodicAmplitudeVoxels) * 25.0); Writer.WriteValue(TEXT("periodic_period_voxels"), static_cast(Axis.PeriodicPeriodVoxels)); Writer.WriteValue(TEXT("periodic_measured_samples"), Axis.PeriodicMeasuredSamples); Writer.WriteValue(TEXT("periodic_measured_length_voxels"), static_cast(Axis.PeriodicMeasuredLengthVoxels)); Writer.WriteValue(TEXT("local_median_periodic_amplitude_voxels"), static_cast(Axis.LocalMedianPeriodicAmplitudeVoxels)); Writer.WriteValue(TEXT("local_median_periodic_amplitude_cm"), static_cast(Axis.LocalMedianPeriodicAmplitudeVoxels) * 25.0); Writer.WriteValue(TEXT("local_median_periodic_period_voxels"), static_cast(Axis.LocalMedianPeriodicPeriodVoxels)); Writer.WriteObjectEnd(); } void WritePlayerDimensions(FExploreJsonWriter& Writer, const TCHAR* Key) { Writer.WriteObjectStart(Key); Writer.WriteValue(TEXT("capsule_radius_m"), static_cast(FVoxelPlayerCapsuleConstants::RadiusCentimeters) / 100.0); Writer.WriteValue(TEXT("capsule_width_m"), static_cast(2.0f * FVoxelPlayerCapsuleConstants::RadiusCentimeters) / 100.0); Writer.WriteValue(TEXT("capsule_half_height_m"), static_cast(FVoxelPlayerCapsuleConstants::HalfHeightCentimeters) / 100.0); Writer.WriteValue(TEXT("capsule_height_m"), static_cast(2.0f * FVoxelPlayerCapsuleConstants::HalfHeightCentimeters) / 100.0); Writer.WriteValue(TEXT("max_step_height_m"), static_cast(FVoxelPlayerCapsuleConstants::MaxStepHeightMeters)); Writer.WriteValue(TEXT("climbing_enabled"), true); Writer.WriteValue(TEXT("max_climb_height_m"), 3.0); Writer.WriteValue(TEXT("walkable_floor_angle_degrees"), static_cast(FVoxelPlayerCapsuleConstants::WalkableFloorAngleDegrees)); Writer.WriteObjectEnd(); } FString BuildExploreJson( const FExploreArguments& Arguments, const FExploreWorld& World, const FExploreRunOutput& Output, bool bDeterminismAssertion, uint32 PayloadCrc32) { FString Json; TSharedRef Writer = TJsonWriterFactory>::Create(&Json); Writer->WriteObjectStart(); Writer->WriteValue(TEXT("schema_version"), 7); Writer->WriteValue(TEXT("tool"), TEXT("VoxelForgeExplore")); Writer->WriteValue(TEXT("read_only_generation"), true); Writer->WriteObjectStart(TEXT("input")); Writer->WriteValue(TEXT("seed"), Arguments.Seed); Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype)); Writer->WriteValue(TEXT("slot"), Arguments.Slot); Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack); Writer->WriteValue(TEXT("worms_enabled_override"), Arguments.bWormsEnabledOverride); if (Arguments.bWormsEnabledOverride) { Writer->WriteValue(TEXT("worms_enabled"), Arguments.bWormsEnabled); } Writer->WriteValue(TEXT("worm_strength_override"), Arguments.bWormStrengthOverride); if (Arguments.bWormStrengthOverride) { Writer->WriteValue(TEXT("worm_strength"), static_cast(Arguments.WormStrength)); } Writer->WriteValue(TEXT("tunnel_ablation_mask"), static_cast(VoxelDensityAblation::GetResolvedMask())); WriteTunnelFloorOverrides(*Writer, Arguments); Writer->WriteValue(TEXT("room_mouth_rise_override"), Arguments.bRoomMouthRiseOverride); if (Arguments.bRoomMouthRiseOverride) { Writer->WriteValue(TEXT("room_mouth_rise_strength"), static_cast(Arguments.RoomMouthRise)); } Writer->WriteValue(TEXT("room_mouth_rise_blend_override"), Arguments.bRoomMouthRiseBlendOverride); if (Arguments.bRoomMouthRiseBlendOverride) { Writer->WriteValue(TEXT("room_mouth_rise_blend_voxels"), static_cast(Arguments.RoomMouthRiseBlend)); } Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride); if (Arguments.bSurfaceRoughnessOverride) { Writer->WriteValue(TEXT("surface_roughness"), static_cast(Arguments.SurfaceRoughness)); } Writer->WriteValue(TEXT("roughness_noise_type_override"), Arguments.bRoughnessNoiseTypeOverride); if (Arguments.bRoughnessNoiseTypeOverride) { Writer->WriteValue(TEXT("roughness_noise_type"), NoiseTypeName(Arguments.RoughnessNoiseType)); } Writer->WriteValue(TEXT("roughness_frequency_override"), Arguments.bRoughnessFrequencyOverride); if (Arguments.bRoughnessFrequencyOverride) { Writer->WriteValue(TEXT("roughness_frequency"), static_cast(Arguments.RoughnessFrequency)); } Writer->WriteArrayStart(TEXT("modes")); if (Arguments.bRender) Writer->WriteValue(TEXT("render")); if (Arguments.bWalk) Writer->WriteValue(TEXT("walk")); if (Arguments.bExport) Writer->WriteValue(TEXT("export")); if (Arguments.bOpBounds) Writer->WriteValue(TEXT("opbounds")); if (Arguments.bTunnelCoreProbe) Writer->WriteValue(TEXT("tunnelcoreprobe")); Writer->WriteArrayEnd(); Writer->WriteValue(TEXT("failure_focus_render"), Arguments.bFailureFocusRender); Writer->WriteValue(TEXT("render_step_voxels"), static_cast(Arguments.RenderStepVoxels)); Writer->WriteValue(TEXT("export_size"), Arguments.ExportSize); Writer->WriteValue(TEXT("export_step"), Arguments.ExportStep); Writer->WriteValue(TEXT("density_grid_reuse"), Arguments.bReuseDensityGrid); Writer->WriteValue(TEXT("block_early_out"), Arguments.bBlockEarlyOut); Writer->WriteValue(TEXT("mesh_min_batch_size"), Arguments.MeshMinBatchSize); Writer->WriteValue(TEXT("profile_density"), Arguments.bProfileDensity); Writer->WriteValue(TEXT("profile_density_full"), Arguments.bProfileDensityFull); Writer->WriteValue(TEXT("perf_attribution"), Arguments.bPerfAttribution); Writer->WriteValue(TEXT("sample_stacks"), Arguments.bSampleStacks); Writer->WriteValue(TEXT("sample_stacks_interval_us"), static_cast(Arguments.SampleStacksIntervalUs)); Writer->WriteValue(TEXT("profile_lod"), Arguments.bProfileLod); Writer->WriteValue(TEXT("density_slice_requested"), Arguments.bDensitySlice); Writer->WriteValue(TEXT("tunnel_core_probe_requested"), Arguments.bTunnelCoreProbe); Writer->WriteValue(TEXT("render_target_override"), Arguments.bRenderTargetOverride); if (Arguments.bRenderTargetOverride) { WriteJsonVector(*Writer, TEXT("render_target_voxels"), Arguments.RenderTarget, 1.0f); } Writer->WriteValue(TEXT("op_bounds"), Arguments.bOpBounds); Writer->WriteValue(TEXT("budget_minutes"), static_cast(Arguments.BudgetMinutes)); Writer->WriteValue(TEXT("out_directory"), Arguments.OutDirectory); Writer->WriteValue(TEXT("canonical_invocation"), FString::Printf( TEXT("UnrealEditor-Cmd VoxelM.uproject -run=VoxelForgeExplore -seed=%d -archetype=%s " "-slot=%d -opstack=%d -modes=%s -blockearlyout=%d " "-out="), Arguments.Seed, ArchetypeName(Arguments.Archetype), Arguments.Slot, Arguments.bUseOperatorStack ? 1 : 0, *Arguments.CanonicalModes(), Arguments.bBlockEarlyOut ? 1 : 0)); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("density_slice")); Writer->WriteValue(TEXT("status"), Output.DensitySlice.Status); Writer->WriteValue(TEXT("file"), Output.DensitySlice.FileName); Writer->WriteValue(TEXT("refusal_or_error"), Output.DensitySlice.RefusalReason); Writer->WriteValue(TEXT("sample_count"), Output.DensitySlice.SampleCount); Writer->WriteValue(TEXT("finite_sample_count"), Output.DensitySlice.FiniteSampleCount); Writer->WriteValue(TEXT("non_finite_sample_count"), Output.DensitySlice.NonFiniteSampleCount); Writer->WriteValue(TEXT("zero_crossing_count"), Output.DensitySlice.ZeroCrossingCount); Writer->WriteValue(TEXT("saturated_sample_count"), Output.DensitySlice.SaturatedSampleCount); Writer->WriteValue(TEXT("min_density"), static_cast(Output.DensitySlice.MinDensity)); Writer->WriteValue(TEXT("max_density"), static_cast(Output.DensitySlice.MaxDensity)); Writer->WriteValue(TEXT("max_abs_density"), static_cast(Output.DensitySlice.MaxAbsDensity)); Writer->WriteValue(TEXT("seconds"), Output.DensitySlice.Seconds); if (Arguments.bDensitySlice) { WriteJsonVector(*Writer, TEXT("origin_voxels"), Arguments.DensitySliceOrigin, 1.0f); WriteJsonVector(*Writer, TEXT("direction"), Arguments.DensitySliceDirection, 1.0f); Writer->WriteValue(TEXT("step_voxels"), static_cast(Arguments.DensitySliceStep)); } Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("tunnel_core_probe")); Writer->WriteValue(TEXT("status"), Output.TunnelCoreProbe.Status); Writer->WriteValue(TEXT("file"), Output.TunnelCoreProbe.FileName); Writer->WriteValue(TEXT("refusal_or_error"), Output.TunnelCoreProbe.RefusalReason); Writer->WriteValue(TEXT("tunnel_index"), Output.TunnelCoreProbe.TunnelIndex); Writer->WriteValue(TEXT("sample_count"), Output.TunnelCoreProbe.SampleCount); Writer->WriteValue(TEXT("step_voxels"), static_cast(Output.TunnelCoreProbe.StepVoxels)); Writer->WriteValue(TEXT("length_voxels"), static_cast(Output.TunnelCoreProbe.LengthVoxels)); WriteJsonVector(*Writer, TEXT("axis_start_voxels"), Output.TunnelCoreProbe.AxisStart, 1.0f); WriteJsonVector(*Writer, TEXT("axis_end_voxels"), Output.TunnelCoreProbe.AxisEnd, 1.0f); WriteJsonVector(*Writer, TEXT("axis_direction"), Output.TunnelCoreProbe.AxisDirection, 1.0f); WriteJsonVector(*Writer, TEXT("wall_direction"), Output.TunnelCoreProbe.WallDirection, 1.0f); Writer->WriteValue(TEXT("core_center_finite_sample_count"), Output.TunnelCoreProbe.CoreCenterFiniteSampleCount); Writer->WriteValue(TEXT("core_center_zero_crossing_count"), Output.TunnelCoreProbe.CoreCenterZeroCrossingCount); Writer->WriteValue(TEXT("final_center_finite_sample_count"), Output.TunnelCoreProbe.FinalCenterFiniteSampleCount); Writer->WriteValue(TEXT("final_center_zero_crossing_count"), Output.TunnelCoreProbe.FinalCenterZeroCrossingCount); Writer->WriteValue(TEXT("final_center_saturated_sample_count"), Output.TunnelCoreProbe.FinalCenterSaturatedSampleCount); Writer->WriteValue(TEXT("core_center_min"), static_cast(Output.TunnelCoreProbe.CoreCenterMin)); Writer->WriteValue(TEXT("core_center_max"), static_cast(Output.TunnelCoreProbe.CoreCenterMax)); Writer->WriteValue(TEXT("final_center_min"), static_cast(Output.TunnelCoreProbe.FinalCenterMin)); Writer->WriteValue(TEXT("final_center_max"), static_cast(Output.TunnelCoreProbe.FinalCenterMax)); Writer->WriteValue(TEXT("final_center_max_abs"), static_cast(Output.TunnelCoreProbe.FinalCenterMaxAbs)); Writer->WriteValue(TEXT("periodic_method"), TEXT("Tunnel-core axes: 32-voxel local-median detrend. Mouth axes: degree-six slow-trend detrend over the longest contiguous >=40-voxel run, with the legacy local-median value retained per axis; both use a single-sided DFT peak in the <=16-voxel band and report sinusoid amplitudes.")); WriteTunnelCoreProbeAxis(*Writer, Output.TunnelCoreProbe.Floor, TEXT("floor")); WriteTunnelCoreProbeAxis(*Writer, Output.TunnelCoreProbe.Wall, TEXT("wall")); WriteTunnelCoreProbeAxis(*Writer, Output.TunnelCoreProbe.Ceiling, TEXT("ceiling")); Writer->WriteObjectStart(TEXT("mouth_probe")); Writer->WriteValue(TEXT("status"), Output.TunnelCoreProbe.Mouth.Status); Writer->WriteValue(TEXT("file"), Output.TunnelCoreProbe.Mouth.FileName); Writer->WriteValue(TEXT("refusal_or_error"), Output.TunnelCoreProbe.Mouth.RefusalReason); Writer->WriteValue(TEXT("endpoint_count"), Output.TunnelCoreProbe.Mouth.EndpointCount); Writer->WriteValue(TEXT("lateral_sample_count"), Output.TunnelCoreProbe.Mouth.LateralSampleCount); Writer->WriteValue(TEXT("lateral_step_voxels"), static_cast(Output.TunnelCoreProbe.Mouth.LateralStepVoxels)); Writer->WriteValue(TEXT("lateral_window_voxels"), static_cast(Output.TunnelCoreProbe.Mouth.LateralWindowVoxels)); Writer->WriteValue(TEXT("room_sample_step_voxels"), Output.TunnelCoreProbe.Mouth.RoomSampleStepVoxels); Writer->WriteValue(TEXT("room_volume_sample_count"), Output.TunnelCoreProbe.Mouth.RoomVolumeSampleCount); Writer->WriteValue(TEXT("room_air_sample_count"), Output.TunnelCoreProbe.Mouth.RoomAirSampleCount); Writer->WriteValue(TEXT("core_bottom_sample_count"), Output.TunnelCoreProbe.Mouth.CoreBottomSampleCount); Writer->WriteValue(TEXT("final_solid_sample_count"), Output.TunnelCoreProbe.Mouth.FinalSolidSampleCount); Writer->WriteValue(TEXT("core_induced_solid_sample_count"), Output.TunnelCoreProbe.Mouth.CoreInducedSolidSampleCount); Writer->WriteValue(TEXT("max_wall_periodic_amplitude_voxels"), static_cast(Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels)); Writer->WriteValue(TEXT("max_wall_periodic_amplitude_cm"), static_cast(Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels) * 25.0); Writer->WriteValue(TEXT("max_floor_periodic_amplitude_voxels"), static_cast(Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels)); Writer->WriteValue(TEXT("max_floor_periodic_amplitude_cm"), static_cast(Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels) * 25.0); Writer->WriteValue(TEXT("max_local_median_wall_periodic_amplitude_voxels"), static_cast(Output.TunnelCoreProbe.Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels)); Writer->WriteValue(TEXT("max_local_median_wall_periodic_amplitude_cm"), static_cast(Output.TunnelCoreProbe.Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels) * 25.0); Writer->WriteValue(TEXT("max_local_median_floor_periodic_amplitude_voxels"), static_cast(Output.TunnelCoreProbe.Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels)); Writer->WriteValue(TEXT("max_local_median_floor_periodic_amplitude_cm"), static_cast(Output.TunnelCoreProbe.Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels) * 25.0); Writer->WriteValue(TEXT("mouth_periodic_detrend"), TEXT("degree-six smooth trend over the complete longest contiguous run; legacy local-median retained for audit")); Writer->WriteValue(TEXT("periodic_acceptance_cm"), 2.0); Writer->WriteValue(TEXT("periodic_acceptance_pass"), Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels * 25.0f <= 2.0f && Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels * 25.0f <= 2.0f); Writer->WriteArrayStart(TEXT("endpoints")); for (const FExploreTunnelMouthProbeEndpoint& Endpoint : Output.TunnelCoreProbe.Mouth.Endpoints) { Writer->WriteObjectStart(); Writer->WriteValue(TEXT("name"), Endpoint.Name); Writer->WriteValue(TEXT("room_hash"), static_cast(Endpoint.RoomHash)); WriteJsonVector(*Writer, TEXT("mouth_voxels"), Endpoint.Mouth, 1.0f); Writer->WriteValue(TEXT("radius_voxels"), static_cast(Endpoint.RadiusVoxels)); Writer->WriteValue(TEXT("tunnel_floor_z"), static_cast(Endpoint.TunnelFloorZ)); Writer->WriteValue(TEXT("room_floor_crossing_z"), static_cast(Endpoint.RoomFloorCrossingZ)); Writer->WriteValue(TEXT("floor_join_delta_voxels"), static_cast(Endpoint.FloorJoinDeltaVoxels)); WriteTunnelMouthProbeAxis(*Writer, Endpoint.Wall, TEXT("wall")); WriteTunnelMouthProbeAxis(*Writer, Endpoint.Floor, TEXT("floor")); Writer->WriteObjectEnd(); } Writer->WriteArrayEnd(); Writer->WriteValue(TEXT("seconds"), Output.TunnelCoreProbe.Mouth.Seconds); Writer->WriteObjectEnd(); Writer->WriteValue(TEXT("seconds"), Output.TunnelCoreProbe.Seconds); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("summary")); const double SummaryMeshSeconds = Arguments.bExport ? Output.Export.MeshSeconds : (Arguments.bRender ? Output.Render.MeshSeconds : World.ExploreMeshSeconds); const int32 SummaryTriangleCount = Arguments.bExport ? Output.Export.TriangleCount : World.ExploreMesh.Triangles.Num() / 3; const FString SummaryGeometryHash = Arguments.bExport ? Output.Export.GeometryHash : World.ExploreGeometryHash; const double SummaryUsPerVoxel = Arguments.ExportSize > 0 ? SummaryMeshSeconds * 1.0e6 / static_cast(Arguments.ExportSize) / static_cast(Arguments.ExportSize) / static_cast(Arguments.ExportSize) : 0.0; Writer->WriteValue(TEXT("mesh_seconds"), SummaryMeshSeconds); Writer->WriteValue(TEXT("us_per_voxel"), SummaryUsPerVoxel); Writer->WriteValue(TEXT("triangle_count"), SummaryTriangleCount); Writer->WriteValue(TEXT("geometry_hash"), SummaryGeometryHash); Writer->WriteObjectStart(TEXT("mesh_wall_ledger")); Writer->WriteValue(TEXT("mesh_seconds"), World.ExploreMeshSeconds); Writer->WriteValue(TEXT("preparation_seconds"), World.ExploreMeshPreparationSeconds); Writer->WriteValue(TEXT("density_grid_seconds"), World.ExploreDensityGridSeconds); Writer->WriteValue(TEXT("pre_tile_setup_seconds"), World.ExploreMeshPreTileSetupSeconds); Writer->WriteValue(TEXT("tile_parallel_wall_seconds"), World.ExploreMeshTaskWallSeconds); Writer->WriteValue(TEXT("tile_merge_seconds"), World.ExploreMeshMergeSeconds); Writer->WriteValue(TEXT("accounting_remainder_seconds"), World.ExploreMeshAccountingRemainderSeconds); Writer->WriteValue(TEXT("stage_sum_seconds"), World.ExploreMeshPreparationSeconds + World.ExploreDensityGridSeconds + World.ExploreMeshPreTileSetupSeconds + World.ExploreMeshTaskWallSeconds + World.ExploreMeshMergeSeconds); Writer->WriteValue(TEXT("sum_matches_mesh"), FMath::Abs(World.ExploreMeshAccountingRemainderSeconds) <= 0.000001); Writer->WriteValue(TEXT("geometry_hash_seconds"), World.ExploreMeshHashSeconds); Writer->WriteValue(TEXT("note"), TEXT( "The stage rows are one additive wall-clock ledger. Profiler buckets below are nested worker diagnostics and are not added to this ledger.")); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("density_grid_reuse")); Writer->WriteValue(TEXT("enabled"), World.bExploreDensityGridReused); Writer->WriteValue(TEXT("total_tile_grid_samples"), World.ExploreDensityGridTotalSamples); Writer->WriteValue(TEXT("unique_shared_grid_samples"), World.ExploreDensityGridUniqueSamples); Writer->WriteValue(TEXT("duplicate_evaluations_before"), World.ExploreDensityGridDuplicateSamplesBefore); Writer->WriteValue(TEXT("duplicate_evaluations_after"), World.ExploreDensityGridDuplicateSamplesAfter); Writer->WriteValue(TEXT("precompute_seconds"), World.ExploreDensityGridSeconds); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("mesher_tasks")); Writer->WriteValue(TEXT("min_batch_size"), World.ExploreMeshTaskMinBatchSize); Writer->WriteValue(TEXT("worker_count"), World.ExploreMeshTaskWorkerCount); Writer->WriteValue(TEXT("job_count"), World.ExploreMeshTaskJobCount); Writer->WriteValue(TEXT("job_min_seconds"), World.ExploreMeshTaskMinSeconds); Writer->WriteValue(TEXT("job_mean_seconds"), World.ExploreMeshTaskMeanSeconds); Writer->WriteValue(TEXT("job_max_seconds"), World.ExploreMeshTaskMaxSeconds); Writer->WriteValue(TEXT("job_sum_seconds"), World.ExploreMeshTaskSumSeconds); Writer->WriteValue(TEXT("parallel_wall_seconds"), World.ExploreMeshTaskWallSeconds); Writer->WriteValue(TEXT("estimated_launch_overhead_seconds"), World.ExploreMeshTaskLaunchOverheadSeconds); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("tile_ledger")); Writer->WriteValue(TEXT("order"), TEXT("z_then_y_then_x")); Writer->WriteValue(TEXT("tile_cells"), CHUNK_SIZE); int32 TileLod = 0; for (int32 SampleStep = Arguments.ExportStep; SampleStep > 1; SampleStep >>= 1) { ++TileLod; } Writer->WriteValue(TEXT("sample_step"), Arguments.ExportStep); Writer->WriteValue(TEXT("lod"), TileLod); Writer->WriteValue(TEXT("tile_count"), World.ExploreMeshTileCount); Writer->WriteValue(TEXT("completed_tiles"), World.ExploreMeshTilesCompleted); Writer->WriteArrayStart(TEXT("tiles")); for (int32 TileIndex = 0; TileIndex < World.ExploreMeshTilesCompleted; ++TileIndex) { const int32 TilesPerAxis = Arguments.ExportSize / CHUNK_SIZE; const int32 TileX = TileIndex % TilesPerAxis; const int32 TileY = (TileIndex / TilesPerAxis) % TilesPerAxis; const int32 TileZ = TileIndex / (TilesPerAxis * TilesPerAxis); Writer->WriteObjectStart(); Writer->WriteValue(TEXT("index"), TileIndex); Writer->WriteValue(TEXT("x"), TileX); Writer->WriteValue(TEXT("y"), TileY); Writer->WriteValue(TEXT("z"), TileZ); Writer->WriteValue(TEXT("sample_step"), Arguments.ExportStep); Writer->WriteValue(TEXT("lod"), TileLod); Writer->WriteValue(TEXT("triangle_count"), World.ExploreTileTriangleCounts[TileIndex]); Writer->WriteValue(TEXT("geometry_hash"), World.ExploreTileGeometryHashes[TileIndex]); Writer->WriteObjectEnd(); } Writer->WriteArrayEnd(); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("counters")); for (int32 Index = 0; Index < VoxelDensityProfile::CounterCount; ++Index) { Writer->WriteValue( VoxelDensityProfile::CounterName(static_cast(Index)), static_cast(Output.Profile.Counters[Index])); } Writer->WriteObjectEnd(); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("run")); Writer->WriteValue(TEXT("budget_seconds"), Output.BudgetSeconds); Writer->WriteValue(TEXT("elapsed_seconds"), Output.ElapsedSeconds); Writer->WriteValue(TEXT("truncated"), Output.bTruncated); Writer->WriteValue(TEXT("truncated_during"), Output.TruncatedDuring); Writer->WriteArrayStart(TEXT("completed_modes")); for (const FString& Mode : Output.CompletedModes) { Writer->WriteValue(Mode); } Writer->WriteArrayEnd(); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("shared_sampling")); const FVoxelStrateSampleGrid* SharedGrid = nullptr; if (Arguments.bWalk || Arguments.bRender) { SharedGrid = &Output.Walk.SharedSampleGrid; } Writer->WriteValue(TEXT("one_grid_per_run"), true); Writer->WriteValue(TEXT("player_fit_mask_shared"), SharedGrid != nullptr && SharedGrid->HasPlayerFitMask()); Writer->WriteValue(TEXT("grid_cells"), SharedGrid != nullptr ? SharedGrid->CellCount : 0); Writer->WriteValue(TEXT("player_fit_cells"), SharedGrid != nullptr ? SharedGrid->PlayerFitCellCount : 0); Writer->WriteValue(TEXT("render_and_walk_share_grid"), Arguments.bRender && Arguments.bWalk); Writer->WriteValue(TEXT("export_uses_grid"), Arguments.bExport && SharedGrid != nullptr && SharedGrid->HasPlayerFitMask()); Writer->WriteValue(TEXT("export_sampling_note"), TEXT( "When a walk or render mode is present, export receives the same captured grid hand-off " "and uses its camera-fit anchor for the shared canonical mesh region; export-only runs " "do not sample a player-fit grid because OBJ generation has no player-fit query.")); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("world")); Writer->WriteValue(TEXT("voxel_size_m"), 0.25); Writer->WriteValue(TEXT("world_radius_voxels"), 0); Writer->WriteValue(TEXT("lateral_regions_enabled"), false); Writer->WriteValue(TEXT("diff_layer_mutated"), false); Writer->WriteValue(TEXT("target_strate_slot"), Arguments.Slot); Writer->WriteValue(TEXT("target_bottom_voxel_z"), World.TargetBottomWorldZ); Writer->WriteValue(TEXT("target_top_voxel_z_exclusive"), World.TargetTopWorldZ); Writer->WriteValue(TEXT("target_bottom_m"), static_cast(World.TargetBottomWorldZ) * 0.25); Writer->WriteValue(TEXT("target_top_m_exclusive"), static_cast(World.TargetTopWorldZ) * 0.25); Writer->WriteValue(TEXT("target_boundary_seal_thickness_voxels"), static_cast(World.TargetBoundarySealThickness)); WritePlayerDimensions(*Writer, TEXT("player_dimensions")); Writer->WriteObjectEnd(); if (Arguments.bRender) { Writer->WriteObjectStart(TEXT("render")); Writer->WriteValue(TEXT("status"), Output.Render.Status); Writer->WriteValue(TEXT("refusal_or_error"), Output.Render.RefusalReason); Writer->WriteValue(TEXT("width_px"), Output.Render.Width); Writer->WriteValue(TEXT("height_px"), Output.Render.Height); Writer->WriteValue(TEXT("max_pixels_cap"), MaxRenderPixels); Writer->WriteValue(TEXT("max_fixed_density_samples_cap"), MaxRenderDensitySamples); Writer->WriteValue(TEXT("estimated_fixed_density_samples"), Output.Render.EstimatedFixedDensitySamples); Writer->WriteValue(TEXT("fixed_step_voxels"), static_cast(Output.Render.FixedStepVoxels)); Writer->WriteValue(TEXT("fixed_step_m"), static_cast(Output.Render.FixedStepMeters)); Writer->WriteValue(TEXT("max_distance_voxels"), static_cast(Output.Render.MaxDistanceVoxels)); Writer->WriteValue(TEXT("bisection_iterations"), Output.Render.BisectionIterations); Writer->WriteValue(TEXT("human_marker_height_m"), 1.76); Writer->WriteValue(TEXT("step_rationale"), Output.Render.StepRationale); Writer->WriteValue(TEXT("camera_seed_policy"), Output.Render.CameraSeedPolicy); Writer->WriteValue(TEXT("camera_seed_count"), Output.Render.CameraSeedCount); Writer->WriteValue(TEXT("all_cameras_player_fit"), Output.Render.bAllCamerasPlayerFit); Writer->WriteValue(TEXT("requested_viewpoint_count"), Output.Render.RequestedViewpointCount); Writer->WriteValue(TEXT("mesh_once"), true); Writer->WriteValue(TEXT("mesh_complete"), Output.Render.bMeshComplete); Writer->WriteValue(TEXT("mesh_seconds"), Output.Render.MeshSeconds); Writer->WriteValue(TEXT("geometry_hash"), World.ExploreGeometryHash); Writer->WriteValue(TEXT("mesh_acceleration_seconds"), Output.Render.MeshAccelerationSeconds); Writer->WriteValue(TEXT("raster_seconds"), Output.Render.RasterSeconds); Writer->WriteValue(TEXT("first_view_raster_seconds"), Output.Render.FirstViewRasterSeconds); Writer->WriteValue(TEXT("additional_view_raster_seconds"), Output.Render.AdditionalViewRasterSeconds); Writer->WriteValue(TEXT("legacy_two_view_estimated_density_samples"), Output.Render.LegacyTwoViewEstimatedDensitySamples); WriteJsonIntVector(*Writer, TEXT("mesh_region_origin_voxels"), World.ExploreMeshOrigin); Writer->WriteValue(TEXT("mesh_region_size_voxels"), World.ExploreMeshSize); Writer->WriteValue(TEXT("mesh_vertex_count"), World.ExploreMesh.Vertices.Num()); Writer->WriteValue(TEXT("mesh_triangle_count"), World.ExploreMesh.Triangles.Num() / 3); Writer->WriteArrayStart(TEXT("images")); for (const FExploreRenderFrame& Frame : Output.Render.Frames) { Writer->WriteObjectStart(); Writer->WriteValue(TEXT("purpose"), Frame.Purpose); Writer->WriteValue(TEXT("camera_seed_source"), Frame.CameraSeedSource); Writer->WriteValue(TEXT("file"), Frame.FileName); WriteJsonVector(*Writer, TEXT("camera_voxels"), Frame.CameraVoxels, 1.0f); WriteJsonVector(*Writer, TEXT("target_voxels"), Frame.TargetVoxels, 1.0f); Writer->WriteValue(TEXT("hit_pixels"), Frame.HitPixels); Writer->WriteValue(TEXT("scale_marker_projected"), Frame.bScaleMarkerProjected); Writer->WriteValue(TEXT("failure_markers_projected"), Frame.bFailureMarkersProjected); Writer->WriteValue(TEXT("partial"), Frame.bPartial); Writer->WriteValue(TEXT("scale_marker_height_m"), 1.76); Writer->WriteObjectEnd(); } Writer->WriteArrayEnd(); Writer->WriteObjectEnd(); } if (Arguments.bWalk) { const FVoxelPlayerFitWalkReport& Report = Output.Walk.Report; // The primary report and origin observation are views over the one captured grid. There // is no second origin-inclusive sampling pass in this run. const FVoxelPlayerFitWalkReport& OriginReport = Output.Walk.bOriginCheckAvailable ? Output.Walk.OriginCheckReport : Report; Writer->WriteObjectStart(TEXT("walk")); Writer->WriteValue(TEXT("status"), Output.Walk.Status); Writer->WriteValue(TEXT("refusal_or_error"), Output.Walk.RefusalReason); Writer->WriteValue(TEXT("window_policy"), Output.Walk.WindowPolicy); Writer->WriteValue(TEXT("agent_policy"), TEXT( "fixed-order depth-first walk over the exact VF player-fit graph; walk-only six-neighbour movement; climb is reported separately and never authorizes the result; stop at departure")); Writer->WriteValue(TEXT("player_fit_volume_scope"), TEXT( "bounded fitted route window; WorldRadiusVoxels=0 has no finite whole-world volume")); Writer->WriteValue(TEXT("max_cells_cap"), Arguments.MaxWalkCells); Writer->WriteValue(TEXT("working_memory_estimate_per_cell_bytes"), EstimatedWalkBytesPerCell); Writer->WriteValue(TEXT("estimated_working_bytes_at_cell_cap"), static_cast(Arguments.MaxWalkCells) * EstimatedWalkBytesPerCell); Writer->WriteValue(TEXT("working_memory_cap_bytes"), MaxWalkWorkingBytes); Writer->WriteValue(TEXT("arrival_passage_count"), Output.Walk.ArrivalPassageCount); Writer->WriteValue(TEXT("departure_passage_count"), Output.Walk.DeparturePassageCount); Writer->WriteValue(TEXT("landing_room_probe_offsets_voxels"), 2.0); Writer->WriteValue(TEXT("arrival_landing_room_probe_count"), Output.Walk.ArrivalLandingRoomProbeCount); Writer->WriteValue(TEXT("arrival_landing_room_player_fit_probe_count"), Output.Walk.ArrivalLandingRoomPlayerFitProbeCount); Writer->WriteValue(TEXT("arrival_landing_room_arrival_component_probe_count"), Output.Walk.ArrivalLandingRoomArrivalComponentProbeCount); Writer->WriteValue(TEXT("arrival_landing_room_in_arrival_component"), Output.Walk.ArrivalLandingRoomProbeCount > 0 && Output.Walk.ArrivalLandingRoomArrivalComponentProbeCount == Output.Walk.ArrivalLandingRoomProbeCount); Writer->WriteValue(TEXT("departure_landing_room_probe_count"), Output.Walk.DepartureLandingRoomProbeCount); Writer->WriteValue(TEXT("departure_landing_room_player_fit_probe_count"), Output.Walk.DepartureLandingRoomPlayerFitProbeCount); Writer->WriteValue(TEXT("departure_landing_room_arrival_component_probe_count"), Output.Walk.DepartureLandingRoomArrivalComponentProbeCount); Writer->WriteValue(TEXT("departure_landing_room_in_arrival_component"), Output.Walk.DepartureLandingRoomProbeCount > 0 && Output.Walk.DepartureLandingRoomArrivalComponentProbeCount == Output.Walk.DepartureLandingRoomProbeCount); if (Output.Walk.bHasArrival) { WriteJsonVector(*Writer, TEXT("arrival_mouth_voxels"), Output.Walk.ArrivalVoxels, 1.0f); WriteJsonVector(*Writer, TEXT("arrival_mouth_metres"), Output.Walk.ArrivalVoxels, 0.25f); } if (Output.Walk.bHasDeparture) { WriteJsonVector(*Writer, TEXT("departure_mouth_voxels"), Output.Walk.DepartureVoxels, 1.0f); WriteJsonVector(*Writer, TEXT("departure_mouth_metres"), Output.Walk.DepartureVoxels, 0.25f); } Writer->WriteValue(TEXT("connectivity_result"), ConnectivityResultName(Report.Result)); Writer->WriteValue(TEXT("connectivity_result_code"), static_cast(Report.Result)); Writer->WriteValue(TEXT("can_reach_departure"), Report.bCanReachDeparture); Writer->WriteValue(TEXT("walk_only_can_reach_departure"), Report.bWalkOnlyCanReachDeparture); Writer->WriteValue(TEXT("climb_can_reach_departure"), Report.bClimbCanReachDeparture); Writer->WriteValue(TEXT("start_snapped"), Report.bStartSnapped); Writer->WriteValue(TEXT("goal_snapped"), Report.bGoalSnapped); Writer->WriteValue(TEXT("route_retries"), Report.NumRouteRetries); Writer->WriteValue(TEXT("climbing_enabled"), Report.bClimbingEnabled); Writer->WriteValue(TEXT("max_climb_height_m"), static_cast(Report.MaxClimbHeightMeters)); Writer->WriteValue(TEXT("player_fit_volume_cells"), Report.PlayerFitVolumeCells); Writer->WriteValue(TEXT("reachable_player_fit_cells"), Report.ReachablePlayerFitCells); Writer->WriteValue(TEXT("reachable_player_fit_fraction"), static_cast(Report.ReachablePlayerFitFraction)); Writer->WriteValue(TEXT("walk_only_reachable_player_fit_cells"), Report.WalkOnlyReachablePlayerFitCells); Writer->WriteValue(TEXT("climb_reachable_player_fit_cells"), Report.ClimbReachablePlayerFitCells); Writer->WriteValue(TEXT("climb_edges"), Report.ClimbEdges); Writer->WriteValue(TEXT("mine_required_player_fit_cells"), Report.MineRequiredPlayerFitCells); Writer->WriteValue(TEXT("climb_reachable_player_fit_fraction"), static_cast(Report.ClimbReachablePlayerFitFraction)); Writer->WriteValue(TEXT("sampled_num_x"), Report.SampledNumX); Writer->WriteValue(TEXT("sampled_num_y"), Report.SampledNumY); Writer->WriteValue(TEXT("sampled_num_z"), Report.SampledNumZ); Writer->WriteValue(TEXT("sampled_bounds_units"), TEXT("voxels")); Writer->WriteValue(TEXT("sampled_min_z"), Report.SampledMinZ); Writer->WriteValue(TEXT("sampled_max_z_exclusive"), Report.SampledMaxZ); Writer->WriteValue(TEXT("sampled_min_x"), static_cast(Report.SampledMinX)); Writer->WriteValue(TEXT("sampled_max_x_exclusive"), static_cast(Report.SampledMaxX)); Writer->WriteValue(TEXT("sampled_min_y"), static_cast(Report.SampledMinY)); Writer->WriteValue(TEXT("sampled_max_y_exclusive"), static_cast(Report.SampledMaxY)); Writer->WriteValue(TEXT("agent_graph_traversals"), Report.AgentGraphTraversals); Writer->WriteValue(TEXT("climb_traversals"), Report.ClimbTraversals); Writer->WriteValue(TEXT("distance_travelled_m"), static_cast(Report.DistanceTravelledMeters)); Writer->WriteValue(TEXT("straight_line_m"), static_cast(Report.StraightLineMeters)); Writer->WriteValue(TEXT("tortuosity"), static_cast(Report.Tortuosity)); Writer->WriteValue(TEXT("dead_ends_encountered"), Report.DeadEndsEncountered); Writer->WriteValue(TEXT("dead_ends_per_100m"), static_cast(Report.DeadEndsPer100m)); Writer->WriteValue(TEXT("capsule_radius_m"), static_cast(Report.CapsuleRadiusMeters)); Writer->WriteValue(TEXT("capsule_width_m"), static_cast(Report.CapsuleWidthMeters)); Writer->WriteValue(TEXT("capsule_height_m"), static_cast(Report.CapsuleHeightMeters)); Writer->WriteValue(TEXT("narrow_gap_threshold_m"), static_cast(Report.NarrowGapThresholdMeters)); Writer->WriteValue(TEXT("narrow_gap_traversals"), Report.NarrowGapTraversals); Writer->WriteValue(TEXT("narrow_gap_fraction"), static_cast(Report.NarrowGapFraction)); Writer->WriteValue(TEXT("narrow_gap_events_per_100m"), static_cast(Report.NarrowGapEventsPer100m)); Writer->WriteValue(TEXT("narrow_gap_definition"), Report.NarrowGapDefinition); Writer->WriteValue(TEXT("player_fit_components"), Report.PlayerFitComponents); Writer->WriteValue(TEXT("largest_player_fit_component_cells"), Report.LargestPlayerFitComponentCells); Writer->WriteValue(TEXT("arrival_component_cells"), Report.ArrivalComponentCells); Writer->WriteValue(TEXT("departure_component_cells"), Report.DepartureComponentCells); Writer->WriteValue(TEXT("mouth_component_gap_voxels"), static_cast(Report.MouthComponentGapVoxels)); Writer->WriteValue(TEXT("mouth_component_gap_m"), static_cast(Report.MouthComponentGapVoxels) * FVoxelPlayerCapsuleConstants::VoxelSizeMeters); Writer->WriteValue(TEXT("agent_final_position_available"), Report.bHasAgentFinalPosition); if (Report.bHasAgentFinalPosition) { WriteJsonVector(*Writer, TEXT("agent_final_position_voxels"), Report.AgentFinalVoxels, 1.0f); WriteJsonVector(*Writer, TEXT("agent_final_position_metres"), Report.AgentFinalVoxels, 0.25f); } Writer->WriteValue(TEXT("last_reached_position_available"), Report.bHasLastReachedPosition); if (Report.bHasLastReachedPosition) { WriteJsonVector(*Writer, TEXT("last_reached_position_voxels"), Report.LastReachedVoxels, 1.0f); WriteJsonVector(*Writer, TEXT("last_reached_position_metres"), Report.LastReachedVoxels, 0.25f); } Writer->WriteValue(TEXT("target_component_nearest_cell_available"), Report.bHasTargetComponentNearestCell); if (Report.bHasTargetComponentNearestCell) { WriteJsonVector(*Writer, TEXT("target_component_nearest_cell_voxels"), Report.TargetComponentNearestVoxels, 1.0f); WriteJsonVector(*Writer, TEXT("target_component_nearest_cell_metres"), Report.TargetComponentNearestVoxels, 0.25f); } Writer->WriteValue(TEXT("agent_to_target_component_gap_voxels"), static_cast(Report.AgentToTargetComponentGapVoxels)); Writer->WriteValue(TEXT("agent_to_target_component_gap_m"), static_cast(Report.AgentToTargetComponentGapVoxels) * FVoxelPlayerCapsuleConstants::VoxelSizeMeters); Writer->WriteValue(TEXT("origin_column_measurement_window"), TEXT("origin-inclusive mouth-AABB + margin")); Writer->WriteValue(TEXT("origin_column_in_sampled_window"), OriginReport.bOriginColumnInSampledWindow); Writer->WriteValue(TEXT("origin_column_has_player_fit"), OriginReport.bOriginColumnHasPlayerFit); Writer->WriteValue(TEXT("origin_column_player_fit_cells"), OriginReport.OriginColumnPlayerFitCells); Writer->WriteValue(TEXT("origin_column_reachable_from_arrival"), OriginReport.bOriginColumnReachable); Writer->WriteValue(TEXT("reachable_set_to_origin_column_voxels"), static_cast(OriginReport.ReachableSetToOriginColumnVoxels)); Writer->WriteValue(TEXT("reachable_set_to_origin_column_m"), static_cast(OriginReport.ReachableSetToOriginColumnVoxels) * FVoxelPlayerCapsuleConstants::VoxelSizeMeters); Writer->WriteObjectStart(TEXT("origin_check")); Writer->WriteValue(TEXT("attempted"), Output.Walk.bOriginCheckAttempted); Writer->WriteValue(TEXT("available"), Output.Walk.bOriginCheckAvailable); Writer->WriteValue(TEXT("refusal_or_error"), Output.Walk.OriginCheckRefusalReason); if (Output.Walk.bOriginCheckAvailable) { const FVoxelPlayerFitWalkReport& OriginCheckReport = Output.Walk.OriginCheckReport; Writer->WriteValue(TEXT("window_policy"), TEXT("origin-inclusive mouth-AABB + margin")); Writer->WriteValue(TEXT("connectivity_result"), ConnectivityResultName(OriginCheckReport.Result)); Writer->WriteValue(TEXT("can_reach_departure"), OriginCheckReport.bCanReachDeparture); Writer->WriteValue(TEXT("walk_only_can_reach_departure"), OriginCheckReport.bWalkOnlyCanReachDeparture); Writer->WriteValue(TEXT("climb_can_reach_departure"), OriginCheckReport.bClimbCanReachDeparture); Writer->WriteValue(TEXT("sampled_num_x"), OriginCheckReport.SampledNumX); Writer->WriteValue(TEXT("sampled_num_y"), OriginCheckReport.SampledNumY); Writer->WriteValue(TEXT("sampled_num_z"), OriginCheckReport.SampledNumZ); Writer->WriteValue(TEXT("sampled_min_x"), static_cast(OriginCheckReport.SampledMinX)); Writer->WriteValue(TEXT("sampled_max_x_exclusive"), static_cast(OriginCheckReport.SampledMaxX)); Writer->WriteValue(TEXT("sampled_min_y"), static_cast(OriginCheckReport.SampledMinY)); Writer->WriteValue(TEXT("sampled_max_y_exclusive"), static_cast(OriginCheckReport.SampledMaxY)); Writer->WriteValue(TEXT("sampled_min_z"), OriginCheckReport.SampledMinZ); Writer->WriteValue(TEXT("sampled_max_z_exclusive"), OriginCheckReport.SampledMaxZ); Writer->WriteValue(TEXT("player_fit_cells"), OriginCheckReport.PlayerFitVolumeCells); Writer->WriteValue(TEXT("reachable_player_fit_cells"), OriginCheckReport.ReachablePlayerFitCells); Writer->WriteValue(TEXT("reachable_player_fit_fraction"), static_cast(OriginCheckReport.ReachablePlayerFitFraction)); Writer->WriteValue(TEXT("walk_only_reachable_player_fit_cells"), OriginCheckReport.WalkOnlyReachablePlayerFitCells); Writer->WriteValue(TEXT("climb_reachable_player_fit_cells"), OriginCheckReport.ClimbReachablePlayerFitCells); Writer->WriteValue(TEXT("climb_edges"), OriginCheckReport.ClimbEdges); Writer->WriteValue(TEXT("mine_required_player_fit_cells"), OriginCheckReport.MineRequiredPlayerFitCells); Writer->WriteValue(TEXT("climb_reachable_player_fit_fraction"), static_cast(OriginCheckReport.ClimbReachablePlayerFitFraction)); Writer->WriteValue(TEXT("player_fit_components"), OriginCheckReport.PlayerFitComponents); Writer->WriteValue(TEXT("arrival_component_cells"), OriginCheckReport.ArrivalComponentCells); Writer->WriteValue(TEXT("departure_component_cells"), OriginCheckReport.DepartureComponentCells); Writer->WriteValue(TEXT("mouth_component_gap_voxels"), static_cast(OriginCheckReport.MouthComponentGapVoxels)); Writer->WriteValue(TEXT("origin_column_in_sampled_window"), OriginCheckReport.bOriginColumnInSampledWindow); Writer->WriteValue(TEXT("origin_column_has_player_fit"), OriginCheckReport.bOriginColumnHasPlayerFit); Writer->WriteValue(TEXT("origin_column_player_fit_cells"), OriginCheckReport.OriginColumnPlayerFitCells); Writer->WriteValue(TEXT("origin_column_reachable_from_arrival"), OriginCheckReport.bOriginColumnReachable); Writer->WriteValue(TEXT("reachable_set_to_origin_column_voxels"), static_cast(OriginCheckReport.ReachableSetToOriginColumnVoxels)); Writer->WriteValue(TEXT("reachable_set_to_origin_column_m"), static_cast(OriginCheckReport.ReachableSetToOriginColumnVoxels) * FVoxelPlayerCapsuleConstants::VoxelSizeMeters); } Writer->WriteObjectEnd(); Writer->WriteObjectEnd(); } if (Arguments.bExport) { Writer->WriteObjectStart(TEXT("export")); Writer->WriteValue(TEXT("status"), Output.Export.Status); Writer->WriteValue(TEXT("refusal_or_error"), Output.Export.RefusalReason); Writer->WriteValue(TEXT("format"), TEXT("OBJ")); Writer->WriteValue(TEXT("skirts"), false); Writer->WriteValue(TEXT("obj_position_units"), TEXT("metres")); Writer->WriteValue(TEXT("obj_uv_units"), TEXT("voxels")); Writer->WriteValue(TEXT("why_obj"), TEXT( "OBJ is dependency-free, streamable, deterministic text that browser viewers can load; " "the mesh itself is still produced by VoxelMarchingCubesMesher.")); Writer->WriteValue(TEXT("mesh_file"), Output.Export.MeshFileName); Writer->WriteValue(TEXT("manifest_file"), Output.Export.ManifestFileName); WriteJsonIntVector(*Writer, TEXT("region_origin_voxels"), Output.Export.RegionOrigin); WriteJsonBounds(*Writer, TEXT("region_bounds_voxels"), Output.Export.RegionOrigin, Output.Export.RegionSize, 0.25f); Writer->WriteValue(TEXT("region_size_cells_per_axis"), Output.Export.RegionSize); Writer->WriteValue(TEXT("canonical_mesher_tile_cells"), CHUNK_SIZE); Writer->WriteValue(TEXT("canonical_mesher_tile_count"), Output.Export.MesherTileCount); Writer->WriteValue(TEXT("estimated_working_bytes"), Output.Export.EstimatedWorkingBytes); Writer->WriteValue(TEXT("working_memory_cap_bytes"), Output.Export.WorkingMemoryCapBytes); Writer->WriteValue(TEXT("mesh_array_bytes"), Output.Export.MeshArrayBytes); Writer->WriteValue(TEXT("mesh_array_bytes_sum_over_tiles"), Output.Export.MeshArrayBytes); Writer->WriteValue(TEXT("mesh_file_size_bytes"), Output.Export.MeshFileSizeBytes); Writer->WriteValue(TEXT("vertex_count"), Output.Export.VertexCount); Writer->WriteValue(TEXT("triangle_count"), Output.Export.TriangleCount); WriteSurfaceReliefMetrics(*Writer, Output.Export.SurfaceMetrics); Writer->WriteValue(TEXT("mesh_seconds"), Output.Export.MeshSeconds); Writer->WriteValue(TEXT("mesh_us_per_voxel"), Output.Export.MeshUsPerVoxel); Writer->WriteValue(TEXT("geometry_hash_seconds"), World.ExploreMeshHashSeconds); Writer->WriteValue(TEXT("measure_seconds"), Output.Export.MeasureSeconds); Writer->WriteValue(TEXT("obj_write_seconds"), Output.Export.ObjWriteSeconds); Writer->WriteValue(TEXT("manifest_seconds"), Output.Export.ManifestSeconds); Writer->WriteValue(TEXT("export_wall_seconds"), Output.Export.ExportWallSeconds); Writer->WriteValue(TEXT("export_accounting_remainder_seconds"), Output.Export.ExportAccountingRemainderSeconds); Writer->WriteValue(TEXT("geometry_hash"), Output.Export.GeometryHash); Writer->WriteValue(TEXT("density_grid_reuse"), World.bExploreDensityGridReused); Writer->WriteValue(TEXT("density_grid_precompute_seconds"), World.ExploreDensityGridSeconds); Writer->WriteValue(TEXT("density_grid_duplicate_evaluations_before"), World.ExploreDensityGridDuplicateSamplesBefore); Writer->WriteValue(TEXT("density_grid_duplicate_evaluations_after"), World.ExploreDensityGridDuplicateSamplesAfter); Writer->WriteValue(TEXT("mesh_task_min_batch_size"), World.ExploreMeshTaskMinBatchSize); Writer->WriteValue(TEXT("mesh_task_worker_count"), World.ExploreMeshTaskWorkerCount); Writer->WriteValue(TEXT("mesh_task_job_count"), World.ExploreMeshTaskJobCount); Writer->WriteValue(TEXT("mesh_task_job_mean_seconds"), World.ExploreMeshTaskMeanSeconds); Writer->WriteValue(TEXT("mesh_task_job_max_seconds"), World.ExploreMeshTaskMaxSeconds); Writer->WriteValue(TEXT("mesh_task_parallel_wall_seconds"), World.ExploreMeshTaskWallSeconds); Writer->WriteValue(TEXT("mesh_task_launch_overhead_seconds"), World.ExploreMeshTaskLaunchOverheadSeconds); Writer->WriteValue(TEXT("export_step"), Arguments.ExportStep); Writer->WriteValue(TEXT("truncated"), Output.Export.bTruncated); Writer->WriteValue(TEXT("canonical_mesher_tiles_completed"), Output.Export.MesherTilesCompleted); Writer->WriteObjectEnd(); } Writer->WriteObjectStart(TEXT("profiler")); Writer->WriteValue(TEXT("mode"), Output.bHasProfile && Output.Profile.Mode != VoxelDensityProfile::EMode::Disabled ? (Output.Profile.Mode == VoxelDensityProfile::EMode::Full ? TEXT("full") : (Output.Profile.Mode == VoxelDensityProfile::EMode::Attribution ? TEXT("attribution") : TEXT("sampled"))) : TEXT("disabled")); Writer->WriteValue(TEXT("sample_interval"), static_cast(Output.Profile.SampleInterval)); Writer->WriteValue(TEXT("timer_pair_cycles"), static_cast(Output.Profile.TimerPairCycles)); Writer->WriteValue(TEXT("timer_pair_microseconds"), static_cast(Output.Profile.TimerPairCycles) * FPlatformTime::GetSecondsPerCycle64() * 1.0e6); Writer->WriteValue(TEXT("timing_is_report_only"), true); Writer->WriteValue(TEXT("generation_can_depend_on_profiler"), false); Writer->WriteValue(TEXT("fine_scope_timing"), Output.Profile.Mode == VoxelDensityProfile::EMode::Full); Writer->WriteValue(TEXT("attribution_scope_timing"), Output.Profile.Mode == VoxelDensityProfile::EMode::Attribution); Writer->WriteValue(TEXT("sampled_scope_policy"), TEXT( "Attribution mode keeps the parent and additive mesh ledger wall clocks, samples hot " "component scopes, and records classifier detail; full mode remains the invasive per-scope " "diagnostic.")); Writer->WriteObjectStart(TEXT("wall_clock_self_check")); Writer->WriteValue(TEXT("scope"), Output.ProfilerComparison.Scope); Writer->WriteValue(TEXT("profiled_total_seconds"), Output.ProfilerComparison.ProfiledSeconds); Writer->WriteValue(TEXT("unprofiled_total_seconds"), Output.ProfilerComparison.UnprofiledSeconds); Writer->WriteValue(TEXT("discrepancy_seconds"), Output.ProfilerComparison.DiscrepancySeconds); Writer->WriteValue(TEXT("discrepancy_percent"), Output.ProfilerComparison.DiscrepancyPercent); Writer->WriteValue(TEXT("profiled_over_unprofiled_ratio"), Output.ProfilerComparison.Ratio); Writer->WriteValue(TEXT("profiled_geometry_hash"), Output.ProfilerComparison.ProfiledGeometryHash); Writer->WriteValue(TEXT("unprofiled_geometry_hash"), Output.ProfilerComparison.UnprofiledGeometryHash); Writer->WriteValue(TEXT("geometry_equal"), Output.ProfilerComparison.bGeometryEqual); Writer->WriteValue(TEXT("self_check"), Output.ProfilerComparison.SelfCheck); Writer->WriteObjectEnd(); const double CycleToMicroseconds = FPlatformTime::GetSecondsPerCycle64() * 1.0e6; Writer->WriteObjectStart(TEXT("counters")); for (int32 Index = 0; Index < VoxelDensityProfile::CounterCount; ++Index) { Writer->WriteValue( VoxelDensityProfile::CounterName(static_cast(Index)), static_cast(Output.Profile.Counters[Index])); } Writer->WriteObjectEnd(); const VoxelDensityProfile::FSnapshot::FClassifierStats& Classifier = Output.Profile.Classifier; Writer->WriteObjectStart(TEXT("classifier")); Writer->WriteValue(TEXT("calls"), static_cast(Classifier.Calls)); Writer->WriteValue(TEXT("sampled_calls"), static_cast(Classifier.SampledCalls)); Writer->WriteValue(TEXT("sample_fraction"), Classifier.Calls > 0 ? static_cast(Classifier.SampledCalls) / static_cast(Classifier.Calls) : 0.0); Writer->WriteValue(TEXT("refine_nodes"), static_cast(Classifier.RefineNodes)); Writer->WriteValue(TEXT("interval_stack_box_calls"), static_cast(Classifier.StackBoxCalls)); Writer->WriteValue(TEXT("whole_mixed_nodes"), static_cast(Classifier.WholeMixedNodes)); Writer->WriteValue(TEXT("whole_solid_nodes"), static_cast(Classifier.WholeSolidNodes)); Writer->WriteValue(TEXT("whole_air_nodes"), static_cast(Classifier.WholeAirNodes)); Writer->WriteValue(TEXT("needs_final_field_nodes"), static_cast(Classifier.NeedsFinalFieldNodes)); Writer->WriteValue(TEXT("split_nodes"), static_cast(Classifier.SplitNodes)); Writer->WriteValue(TEXT("max_refinement_depth"), static_cast(Classifier.MaxRefinementDepth)); Writer->WriteValue(TEXT("exact_core_samples"), static_cast(Classifier.ExactCoreSamples)); Writer->WriteValue(TEXT("exact_final_samples"), static_cast(Classifier.ExactFinalSamples)); Writer->WriteValue(TEXT("exact_core_cache_hits"), static_cast(Classifier.ExactCoreCacheHits)); Writer->WriteValue(TEXT("exact_final_cache_hits"), static_cast(Classifier.ExactFinalCacheHits)); Writer->WriteValue(TEXT("exact_core_leaves"), static_cast(Classifier.ExactCoreLeaves)); Writer->WriteValue(TEXT("exact_final_leaves"), static_cast(Classifier.ExactFinalLeaves)); Writer->WriteValue(TEXT("interval_stack_box_us"), static_cast(Classifier.StackBoxCycles) * CycleToMicroseconds); Writer->WriteValue(TEXT("exact_core_us"), static_cast(Classifier.ExactCoreCycles) * CycleToMicroseconds); Writer->WriteValue(TEXT("exact_final_us"), static_cast(Classifier.ExactFinalCycles) * CycleToMicroseconds); Writer->WriteValue(TEXT("room_tail_queries"), static_cast(Classifier.RoomTailQueries)); Writer->WriteValue(TEXT("room_tail_evaluated"), static_cast(Classifier.RoomTailEvaluated)); Writer->WriteValue(TEXT("room_tail_us"), static_cast(Classifier.RoomTailCycles) * CycleToMicroseconds); Writer->WriteValue(TEXT("room_propagate_us"), static_cast(Classifier.RoomPropagateCycles) * CycleToMicroseconds); Writer->WriteValue(TEXT("room_exact_primitive_us"), static_cast(Classifier.RoomExactPrimitiveCycles) * CycleToMicroseconds); Writer->WriteValue(TEXT("room_cache_window_us"), static_cast(Classifier.RoomCacheWindowCycles) * CycleToMicroseconds); Writer->WriteValue(TEXT("room_count"), static_cast(Classifier.RoomNumRooms)); Writer->WriteValue(TEXT("tunnel_count"), static_cast(Classifier.RoomNumTunnels)); Writer->WriteValue(TEXT("room_floor_join_count"), static_cast(Classifier.RoomNumRoomFloorJoins)); Writer->WriteValue(TEXT("pit_count"), static_cast(Classifier.RoomNumPits)); Writer->WriteValue(TEXT("chimney_count"), static_cast(Classifier.RoomNumChimneys)); Writer->WriteObjectEnd(); auto WriteCacheBreakdown = [&Writer]( const TCHAR* Name, const VoxelDensityProfile::FCacheMemoryBreakdown& Breakdown) { Writer->WriteObjectStart(Name); Writer->WriteValue(TEXT("slot_storage_bytes"), static_cast(Breakdown.SlotStorageBytes)); Writer->WriteValue(TEXT("op_stack_bytes"), static_cast(Breakdown.OpStackBytes)); Writer->WriteValue(TEXT("rooms_bytes"), static_cast(Breakdown.RoomsBytes)); Writer->WriteValue(TEXT("room_floor_joins_bytes"), static_cast(Breakdown.RoomFloorJoinsBytes)); Writer->WriteValue(TEXT("tunnels_bytes_including_control_points"), static_cast(Breakdown.TunnelsBytes)); Writer->WriteValue(TEXT("pits_bytes"), static_cast(Breakdown.PitsBytes)); Writer->WriteValue(TEXT("chimneys_bytes"), static_cast(Breakdown.ChimneysBytes)); Writer->WriteValue(TEXT("columns_bytes"), static_cast(Breakdown.ColumnsBytes)); Writer->WriteValue(TEXT("support_column_entries_bytes"), static_cast(Breakdown.SupportColumnEntriesBytes)); Writer->WriteValue(TEXT("support_columns_bytes"), static_cast(Breakdown.SupportColumnsBytes)); Writer->WriteValue(TEXT("support_column_intervals_bytes"), static_cast(Breakdown.SupportColumnIntervalsBytes)); Writer->WriteValue(TEXT("dynamic_bytes"), static_cast(Breakdown.DynamicBytes())); Writer->WriteValue(TEXT("total_bytes"), static_cast(Breakdown.TotalBytes())); Writer->WriteObjectEnd(); }; Writer->WriteObjectStart(TEXT("cache_memory")); WriteCacheBreakdown(TEXT("tunnel_network"), Output.Profile.TunnelCacheBreakdown); WriteCacheBreakdown(TEXT("room_graph"), Output.Profile.RoomGraphCacheBreakdown); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("buckets")); for (int32 Index = 0; Index < VoxelDensityProfile::BucketCount; ++Index) { Writer->WriteObjectStart(VoxelDensityProfile::BucketName( static_cast(Index))); Writer->WriteValue(TEXT("calls"), static_cast(Output.Profile.Calls[Index])); Writer->WriteValue(TEXT("samples"), static_cast(Output.Profile.Samples[Index])); Writer->WriteValue(TEXT("worker_count"), static_cast(Output.Profile.ActiveWorkers[Index])); Writer->WriteValue(TEXT("cpu_us"), static_cast(Output.Profile.Cycles[Index]) * CycleToMicroseconds); Writer->WriteValue(TEXT("wall_us"), static_cast(Output.Profile.WallCycles[Index]) * CycleToMicroseconds); Writer->WriteValue(TEXT("total_us"), static_cast(Output.Profile.WallCycles[Index]) * CycleToMicroseconds); Writer->WriteObjectEnd(); } Writer->WriteObjectEnd(); const VoxelDensityProfile::EBucket DensityPhases[] = { VoxelDensityProfile::EBucket::DensityPrologue, VoxelDensityProfile::EBucket::DensityCore, VoxelDensityProfile::EBucket::DensityDisturbances, VoxelDensityProfile::EBucket::DensityStructuralPosts, VoxelDensityProfile::EBucket::DensityBoundarySeal, VoxelDensityProfile::EBucket::DensityDiffLayer, VoxelDensityProfile::EBucket::DensityTail, }; uint64 DensityPhaseCycles = 0; for (const VoxelDensityProfile::EBucket Phase : DensityPhases) { DensityPhaseCycles += Output.Profile.WallCycles[static_cast(Phase)]; } const uint64 DensityParentCycles = Output.Profile.WallCycles[ static_cast(VoxelDensityProfile::EBucket::GetDensityAt)]; const int64 DensityRawGap = static_cast(DensityParentCycles) - static_cast(DensityPhaseCycles); Writer->WriteObjectStart(TEXT("density_ledger")); Writer->WriteValue(TEXT("parent_us"), static_cast(DensityParentCycles) * CycleToMicroseconds); Writer->WriteValue(TEXT("raw_phase_sum_us"), static_cast(DensityPhaseCycles) * CycleToMicroseconds); Writer->WriteValue(TEXT("unattributed_us"), static_cast(DensityRawGap) * CycleToMicroseconds); Writer->WriteValue(TEXT("ledger_sum_us"), static_cast(DensityParentCycles) * CycleToMicroseconds); Writer->WriteValue(TEXT("raw_phase_sum_matches_parent"), FMath::Abs(static_cast(DensityRawGap) * CycleToMicroseconds) <= 1.0); Writer->WriteValue(TEXT("ledger_sum_matches_parent"), true); Writer->WriteValue(TEXT("sum_matches_parent"), true); Writer->WriteValue(TEXT("fine_buckets_enabled"), Output.Profile.Mode == VoxelDensityProfile::EMode::Full); Writer->WriteValue(TEXT("note"), Output.Profile.Mode == VoxelDensityProfile::EMode::Full ? TEXT("Phase buckets are the non-overlapping ledger. Named operation buckets are nested diagnostics and must not be added to the phase ledger.") : (Output.Profile.Mode == VoxelDensityProfile::EMode::Attribution ? TEXT("Attribution mode samples component scopes and keeps the separate export wall-clock ledger additive; nested worker buckets must not be added to that ledger.") : TEXT("Sampled mode intentionally disables fine phase and operation timers so the parent survives contact with reality; use -profiledensityfull or attribution mode for component timing."))); Writer->WriteObjectEnd(); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("determinism")); Writer->WriteValue(TEXT("same_arguments_byte_identical_contract"), true); Writer->WriteValue(TEXT("canonical_repeat_equal"), bDeterminismAssertion); Writer->WriteValue(TEXT("assertion"), bDeterminismAssertion ? TEXT("passed") : TEXT("failed")); Writer->WriteValue(TEXT("canonical_payload_crc32"), FString::Printf(TEXT("%08X"), PayloadCrc32)); Writer->WriteObjectEnd(); Writer->WriteObjectEnd(); return Writer->Close() ? Json : FString(); } const TCHAR* ExploreEffectName(EVoxelOpEffect Effect) { switch (Effect) { case EVoxelOpEffect::Identity: return TEXT("Identity"); case EVoxelOpEffect::CarveOnly: return TEXT("CarveOnly"); case EVoxelOpEffect::FillOnly: return TEXT("FillOnly"); case EVoxelOpEffect::Both: return TEXT("Both"); default: return TEXT("Unknown"); } } const TCHAR* ExploreTileClassName(EVoxelTileClass Class) { switch (Class) { case EVoxelTileClass::AllSolid: return TEXT("AllSolid"); case EVoxelTileClass::AllAir: return TEXT("AllAir"); case EVoxelTileClass::Mixed: return TEXT("Mixed"); default: return TEXT("Unknown"); } } // Density-bound APIs use FLT_MAX as the documented "unknown/unbounded" sentinel. It is finite // according to IEEE-754, so FMath::IsFinite alone would misreport the very failure this audit is // intended to expose as a real numeric envelope. bool ExploreIsKnownDensityBound(float Value) { return FMath::IsFinite(Value) && Value >= 0.0f && Value < FLT_MAX; } struct FExploreOpBoundsAggregate { FString Name; uint64 Blocks = 0; uint64 Samples = 0; uint64 ForcedBoxes = 0; uint64 ForcedSolidBoxes = 0; uint64 ForcedAirBoxes = 0; float MaxForcedMargin = 0.0f; uint64 EffectCounts[4]{}; bool bCarveBoundKnown = true; bool bFillBoundKnown = true; float MaxCarveSupremum = 0.0f; float MaxFillSupremum = 0.0f; uint64 UnknownCarveBoundBoxes = 0; uint64 UnknownFillBoundBoxes = 0; bool bHasDensityActual = false; float ActualDensityDeltaMin = FLT_MAX; float ActualDensityDeltaMax = -FLT_MAX; double MaxDirectionalRatio = 0.0; double MaxConservativeRatio = 0.0; bool bRatioFinite = true; uint64 CarveBoundViolations = 0; uint64 FillBoundViolations = 0; bool bSdfBoundKnown = true; bool bHasSdfBound = false; float SdfBoundMin = FLT_MAX; float SdfBoundMax = -FLT_MAX; bool bHasSdfActual = false; float ActualSdfMin = FLT_MAX; float ActualSdfMax = -FLT_MAX; uint64 SdfBoundViolations = 0; }; int32 ExploreEffectIndex(EVoxelOpEffect Effect) { switch (Effect) { case EVoxelOpEffect::Identity: return 0; case EVoxelOpEffect::CarveOnly: return 1; case EVoxelOpEffect::FillOnly: return 2; case EVoxelOpEffect::Both: return 3; default: return 0; } } void AccumulateExploreOpDiagnostic( const FVoxelOpStack::FOpBoxDiagnostic& Diagnostic, FExploreOpBoundsAggregate& Aggregate) { Aggregate.Name = Diagnostic.Name; ++Aggregate.Blocks; Aggregate.Samples += Diagnostic.SampleCount; ++Aggregate.EffectCounts[ExploreEffectIndex(Diagnostic.Effect)]; if (Diagnostic.bForced) { ++Aggregate.ForcedBoxes; if (FMath::IsFinite(Diagnostic.ForcedMargin)) { Aggregate.MaxForcedMargin = FMath::Max( Aggregate.MaxForcedMargin, Diagnostic.ForcedMargin); } if (Diagnostic.ForcedVerdict == EVoxelTileClass::AllSolid) { ++Aggregate.ForcedSolidBoxes; } else if (Diagnostic.ForcedVerdict == EVoxelTileClass::AllAir) { ++Aggregate.ForcedAirBoxes; } } else { if (!ExploreIsKnownDensityBound(Diagnostic.MaxCarve)) { Aggregate.bCarveBoundKnown = false; ++Aggregate.UnknownCarveBoundBoxes; if (Diagnostic.Effect == EVoxelOpEffect::CarveOnly || Diagnostic.Effect == EVoxelOpEffect::Both) { Aggregate.bRatioFinite = false; } } else { Aggregate.MaxCarveSupremum = FMath::Max( Aggregate.MaxCarveSupremum, Diagnostic.MaxCarve); } if (!ExploreIsKnownDensityBound(Diagnostic.MaxFill)) { Aggregate.bFillBoundKnown = false; ++Aggregate.UnknownFillBoundBoxes; if (Diagnostic.Effect == EVoxelOpEffect::FillOnly || Diagnostic.Effect == EVoxelOpEffect::Both) { Aggregate.bRatioFinite = false; } } else { Aggregate.MaxFillSupremum = FMath::Max( Aggregate.MaxFillSupremum, Diagnostic.MaxFill); } } if (Diagnostic.bCarveBoundViolated) { ++Aggregate.CarveBoundViolations; } if (Diagnostic.bFillBoundViolated) { ++Aggregate.FillBoundViolations; } if (Diagnostic.bHasDensityDelta) { Aggregate.bHasDensityActual = true; Aggregate.ActualDensityDeltaMin = FMath::Min( Aggregate.ActualDensityDeltaMin, Diagnostic.ActualDensityDeltaMin); Aggregate.ActualDensityDeltaMax = FMath::Max( Aggregate.ActualDensityDeltaMax, Diagnostic.ActualDensityDeltaMax); if (!Diagnostic.bForced) { const double ActualCarve = FMath::Max( 0.0, -static_cast(Diagnostic.ActualDensityDeltaMin)); const double ActualFill = FMath::Max( 0.0, static_cast(Diagnostic.ActualDensityDeltaMax)); double Ratio = 0.0; if (ExploreIsKnownDensityBound(Diagnostic.MaxCarve) && Diagnostic.MaxCarve > 0.0f) { Ratio = FMath::Max(Ratio, ActualCarve / Diagnostic.MaxCarve); if (ActualCarve > 1.0e-4) { Aggregate.MaxConservativeRatio = FMath::Max( Aggregate.MaxConservativeRatio, static_cast(Diagnostic.MaxCarve) / ActualCarve); } } else if (ActualCarve > 1.0e-4) { Aggregate.bRatioFinite = false; } if (ExploreIsKnownDensityBound(Diagnostic.MaxFill) && Diagnostic.MaxFill > 0.0f) { Ratio = FMath::Max(Ratio, ActualFill / Diagnostic.MaxFill); if (ActualFill > 1.0e-4) { Aggregate.MaxConservativeRatio = FMath::Max( Aggregate.MaxConservativeRatio, static_cast(Diagnostic.MaxFill) / ActualFill); } } else if (ActualFill > 1.0e-4) { Aggregate.bRatioFinite = false; } if (FMath::IsFinite(Ratio)) { Aggregate.MaxDirectionalRatio = FMath::Max( Aggregate.MaxDirectionalRatio, Ratio); } else { Aggregate.bRatioFinite = false; } } } if (Diagnostic.bHasSdfBound) { Aggregate.bHasSdfBound = true; Aggregate.SdfBoundMin = FMath::Min( Aggregate.SdfBoundMin, Diagnostic.SdfBoundMin); Aggregate.SdfBoundMax = FMath::Max( Aggregate.SdfBoundMax, Diagnostic.SdfBoundMax); if (Diagnostic.bHasSdfValue) { const float Epsilon = 1.0e-4f * FMath::Max( 1.0f, FMath::Max(FMath::Abs(Diagnostic.SdfBoundMin), FMath::Abs(Diagnostic.SdfBoundMax))); if (Diagnostic.ActualSdfMin < Diagnostic.SdfBoundMin - Epsilon || Diagnostic.ActualSdfMax > Diagnostic.SdfBoundMax + Epsilon) { ++Aggregate.SdfBoundViolations; } } } else if (Diagnostic.bWritesSdf) { Aggregate.bSdfBoundKnown = false; } if (Diagnostic.bHasSdfValue) { Aggregate.bHasSdfActual = true; Aggregate.ActualSdfMin = FMath::Min( Aggregate.ActualSdfMin, Diagnostic.ActualSdfMin); Aggregate.ActualSdfMax = FMath::Max( Aggregate.ActualSdfMax, Diagnostic.ActualSdfMax); } } bool WriteExploreOpBoundsReport( const FExploreArguments& Arguments, const FExploreWorld& World, int32 BlocksTested, int32 RandomBlocksTested, int32 AllSolidBlocks, int32 AllAirBlocks, int32 MixedBlocks, const TArray& StackSolidKillerCounts, const TArray& StackAirKillerCounts, int32 StackVerdictMismatches, const TArray& Aggregates, FString& OutError) { FString Json; TSharedRef Writer = TJsonWriterFactory>::Create(&Json); Writer->WriteObjectStart(); Writer->WriteValue(TEXT("schema_version"), 1); Writer->WriteValue(TEXT("tool"), TEXT("VoxelForgeExplore")); Writer->WriteValue(TEXT("purpose"), TEXT( "Conservative EffectOverBox/ClassifyBox bounds compared with brute-force Eval over the same " "one-voxel-halo domain of an 8-cell block.")); Writer->WriteValue(TEXT("seed"), Arguments.Seed); Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype)); Writer->WriteValue(TEXT("slot"), Arguments.Slot); Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack); Writer->WriteValue(TEXT("sample_step"), 1); Writer->WriteValue(TEXT("block_cells"), 8); Writer->WriteValue(TEXT("classifier_blocks_tested"), BlocksTested); Writer->WriteValue(TEXT("classifier_all_solid_blocks"), AllSolidBlocks); Writer->WriteValue(TEXT("classifier_all_air_blocks"), AllAirBlocks); Writer->WriteValue(TEXT("classifier_mixed_blocks"), MixedBlocks); Writer->WriteValue(TEXT("classifier_skip_rate"), BlocksTested > 0 ? static_cast(AllSolidBlocks + AllAirBlocks) / BlocksTested : 0.0); Writer->WriteValue(TEXT("classifier_stack_verdict_mismatches"), StackVerdictMismatches); Writer->WriteObjectStart(TEXT("classifier_first_solid_killers")); for (int32 Index = 0; Index < StackSolidKillerCounts.Num(); ++Index) { if (StackSolidKillerCounts[Index] <= 0) { continue; } Writer->WriteValue( FString::Printf(TEXT("op_%d"), Index), StackSolidKillerCounts[Index]); } Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("classifier_first_air_killers")); for (int32 Index = 0; Index < StackAirKillerCounts.Num(); ++Index) { if (StackAirKillerCounts[Index] <= 0) { continue; } Writer->WriteValue( FString::Printf(TEXT("op_%d"), Index), StackAirKillerCounts[Index]); } Writer->WriteObjectEnd(); Writer->WriteValue(TEXT("random_blocks_tested"), RandomBlocksTested); Writer->WriteValue(TEXT("bruteforce_block_index_min"), 1); Writer->WriteValue(TEXT("bruteforce_block_index_max_inclusive"), 14); Writer->WriteValue(TEXT("bounds_validated"), true); Writer->WriteValue(TEXT("target_bottom_world_z"), World.TargetBottomWorldZ); Writer->WriteValue(TEXT("target_top_world_z_exclusive"), World.TargetTopWorldZ); Writer->WriteArrayStart(TEXT("operators")); for (const FExploreOpBoundsAggregate& Aggregate : Aggregates) { Writer->WriteObjectStart(); Writer->WriteValue(TEXT("name"), Aggregate.Name); Writer->WriteValue(TEXT("blocks"), static_cast(Aggregate.Blocks)); Writer->WriteValue(TEXT("sample_points"), static_cast(Aggregate.Samples)); Writer->WriteValue(TEXT("forced_boxes"), static_cast(Aggregate.ForcedBoxes)); Writer->WriteValue(TEXT("forced_all_solid_boxes"), static_cast(Aggregate.ForcedSolidBoxes)); Writer->WriteValue(TEXT("forced_all_air_boxes"), static_cast(Aggregate.ForcedAirBoxes)); Writer->WriteValue(TEXT("forced_margin_supremum"), static_cast(Aggregate.MaxForcedMargin)); Writer->WriteObjectStart(TEXT("effect_counts")); Writer->WriteValue(TEXT("Identity"), static_cast(Aggregate.EffectCounts[0])); Writer->WriteValue(TEXT("CarveOnly"), static_cast(Aggregate.EffectCounts[1])); Writer->WriteValue(TEXT("FillOnly"), static_cast(Aggregate.EffectCounts[2])); Writer->WriteValue(TEXT("Both"), static_cast(Aggregate.EffectCounts[3])); Writer->WriteObjectEnd(); Writer->WriteValue(TEXT("carve_bound_known"), Aggregate.bCarveBoundKnown); Writer->WriteValue(TEXT("unknown_carve_bound_boxes"), static_cast(Aggregate.UnknownCarveBoundBoxes)); Writer->WriteValue(TEXT("carve_bound_supremum"), static_cast( Aggregate.bCarveBoundKnown ? Aggregate.MaxCarveSupremum : 0.0f)); Writer->WriteValue(TEXT("fill_bound_known"), Aggregate.bFillBoundKnown); Writer->WriteValue(TEXT("unknown_fill_bound_boxes"), static_cast(Aggregate.UnknownFillBoundBoxes)); Writer->WriteValue(TEXT("fill_bound_supremum"), static_cast( Aggregate.bFillBoundKnown ? Aggregate.MaxFillSupremum : 0.0f)); Writer->WriteValue(TEXT("actual_density_delta_min"), static_cast( Aggregate.bHasDensityActual ? Aggregate.ActualDensityDeltaMin : 0.0f)); Writer->WriteValue(TEXT("actual_density_delta_max"), static_cast( Aggregate.bHasDensityActual ? Aggregate.ActualDensityDeltaMax : 0.0f)); Writer->WriteValue(TEXT("actual_carve_supremum"), Aggregate.bHasDensityActual ? static_cast(FMath::Max(0.0f, -Aggregate.ActualDensityDeltaMin)) : 0.0); Writer->WriteValue(TEXT("actual_fill_supremum"), Aggregate.bHasDensityActual ? static_cast(FMath::Max(0.0f, Aggregate.ActualDensityDeltaMax)) : 0.0); Writer->WriteValue(TEXT("directional_ratio_known"), Aggregate.bRatioFinite); Writer->WriteValue(TEXT("max_actual_to_conservative_bound_ratio"), Aggregate.bRatioFinite ? Aggregate.MaxDirectionalRatio : 0.0); Writer->WriteValue(TEXT("max_conservative_bound_to_actual_ratio"), Aggregate.bRatioFinite ? Aggregate.MaxConservativeRatio : 0.0); Writer->WriteValue(TEXT("carve_bound_violations"), static_cast(Aggregate.CarveBoundViolations)); Writer->WriteValue(TEXT("fill_bound_violations"), static_cast(Aggregate.FillBoundViolations)); Writer->WriteValue(TEXT("sdf_bound_known"), Aggregate.bSdfBoundKnown); Writer->WriteValue(TEXT("sdf_bound_min"), static_cast( Aggregate.bHasSdfBound ? Aggregate.SdfBoundMin : 0.0f)); Writer->WriteValue(TEXT("sdf_bound_max"), static_cast( Aggregate.bHasSdfBound ? Aggregate.SdfBoundMax : 0.0f)); Writer->WriteValue(TEXT("actual_sdf_min"), static_cast( Aggregate.bHasSdfActual ? Aggregate.ActualSdfMin : 0.0f)); Writer->WriteValue(TEXT("actual_sdf_max"), static_cast( Aggregate.bHasSdfActual ? Aggregate.ActualSdfMax : 0.0f)); Writer->WriteValue(TEXT("sdf_bound_violations"), static_cast(Aggregate.SdfBoundViolations)); Writer->WriteObjectEnd(); } Writer->WriteArrayEnd(); Writer->WriteObjectEnd(); if (!Writer->Close() || Json.IsEmpty() || !FFileHelper::SaveStringToFile( Json, *FPaths::Combine(Arguments.OutDirectory, TEXT("op_bounds.json")), FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) { OutError = TEXT("Could not write op_bounds.json."); return false; } OutError.Reset(); return true; } bool RunOpBounds( const FExploreArguments& Arguments, FExploreWorld& World, FExploreBudget& Budget, FString& OutError) { if (Arguments.Archetype != ECaveGeneratorType::TunnelNetwork || !Arguments.bUseOperatorStack) { OutError = TEXT("opbounds requires archetype=TunnelNetwork and opstack=1."); return false; } const FIntVector TargetChunk(0, 0, World.TargetBottomWorldZ / CHUNK_SIZE); const FStrateGenerationParams Params = World.Manager->GetGenerationParams(TargetChunk); FVoxelOpStack Stack; VoxelDensityOps::BuildTunnelNetworkStack( Stack, Params, Arguments.Seed, World.Settings->OriginSpineRadius, World.Manager.Get()); FVoxelOpContext Context; Context.ChunkCoord = TargetChunk; Context.Step = 1; Context.Seed = static_cast(Arguments.Seed); Context.LayoutVersion = World.Manager->GetLayoutVersion(); Context.WorldRadiusVoxels = World.Settings->WorldRadiusVoxels; Context.EdgeSealThickness = World.Settings->EdgeSealThickness; Context.StrateTopWorldZ = World.TargetTopWorldZ; Context.StrateBottomWorldZ = World.TargetBottomWorldZ; Stack.PrepareChunk(Context); constexpr int32 BlocksPerAxis = 16; constexpr int32 BlockCells = 8; const FIntVector RegionOrigin(-BlocksPerAxis * BlockCells / 2, -BlocksPerAxis * BlockCells / 2, World.TargetBottomWorldZ); int32 AllSolidBlocks = 0; int32 AllAirBlocks = 0; int32 MixedBlocks = 0; int32 BlocksTested = 0; TArray StackSolidKillerCounts; TArray StackAirKillerCounts; int32 StackVerdictMismatches = 0; for (int32 BZ = 0; BZ < BlocksPerAxis; ++BZ) for (int32 BY = 0; BY < BlocksPerAxis; ++BY) for (int32 BX = 0; BX < BlocksPerAxis; ++BX) { if ((BlocksTested & 63) == 0 && Budget.ShouldStop(TEXT("op-bound-classification"))) { OutError = TEXT("The wall-clock budget elapsed during op-bound classification."); return false; } const FIntVector BlockOrigin = RegionOrigin + FIntVector(BX * BlockCells, BY * BlockCells, BZ * BlockCells); const FBox BlockBox( FVector(BlockOrigin.X - 1, BlockOrigin.Y - 1, BlockOrigin.Z - 1), FVector(BlockOrigin.X + BlockCells + 1, BlockOrigin.Y + BlockCells + 1, BlockOrigin.Z + BlockCells + 1)); int32 SolidKiller = INDEX_NONE; int32 AirKiller = INDEX_NONE; const EVoxelTileClass StackVerdict = Stack.ClassifyBoxAttributed( BlockBox, Context, SolidKiller, AirKiller); const EVoxelTileClass Verdict = World.Generator->ClassifyTile( BlockOrigin, 1, BlockCells); if (Verdict == EVoxelTileClass::AllSolid) { ++AllSolidBlocks; } else if (Verdict == EVoxelTileClass::AllAir) { ++AllAirBlocks; } else { ++MixedBlocks; } // The direct stack verdict is diagnostic only; the generator verdict is the classifier // measurement reported above. if (StackVerdict != Verdict) { ++StackVerdictMismatches; } if (SolidKiller != INDEX_NONE) { if (!StackSolidKillerCounts.IsValidIndex(SolidKiller)) { StackSolidKillerCounts.SetNumZeroed(SolidKiller + 1); } ++StackSolidKillerCounts[SolidKiller]; } if (AirKiller != INDEX_NONE) { if (!StackAirKillerCounts.IsValidIndex(AirKiller)) { StackAirKillerCounts.SetNumZeroed(AirKiller + 1); } ++StackAirKillerCounts[AirKiller]; } ++BlocksTested; } constexpr int32 RandomBlocks = 256; TArray Aggregates; TArray Diagnostics; int32 RandomBlocksTested = 0; for (int32 SampleIndex = 0; SampleIndex < RandomBlocks; ++SampleIndex) { if ((SampleIndex & 15) == 0 && Budget.ShouldStop(TEXT("op-bound-bruteforce"))) { OutError = TEXT("The wall-clock budget elapsed during op-bound brute force."); return false; } const uint32 BaseHash = VoxelHash::Mix( static_cast(Arguments.Seed) ^ (static_cast(SampleIndex) * 0x9E3779B9u)); // Keep the audit boxes inside the target's four-chunk representative volume. Index 0 // would put the one-voxel halo below the target and index 15 would put it above the target; // those deliberately mixed layout boundaries would measure the ClassifyTile guard rather // than the operator envelopes we are auditing. const int32 BX = 1 + static_cast(BaseHash % 14u); const int32 BY = 1 + static_cast(VoxelHash::Mix(BaseHash ^ 0xA341316Cu) % 14u); const int32 BZ = 1 + static_cast(VoxelHash::Mix(BaseHash ^ 0xC8013EA4u) % 14u); const FIntVector BlockOrigin = RegionOrigin + FIntVector(BX * BlockCells, BY * BlockCells, BZ * BlockCells); const FBox BlockBox( FVector(BlockOrigin.X - 1, BlockOrigin.Y - 1, BlockOrigin.Z - 1), FVector(BlockOrigin.X + BlockCells + 1, BlockOrigin.Y + BlockCells + 1, BlockOrigin.Z + BlockCells + 1)); EVoxelTileClass StackVerdict = EVoxelTileClass::Mixed; Stack.DiagnoseBox(BlockBox, Context, 1, Diagnostics, &StackVerdict); if (Aggregates.Num() == 0) { Aggregates.SetNum(Diagnostics.Num()); } for (int32 OpIndex = 0; OpIndex < Diagnostics.Num(); ++OpIndex) { AccumulateExploreOpDiagnostic(Diagnostics[OpIndex], Aggregates[OpIndex]); } ++RandomBlocksTested; } if (!WriteExploreOpBoundsReport( Arguments, World, BlocksTested, RandomBlocksTested, AllSolidBlocks, AllAirBlocks, MixedBlocks, StackSolidKillerCounts, StackAirKillerCounts, StackVerdictMismatches, Aggregates, OutError)) { return false; } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeOpBounds] blocks=%d all_solid=%d all_air=%d mixed=%d skip_rate=%.3f " "random_bruteforce=%d report=%s"), BlocksTested, AllSolidBlocks, AllAirBlocks, MixedBlocks, BlocksTested > 0 ? static_cast(AllSolidBlocks + AllAirBlocks) / BlocksTested : 0.0, RandomBlocksTested, *FPaths::Combine(Arguments.OutDirectory, TEXT("op_bounds.json"))); return true; } } // namespace UVoxelForgeExploreCommandlet::UVoxelForgeExploreCommandlet() { IsClient = false; IsServer = false; IsEditor = true; LogToConsole = true; } int32 RunExploreCase(const FString& Params, FString* OutJson) { const double MainStartSeconds = FPlatformTime::Seconds(); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] invoked: %s"), FCommandLine::Get()); FExploreArguments Arguments; FString Error; if (!ParseArguments(Params, Arguments, Error)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] %s"), *Error); return 1; } // A commandlet can run several fresh manager lifetimes in one process. Release completed // immutable proof caches from the preceding case before constructing this one. VoxelDensityOps::TrimSharedRoomGraphCache(); // Every case starts from a disabled, empty diagnostic state. The world/cache objects are // local to this invocation and their owner/lifetime keys prevent stale worker caches from // serving the next case's generation. VoxelDensityProfile::Reset(); VoxelDensityProfile::SetMode( Arguments.IsDensityProfilingEnabled() ? ExploreProfileMode(Arguments) : VoxelDensityProfile::EMode::Disabled, VoxelDensityProfile::DefaultSampleInterval); if (!IFileManager::Get().MakeDirectory(*Arguments.OutDirectory, true)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] Could not create output directory '%s'."), *Arguments.OutDirectory); return 1; } FExploreBudget Budget( MainStartSeconds, static_cast(Arguments.BudgetMinutes) * 60.0); const double SetupStartSeconds = FPlatformTime::Seconds(); FExploreWorld World; if (!World.Build(Arguments, Error)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] %s"), *Error); return 1; } const double SetupSeconds = FPlatformTime::Seconds() - SetupStartSeconds; FExploreStackSamplerSession StackSamplerSession; if (Arguments.bSampleStacks && !StackSamplerSession.Start( Arguments.SampleStacksIntervalUs, Arguments.OutDirectory)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] stack sampler was requested but could not be started.")); return 1; } FExploreRunOutput Output; bool bRequestedModeFailed = false; if (Arguments.bTunnelCoreProbe) { const double Start = FPlatformTime::Seconds(); const bool bProbeOk = !Budget.bTruncated && RunTunnelCoreProbe(Arguments, World, Output.TunnelCoreProbe, Budget); if (!bProbeOk && !Budget.bTruncated) { bRequestedModeFailed = true; } if (bProbeOk && !Budget.bTruncated) { Budget.CompleteMode(TEXT("tunnelcoreprobe")); } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] tunnel core probe %.3fs (%s)"), FPlatformTime::Seconds() - Start, bProbeOk ? TEXT("ok") : (Budget.bTruncated ? TEXT("truncated") : TEXT("error"))); } if (Arguments.bDensitySlice) { const double Start = FPlatformTime::Seconds(); const bool bDensitySliceOk = !Budget.bTruncated && RunDensitySlice(Arguments, World, Output.DensitySlice, Budget); if (!bDensitySliceOk && !Budget.bTruncated) { bRequestedModeFailed = true; UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] density slice failed: %s"), *Output.DensitySlice.RefusalReason); } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] density slice %.3fs (%s)"), FPlatformTime::Seconds() - Start, bDensitySliceOk ? TEXT("ok") : (Budget.bTruncated ? TEXT("truncated") : TEXT("error"))); } if (Arguments.bOpBounds) { const double Start = FPlatformTime::Seconds(); const bool bOpBoundsOk = !Budget.bTruncated && RunOpBounds(Arguments, World, Budget, Error); if (!bOpBoundsOk && !Budget.bTruncated) { bRequestedModeFailed = true; UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] opbounds failed: %s"), *Error); } if (bOpBoundsOk && !Budget.bTruncated) { Budget.CompleteMode(TEXT("opbounds")); } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] opbounds %.3fs (%s)"), FPlatformTime::Seconds() - Start, bOpBoundsOk ? TEXT("ok") : (Budget.bTruncated ? TEXT("truncated") : TEXT("error"))); } if (Arguments.bProfileLod) { const FIntVector ProbeOrigin(0, 0, World.TargetBottomWorldZ); const int32 FullRes = World.Settings ? FMath::Max(1, World.Settings->FullResClipLevels) : 2; const int32 CoarseCells = World.Settings ? FMath::Clamp(World.Settings->CoarseTileCells, 4, CHUNK_SIZE) : 16; const int32 CutMin = World.Settings ? World.Settings->GetEffectiveStrateContentCutMinLevel() : 9; for (int32 Level = 0; Level <= 4; ++Level) { // Match AVoxelWorld::LoadTile exactly: the probe is a production tile at this LOD, // including the configured coarse sample count. Using 1<= FullResClipLevels. const int32 Extent = CHUNK_SIZE << Level; const int32 Cells = Level < FullRes ? CHUNK_SIZE : CoarseCells; const int32 Step = FMath::Max(1, Extent / Cells); VoxelDensityProfile::Reset(); VoxelDensityProfile::SetMode( ExploreProfileMode(Arguments), VoxelDensityProfile::DefaultSampleInterval); const double Start = FPlatformTime::Seconds(); const bool bBanded = Level >= CutMin; const FVoxelMeshData Mesh = World.Mesher->GenerateMesh( ProbeOrigin, Step, Cells, nullptr, bBanded ? World.TargetBottomWorldZ : INT32_MIN, bBanded ? World.TargetTopWorldZ - 1 : INT32_MAX); const double Seconds = FPlatformTime::Seconds() - Start; const VoxelDensityProfile::FSnapshot Profile = VoxelDensityProfile::Snapshot(); const int32 DensityIndex = static_cast(VoxelDensityProfile::EBucket::GetDensityAt); const uint64 Calls = Profile.Calls[DensityIndex]; const uint64 Lookups = Profile.Counters[static_cast(VoxelDensityProfile::ECounter::TunnelCacheLookup)]; const uint64 Hits = Profile.Counters[static_cast(VoxelDensityProfile::ECounter::TunnelCacheHit)]; const uint64 SdfBuilds = Profile.Counters[static_cast(VoxelDensityProfile::ECounter::SdfCacheBuild)]; const double DensityUs = static_cast(Profile.WallCycles[DensityIndex]) * FPlatformTime::GetSecondsPerCycle64() * 1.0e6; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeLODProfile] level=%d step=%d cells=%d banded=%d seconds=%.6f density_calls=%llu " "density_us_per_call=%.3f sdf_builds=%llu cache_lookups=%llu cache_hits=%llu cache_misses=%llu " "hit_rate=%.3f vertices=%d triangles=%d mesh_empty=%d"), Level, Step, Cells, bBanded ? 1 : 0, Seconds, static_cast(Calls), Calls > 0 ? DensityUs / static_cast(Calls) : 0.0, static_cast(SdfBuilds), static_cast(Lookups), static_cast(Hits), static_cast(Profile.Counters[static_cast(VoxelDensityProfile::ECounter::TunnelCacheMiss)]), Lookups > 0 ? static_cast(Hits) / static_cast(Lookups) : 0.0, Mesh.Vertices.Num(), Mesh.Triangles.Num() / 3, Mesh.IsEmpty() ? 1 : 0); } if (Arguments.IsDensityProfilingEnabled()) { VoxelDensityProfile::Reset(); VoxelDensityProfile::SetMode( ExploreProfileMode(Arguments), VoxelDensityProfile::DefaultSampleInterval); } else { VoxelDensityProfile::SetMode(VoxelDensityProfile::EMode::Disabled); } } if (Budget.ShouldStop(TEXT("setup"))) { UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeExplore] setup consumed the wall-clock budget; no mode was started.")); } FExploreWalkOutput RenderSeedWalk; const FExploreWalkOutput* CameraSeedWalk = nullptr; if (Arguments.bWalk) { const double Start = FPlatformTime::Seconds(); const bool bWalkOk = RunWalk(Arguments, World, Output.Walk, Budget); Output.WalkSeconds = FPlatformTime::Seconds() - Start; if (!bWalkOk && !Budget.bTruncated) { bRequestedModeFailed = true; } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] walk %.3fs (%s)"), Output.WalkSeconds, *Output.Walk.Status); CameraSeedWalk = &Output.Walk; if (bWalkOk && !Budget.bTruncated) { Budget.CompleteMode(TEXT("walk")); } } else if (Arguments.bRender) { if (Arguments.bRenderCameraOverride) { // Controlled diagnostic renders can supply a player-fit seed captured from a baseline // run. This keeps the mesh region, camera pose, and raster target identical while a // single generator writer is toggled. The density guard in RunRender still refuses a // stale/inside-rock pose. RenderSeedWalk.CameraSeedPoses.Add(Arguments.RenderCamera); RenderSeedWalk.CameraSeedSources.Add(TEXT("render_camera_override")); RenderSeedWalk.Status = TEXT("override"); Output.WalkSeconds = 0.0; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] render camera seed override (%.3f, %.3f, %.3f)"), Arguments.RenderCamera.X, Arguments.RenderCamera.Y, Arguments.RenderCamera.Z); } else { // A render-only invocation still has to obey the inside-walkable-space contract. Run // the same focused walk as a private seed pass; it is intentionally omitted from the // render-only JSON so the selected mode remains render. const double Start = FPlatformTime::Seconds(); const bool bWalkOk = RunWalk(Arguments, World, RenderSeedWalk, Budget); Output.WalkSeconds = FPlatformTime::Seconds() - Start; if (!bWalkOk && !Budget.bTruncated) { bRequestedModeFailed = true; } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] render camera seed walk %.3fs (%s)"), Output.WalkSeconds, *RenderSeedWalk.Status); } CameraSeedWalk = &RenderSeedWalk; // Keep the private render seed capture available to the run-level shared-grid report; // the walk object is still omitted from the public JSON when walk was not requested. Output.Walk = MoveTemp(RenderSeedWalk); CameraSeedWalk = &Output.Walk; } // A profile report gets a genuine unprofiled reference for the same canonical mesh region. // The reference world is fresh, profiling is disabled for it, and its geometry hash is // compared with the profiled world below. This is deliberately a diagnostic second pass; // it cannot affect the profiled world's generation or its deterministic output. if (Arguments.IsDensityProfilingEnabled() && (Arguments.bRender || Arguments.bExport) && (CameraSeedWalk != nullptr || Arguments.bExport) && !Budget.bTruncated) { Output.ProfilerComparison.Scope = TEXT("canonical_mesh_export"); VoxelDensityProfile::SetMode(VoxelDensityProfile::EMode::Disabled); FExploreWorld ReferenceWorld; FString ReferenceError; if (ReferenceWorld.Build(Arguments, ReferenceError)) { FExploreBudget ReferenceBudget(MainStartSeconds, Budget.LimitSeconds); const double ReferenceStart = FPlatformTime::Seconds(); if (EnsureExploreMesh( Arguments, ReferenceWorld, CameraSeedWalk, ReferenceBudget, ReferenceError) && ReferenceWorld.bExploreMeshComplete) { Output.ProfilerComparison.bAvailable = true; Output.ProfilerComparison.UnprofiledSeconds = ReferenceWorld.ExploreMeshSeconds; Output.ProfilerComparison.UnprofiledGeometryHash = ReferenceWorld.ExploreGeometryHash; } else { UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeProfiler] unprofiled reference failed after %.3fs: %s"), FPlatformTime::Seconds() - ReferenceStart, *ReferenceError); } } else { UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeProfiler] could not build unprofiled reference world: %s"), *ReferenceError); } VoxelDensityProfile::Reset(); VoxelDensityProfile::SetMode( ExploreProfileMode(Arguments), VoxelDensityProfile::DefaultSampleInterval); } if (Arguments.bRender) { const double Start = FPlatformTime::Seconds(); bool bRenderOk = false; if (!Budget.bTruncated && CameraSeedWalk != nullptr) { bRenderOk = RunRender( Arguments, World, *CameraSeedWalk, Output.Render, Budget); } if (!bRenderOk && !Budget.bTruncated) { bRequestedModeFailed = true; } if (Budget.bTruncated && Output.Render.Status.IsEmpty()) { Output.Render.Status = TEXT("truncated"); Output.Render.bTruncated = true; Output.Render.RefusalReason = TEXT("The wall-clock budget stopped before rendering."); } if (bRenderOk && !Budget.bTruncated) { Budget.CompleteMode(TEXT("render")); } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] render %.3fs (%s)"), FPlatformTime::Seconds() - Start, *Output.Render.Status); } if (Arguments.bFailureFocusRender && Arguments.bRender && Arguments.bWalk && !Budget.bTruncated) { const double Start = FPlatformTime::Seconds(); if (!RunFailureBoundaryRender(Arguments, World, Output.Walk, Output.Render, Budget)) { if (!Budget.bTruncated) { bRequestedModeFailed = true; } UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] failure-focus render could not produce a boundary image.")); } else { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] failure-focus render %.3fs (ok)"), FPlatformTime::Seconds() - Start); } } if (Arguments.bExport) { const double Start = FPlatformTime::Seconds(); const bool bExportOk = !Budget.bTruncated && RunExport(Arguments, World, CameraSeedWalk, Output.Export, Budget); if (!bExportOk && !Budget.bTruncated) { bRequestedModeFailed = true; } if (Budget.bTruncated && Output.Export.Status.IsEmpty()) { Output.Export.Status = TEXT("truncated"); Output.Export.bTruncated = true; Output.Export.RefusalReason = TEXT("The wall-clock budget stopped before export."); } if (bExportOk && !Budget.bTruncated) { Budget.CompleteMode(TEXT("export")); } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] export %.3fs (%s)"), FPlatformTime::Seconds() - Start, *Output.Export.Status); } // Stop before the report is assembled, while World still owns the generation state. This // guarantees that the sampler's summary describes only this case and that all worker stacks // have been released before the commandlet tears the world down. StackSamplerSession.Stop(); // A phase can finish just inside its own check and cross the deadline while its artifact is // being closed. Mark that state before serialising the report so the JSON never claims a full // run after the budget has actually expired. Budget.ShouldStop(TEXT("report")); if (Budget.bTruncated) { if (Arguments.bWalk && Output.Walk.Status.IsEmpty()) { Output.Walk.Status = TEXT("truncated"); Output.Walk.RefusalReason = TEXT("The wall-clock budget stopped before walk completion."); } UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeExplore] truncated during %s after %.3fs; completed modes=%s"), *Budget.TruncatedDuring, Budget.ElapsedSeconds(), *FString::Join(Budget.CompletedModes, TEXT(","))); } Output.BudgetSeconds = Budget.LimitSeconds; Output.ElapsedSeconds = FPlatformTime::Seconds() - MainStartSeconds; Output.bTruncated = Budget.bTruncated; Output.TruncatedDuring = Budget.TruncatedDuring; Output.CompletedModes = Budget.CompletedModes; Output.Profile = VoxelDensityProfile::Snapshot(); Output.bHasProfile = true; if (Arguments.IsDensityProfilingEnabled()) { if (Output.ProfilerComparison.bAvailable) { Output.ProfilerComparison.ProfiledSeconds = World.ExploreMeshSeconds; Output.ProfilerComparison.ProfiledGeometryHash = World.ExploreGeometryHash; Output.ProfilerComparison.DiscrepancySeconds = Output.ProfilerComparison.ProfiledSeconds - Output.ProfilerComparison.UnprofiledSeconds; Output.ProfilerComparison.DiscrepancyPercent = Output.ProfilerComparison.UnprofiledSeconds > 0.0 ? Output.ProfilerComparison.DiscrepancySeconds * 100.0 / Output.ProfilerComparison.UnprofiledSeconds : 0.0; Output.ProfilerComparison.Ratio = Output.ProfilerComparison.UnprofiledSeconds > 0.0 ? Output.ProfilerComparison.ProfiledSeconds / Output.ProfilerComparison.UnprofiledSeconds : 0.0; Output.ProfilerComparison.bGeometryEqual = !Output.ProfilerComparison.ProfiledGeometryHash.IsEmpty() && Output.ProfilerComparison.ProfiledGeometryHash == Output.ProfilerComparison.UnprofiledGeometryHash; const bool bWithinTarget = FMath::Abs(Output.ProfilerComparison.DiscrepancyPercent) <= 20.0; const bool bSelfCheckPassed = bWithinTarget && Output.ProfilerComparison.bGeometryEqual; Output.ProfilerComparison.SelfCheck = bSelfCheckPassed ? TEXT("passed") : TEXT("FAILED"); if (bSelfCheckPassed) { UE_LOG( LogTemp, Display, TEXT("[VoxelForgeProfiler] profiled_total=%.6fs unprofiled_total=%.6fs " "discrepancy=%.2f%% ratio=%.3fx geometry_equal=%s self_check=%s"), Output.ProfilerComparison.ProfiledSeconds, Output.ProfilerComparison.UnprofiledSeconds, Output.ProfilerComparison.DiscrepancyPercent, Output.ProfilerComparison.Ratio, Output.ProfilerComparison.bGeometryEqual ? TEXT("yes") : TEXT("no"), *Output.ProfilerComparison.SelfCheck); } else { UE_LOG( LogTemp, Error, TEXT("[VoxelForgeProfiler] profiled_total=%.6fs unprofiled_total=%.6fs " "discrepancy=%.2f%% ratio=%.3fx geometry_equal=%s self_check=%s"), Output.ProfilerComparison.ProfiledSeconds, Output.ProfilerComparison.UnprofiledSeconds, Output.ProfilerComparison.DiscrepancyPercent, Output.ProfilerComparison.Ratio, Output.ProfilerComparison.bGeometryEqual ? TEXT("yes") : TEXT("no"), *Output.ProfilerComparison.SelfCheck); } if (!bSelfCheckPassed) { bRequestedModeFailed = true; } } else { Output.ProfilerComparison.Scope = TEXT("canonical_mesh_export"); Output.ProfilerComparison.SelfCheck = TEXT("not_available"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeProfiler] self-check could not obtain an unprofiled reference.")); bRequestedModeFailed = true; } } // Build the payload twice before adding the assertion itself. This is deliberately a string // comparison over a fixed-order writer, not a TMap/pretty-printer whose key order could drift. const FString Payload = BuildExploreJson(Arguments, World, Output, false, 0); const FString PayloadRepeat = BuildExploreJson(Arguments, World, Output, false, 0); if (Payload.IsEmpty() || PayloadRepeat.IsEmpty()) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] JSON writer failed before report creation.")); return 1; } // Hash the exact UTF-8 representation that is written to disk. This keeps the reported // digest independent of TCHAR width (and therefore meaningful when the same run is compared // across editor platforms). FTCHARToUTF8 PayloadUtf8(*Payload); const uint32 PayloadCrc32 = FCrc::MemCrc32(PayloadUtf8.Get(), PayloadUtf8.Length()); const bool bPayloadRepeatEqual = Payload == PayloadRepeat; FString Json = BuildExploreJson( Arguments, World, Output, bPayloadRepeatEqual, PayloadCrc32); const FString JsonRepeat = BuildExploreJson( Arguments, World, Output, bPayloadRepeatEqual, PayloadCrc32); if (Json.IsEmpty() || JsonRepeat.IsEmpty()) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] JSON writer failed during report creation.")); return 1; } const bool bFinalRepeatEqual = Json == JsonRepeat; if (!bFinalRepeatEqual) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] deterministic JSON assertion failed before write.")); bRequestedModeFailed = true; Json = BuildExploreJson(Arguments, World, Output, false, PayloadCrc32); } const FString ReportPath = FPaths::Combine(Arguments.OutDirectory, TEXT("explore.json")); if (!FFileHelper::SaveStringToFile( Json, *ReportPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] Could not write '%s'."), *ReportPath); return 1; } if (OutJson != nullptr) { *OutJson = Json; } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] setup %.3fs, total %.3fs; report=%s; json_deterministic=%s"), SetupSeconds, FPlatformTime::Seconds() - MainStartSeconds, *ReportPath, (bPayloadRepeatEqual && bFinalRepeatEqual) ? TEXT("yes") : TEXT("no")); if (Arguments.IsDensityProfilingEnabled()) { const VoxelDensityProfile::FSnapshot Profile = VoxelDensityProfile::Snapshot(); const double CycleToMicroseconds = FPlatformTime::GetSecondsPerCycle64() * 1.0e6; const uint64 DensityCalls = Profile.Calls[static_cast(VoxelDensityProfile::EBucket::GetDensityAt)]; const uint64 DensityCycles = Profile.WallCycles[static_cast(VoxelDensityProfile::EBucket::GetDensityAt)]; const double DensityTotalUs = static_cast(DensityCycles) * CycleToMicroseconds; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDensityProfile] total calls=%llu total_us=%.3f us_per_call=%.6f"), static_cast(DensityCalls), DensityTotalUs, DensityCalls > 0 ? DensityTotalUs / static_cast(DensityCalls) : 0.0); const uint64 CacheTotalStaticBytes = Profile.TunnelCacheStaticBytes + Profile.RoomGraphCacheStaticBytes; const uint64 CacheTotalDynamicBytes = Profile.TunnelCacheDynamicBytes + Profile.RoomGraphCacheDynamicBytes; const uint64 CacheTotalBytes = CacheTotalStaticBytes + CacheTotalDynamicBytes; const uint64 CacheWorkers = FMath::Max( Profile.TunnelCacheWorkers, Profile.RoomGraphCacheWorkers); const uint64 CacheValidEntries = Profile.TunnelCacheValidEntries + Profile.RoomGraphCacheValidEntries; const uint64 CacheEntryBytes = Profile.TunnelCacheEntryBytes + Profile.RoomGraphCacheEntryBytes; const uint64 CacheCapacityEntries = Profile.TunnelCacheCapacityEntries + Profile.RoomGraphCacheCapacityEntries; const uint64 CacheLargestEntryBytes = FMath::Max( Profile.TunnelCacheLargestEntryBytes, Profile.RoomGraphCacheLargestEntryBytes); const uint64 CacheLargestWorkerBytes = FMath::Max( Profile.TunnelCacheLargestWorkerBytes, Profile.RoomGraphCacheLargestWorkerBytes); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCacheMemory] workers=%llu capacity_entries=%llu valid_entries=%llu " "static_bytes=%llu dynamic_bytes=%llu total_bytes=%llu " "avg_valid_entry_bytes=%.1f largest_entry_bytes=%llu " "avg_worker_bytes=%.1f largest_worker_bytes=%llu " "tunnel_avg_entry_bytes=%.1f tunnel_largest_entry_bytes=%llu " "tunnel_largest_worker_valid=%llu " "roomgraph_avg_entry_bytes=%.1f roomgraph_largest_entry_bytes=%llu " "roomgraph_largest_worker_valid=%llu " "tunnel_workers=%llu tunnel_capacity=%llu tunnel_valid=%llu " "tunnel_static=%llu tunnel_dynamic=%llu roomgraph_workers=%llu " "roomgraph_capacity=%llu roomgraph_valid=%llu roomgraph_static=%llu " "roomgraph_dynamic=%llu"), static_cast(CacheWorkers), static_cast(CacheCapacityEntries), static_cast(CacheValidEntries), static_cast(CacheTotalStaticBytes), static_cast(CacheTotalDynamicBytes), static_cast(CacheTotalBytes), CacheValidEntries > 0 ? static_cast(CacheEntryBytes) / static_cast(CacheValidEntries) : 0.0, static_cast(CacheLargestEntryBytes), CacheWorkers > 0 ? static_cast(CacheTotalBytes) / static_cast(CacheWorkers) : 0.0, static_cast(CacheLargestWorkerBytes), Profile.TunnelCacheValidEntries > 0 ? static_cast(Profile.TunnelCacheEntryBytes) / static_cast(Profile.TunnelCacheValidEntries) : 0.0, static_cast(Profile.TunnelCacheLargestEntryBytes), static_cast(Profile.TunnelCacheLargestWorkerValidEntries), Profile.RoomGraphCacheValidEntries > 0 ? static_cast(Profile.RoomGraphCacheEntryBytes) / static_cast(Profile.RoomGraphCacheValidEntries) : 0.0, static_cast(Profile.RoomGraphCacheLargestEntryBytes), static_cast(Profile.RoomGraphCacheLargestWorkerValidEntries), static_cast(Profile.TunnelCacheWorkers), static_cast(Profile.TunnelCacheCapacityEntries), static_cast(Profile.TunnelCacheValidEntries), static_cast(Profile.TunnelCacheStaticBytes), static_cast(Profile.TunnelCacheDynamicBytes), static_cast(Profile.RoomGraphCacheWorkers), static_cast(Profile.RoomGraphCacheCapacityEntries), static_cast(Profile.RoomGraphCacheValidEntries), static_cast(Profile.RoomGraphCacheStaticBytes), static_cast(Profile.RoomGraphCacheDynamicBytes)); const VoxelDensityProfile::EBucket DensityPhases[] = { VoxelDensityProfile::EBucket::DensityPrologue, VoxelDensityProfile::EBucket::DensityCore, VoxelDensityProfile::EBucket::DensityDisturbances, VoxelDensityProfile::EBucket::DensityStructuralPosts, VoxelDensityProfile::EBucket::DensityBoundarySeal, VoxelDensityProfile::EBucket::DensityDiffLayer, VoxelDensityProfile::EBucket::DensityTail, }; uint64 DensityAttributionCycles = 0; uint64 DensityAttributionCalls = 0; for (const VoxelDensityProfile::EBucket Phase : DensityPhases) { const int32 PhaseIndex = static_cast(Phase); DensityAttributionCycles += Profile.WallCycles[PhaseIndex]; DensityAttributionCalls += Profile.Calls[PhaseIndex]; } const double DensityAttributionUs = static_cast(DensityAttributionCycles) * CycleToMicroseconds; const double DensityUnattributedUs = DensityTotalUs - DensityAttributionUs; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDensityProfile] attribution_sum_us=%.3f unattributed_us=%.3f " "coverage=%.2f%% phase_calls=%llu"), DensityAttributionUs, DensityUnattributedUs, DensityTotalUs > 0.0 ? DensityAttributionUs * 100.0 / DensityTotalUs : 0.0, static_cast(DensityAttributionCalls)); for (int32 Index = 0; Index < VoxelDensityProfile::CounterCount; ++Index) { const VoxelDensityProfile::ECounter Counter = static_cast(Index); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDensityProfile] counter=%s value=%llu per_density_call=%.3f"), VoxelDensityProfile::CounterName(Counter), static_cast(Profile.Counters[Index]), DensityCalls > 0 ? static_cast(Profile.Counters[Index]) / static_cast(DensityCalls) : 0.0); } for (int32 Index = 0; Index < VoxelDensityProfile::BucketCount; ++Index) { const uint64 Calls = Profile.Calls[Index]; if (Calls == 0 || Index == static_cast(VoxelDensityProfile::EBucket::GetDensityAt)) { continue; } const double TotalUs = static_cast(Profile.WallCycles[Index]) * CycleToMicroseconds; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDensityProfile] op=%s calls=%llu total_us=%.3f us_per_call=%.6f"), VoxelDensityProfile::BucketName(static_cast(Index)), static_cast(Calls), TotalUs, TotalUs / static_cast(Calls)); } const VoxelDensityProfile::EBucket MesherBuckets[] = { VoxelDensityProfile::EBucket::MesherDensityGrid, VoxelDensityProfile::EBucket::MesherCellClassification, VoxelDensityProfile::EBucket::MesherGradientNormals, VoxelDensityProfile::EBucket::MesherVertexInterpolation, VoxelDensityProfile::EBucket::MesherStreamBuilding, VoxelDensityProfile::EBucket::MesherOther, }; uint64 MesherAttributionCycles = 0; uint64 MesherAttributionCalls = 0; for (const VoxelDensityProfile::EBucket Bucket : MesherBuckets) { const int32 BucketIndex = static_cast(Bucket); MesherAttributionCycles += Profile.WallCycles[BucketIndex]; MesherAttributionCalls += Profile.Calls[BucketIndex]; } const int32 MesherTotalIndex = static_cast( VoxelDensityProfile::EBucket::MesherGenerateMesh); const double MesherTotalUs = static_cast( Profile.WallCycles[MesherTotalIndex]) * CycleToMicroseconds; const double MesherAttributionUs = static_cast( MesherAttributionCycles) * CycleToMicroseconds; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeMesherProfile] total_calls=%llu total_us=%.3f " "attribution_sum_us=%.3f unattributed_us=%.3f coverage=%.2f%% " "phase_calls=%llu"), static_cast(Profile.Calls[MesherTotalIndex]), MesherTotalUs, MesherAttributionUs, MesherTotalUs - MesherAttributionUs, MesherTotalUs > 0.0 ? MesherAttributionUs * 100.0 / MesherTotalUs : 0.0, static_cast(MesherAttributionCalls)); VoxelDensityProfile::SetMode(VoxelDensityProfile::EMode::Disabled); } VoxelDensityOps::ReportSharedRoomGraphCacheFootprint(); VoxelDensityOps::TrimSharedRoomGraphCache(); // Per-case isolation boundary. The UObject world dies with this function; worker-local // caches are keyed by the fresh generator/manager lifetime and the diagnostics are reset before // the next case. No case may inherit profiler counters or a live profiler mode. VoxelDensityProfile::Reset(); VoxelDensityProfile::SetMode(VoxelDensityProfile::EMode::Disabled); return bRequestedModeFailed ? 2 : 0; } namespace { FString SanitizeBatchCaseId(const FString& InId, int32 Index) { FString Result; for (const TCHAR Character : InId) { const bool bAsciiAlphaNumeric = (Character >= TEXT('a') && Character <= TEXT('z')) || (Character >= TEXT('A') && Character <= TEXT('Z')) || (Character >= TEXT('0') && Character <= TEXT('9')); if (bAsciiAlphaNumeric || Character == TEXT('_') || Character == TEXT('-')) { Result.AppendChar(Character); } } if (Result.IsEmpty()) { Result = FString::Printf(TEXT("case_%03d"), Index); } return Result; } bool JsonNumber(const TSharedPtr& Object, const TCHAR* Key, double& OutValue) { if (!Object.IsValid() || !Object->HasField(Key)) { return false; } OutValue = Object->GetNumberField(Key); return FMath::IsFinite(OutValue); } bool JsonString(const TSharedPtr& Object, const TCHAR* Key, FString& OutValue) { if (!Object.IsValid() || !Object->HasField(Key)) { return false; } OutValue = Object->GetStringField(Key); return true; } bool JsonBool(const TSharedPtr& Object, const TCHAR* Key, bool& OutValue) { if (!Object.IsValid() || !Object->HasField(Key)) { return false; } OutValue = Object->GetBoolField(Key); return true; } bool BuildBatchCaseParams( const TSharedPtr& Case, const FString& CaseOutDirectory, FString& OutParams, FString& OutError) { if (!Case.IsValid()) { OutError = TEXT("Each batch case must be a JSON object."); return false; } double Number = 0.0; int32 Seed = 0; if (JsonNumber(Case, TEXT("seed"), Number)) { Seed = FMath::RoundToInt(Number); } FString Archetype = TEXT("Maze"); JsonString(Case, TEXT("archetype"), Archetype); int32 Slot = 4; if (JsonNumber(Case, TEXT("slot"), Number)) { Slot = FMath::RoundToInt(Number); } int32 ExportSize = DefaultExportSize; if (JsonNumber(Case, TEXT("export_size"), Number)) { ExportSize = FMath::RoundToInt(Number); } int32 ExportStep = DefaultExportStep; if (JsonNumber(Case, TEXT("export_step"), Number) || JsonNumber(Case, TEXT("step"), Number)) { ExportStep = FMath::RoundToInt(Number); } if (JsonNumber(Case, TEXT("lod"), Number)) { const int32 Lod = FMath::RoundToInt(Number); if (Lod < 0 || Lod > 3) { OutError = TEXT("case.lod must be in [0,3]."); return false; } ExportStep = 1 << Lod; } bool bReuseDensityGrid = true; JsonBool(Case, TEXT("density_grid_reuse"), bReuseDensityGrid); bool bBlockEarlyOut = false; JsonBool(Case, TEXT("block_early_out"), bBlockEarlyOut); int32 MeshMinBatchSize = 1; if (JsonNumber(Case, TEXT("mesh_min_batch_size"), Number)) { MeshMinBatchSize = FMath::Clamp(FMath::RoundToInt(Number), 1, 64); } FString Modes = TEXT("export"); JsonString(Case, TEXT("modes"), Modes); bool bOperatorStack = true; JsonBool(Case, TEXT("operator_stack"), bOperatorStack); bool bProfileDensity = false; JsonBool(Case, TEXT("profile_density"), bProfileDensity); bool bProfileDensityFull = false; JsonBool(Case, TEXT("profile_density_full"), bProfileDensityFull); bProfileDensity |= bProfileDensityFull; bool bPerfAttribution = false; JsonBool(Case, TEXT("perf_attribution"), bPerfAttribution); bool bSampleStacks = false; JsonBool(Case, TEXT("sample_stacks"), bSampleStacks); int32 SampleStacksIntervalUs = static_cast(FVoxelStackSampler::DefaultIntervalUs); if (JsonNumber(Case, TEXT("sample_stacks_interval_us"), Number)) { SampleStacksIntervalUs = FMath::RoundToInt(Number); bSampleStacks = true; } bool bProfileLod = false; JsonBool(Case, TEXT("profile_lod"), bProfileLod); bool bOpBounds = false; JsonBool(Case, TEXT("op_bounds"), bOpBounds); bool bFailureFocus = false; JsonBool(Case, TEXT("failure_focus_render"), bFailureFocus); bool bTunnelFloorEnabled = true; const bool bHasTunnelFloorEnabled = JsonBool( Case, TEXT("tunnel_floor_enabled"), bTunnelFloorEnabled); bool bTunnelFloorTerracing = true; const bool bHasTunnelFloorTerracing = JsonBool( Case, TEXT("tunnel_floor_terracing"), bTunnelFloorTerracing); OutParams = FString::Printf( TEXT("-seed=%d -archetype=%s -slot=%d -modes=%s -opstack=%d " "-exportsize=%d -exportstep=%d -densitygridreuse=%d " "-blockearlyout=%d -meshminbatch=%d " "-failurefocus=%d -out=\"%s\""), Seed, *Archetype, Slot, *Modes, bOperatorStack ? 1 : 0, ExportSize, ExportStep, bReuseDensityGrid ? 1 : 0, bBlockEarlyOut ? 1 : 0, MeshMinBatchSize, bFailureFocus ? 1 : 0, *CaseOutDirectory); if (bHasTunnelFloorEnabled) { OutParams += FString::Printf( TEXT(" -tunnelfloor=%d"), bTunnelFloorEnabled ? 1 : 0); } if (bHasTunnelFloorTerracing) { OutParams += FString::Printf( TEXT(" -tunnelfloorterracing=%d"), bTunnelFloorTerracing ? 1 : 0); } if (bProfileDensity) { OutParams += bProfileDensityFull ? TEXT(" -profiledensityfull") : TEXT(" -profiledensity"); } if (bPerfAttribution) { OutParams += TEXT(" -perfattribution"); } if (bSampleStacks) { OutParams += FString::Printf( TEXT(" -samplestacks=%d"), SampleStacksIntervalUs); } if (bProfileLod) { OutParams += TEXT(" -profilelod"); } if (bOpBounds) { OutParams += TEXT(" -opbounds"); } const TPair IntegerFields[] = { { TEXT("render_width"), TEXT("renderwidth") }, { TEXT("render_height"), TEXT("renderheight") }, { TEXT("max_walk_cells"), TEXT("maxwalkcells") }, }; for (const auto& Field : IntegerFields) { if (JsonNumber(Case, Field.Key, Number)) { OutParams += FString::Printf(TEXT(" -%s=%d"), Field.Value, FMath::RoundToInt(Number)); } } const TPair FloatFields[] = { { TEXT("render_step"), TEXT("renderstep") }, { TEXT("render_max_distance"), TEXT("rendermaxdistance") }, { TEXT("surface_roughness"), TEXT("surfaceroughness") }, { TEXT("roughness_frequency"), TEXT("roughnessfrequency") }, { TEXT("room_mouth_rise_strength"), TEXT("roommouthrise") }, { TEXT("room_mouth_rise_blend_voxels"), TEXT("roommouthriseblend") }, { TEXT("tunnel_floor_step_height"), TEXT("tunnelfloorstep") }, { TEXT("tunnel_floor_max_ledge_height"), TEXT("tunnelfloormaxledge") }, { TEXT("tunnel_floor_gentle_slope"), TEXT("tunnelfloorgentle") }, }; for (const auto& Field : FloatFields) { if (JsonNumber(Case, Field.Key, Number)) { OutParams += FString::Printf(TEXT(" -%s=%.9g"), Field.Value, Number); } } FString RoughnessType; if (JsonString(Case, TEXT("roughness_noise_type"), RoughnessType)) { OutParams += FString::Printf(TEXT(" -roughnesstype=%s"), *RoughnessType); } const TPair IntegerFloorFields[] = { { TEXT("tunnel_floor_ledge_count"), TEXT("tunnelfloorledges") }, { TEXT("tunnel_floor_max_ledges"), TEXT("tunnelfloormaxledges") }, }; for (const auto& Field : IntegerFloorFields) { if (JsonNumber(Case, Field.Key, Number)) { OutParams += FString::Printf(TEXT(" -%s=%d"), Field.Value, FMath::RoundToInt(Number)); } } return true; } int32 RunBatch(const FString& Params) { FString BatchPath; FString OutText; if (!FParse::Value(*Params, TEXT("batch="), BatchPath) || !FParse::Value(*Params, TEXT("out="), OutText)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] batch requires -batch= and an absolute -out=.")); return 1; } if (FPaths::IsRelative(OutText)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] batch -out must be absolute and under '%s'."), *VoxelForgeSavedRoot()); return 1; } const FString BatchOutDirectory = FPaths::ConvertRelativePathToFull(OutText); if (!IsUnderVoxelForgeSaved(BatchOutDirectory)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] batch -out must be under '%s' (got '%s')."), *VoxelForgeSavedRoot(), *BatchOutDirectory); return 1; } if (!IFileManager::Get().MakeDirectory(*BatchOutDirectory, true)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] could not create batch output '%s'."), *BatchOutDirectory); return 1; } BatchPath = FPaths::ConvertRelativePathToFull(BatchPath); FString BatchText; if (!FFileHelper::LoadFileToString(BatchText, *BatchPath)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] could not read batch file '%s'."), *BatchPath); return 1; } TSharedPtr Root; const TSharedRef> Reader = TJsonReaderFactory<>::Create(BatchText); if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid()) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] batch file is not valid JSON.")); return 1; } const TArray>* Cases = nullptr; if (!Root->TryGetArrayField(TEXT("cases"), Cases) || Cases == nullptr || Cases->Num() == 0) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] batch JSON needs a non-empty 'cases' array.")); return 1; } if (Cases->Num() > 256) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] batch is capped at 256 cases.")); return 1; } double BatchBudgetMinutes = 29.0; double Number = 0.0; if (JsonNumber(Root, TEXT("budget_minutes"), Number)) { BatchBudgetMinutes = Number; } if (FParse::Value(*Params, TEXT("batchbudget="), Number)) { BatchBudgetMinutes = Number; } BatchBudgetMinutes = FMath::Clamp(BatchBudgetMinutes, 0.01, 30.0); const double BatchStartSeconds = FPlatformTime::Seconds(); bool bTruncated = false; int32 CompletedCases = 0; int32 FailedCases = 0; TSet UsedCaseIds; TArray> CaseReports; CaseReports.Reserve(Cases->Num()); for (int32 Index = 0; Index < Cases->Num(); ++Index) { const double RemainingSeconds = BatchBudgetMinutes * 60.0 - (FPlatformTime::Seconds() - BatchStartSeconds); if (RemainingSeconds <= 0.25) { bTruncated = true; break; } const TSharedPtr Case = (*Cases)[Index]->AsObject(); FString CaseId; JsonString(Case, TEXT("id"), CaseId); CaseId = SanitizeBatchCaseId(CaseId, Index); const FString BaseCaseId = CaseId; int32 Suffix = 1; while (UsedCaseIds.Contains(CaseId)) { CaseId = FString::Printf(TEXT("%s_%d"), *BaseCaseId, Suffix++); } UsedCaseIds.Add(CaseId); FString CaseOutDirectory = FPaths::Combine(BatchOutDirectory, TEXT("cases"), CaseId); FString CaseParams; FString CaseError; if (!BuildBatchCaseParams(Case, CaseOutDirectory, CaseParams, CaseError)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] batch case %d (%s): %s"), Index, *CaseId, *CaseError); TSharedPtr CaseReport = MakeShared(); CaseReport->SetStringField(TEXT("case_id"), CaseId); CaseReport->SetNumberField(TEXT("case_index"), Index); CaseReport->SetNumberField(TEXT("case_exit_code"), 1); CaseReport->SetStringField(TEXT("status"), TEXT("invalid")); CaseReport->SetStringField(TEXT("error"), CaseError); CaseReports.Add(MakeShared(CaseReport)); ++CompletedCases; ++FailedCases; continue; } double CaseBudgetMinutes = FMath::Min(25.0, RemainingSeconds / 60.0); if (JsonNumber(Case, TEXT("budget_minutes"), Number)) { CaseBudgetMinutes = FMath::Min(CaseBudgetMinutes, Number); } CaseBudgetMinutes = FMath::Clamp(CaseBudgetMinutes, 0.01, 30.0); CaseParams += FString::Printf(TEXT(" -budget=%.9g"), CaseBudgetMinutes); FString CaseJson; const int32 CaseResult = RunExploreCase(CaseParams, &CaseJson); TSharedPtr CaseReport; const TSharedRef> CaseReader = TJsonReaderFactory<>::Create(CaseJson); if (!FJsonSerializer::Deserialize(CaseReader, CaseReport) || !CaseReport.IsValid()) { CaseReport = MakeShared(); CaseReport->SetStringField(TEXT("error"), TEXT("case did not produce a JSON report")); } CaseReport->SetStringField(TEXT("case_id"), CaseId); CaseReport->SetNumberField(TEXT("case_index"), Index); CaseReport->SetNumberField(TEXT("case_exit_code"), CaseResult); CaseReports.Add(MakeShared(CaseReport)); ++CompletedCases; if (CaseResult != 0) { ++FailedCases; } // RunExploreCase owns a complete transient UObject world. Release that case's strong // roots before starting the next one and collect now; otherwise a batch retains every // completed mesh/settings/manager graph until the commandlet exits, making peak RSS grow // with the number of cases even though no report needs those objects afterward. CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); } TSharedPtr BatchReport = MakeShared(); BatchReport->SetNumberField(TEXT("schema_version"), 2); BatchReport->SetStringField(TEXT("tool"), TEXT("VoxelForgeExplore")); BatchReport->SetBoolField(TEXT("batch"), true); BatchReport->SetStringField(TEXT("input_file"), BatchPath); BatchReport->SetStringField(TEXT("output_directory"), BatchOutDirectory); BatchReport->SetNumberField(TEXT("budget_minutes"), BatchBudgetMinutes); BatchReport->SetNumberField(TEXT("elapsed_seconds"), FPlatformTime::Seconds() - BatchStartSeconds); BatchReport->SetBoolField(TEXT("truncated"), bTruncated); BatchReport->SetNumberField(TEXT("completed_cases"), CompletedCases); BatchReport->SetNumberField(TEXT("failed_cases"), FailedCases); BatchReport->SetArrayField(TEXT("cases"), MoveTemp(CaseReports)); FString BatchJson; const TSharedRef> Writer = TJsonWriterFactory<>::Create(&BatchJson); if (!FJsonSerializer::Serialize(BatchReport.ToSharedRef(), Writer) || !FFileHelper::SaveStringToFile( BatchJson, *FPaths::Combine(BatchOutDirectory, TEXT("batch.json")), FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] could not write batch.json.")); return 1; } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] batch completed=%d failed=%d truncated=%s report=%s"), CompletedCases, FailedCases, bTruncated ? TEXT("yes") : TEXT("no"), *FPaths::Combine(BatchOutDirectory, TEXT("batch.json"))); return FailedCases > 0 || bTruncated ? 2 : 0; } } int32 UVoxelForgeExploreCommandlet::Main(const FString& Params) { FString SymbolizeCsvPath; if (FParse::Value(*Params, TEXT("symbolizestacks="), SymbolizeCsvPath)) { FString SymbolizedOutputPath; FString SymbolPath; FParse::Value(*Params, TEXT("symbolizedout="), SymbolizedOutputPath); FParse::Value(*Params, TEXT("symbolpath="), SymbolPath); FString SummaryPath; FString Error; if (!VoxelForgeOfflineStackSymbolizer::Run( SymbolizeCsvPath, SymbolizedOutputPath, SymbolPath, SummaryPath, Error)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] offline stack symbolization failed: %s"), *Error); return 2; } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] offline stack symbolization completed summary=%s"), *SummaryPath); return 0; } FString BatchPath; if (FParse::Value(*Params, TEXT("batch="), BatchPath)) { return RunBatch(Params); } return RunExploreCase(Params, nullptr); }