Files
VoxelForge/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp
T
Fr0zkaandClaude Opus 5.5 41b001b652 measure(step1): cook is 1.5-2.3 ms, RMC's route is 8-11 s; CMC cannot take a field floor; cap 16 uploads
Rebuild plan step 1 (RESEARCH-SPEED-2026-09.md), all behind default-off switches.
A. Direct cook on a dedicated thread, 27-tile support cohort: core cook p50 1.50 ms, p95 2.28 ms;
   a 3,949-tri tile 1.67 ms extract+cook. PASSES the 10 ms gate. Through RMC's route the same
   support bodies became ready 8.46 s p50 / 10.80 s p95 after their meshes existed: FAILS 150 ms.
   Exact-target capsule sweep confirmed (0.75 ms after body update).
B. 100 rapid underfoot digs on CMC with a field override: 883 false-support frames, 0 false-air,
   0 invented velocity. CMC's floor comes from collision-scene traces -> step 4 = field-aware Mover.
C. 341 prequeued tiles, render on: cap 4 = 238 applies/s, cap 8 = 476, cap 16 = 639 (drain p95
   2.22 ms vs 2 ms gate); first visible 1 frame after release; no RT/RHI backlog. Tune drain,
   no grouping needed yet.
D. Same 128^3 region, 64/64 tile hashes equal in export and game. The "263 ns/sample" was export
   WALL time with 15 workers sharing a lattice (~3.4 us CPU/sample); game tile time includes
   meshing and duplicated borders (+22%); contention cap 1 vs 14: 74 vs 112 ms per tile.
   Profiling is not the cause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2026-09-30 00:07:59 +02:00

8626 lines
379 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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/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 "VoxelGeometryHash.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 <algorithm>
#include <atomic>
#include <limits>
namespace
{
using FExploreJsonWriter = TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>;
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<const TCHAR*, ECaveGeneratorType> 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<FString> 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<FString> 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 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;
bool bExportTileOverride = false;
int32 ExportTileX = 0;
int32 ExportTileY = 0;
int32 ExportTileZ = 0;
// 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;
// Opt-in parity mode: copy the runtime mesher presentation/LOD settings from the authored
// settings object. The default remains the historical bounded export configuration.
bool bGameRuntimeConfig = 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 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<int32>(
FVoxelStackSampler::DefaultIntervalUs);
if (FParse::Value(*Params, TEXT("samplestacks="), SampleStacksIntervalUs))
{
OutArguments.bSampleStacks = true;
OutArguments.SampleStacksIntervalUs = static_cast<uint32>(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);
const bool bExportTileXSpecified = Params.Contains(
TEXT("exporttilex="), ESearchCase::IgnoreCase);
const bool bExportTileYSpecified = Params.Contains(
TEXT("exporttiley="), ESearchCase::IgnoreCase);
const bool bExportTileZSpecified = Params.Contains(
TEXT("exporttilez="), ESearchCase::IgnoreCase);
if (bExportTileXSpecified || bExportTileYSpecified || bExportTileZSpecified)
{
if (!bExportTileXSpecified || !bExportTileYSpecified || !bExportTileZSpecified
|| !FParse::Value(*Params, TEXT("exporttilex="), OutArguments.ExportTileX)
|| !FParse::Value(*Params, TEXT("exporttiley="), OutArguments.ExportTileY)
|| !FParse::Value(*Params, TEXT("exporttilez="), OutArguments.ExportTileZ))
{
OutError = TEXT("exporttilex, exporttiley, and exporttilez must be supplied together as level-0 tile coordinates.");
return false;
}
OutArguments.bExportTileOverride = true;
}
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;
int32 GameRuntimeConfig = OutArguments.bGameRuntimeConfig ? 1 : 0;
FParse::Value(*Params, TEXT("gameconfig="), GameRuntimeConfig);
FParse::Value(*Params, TEXT("paritygame="), GameRuntimeConfig);
OutArguments.bGameRuntimeConfig = GameRuntimeConfig != 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<FString> 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;
}
// The historical explorer contract needs two adjacent mouths around an interior slot. A
// parity export can instead target the game's authored single-strate layout, which has no
// such mouths and is valid for export-only mode. Build() still validates the slot against
// the loaded settings layout.
const int32 MinimumSlot = OutArguments.bGameRuntimeConfig ? 0 : 1;
const int32 MaximumSlot = OutArguments.bGameRuntimeConfig
? MaxSyntheticStrates - 1 : MaxSyntheticStrates - 2;
if (OutArguments.Slot < MinimumSlot || OutArguments.Slot > MaximumSlot)
{
if (OutArguments.bGameRuntimeConfig)
{
OutError = FString::Printf(
TEXT("slot must be in [0,%d] for game-runtime configuration parity."),
MaxSyntheticStrates - 1);
}
else
{
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<int64>(OutArguments.RenderWidth)
* static_cast<int64>(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 does not budget 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<FString> 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<UVoxelSettings> Settings;
TStrongObjectPtr<UVoxelStrateManager> Manager;
TStrongObjectPtr<UVoxelDiffLayer> DiffLayer;
TStrongObjectPtr<UVoxelGenerator> Generator;
TStrongObjectPtr<UVoxelMarchingCubesMesher> Mesher;
TArray<TStrongObjectPtr<UVoxelStrateDefinition>> 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<int32> ExploreTileTriangleCounts;
TArray<FString> 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<FExploreMeshTriangle> ExploreTriangles;
TArray<int32> ExploreTriangleOrder;
TArray<FExploreMeshBvhNode> 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<UVoxelStrateDefinition>(
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<UVoxelSettings>(
nullptr, *Arguments.SettingsReference);
if (AuthoredSettings == nullptr)
{
OutError = FString::Printf(
TEXT("Could not load staged settings asset '%s'."),
*Arguments.SettingsReference);
return false;
}
Settings = TStrongObjectPtr<UVoxelSettings>(
DuplicateObject<UVoxelSettings>(AuthoredSettings, GetTransientPackage()));
if (!Settings.IsValid())
{
OutError = TEXT("Could not duplicate the staged settings asset into the transient package.");
return false;
}
}
else
{
Settings = TStrongObjectPtr<UVoxelSettings>(
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
}
const int32 NumStrates = bUseAuthoredSettings
? (Arguments.bGameRuntimeConfig
? FMath::Max(Settings->TotalStrates, 1)
: 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<UVoxelStrateDefinition>(
AuthoredDefinition, GetTransientPackage())
: NewObject<UVoxelStrateDefinition>(
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;
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<UVoxelStrateDefinition> SoftDefinition(Definition);
Settings->FixedStrates.Add(Index, SoftDefinition);
if (!bUseAuthoredStrate)
{
Settings->StratePool.Add(SoftDefinition);
}
Definitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(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<UVoxelStrateManager>(
NewObject<UVoxelStrateManager>(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 "
"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),
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<UVoxelDiffLayer>(
NewObject<UVoxelDiffLayer>(GetTransientPackage(), NAME_None, RF_Transient));
Generator = TStrongObjectPtr<UVoxelGenerator>(
NewObject<UVoxelGenerator>(GetTransientPackage(), NAME_None, RF_Transient));
Generator->InitializeSettings(Settings.Get());
Generator->SetStrateManager(Manager.Get());
Generator->SetDiffLayer(DiffLayer.Get());
Mesher = TStrongObjectPtr<UVoxelMarchingCubesMesher>(
NewObject<UVoxelMarchingCubesMesher>(GetTransientPackage(), NAME_None, RF_Transient));
Mesher->SetGenerator(Generator.Get());
Mesher->bUseBlockEarlyOut = Arguments.bBlockEarlyOut;
if (Arguments.bGameRuntimeConfig)
{
Mesher->bGenerateSkirts = Settings->bGenerateSkirts;
Mesher->SkirtCells = Settings->SkirtCells;
Mesher->LODOctaveDrop = Settings->LODOctaveDrop;
}
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<FExploreRenderFrame> 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<FVector> CameraSeedPoses;
TArray<FString> 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<float> CoreCrossingsVoxels;
TArray<float> 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<float> 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<FExploreTunnelMouthProbeEndpoint> 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<FString> 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<float>(Width) / static_cast<float>(Height);
OutBasis.TanHalfFov = FMath::Tan(FMath::DegreesToRadians(FieldOfViewDegrees * 0.5f));
return OutBasis.Aspect > 0.0f && OutBasis.TanHalfFov > 0.0f;
}
void PutPixel(TArray<FColor>& 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<FColor>& 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<float>(Width - 1));
OutPixel.Y = FMath::RoundToInt((1.0f - (NdcY * 0.5f + 0.5f))
* static_cast<float>(Height - 1));
return NdcX >= -1.0f && NdcX <= 1.0f && NdcY >= -1.0f && NdcY <= 1.0f;
}
void DrawScaleMarker(
TArray<FColor>& 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<FColor>& Pixels,
int32 Width,
int32 Height,
FString& OutError)
{
IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(
FName(TEXT("ImageWrapper")));
TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule.CreateImageWrapper(
EImageFormat::PNG);
if (!ImageWrapper.IsValid()
|| !ImageWrapper->SetRaw(
Pixels.GetData(),
static_cast<int64>(Pixels.Num()) * sizeof(FColor),
Width,
Height,
ERGBFormat::BGRA,
8))
{
OutError = TEXT("ImageWrapper could not accept the BGRA render buffer.");
return false;
}
const TArray64<uint8>& 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)
{
if (Arguments.bExportTileOverride)
{
return FIntVector(
Arguments.ExportTileX * CHUNK_SIZE,
Arguments.ExportTileY * CHUNK_SIZE,
Arguments.ExportTileZ * CHUNK_SIZE);
}
FVector Centre(0.0f, 0.0f, static_cast<float>(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<float>(MeshSize))
/ static_cast<float>(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)
{
return VoxelForgeGeometryHash::Compute(MeshData);
}
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<float>(
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. Historical export deliberately omitted presentation skirts; parity opts into
// the authored runtime settings so the requested comparison has the same mesher inputs.
World.Mesher->bGenerateSkirts = Arguments.bGameRuntimeConfig
? World.Settings->bGenerateSkirts : false;
World.Mesher->SkirtCells = Arguments.bGameRuntimeConfig
? World.Settings->SkirtCells : 2.0f;
World.Mesher->LODOctaveDrop = Arguments.bGameRuntimeConfig
? World.Settings->LODOctaveDrop : 0;
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<int64>(PointsPerTile)
* static_cast<int64>(PointsPerTile)
* static_cast<int64>(PointsPerTile);
const int32 SharedGridDim = Arguments.ExportSize / Arguments.ExportStep + 3;
const int64 SharedGridSamples = static_cast<int64>(SharedGridDim)
* static_cast<int64>(SharedGridDim)
* static_cast<int64>(SharedGridDim);
World.ExploreDensityGridTotalSamples = static_cast<int64>(World.ExploreMeshTileCount)
* TileGridSamples;
World.ExploreDensityGridUniqueSamples = SharedGridSamples;
World.ExploreDensityGridDuplicateSamplesBefore = FMath::Max<int64>(
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<bool> 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<FVoxelMeshData> TileMeshes;
TileMeshes.SetNum(World.ExploreMeshTileCount);
TArray<FString> TileErrors;
TileErrors.SetNum(World.ExploreMeshTileCount);
TArray<uint8> TileCompleted;
TileCompleted.Init(0, World.ExploreMeshTileCount);
TArray<double> TileDurations;
TileDurations.Init(0.0, World.ExploreMeshTileCount);
TArray<double> TileStartOffsets;
TileStartOffsets.Init(-1.0, World.ExploreMeshTileCount);
TArray<double> TileEndOffsets;
TileEndOffsets.Init(-1.0, World.ExploreMeshTileCount);
TArray<uint32> TileThreadIds;
TileThreadIds.Init(0, World.ExploreMeshTileCount);
std::atomic<bool> 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<double> CompletedTileDurations;
TSet<uint32> 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<double>(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<int32(int32, int32)> 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<FColor>& 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<float>(PixelX) + 0.5f)
/ static_cast<float>(Arguments.RenderWidth)) - 1.0f;
const float NormalizedY = 1.0f - (2.0f * (static_cast<float>(PixelY) + 0.5f)
/ static_cast<float>(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<uint8>(FMath::Clamp(100.0f * Brightness, 0.0f, 255.0f)),
static_cast<uint8>(FMath::Clamp(128.0f * Brightness, 0.0f, 255.0f)),
static_cast<uint8>(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<int64>(Arguments.RenderWidth)
* static_cast<int64>(Arguments.RenderHeight)
* 3ll
* static_cast<int64>(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<TPair<FVector, FString>, 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<FVector, FString>& 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<FVector, FString>& 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<float>(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<float>(ViewIndex),
FVector::UpVector).GetSafeNormal();
const FVector Target = CameraPosition + LookDirection * 96.0f + FVector::UpVector * 8.0f;
const double ViewStartSeconds = FPlatformTime::Seconds();
TArray<FColor> 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<double>(AdditionalViewCount)
: 0.0;
if (!OutOutput.bTruncated)
{
OutOutput.Status = TEXT("ok");
}
return !OutOutput.bTruncated;
}
void DrawPointMarker(
TArray<FColor>& 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<FColor> 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<int64>(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<long long>(EstimatedWorkingBytes),
static_cast<long long>(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;
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<float>(World.TargetBottomWorldZ)
|| Point.Z > static_cast<float>(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<FVector> 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<float>(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<float>(X) + 0.5f)
* OutOutput.SharedSampleGrid.SampleStep,
OutOutput.SharedSampleGrid.MinY
+ (static_cast<float>(Y) + 0.5f)
* OutOutput.SharedSampleGrid.SampleStep,
OutOutput.SharedSampleGrid.MinZ
+ (static_cast<float>(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<float>(X) + 0.5f)
* OutOutput.SharedSampleGrid.SampleStep,
OutOutput.SharedSampleGrid.MinY
+ (static_cast<float>(Y) + 0.5f)
* OutOutput.SharedSampleGrid.SampleStep,
OutOutput.SharedSampleGrid.MinZ
+ (static_cast<float>(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<int64>(CHUNK_SIZE)
* static_cast<int64>(CHUNK_SIZE)
* static_cast<int64>(CHUNK_SIZE);
const int64 TileGridSamples = static_cast<int64>(CHUNK_SIZE + 3)
* static_cast<int64>(CHUNK_SIZE + 3)
* static_cast<int64>(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<int64>(sizeof(float)) + TileCells * 448ll;
}
bool WriteUtf8(FArchive& Archive, const FString& Text)
{
FTCHARToUTF8 Converted(*Text);
if (Converted.Length() > 0)
{
Archive.Serialize(const_cast<ANSICHAR*>(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<float>& SortedValues, float Quantile)
{
if (SortedValues.Num() == 0)
{
return 0.0f;
}
const float Position = FMath::Clamp(Quantile, 0.0f, 1.0f)
* static_cast<float>(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<float>(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<float> EdgeLengthsMeters;
TArray<float> DihedralAnglesDegrees;
TArray<float> FaceNormalDivergencesDegrees;
EdgeLengthsMeters.Reserve(NumTriangles * 3);
DihedralAnglesDegrees.Reserve(NumTriangles * 2);
FaceNormalDivergencesDegrees.Reserve(NumTriangles);
TArray<FVector> FaceNormals;
FaceNormals.Init(FVector::ZeroVector, NumTriangles);
TArray<uint8> ValidFaceNormals;
ValidFaceNormals.Init(0, NumTriangles);
TMap<FExploreMeshEdgeKey, FExploreMeshEdgeIncident> 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<FExploreMeshEdgeKey, FExploreMeshEdgeIncident>& 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<float>(Over20)
/ static_cast<float>(DihedralAnglesDegrees.Num());
OutMetrics.DihedralFractionOver40Degrees = static_cast<float>(Over40)
/ static_cast<float>(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 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<double>(Value.X) * Scale);
Writer.WriteValue(TEXT("y"), static_cast<double>(Value.Y) * Scale);
Writer.WriteValue(TEXT("z"), static_cast<double>(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<double>(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<double>(Metrics.EdgeLengthP50Meters));
Writer.WriteValue(TEXT("p99"), static_cast<double>(Metrics.EdgeLengthP99Meters));
Writer.WriteValue(TEXT("max"), static_cast<double>(Metrics.EdgeLengthMaxMeters));
Writer.WriteObjectEnd();
Writer.WriteObjectStart(TEXT("dihedral_angle_degrees"));
Writer.WriteValue(TEXT("p50"), static_cast<double>(Metrics.DihedralP50Degrees));
Writer.WriteValue(TEXT("p75"), static_cast<double>(Metrics.DihedralP75Degrees));
Writer.WriteValue(TEXT("p90"), static_cast<double>(Metrics.DihedralP90Degrees));
Writer.WriteValue(TEXT("p95"), static_cast<double>(Metrics.DihedralP95Degrees));
Writer.WriteValue(TEXT("p99"), static_cast<double>(Metrics.DihedralP99Degrees));
Writer.WriteValue(TEXT("fraction_over_20_degrees"),
static_cast<double>(Metrics.DihedralFractionOver20Degrees));
Writer.WriteValue(TEXT("fraction_over_40_degrees"),
static_cast<double>(Metrics.DihedralFractionOver40Degrees));
Writer.WriteObjectEnd();
Writer.WriteObjectStart(TEXT("face_normal_vs_mean_vertex_normal_degrees"));
Writer.WriteValue(TEXT("p50"),
static_cast<double>(Metrics.FaceNormalVsMeanVertexNormalP50Degrees));
Writer.WriteValue(TEXT("p90"),
static_cast<double>(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<double>(Arguments.TunnelFloorStep));
}
Writer.WriteValue(TEXT("max_ledge_height_override"), Arguments.bTunnelFloorMaxLedgeOverride);
if (Arguments.bTunnelFloorMaxLedgeOverride)
{
Writer.WriteValue(TEXT("max_ledge_height_voxels"),
static_cast<double>(Arguments.TunnelFloorMaxLedge));
}
Writer.WriteValue(TEXT("gentle_slope_override"), Arguments.bTunnelFloorGentleOverride);
if (Arguments.bTunnelFloorGentleOverride)
{
Writer.WriteValue(TEXT("gentle_slope"),
static_cast<double>(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<FExploreJsonWriter> Writer =
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::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("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<double>(Arguments.WormStrength));
}
Writer->WriteValue(TEXT("tunnel_ablation_mask"),
static_cast<int64>(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<double>(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<double>(Arguments.RoomMouthRiseBlend));
}
Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride);
if (Arguments.bSurfaceRoughnessOverride)
{
Writer->WriteValue(TEXT("surface_roughness"),
static_cast<double>(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<double>(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<double>(Export.RegionOrigin.X) * 0.25);
Writer->WriteValue(TEXT("y"), static_cast<double>(Export.RegionOrigin.Y) * 0.25);
Writer->WriteValue(TEXT("z"), static_cast<double>(Export.RegionOrigin.Z) * 0.25);
Writer->WriteObjectEnd();
Writer->WriteObjectStart(TEXT("max_exclusive"));
Writer->WriteValue(TEXT("x"), static_cast<double>(Export.RegionOrigin.X + Export.RegionSize) * 0.25);
Writer->WriteValue(TEXT("y"), static_cast<double>(Export.RegionOrigin.Y + Export.RegionSize) * 0.25);
Writer->WriteValue(TEXT("z"), static_cast<double>(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<double>(FVoxelPlayerCapsuleConstants::RadiusCentimeters) / 100.0);
Writer->WriteValue(TEXT("capsule_width_m"), static_cast<double>(2.0f * FVoxelPlayerCapsuleConstants::RadiusCentimeters) / 100.0);
Writer->WriteValue(TEXT("capsule_half_height_m"), static_cast<double>(FVoxelPlayerCapsuleConstants::HalfHeightCentimeters) / 100.0);
Writer->WriteValue(TEXT("capsule_height_m"), static_cast<double>(2.0f * FVoxelPlayerCapsuleConstants::HalfHeightCentimeters) / 100.0);
Writer->WriteValue(TEXT("max_step_height_m"), static_cast<double>(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<long long>(OutOutput.EstimatedWorkingBytes),
static_cast<long long>(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<FArchive> 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<int64>(World.ExploreMesh.Vertices.Num()) * sizeof(FVector)
+ static_cast<int64>(World.ExploreMesh.Normals.Num()) * sizeof(FVector)
+ static_cast<int64>(World.ExploreMesh.UVs.Num()) * sizeof(FVector2D)
+ static_cast<int64>(World.ExploreMesh.Colors.Num()) * sizeof(FColor)
+ static_cast<int64>(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<double>(Arguments.ExportSize)
/ static_cast<double>(Arguments.ExportSize)
/ static_cast<double>(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<float>::Max();
float MaxDensity = -TNumericLimits<float>::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<float>(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<float>& 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<double>(OutValidCount);
OutMean = static_cast<float>(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<float>(AnalysisCount - 1) * StepVoxels;
TArray<float, TInlineAllocator<257>> 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<double>(FMath::Max(
BestRunLength - 1, 1));
for (int32 Index = BestRunStart; Index <= BestRunEnd; ++Index)
{
const double NormalizedX = -1.0
+ 2.0 * static_cast<double>(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<double>(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<double>(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<float>(static_cast<double>(Samples[Index]) - Trend);
}
}
}
constexpr float MaxRibPeriodVoxels = 16.0f;
const int32 MinRibFrequency = FMath::Max(
1, FMath::CeilToInt(StepVoxels * static_cast<float>(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<float, TInlineAllocator<129>> 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<double>(Window[Middle])
: 0.5 * (static_cast<double>(Window[Middle - 1])
+ static_cast<double>(Window[Middle]));
Residuals[AnalysisIndex] = static_cast<float>(
static_cast<double>(Samples[Index]) - Median);
}
const double Residual = static_cast<double>(Residuals[AnalysisIndex]);
const double Angle = 2.0 * PI * static_cast<double>(Frequency)
* static_cast<double>(AnalysisIndex) / static_cast<double>(AnalysisCount);
Real += Residual * FMath::Cos(static_cast<float>(Angle));
Imaginary -= Residual * FMath::Sin(static_cast<float>(Angle));
}
const double Amplitude = 2.0 * FMath::Sqrt(Real * Real + Imaginary * Imaginary)
/ static_cast<double>(AnalysisCount);
if (Amplitude > BestAmplitude)
{
BestAmplitude = Amplitude;
BestFrequency = Frequency;
}
}
OutPeriodicAmplitudeVoxels = static_cast<float>(BestAmplitude);
OutPeriodicPeriodVoxels = BestFrequency > 0
? StepVoxels * static_cast<float>(AnalysisCount)
/ static_cast<float>(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<float>(World.TargetBottomWorldZ) / static_cast<float>(CHUNK_SIZE));
const int32 TargetTopChunkZ = FMath::FloorToInt(
(static_cast<float>(World.TargetTopWorldZ) - KINDA_SMALL_NUMBER)
/ static_cast<float>(CHUNK_SIZE));
const float TargetMinZ = static_cast<float>(World.TargetBottomWorldZ);
const float TargetMaxZ = static_cast<float>(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<uint32>(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<float>(CandidateChunk.Z) + 0.5f)
* static_cast<float>(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<FStrateTerrainOpEntry>* 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<FVector>& Points = Tunnel.WorldControlPoints.Num() >= 2
&& Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num()
? Tunnel.WorldControlPoints : Tunnel.ControlPoints;
const TArray<float>& Radii = Tunnel.WorldControlRadii.Num() == Points.Num()
? Tunnel.WorldControlRadii : Tunnel.ControlRadii;
if (Points.Num() < 2 || Radii.Num() != Points.Num())
{
continue;
}
TArray<float> 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<float>(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<float>(CHUNK_SIZE)),
FMath::FloorToInt(Point.Y / static_cast<float>(CHUNK_SIZE)),
FMath::FloorToInt(Point.Z / static_cast<float>(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<float>(CHUNK_SIZE)),
FMath::FloorToInt(RadialPoint.Y / static_cast<float>(CHUNK_SIZE)),
FMath::FloorToInt(RadialPoint.Z / static_cast<float>(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<FVector>& Points = BestTunnel->WorldControlPoints.Num() >= 2
&& BestTunnel->WorldControlRadii.Num() == BestTunnel->WorldControlPoints.Num()
? BestTunnel->WorldControlPoints : BestTunnel->ControlPoints;
TArray<float> 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<float>(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<float>(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<float>(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<float>& 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<float>(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<float>(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<float>::quiet_NaN();
Roots[AxisIndex][1] = std::numeric_limits<float>::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<float>& MouthRadii = Radii;
const TArray<FVector>& 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<uint32> 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<float>(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<float>(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<float>(Endpoint.Mouth.Z)
- Endpoint.RadiusVoxels - 8.0f;
for (int32 SampleIndex = 0; SampleIndex < LateralSamples; ++SampleIndex)
{
const float LateralOffset = -ActualHalfWidth
+ static_cast<float>(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<float>::quiet_NaN();
float FloorCrossing = std::numeric_limits<float>::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<float>(X), static_cast<float>(Y), static_cast<float>(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<double>(Axis.CoreMeanCrossingVoxels));
Writer.WriteValue(TEXT("final_mean_crossing_voxels"), static_cast<double>(Axis.FinalMeanCrossingVoxels));
Writer.WriteValue(TEXT("core_min_crossing_voxels"), static_cast<double>(Axis.CoreMinCrossingVoxels));
Writer.WriteValue(TEXT("core_max_crossing_voxels"), static_cast<double>(Axis.CoreMaxCrossingVoxels));
Writer.WriteValue(TEXT("final_min_crossing_voxels"), static_cast<double>(Axis.FinalMinCrossingVoxels));
Writer.WriteValue(TEXT("final_max_crossing_voxels"), static_cast<double>(Axis.FinalMaxCrossingVoxels));
Writer.WriteValue(TEXT("core_periodic_amplitude_voxels"), static_cast<double>(Axis.CorePeriodicAmplitudeVoxels));
Writer.WriteValue(TEXT("core_periodic_amplitude_cm"), static_cast<double>(Axis.CorePeriodicAmplitudeVoxels) * 25.0);
Writer.WriteValue(TEXT("core_periodic_period_voxels"), static_cast<double>(Axis.CorePeriodicPeriodVoxels));
Writer.WriteValue(TEXT("core_periodic_measured_samples"), Axis.CorePeriodicMeasuredSamples);
Writer.WriteValue(TEXT("core_periodic_measured_length_voxels"), static_cast<double>(Axis.CorePeriodicMeasuredLengthVoxels));
Writer.WriteValue(TEXT("final_periodic_amplitude_voxels"), static_cast<double>(Axis.FinalPeriodicAmplitudeVoxels));
Writer.WriteValue(TEXT("final_periodic_amplitude_cm"), static_cast<double>(Axis.FinalPeriodicAmplitudeVoxels) * 25.0);
Writer.WriteValue(TEXT("final_periodic_period_voxels"), static_cast<double>(Axis.FinalPeriodicPeriodVoxels));
Writer.WriteValue(TEXT("final_periodic_measured_samples"), Axis.FinalPeriodicMeasuredSamples);
Writer.WriteValue(TEXT("final_periodic_measured_length_voxels"), static_cast<double>(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<double>(Axis.MeanCrossingVoxels));
Writer.WriteValue(TEXT("min_crossing_voxels"), static_cast<double>(Axis.MinCrossingVoxels));
Writer.WriteValue(TEXT("max_crossing_voxels"), static_cast<double>(Axis.MaxCrossingVoxels));
Writer.WriteValue(TEXT("periodic_amplitude_voxels"), static_cast<double>(Axis.PeriodicAmplitudeVoxels));
Writer.WriteValue(TEXT("periodic_amplitude_cm"), static_cast<double>(Axis.PeriodicAmplitudeVoxels) * 25.0);
Writer.WriteValue(TEXT("periodic_period_voxels"), static_cast<double>(Axis.PeriodicPeriodVoxels));
Writer.WriteValue(TEXT("periodic_measured_samples"), Axis.PeriodicMeasuredSamples);
Writer.WriteValue(TEXT("periodic_measured_length_voxels"), static_cast<double>(Axis.PeriodicMeasuredLengthVoxels));
Writer.WriteValue(TEXT("local_median_periodic_amplitude_voxels"), static_cast<double>(Axis.LocalMedianPeriodicAmplitudeVoxels));
Writer.WriteValue(TEXT("local_median_periodic_amplitude_cm"), static_cast<double>(Axis.LocalMedianPeriodicAmplitudeVoxels) * 25.0);
Writer.WriteValue(TEXT("local_median_periodic_period_voxels"), static_cast<double>(Axis.LocalMedianPeriodicPeriodVoxels));
Writer.WriteObjectEnd();
}
void WritePlayerDimensions(FExploreJsonWriter& Writer, const TCHAR* Key)
{
Writer.WriteObjectStart(Key);
Writer.WriteValue(TEXT("capsule_radius_m"), static_cast<double>(FVoxelPlayerCapsuleConstants::RadiusCentimeters) / 100.0);
Writer.WriteValue(TEXT("capsule_width_m"), static_cast<double>(2.0f * FVoxelPlayerCapsuleConstants::RadiusCentimeters) / 100.0);
Writer.WriteValue(TEXT("capsule_half_height_m"), static_cast<double>(FVoxelPlayerCapsuleConstants::HalfHeightCentimeters) / 100.0);
Writer.WriteValue(TEXT("capsule_height_m"), static_cast<double>(2.0f * FVoxelPlayerCapsuleConstants::HalfHeightCentimeters) / 100.0);
Writer.WriteValue(TEXT("max_step_height_m"), static_cast<double>(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<double>(FVoxelPlayerCapsuleConstants::WalkableFloorAngleDegrees));
Writer.WriteObjectEnd();
}
FString BuildExploreJson(
const FExploreArguments& Arguments,
const FExploreWorld& World,
const FExploreRunOutput& Output,
bool bDeterminismAssertion,
uint32 PayloadCrc32)
{
FString Json;
TSharedRef<FExploreJsonWriter> Writer =
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::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("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<double>(Arguments.WormStrength));
}
Writer->WriteValue(TEXT("tunnel_ablation_mask"),
static_cast<int64>(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<double>(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<double>(Arguments.RoomMouthRiseBlend));
}
Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride);
if (Arguments.bSurfaceRoughnessOverride)
{
Writer->WriteValue(TEXT("surface_roughness"),
static_cast<double>(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<double>(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<double>(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("game_runtime_config"), Arguments.bGameRuntimeConfig);
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<double>(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<double>(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 -modes=%s -blockearlyout=%d "
"-out=<ABSOLUTE_PLUGIN_SAVED_PATH>"),
Arguments.Seed,
ArchetypeName(Arguments.Archetype),
Arguments.Slot,
*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<double>(Output.DensitySlice.MinDensity));
Writer->WriteValue(TEXT("max_density"), static_cast<double>(Output.DensitySlice.MaxDensity));
Writer->WriteValue(TEXT("max_abs_density"), static_cast<double>(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<double>(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<double>(Output.TunnelCoreProbe.StepVoxels));
Writer->WriteValue(TEXT("length_voxels"), static_cast<double>(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<double>(Output.TunnelCoreProbe.CoreCenterMin));
Writer->WriteValue(TEXT("core_center_max"), static_cast<double>(Output.TunnelCoreProbe.CoreCenterMax));
Writer->WriteValue(TEXT("final_center_min"), static_cast<double>(Output.TunnelCoreProbe.FinalCenterMin));
Writer->WriteValue(TEXT("final_center_max"), static_cast<double>(Output.TunnelCoreProbe.FinalCenterMax));
Writer->WriteValue(TEXT("final_center_max_abs"), static_cast<double>(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<double>(Output.TunnelCoreProbe.Mouth.LateralStepVoxels));
Writer->WriteValue(TEXT("lateral_window_voxels"), static_cast<double>(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<double>(Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels));
Writer->WriteValue(TEXT("max_wall_periodic_amplitude_cm"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels) * 25.0);
Writer->WriteValue(TEXT("max_floor_periodic_amplitude_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels));
Writer->WriteValue(TEXT("max_floor_periodic_amplitude_cm"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels) * 25.0);
Writer->WriteValue(TEXT("max_local_median_wall_periodic_amplitude_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels));
Writer->WriteValue(TEXT("max_local_median_wall_periodic_amplitude_cm"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels) * 25.0);
Writer->WriteValue(TEXT("max_local_median_floor_periodic_amplitude_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels));
Writer->WriteValue(TEXT("max_local_median_floor_periodic_amplitude_cm"), static_cast<double>(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<int64>(Endpoint.RoomHash));
WriteJsonVector(*Writer, TEXT("mouth_voxels"), Endpoint.Mouth, 1.0f);
Writer->WriteValue(TEXT("radius_voxels"), static_cast<double>(Endpoint.RadiusVoxels));
Writer->WriteValue(TEXT("tunnel_floor_z"), static_cast<double>(Endpoint.TunnelFloorZ));
Writer->WriteValue(TEXT("room_floor_crossing_z"), static_cast<double>(Endpoint.RoomFloorCrossingZ));
Writer->WriteValue(TEXT("floor_join_delta_voxels"), static_cast<double>(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<double>(Arguments.ExportSize)
/ static_cast<double>(Arguments.ExportSize)
/ static_cast<double>(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("absolute_tile_x"),
World.ExploreMeshOrigin.X / CHUNK_SIZE + TileX);
Writer->WriteValue(TEXT("absolute_tile_y"),
World.ExploreMeshOrigin.Y / CHUNK_SIZE + TileY);
Writer->WriteValue(TEXT("absolute_tile_z"),
World.ExploreMeshOrigin.Z / CHUNK_SIZE + TileZ);
Writer->WriteValue(TEXT("origin_x_voxels"),
World.ExploreMeshOrigin.X + TileX * CHUNK_SIZE);
Writer->WriteValue(TEXT("origin_y_voxels"),
World.ExploreMeshOrigin.Y + TileY * CHUNK_SIZE);
Writer->WriteValue(TEXT("origin_z_voxels"),
World.ExploreMeshOrigin.Z + TileZ * CHUNK_SIZE);
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<VoxelDensityProfile::ECounter>(Index)),
static_cast<int64>(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<double>(World.TargetBottomWorldZ) * 0.25);
Writer->WriteValue(TEXT("target_top_m_exclusive"), static_cast<double>(World.TargetTopWorldZ) * 0.25);
Writer->WriteValue(TEXT("target_boundary_seal_thickness_voxels"),
static_cast<double>(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<double>(Output.Render.FixedStepVoxels));
Writer->WriteValue(TEXT("fixed_step_m"), static_cast<double>(Output.Render.FixedStepMeters));
Writer->WriteValue(TEXT("max_distance_voxels"), static_cast<double>(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<int64>(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<int32>(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<double>(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<double>(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<double>(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<double>(Report.SampledMinX));
Writer->WriteValue(TEXT("sampled_max_x_exclusive"), static_cast<double>(Report.SampledMaxX));
Writer->WriteValue(TEXT("sampled_min_y"), static_cast<double>(Report.SampledMinY));
Writer->WriteValue(TEXT("sampled_max_y_exclusive"), static_cast<double>(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<double>(Report.DistanceTravelledMeters));
Writer->WriteValue(TEXT("straight_line_m"), static_cast<double>(Report.StraightLineMeters));
Writer->WriteValue(TEXT("tortuosity"), static_cast<double>(Report.Tortuosity));
Writer->WriteValue(TEXT("dead_ends_encountered"), Report.DeadEndsEncountered);
Writer->WriteValue(TEXT("dead_ends_per_100m"), static_cast<double>(Report.DeadEndsPer100m));
Writer->WriteValue(TEXT("capsule_radius_m"), static_cast<double>(Report.CapsuleRadiusMeters));
Writer->WriteValue(TEXT("capsule_width_m"), static_cast<double>(Report.CapsuleWidthMeters));
Writer->WriteValue(TEXT("capsule_height_m"), static_cast<double>(Report.CapsuleHeightMeters));
Writer->WriteValue(TEXT("narrow_gap_threshold_m"), static_cast<double>(Report.NarrowGapThresholdMeters));
Writer->WriteValue(TEXT("narrow_gap_traversals"), Report.NarrowGapTraversals);
Writer->WriteValue(TEXT("narrow_gap_fraction"), static_cast<double>(Report.NarrowGapFraction));
Writer->WriteValue(TEXT("narrow_gap_events_per_100m"), static_cast<double>(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<double>(Report.MouthComponentGapVoxels));
Writer->WriteValue(TEXT("mouth_component_gap_m"), static_cast<double>(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<double>(Report.AgentToTargetComponentGapVoxels));
Writer->WriteValue(TEXT("agent_to_target_component_gap_m"),
static_cast<double>(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<double>(OriginReport.ReachableSetToOriginColumnVoxels));
Writer->WriteValue(TEXT("reachable_set_to_origin_column_m"),
static_cast<double>(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<double>(OriginCheckReport.SampledMinX));
Writer->WriteValue(TEXT("sampled_max_x_exclusive"), static_cast<double>(OriginCheckReport.SampledMaxX));
Writer->WriteValue(TEXT("sampled_min_y"), static_cast<double>(OriginCheckReport.SampledMinY));
Writer->WriteValue(TEXT("sampled_max_y_exclusive"), static_cast<double>(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<double>(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<double>(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<double>(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<double>(OriginCheckReport.ReachableSetToOriginColumnVoxels));
Writer->WriteValue(TEXT("reachable_set_to_origin_column_m"),
static_cast<double>(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<int32>(Output.Profile.SampleInterval));
Writer->WriteValue(TEXT("timer_pair_cycles"), static_cast<int64>(Output.Profile.TimerPairCycles));
Writer->WriteValue(TEXT("timer_pair_microseconds"),
static_cast<double>(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<VoxelDensityProfile::ECounter>(Index)),
static_cast<int64>(Output.Profile.Counters[Index]));
}
Writer->WriteObjectEnd();
const VoxelDensityProfile::FSnapshot::FClassifierStats& Classifier = Output.Profile.Classifier;
Writer->WriteObjectStart(TEXT("classifier"));
Writer->WriteValue(TEXT("calls"), static_cast<int64>(Classifier.Calls));
Writer->WriteValue(TEXT("sampled_calls"), static_cast<int64>(Classifier.SampledCalls));
Writer->WriteValue(TEXT("sample_fraction"), Classifier.Calls > 0
? static_cast<double>(Classifier.SampledCalls) / static_cast<double>(Classifier.Calls)
: 0.0);
Writer->WriteValue(TEXT("refine_nodes"), static_cast<int64>(Classifier.RefineNodes));
Writer->WriteValue(TEXT("interval_stack_box_calls"), static_cast<int64>(Classifier.StackBoxCalls));
Writer->WriteValue(TEXT("whole_mixed_nodes"), static_cast<int64>(Classifier.WholeMixedNodes));
Writer->WriteValue(TEXT("whole_solid_nodes"), static_cast<int64>(Classifier.WholeSolidNodes));
Writer->WriteValue(TEXT("whole_air_nodes"), static_cast<int64>(Classifier.WholeAirNodes));
Writer->WriteValue(TEXT("needs_final_field_nodes"), static_cast<int64>(Classifier.NeedsFinalFieldNodes));
Writer->WriteValue(TEXT("split_nodes"), static_cast<int64>(Classifier.SplitNodes));
Writer->WriteValue(TEXT("max_refinement_depth"), static_cast<int64>(Classifier.MaxRefinementDepth));
Writer->WriteValue(TEXT("exact_core_samples"), static_cast<int64>(Classifier.ExactCoreSamples));
Writer->WriteValue(TEXT("exact_final_samples"), static_cast<int64>(Classifier.ExactFinalSamples));
Writer->WriteValue(TEXT("exact_core_cache_hits"), static_cast<int64>(Classifier.ExactCoreCacheHits));
Writer->WriteValue(TEXT("exact_final_cache_hits"), static_cast<int64>(Classifier.ExactFinalCacheHits));
Writer->WriteValue(TEXT("exact_core_leaves"), static_cast<int64>(Classifier.ExactCoreLeaves));
Writer->WriteValue(TEXT("exact_final_leaves"), static_cast<int64>(Classifier.ExactFinalLeaves));
Writer->WriteValue(TEXT("interval_stack_box_us"), static_cast<double>(Classifier.StackBoxCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("exact_core_us"), static_cast<double>(Classifier.ExactCoreCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("exact_final_us"), static_cast<double>(Classifier.ExactFinalCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("room_tail_queries"), static_cast<int64>(Classifier.RoomTailQueries));
Writer->WriteValue(TEXT("room_tail_evaluated"), static_cast<int64>(Classifier.RoomTailEvaluated));
Writer->WriteValue(TEXT("room_tail_us"), static_cast<double>(Classifier.RoomTailCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("room_propagate_us"), static_cast<double>(Classifier.RoomPropagateCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("room_exact_primitive_us"), static_cast<double>(Classifier.RoomExactPrimitiveCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("room_cache_window_us"), static_cast<double>(Classifier.RoomCacheWindowCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("room_count"), static_cast<int64>(Classifier.RoomNumRooms));
Writer->WriteValue(TEXT("tunnel_count"), static_cast<int64>(Classifier.RoomNumTunnels));
Writer->WriteValue(TEXT("room_floor_join_count"), static_cast<int64>(Classifier.RoomNumRoomFloorJoins));
Writer->WriteValue(TEXT("pit_count"), static_cast<int64>(Classifier.RoomNumPits));
Writer->WriteValue(TEXT("chimney_count"), static_cast<int64>(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<int64>(Breakdown.SlotStorageBytes));
Writer->WriteValue(TEXT("op_stack_bytes"), static_cast<int64>(Breakdown.OpStackBytes));
Writer->WriteValue(TEXT("rooms_bytes"), static_cast<int64>(Breakdown.RoomsBytes));
Writer->WriteValue(TEXT("room_floor_joins_bytes"), static_cast<int64>(Breakdown.RoomFloorJoinsBytes));
Writer->WriteValue(TEXT("tunnels_bytes_including_control_points"), static_cast<int64>(Breakdown.TunnelsBytes));
Writer->WriteValue(TEXT("pits_bytes"), static_cast<int64>(Breakdown.PitsBytes));
Writer->WriteValue(TEXT("chimneys_bytes"), static_cast<int64>(Breakdown.ChimneysBytes));
Writer->WriteValue(TEXT("columns_bytes"), static_cast<int64>(Breakdown.ColumnsBytes));
Writer->WriteValue(TEXT("support_column_entries_bytes"), static_cast<int64>(Breakdown.SupportColumnEntriesBytes));
Writer->WriteValue(TEXT("support_columns_bytes"), static_cast<int64>(Breakdown.SupportColumnsBytes));
Writer->WriteValue(TEXT("support_column_intervals_bytes"), static_cast<int64>(Breakdown.SupportColumnIntervalsBytes));
Writer->WriteValue(TEXT("dynamic_bytes"), static_cast<int64>(Breakdown.DynamicBytes()));
Writer->WriteValue(TEXT("total_bytes"), static_cast<int64>(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<VoxelDensityProfile::EBucket>(Index)));
Writer->WriteValue(TEXT("calls"), static_cast<int64>(Output.Profile.Calls[Index]));
Writer->WriteValue(TEXT("samples"), static_cast<int64>(Output.Profile.Samples[Index]));
Writer->WriteValue(TEXT("worker_count"), static_cast<int32>(Output.Profile.ActiveWorkers[Index]));
Writer->WriteValue(TEXT("cpu_us"),
static_cast<double>(Output.Profile.Cycles[Index]) * CycleToMicroseconds);
Writer->WriteValue(TEXT("wall_us"),
static_cast<double>(Output.Profile.WallCycles[Index]) * CycleToMicroseconds);
Writer->WriteValue(TEXT("total_us"),
static_cast<double>(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<int32>(Phase)];
}
const uint64 DensityParentCycles = Output.Profile.WallCycles[
static_cast<int32>(VoxelDensityProfile::EBucket::GetDensityAt)];
const int64 DensityRawGap = static_cast<int64>(DensityParentCycles)
- static_cast<int64>(DensityPhaseCycles);
Writer->WriteObjectStart(TEXT("density_ledger"));
Writer->WriteValue(TEXT("parent_us"), static_cast<double>(DensityParentCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("raw_phase_sum_us"), static_cast<double>(DensityPhaseCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("unattributed_us"), static_cast<double>(DensityRawGap) * CycleToMicroseconds);
Writer->WriteValue(TEXT("ledger_sum_us"), static_cast<double>(DensityParentCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("raw_phase_sum_matches_parent"),
FMath::Abs(static_cast<double>(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();
}
// 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<double>(Diagnostic.ActualDensityDeltaMin));
const double ActualFill = FMath::Max(
0.0, static_cast<double>(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<double>(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<double>(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<int32>& StackSolidKillerCounts,
const TArray<int32>& StackAirKillerCounts,
int32 StackVerdictMismatches,
const TArray<FExploreOpBoundsAggregate>& Aggregates,
FString& OutError)
{
FString Json;
TSharedRef<FExploreJsonWriter> Writer =
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::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("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<double>(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<int64>(Aggregate.Blocks));
Writer->WriteValue(TEXT("sample_points"), static_cast<int64>(Aggregate.Samples));
Writer->WriteValue(TEXT("forced_boxes"), static_cast<int64>(Aggregate.ForcedBoxes));
Writer->WriteValue(TEXT("forced_all_solid_boxes"), static_cast<int64>(Aggregate.ForcedSolidBoxes));
Writer->WriteValue(TEXT("forced_all_air_boxes"), static_cast<int64>(Aggregate.ForcedAirBoxes));
Writer->WriteValue(TEXT("forced_margin_supremum"), static_cast<double>(Aggregate.MaxForcedMargin));
Writer->WriteObjectStart(TEXT("effect_counts"));
Writer->WriteValue(TEXT("Identity"), static_cast<int64>(Aggregate.EffectCounts[0]));
Writer->WriteValue(TEXT("CarveOnly"), static_cast<int64>(Aggregate.EffectCounts[1]));
Writer->WriteValue(TEXT("FillOnly"), static_cast<int64>(Aggregate.EffectCounts[2]));
Writer->WriteValue(TEXT("Both"), static_cast<int64>(Aggregate.EffectCounts[3]));
Writer->WriteObjectEnd();
Writer->WriteValue(TEXT("carve_bound_known"), Aggregate.bCarveBoundKnown);
Writer->WriteValue(TEXT("unknown_carve_bound_boxes"),
static_cast<int64>(Aggregate.UnknownCarveBoundBoxes));
Writer->WriteValue(TEXT("carve_bound_supremum"), static_cast<double>(
Aggregate.bCarveBoundKnown ? Aggregate.MaxCarveSupremum : 0.0f));
Writer->WriteValue(TEXT("fill_bound_known"), Aggregate.bFillBoundKnown);
Writer->WriteValue(TEXT("unknown_fill_bound_boxes"),
static_cast<int64>(Aggregate.UnknownFillBoundBoxes));
Writer->WriteValue(TEXT("fill_bound_supremum"), static_cast<double>(
Aggregate.bFillBoundKnown ? Aggregate.MaxFillSupremum : 0.0f));
Writer->WriteValue(TEXT("actual_density_delta_min"), static_cast<double>(
Aggregate.bHasDensityActual ? Aggregate.ActualDensityDeltaMin : 0.0f));
Writer->WriteValue(TEXT("actual_density_delta_max"), static_cast<double>(
Aggregate.bHasDensityActual ? Aggregate.ActualDensityDeltaMax : 0.0f));
Writer->WriteValue(TEXT("actual_carve_supremum"), Aggregate.bHasDensityActual
? static_cast<double>(FMath::Max(0.0f, -Aggregate.ActualDensityDeltaMin)) : 0.0);
Writer->WriteValue(TEXT("actual_fill_supremum"), Aggregate.bHasDensityActual
? static_cast<double>(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<int64>(Aggregate.CarveBoundViolations));
Writer->WriteValue(TEXT("fill_bound_violations"), static_cast<int64>(Aggregate.FillBoundViolations));
Writer->WriteValue(TEXT("sdf_bound_known"), Aggregate.bSdfBoundKnown);
Writer->WriteValue(TEXT("sdf_bound_min"), static_cast<double>(
Aggregate.bHasSdfBound ? Aggregate.SdfBoundMin : 0.0f));
Writer->WriteValue(TEXT("sdf_bound_max"), static_cast<double>(
Aggregate.bHasSdfBound ? Aggregate.SdfBoundMax : 0.0f));
Writer->WriteValue(TEXT("actual_sdf_min"), static_cast<double>(
Aggregate.bHasSdfActual ? Aggregate.ActualSdfMin : 0.0f));
Writer->WriteValue(TEXT("actual_sdf_max"), static_cast<double>(
Aggregate.bHasSdfActual ? Aggregate.ActualSdfMax : 0.0f));
Writer->WriteValue(TEXT("sdf_bound_violations"), static_cast<int64>(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)
{
OutError = TEXT("opbounds requires archetype=TunnelNetwork.");
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<uint32>(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<int32> StackSolidKillerCounts;
TArray<int32> 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<FExploreOpBoundsAggregate> Aggregates;
TArray<FVoxelOpStack::FOpBoxDiagnostic> 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<uint32>(Arguments.Seed)
^ (static_cast<uint32>(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<int32>(BaseHash % 14u);
const int32 BY = 1 + static_cast<int32>(VoxelHash::Mix(BaseHash ^ 0xA341316Cu) % 14u);
const int32 BZ = 1 + static_cast<int32>(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<double>(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<double>(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<<Level here would measure
// a denser mesh than the game generates at levels >= 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<int32>(VoxelDensityProfile::EBucket::GetDensityAt);
const uint64 Calls = Profile.Calls[DensityIndex];
const uint64 Lookups = Profile.Counters[static_cast<int32>(VoxelDensityProfile::ECounter::TunnelCacheLookup)];
const uint64 Hits = Profile.Counters[static_cast<int32>(VoxelDensityProfile::ECounter::TunnelCacheHit)];
const uint64 SdfBuilds = Profile.Counters[static_cast<int32>(VoxelDensityProfile::ECounter::SdfCacheBuild)];
const double DensityUs = static_cast<double>(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<unsigned long long>(Calls),
Calls > 0 ? DensityUs / static_cast<double>(Calls) : 0.0,
static_cast<unsigned long long>(SdfBuilds),
static_cast<unsigned long long>(Lookups),
static_cast<unsigned long long>(Hits),
static_cast<unsigned long long>(Profile.Counters[static_cast<int32>(VoxelDensityProfile::ECounter::TunnelCacheMiss)]),
Lookups > 0 ? static_cast<double>(Hits) / static_cast<double>(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<int32>(VoxelDensityProfile::EBucket::GetDensityAt)];
const uint64 DensityCycles = Profile.WallCycles[static_cast<int32>(VoxelDensityProfile::EBucket::GetDensityAt)];
const double DensityTotalUs = static_cast<double>(DensityCycles) * CycleToMicroseconds;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeDensityProfile] total calls=%llu total_us=%.3f us_per_call=%.6f"),
static_cast<unsigned long long>(DensityCalls), DensityTotalUs,
DensityCalls > 0 ? DensityTotalUs / static_cast<double>(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<unsigned long long>(CacheWorkers),
static_cast<unsigned long long>(CacheCapacityEntries),
static_cast<unsigned long long>(CacheValidEntries),
static_cast<unsigned long long>(CacheTotalStaticBytes),
static_cast<unsigned long long>(CacheTotalDynamicBytes),
static_cast<unsigned long long>(CacheTotalBytes),
CacheValidEntries > 0
? static_cast<double>(CacheEntryBytes) / static_cast<double>(CacheValidEntries)
: 0.0,
static_cast<unsigned long long>(CacheLargestEntryBytes),
CacheWorkers > 0
? static_cast<double>(CacheTotalBytes) / static_cast<double>(CacheWorkers)
: 0.0,
static_cast<unsigned long long>(CacheLargestWorkerBytes),
Profile.TunnelCacheValidEntries > 0
? static_cast<double>(Profile.TunnelCacheEntryBytes)
/ static_cast<double>(Profile.TunnelCacheValidEntries)
: 0.0,
static_cast<unsigned long long>(Profile.TunnelCacheLargestEntryBytes),
static_cast<unsigned long long>(Profile.TunnelCacheLargestWorkerValidEntries),
Profile.RoomGraphCacheValidEntries > 0
? static_cast<double>(Profile.RoomGraphCacheEntryBytes)
/ static_cast<double>(Profile.RoomGraphCacheValidEntries)
: 0.0,
static_cast<unsigned long long>(Profile.RoomGraphCacheLargestEntryBytes),
static_cast<unsigned long long>(Profile.RoomGraphCacheLargestWorkerValidEntries),
static_cast<unsigned long long>(Profile.TunnelCacheWorkers),
static_cast<unsigned long long>(Profile.TunnelCacheCapacityEntries),
static_cast<unsigned long long>(Profile.TunnelCacheValidEntries),
static_cast<unsigned long long>(Profile.TunnelCacheStaticBytes),
static_cast<unsigned long long>(Profile.TunnelCacheDynamicBytes),
static_cast<unsigned long long>(Profile.RoomGraphCacheWorkers),
static_cast<unsigned long long>(Profile.RoomGraphCacheCapacityEntries),
static_cast<unsigned long long>(Profile.RoomGraphCacheValidEntries),
static_cast<unsigned long long>(Profile.RoomGraphCacheStaticBytes),
static_cast<unsigned long long>(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<int32>(Phase);
DensityAttributionCycles += Profile.WallCycles[PhaseIndex];
DensityAttributionCalls += Profile.Calls[PhaseIndex];
}
const double DensityAttributionUs =
static_cast<double>(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<unsigned long long>(DensityAttributionCalls));
for (int32 Index = 0; Index < VoxelDensityProfile::CounterCount; ++Index)
{
const VoxelDensityProfile::ECounter Counter =
static_cast<VoxelDensityProfile::ECounter>(Index);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeDensityProfile] counter=%s value=%llu per_density_call=%.3f"),
VoxelDensityProfile::CounterName(Counter),
static_cast<unsigned long long>(Profile.Counters[Index]),
DensityCalls > 0
? static_cast<double>(Profile.Counters[Index])
/ static_cast<double>(DensityCalls)
: 0.0);
}
for (int32 Index = 0; Index < VoxelDensityProfile::BucketCount; ++Index)
{
const uint64 Calls = Profile.Calls[Index];
if (Calls == 0 || Index == static_cast<int32>(VoxelDensityProfile::EBucket::GetDensityAt))
{
continue;
}
const double TotalUs = static_cast<double>(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<VoxelDensityProfile::EBucket>(Index)),
static_cast<unsigned long long>(Calls), TotalUs,
TotalUs / static_cast<double>(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<int32>(Bucket);
MesherAttributionCycles += Profile.WallCycles[BucketIndex];
MesherAttributionCalls += Profile.Calls[BucketIndex];
}
const int32 MesherTotalIndex = static_cast<int32>(
VoxelDensityProfile::EBucket::MesherGenerateMesh);
const double MesherTotalUs = static_cast<double>(
Profile.WallCycles[MesherTotalIndex]) * CycleToMicroseconds;
const double MesherAttributionUs = static_cast<double>(
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<unsigned long long>(Profile.Calls[MesherTotalIndex]),
MesherTotalUs, MesherAttributionUs, MesherTotalUs - MesherAttributionUs,
MesherTotalUs > 0.0 ? MesherAttributionUs * 100.0 / MesherTotalUs : 0.0,
static_cast<unsigned long long>(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<FJsonObject>& 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<FJsonObject>& Object, const TCHAR* Key, FString& OutValue)
{
if (!Object.IsValid() || !Object->HasField(Key))
{
return false;
}
OutValue = Object->GetStringField(Key);
return true;
}
bool JsonBool(const TSharedPtr<FJsonObject>& Object, const TCHAR* Key, bool& OutValue)
{
if (!Object.IsValid() || !Object->HasField(Key))
{
return false;
}
OutValue = Object->GetBoolField(Key);
return true;
}
bool BuildBatchCaseParams(
const TSharedPtr<FJsonObject>& 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 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<int32>(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 "
"-exportsize=%d -exportstep=%d -densitygridreuse=%d "
"-blockearlyout=%d -meshminbatch=%d "
"-failurefocus=%d -out=\"%s\""),
Seed, *Archetype, Slot, *Modes,
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<const TCHAR*, const TCHAR*> 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<const TCHAR*, const TCHAR*> 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<const TCHAR*, const TCHAR*> 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=<json> and an absolute -out=<Saved path>."));
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<FJsonObject> Root;
const TSharedRef<TJsonReader<>> 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<TSharedPtr<FJsonValue>>* 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<FString> UsedCaseIds;
TArray<TSharedPtr<FJsonValue>> 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<FJsonObject> 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<FJsonObject> CaseReport = MakeShared<FJsonObject>();
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<FJsonValueObject>(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<FJsonObject> CaseReport;
const TSharedRef<TJsonReader<>> CaseReader = TJsonReaderFactory<>::Create(CaseJson);
if (!FJsonSerializer::Deserialize(CaseReader, CaseReport) || !CaseReport.IsValid())
{
CaseReport = MakeShared<FJsonObject>();
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<FJsonValueObject>(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<FJsonObject> BatchReport = MakeShared<FJsonObject>();
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<TJsonWriter<>> 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);
}