#include "VoxelForgeExploreCommandlet.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 "IImageWrapper.h" #include "IImageWrapperModule.h" #include "Misc/CommandLine.h" #include "Misc/Crc.h" #include "Misc/FileHelper.h" #include "Misc/Parse.h" #include "Misc/Paths.h" #include "Modules/ModuleManager.h" #include "Policies/CondensedJsonPrintPolicy.h" #include "Serialization/Archive.h" #include "Serialization/JsonWriter.h" #include "UObject/Package.h" #include "UObject/StrongObjectPtr.h" #include "UObject/UObjectGlobals.h" #include "VoxelDiffLayer.h" #include "VoxelDensityProfile.h" #include "VoxelGenerator.h" #include "VoxelMarchingCubesMesher.h" #include "VoxelNoise.h" #include "VoxelSettings.h" #include "VoxelStrateDefinition.h" #include "VoxelStrateManager.h" #include "VoxelStrateMeasure.h" #include "VoxelTypes.h" #include #include namespace { using FExploreJsonWriter = TJsonWriter>; constexpr int32 DefaultRenderWidth = 512; constexpr int32 DefaultRenderHeight = 288; constexpr float DefaultRenderStepVoxels = 0.25f; constexpr float DefaultRenderMaxDistanceVoxels = 160.0f; constexpr int32 DefaultExportSize = 128; constexpr int32 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; const TCHAR* ArchetypeName(ECaveGeneratorType Archetype) { switch (Archetype) { case ECaveGeneratorType::TunnelNetwork: return TEXT("TunnelNetwork"); case ECaveGeneratorType::FlatPlain: return TEXT("FlatPlain"); case ECaveGeneratorType::CrystalChamber: return TEXT("CrystalChamber"); case ECaveGeneratorType::Maze: return TEXT("Maze"); case ECaveGeneratorType::SurfaceWorld: return TEXT("SurfaceWorld"); case ECaveGeneratorType::VerticalShafts: return TEXT("VerticalShafts"); case ECaveGeneratorType::FloatingIslands: return TEXT("FloatingIslands"); case ECaveGeneratorType::Underwater: return TEXT("Underwater"); default: return TEXT("Unknown"); } } const TCHAR* ConnectivityResultName(EVoxelConnectivityResult Result) { switch (Result) { case EVoxelConnectivityResult::Connected: return TEXT("Connected"); case EVoxelConnectivityResult::NotConnectedAtThisResolution: return TEXT("NotConnectedAtThisResolution"); case EVoxelConnectivityResult::StartCellSolid: return TEXT("StartCellSolid"); case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GoalCellSolid"); case EVoxelConnectivityResult::OutOfWindow: return TEXT("OutOfWindow"); case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return TEXT("CoarseLiedBudgetExhausted"); case EVoxelConnectivityResult::StartCellNotPlayerFit: return TEXT("StartCellNotPlayerFit"); case EVoxelConnectivityResult::GoalCellNotPlayerFit: return TEXT("GoalCellNotPlayerFit"); default: return TEXT("Unknown"); } } bool ParseArchetype(const FString& Text, ECaveGeneratorType& OutArchetype) { FString Normalized = Text; Normalized.TrimStartAndEndInline(); Normalized.ToLowerInline(); Normalized.ReplaceInline(TEXT("_"), TEXT("")); Normalized.ReplaceInline(TEXT("-"), TEXT("")); const TPair Names[] = { { TEXT("tunnelnetwork"), ECaveGeneratorType::TunnelNetwork }, { TEXT("flatplain"), ECaveGeneratorType::FlatPlain }, { TEXT("crystalchamber"), ECaveGeneratorType::CrystalChamber }, { TEXT("maze"), ECaveGeneratorType::Maze }, { TEXT("surfaceworld"), ECaveGeneratorType::SurfaceWorld }, { TEXT("verticalshafts"), ECaveGeneratorType::VerticalShafts }, { TEXT("floatingislands"), ECaveGeneratorType::FloatingIslands }, { TEXT("floatingisland"), ECaveGeneratorType::FloatingIslands }, { TEXT("underwater"), ECaveGeneratorType::Underwater }, }; for (const auto& Pair : Names) { if (Normalized == Pair.Key) { OutArchetype = Pair.Value; return true; } } return false; } 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; ECaveGeneratorType Archetype = ECaveGeneratorType::Maze; int32 Slot = 4; bool bRender = true; bool bWalk = true; bool bExport = true; bool bUseOperatorStack = true; bool bProfileDensity = false; bool bProfileLod = false; bool bFailureFocusRender = false; FString OutDirectory; int32 RenderWidth = DefaultRenderWidth; int32 RenderHeight = DefaultRenderHeight; float RenderStepVoxels = DefaultRenderStepVoxels; float RenderMaxDistanceVoxels = DefaultRenderMaxDistanceVoxels; int32 ExportSize = DefaultExportSize; int32 MaxWalkCells = DefaultMaxWalkCells; float BudgetMinutes = DefaultBudgetMinutes; bool bSurfaceRoughnessOverride = false; float SurfaceRoughness = 0.0f; FString CanonicalModes() const { FString Result; if (bRender) Result += TEXT("render,"); if (bWalk) Result += TEXT("walk,"); if (bExport) Result += TEXT("export,"); 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; FParse::Value(*Params, TEXT("seed="), OutArguments.Seed); FParse::Value(*Params, TEXT("archetype="), ArchetypeText); FParse::Value(*Params, TEXT("slot="), OutArguments.Slot); FParse::Value(*Params, TEXT("modes="), ModesText); int32 UseOperatorStack = 1; FParse::Value(*Params, TEXT("opstack="), UseOperatorStack); OutArguments.bUseOperatorStack = UseOperatorStack != 0; OutArguments.bProfileDensity = FParse::Param(*Params, TEXT("profiledensity")); OutArguments.bProfileLod = FParse::Param(*Params, TEXT("profilelod")); 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("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; int32 FailureFocusRender = 0; FParse::Value(*Params, TEXT("failurefocus="), FailureFocusRender); OutArguments.bFailureFocusRender = FailureFocusRender != 0; if (!ParseArchetype(ArchetypeText, OutArguments.Archetype)) { OutError = FString::Printf( TEXT("Unknown archetype '%s'. Expected Maze, TunnelNetwork, FlatPlain, " "CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, or Underwater."), *ArchetypeText); return false; } OutArguments.bRender = false; OutArguments.bWalk = false; OutArguments.bExport = false; TArray Modes; ModesText.ParseIntoArray(Modes, TEXT(","), true); for (FString Mode : Modes) { Mode.TrimStartAndEndInline(); Mode.ToLowerInline(); if (Mode == TEXT("all")) { OutArguments.bRender = true; OutArguments.bWalk = true; OutArguments.bExport = true; } else if (Mode == TEXT("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, or all."), *Mode); return false; } } if (!OutArguments.bRender && !OutArguments.bWalk && !OutArguments.bExport) { OutError = TEXT("At least one mode must be selected."); return false; } if (OutArguments.Slot < 1 || OutArguments.Slot > MaxSyntheticStrates - 2) { OutError = FString::Printf( TEXT("slot must be an interior slot in [1,%d] so arrival and departure mouths exist."), MaxSyntheticStrates - 2); return false; } if (OutArguments.RenderWidth < 16 || OutArguments.RenderHeight < 16 || static_cast(OutArguments.RenderWidth) * static_cast(OutArguments.RenderHeight) > MaxRenderPixels) { OutError = FString::Printf( TEXT("renderwidth*renderheight must be between 256 pixels and %d pixels."), MaxRenderPixels); return false; } if (!FMath::IsFinite(OutArguments.RenderStepVoxels) || OutArguments.RenderStepVoxels < 0.125f || OutArguments.RenderStepVoxels > 8.0f) { OutError = TEXT("renderstep must be finite and in [0.125,8] voxels."); return false; } if (!FMath::IsFinite(OutArguments.RenderMaxDistanceVoxels) || OutArguments.RenderMaxDistanceVoxels <= 0.0f || OutArguments.RenderMaxDistanceVoxels > 2048.0f) { OutError = TEXT("rendermaxdistance must be finite and in (0,2048] voxels."); return false; } // Render no longer budgets density samples: pixels are rasterised from the one canonical // mesh. Keep the legacy renderstep/maxdistance arguments for compatible invocations and use // only maxdistance as the raster depth clip. if (OutArguments.ExportSize < CHUNK_SIZE || OutArguments.ExportSize > MaxExportSize || OutArguments.ExportSize % CHUNK_SIZE != 0) { OutError = FString::Printf( TEXT("exportsize must be a multiple of CHUNK_SIZE (%d) in [%d,%d] cells per axis."), CHUNK_SIZE, CHUNK_SIZE, MaxExportSize); return false; } if (OutArguments.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.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 (OutText.IsEmpty()) { OutText = FString::Printf( TEXT("Saved/VoxelForge/Explore/seed%d_%s_slot%d"), OutArguments.Seed, ArchetypeName(OutArguments.Archetype), OutArguments.Slot); } if (FPaths::IsRelative(OutText)) { OutText = FPaths::Combine(FPaths::ProjectDir(), OutText); } OutArguments.OutDirectory = FPaths::ConvertRelativePathToFull(OutText); FPaths::NormalizeDirectoryName(OutArguments.OutDirectory); return true; } struct FExploreBudget { double StartSeconds = 0.0; double LimitSeconds = 0.0; bool bTruncated = false; FString TruncatedDuring; TArray CompletedModes; FExploreBudget(double InStartSeconds, double InLimitSeconds) : StartSeconds(InStartSeconds) , LimitSeconds(InLimitSeconds) { } double ElapsedSeconds() const { return FPlatformTime::Seconds() - StartSeconds; } double RemainingSeconds() const { return LimitSeconds - ElapsedSeconds(); } bool ShouldStop(const TCHAR* Phase) { if (bTruncated) { return true; } // Leave a small deterministic cleanup window for the current phase to close files and // emit the report. The owner-facing hard ceiling is still enforced by the argument cap at // 30 minutes; the default 25-minute budget leaves substantially more headroom. constexpr double CleanupReserveSeconds = 0.25; if (RemainingSeconds() <= CleanupReserveSeconds) { bTruncated = true; TruncatedDuring = Phase != nullptr ? FString(Phase) : TEXT("unknown"); UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeExplore] budget reached during %s; keeping completed artifacts."), *TruncatedDuring); return true; } return false; } void CompleteMode(const TCHAR* Mode) { if (Mode == nullptr || bTruncated) { return; } CompletedModes.AddUnique(FString(Mode)); } }; struct FExploreMeshTriangle { FVector A = FVector::ZeroVector; FVector B = FVector::ZeroVector; FVector C = FVector::ZeroVector; FVector NormalA = FVector::UpVector; FVector NormalB = FVector::UpVector; FVector NormalC = FVector::UpVector; FVector Min = FVector::ZeroVector; FVector Max = FVector::ZeroVector; FVector Centre = FVector::ZeroVector; FVector Normal = FVector::UpVector; }; struct FExploreMeshBvhNode { FVector Min = FVector::ZeroVector; FVector Max = FVector::ZeroVector; int32 Left = INDEX_NONE; int32 Right = INDEX_NONE; int32 FirstTriangle = 0; int32 TriangleCount = 0; bool IsLeaf() const { return Left == INDEX_NONE && Right == INDEX_NONE; } }; struct FExploreWorld { TStrongObjectPtr Settings; TStrongObjectPtr Manager; TStrongObjectPtr DiffLayer; TStrongObjectPtr Generator; TStrongObjectPtr Mesher; TArray> Definitions; // One canonical mesh cache for this commandlet run. Render and export consume these exact // triangles; neither path is allowed to ask the density field once per pixel. FVoxelMeshData ExploreMesh; FIntVector ExploreMeshOrigin = FIntVector::ZeroValue; int32 ExploreMeshSize = 0; int32 ExploreMeshTileCount = 0; int32 ExploreMeshTilesCompleted = 0; double ExploreMeshSeconds = 0.0; bool bExploreMeshBuilt = false; bool bExploreMeshComplete = false; FString ExploreMeshError; // A deterministic CPU acceleration structure over the canonical triangles. It is built once // after meshing and reused by every viewpoint; no density query is reachable from raster. TArray ExploreTriangles; TArray ExploreTriangleOrder; TArray ExploreBvhNodes; int32 ExploreBvhRoot = INDEX_NONE; double ExploreAccelerationSeconds = 0.0; bool bExploreAccelerationBuilt = false; int32 TargetBottomWorldZ = 0; int32 TargetTopWorldZ = 0; float TargetBoundarySealThickness = 4.0f; bool Build(const FExploreArguments& Arguments, FString& OutError) { const int32 NumStrates = Arguments.Slot + 2; if (NumStrates > MaxSyntheticStrates) { OutError = TEXT("The synthetic layout would exceed the commandlet strate cap."); return false; } Settings = TStrongObjectPtr( NewObject(GetTransientPackage(), NAME_None, RF_Transient)); 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; Definitions.Reserve(NumStrates); for (int32 Index = 0; Index < NumStrates; ++Index) { UVoxelStrateDefinition* Definition = NewObject( GetTransientPackage(), NAME_None, RF_Transient); Definition->StrateName = FText::FromString(FString::Printf( TEXT("VoxelForgeExplore_%s_slot%d"), ArchetypeName(Index == Arguments.Slot ? Arguments.Archetype : ECaveGeneratorType::TunnelNetwork), Index)); Definition->StrateDescription = FText::FromString( TEXT("Transient, deterministic commandlet definition.")); // The commandlet's acceptance sweep uses the owner-facing base spacing: // 4 chunks * 32 voxels * 0.25 m = 32 m per strate. Production assets may be taller; // this keeps the measured tunnel arithmetic tied to the stated ~32 m descent. Definition->StrateHeightInChunks = 4; Definition->TransitionType = EVoxelStrateTransition::Hard; Definition->GeneratorType = Index == Arguments.Slot ? Arguments.Archetype : ECaveGeneratorType::TunnelNetwork; // Explicitly select the production switch so the same synthetic layout, seed and // mesher invocation can be measured through both density branches. Definition->bUseOperatorStack = Arguments.bUseOperatorStack; const TSoftObjectPtr SoftDefinition(Definition); Settings->FixedStrates.Add(Index, SoftDefinition); Settings->StratePool.Add(SoftDefinition); Definitions.Add(TStrongObjectPtr(Definition)); } Manager = TStrongObjectPtr( NewObject(GetTransientPackage(), NAME_None, RF_Transient)); if (!Manager->Initialize(Settings.Get(), Settings->Seed)) { OutError = TEXT("UVoxelStrateManager::Initialize refused the transient layout."); return false; } if (!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; } if (Arguments.bSurfaceRoughnessOverride) { // This is deliberately applied after layout construction and only to the target slot's // resolved definition. Passage placement and every neighbouring strate retain their // authored/default values; only the sampled target field gets the experiment knob. switch (Arguments.Archetype) { case ECaveGeneratorType::FlatPlain: case ECaveGeneratorType::CrystalChamber: Target.Definition->SlabParams.FloorRoughness = Arguments.SurfaceRoughness; Target.Definition->SlabParams.CeilingRoughness = Arguments.SurfaceRoughness; break; case ECaveGeneratorType::Maze: Target.Definition->MazeParams.SurfaceRoughness = Arguments.SurfaceRoughness; break; case ECaveGeneratorType::SurfaceWorld: Target.Definition->SurfaceParams.SurfaceRoughness = Arguments.SurfaceRoughness; break; case ECaveGeneratorType::VerticalShafts: Target.Definition->VerticalShaftParams.SurfaceRoughness = Arguments.SurfaceRoughness; break; case ECaveGeneratorType::FloatingIslands: Target.Definition->FloatingIslandParams.SurfaceRoughness = Arguments.SurfaceRoughness; break; case ECaveGeneratorType::TunnelNetwork: case ECaveGeneratorType::Underwater: default: Target.Definition->GenerationParams.SurfaceRoughness = Arguments.SurfaceRoughness; break; } } TargetBottomWorldZ = Target.BottomChunkZ * CHUNK_SIZE; TargetTopWorldZ = (Target.TopChunkZ + 1) * CHUNK_SIZE; TargetBoundarySealThickness = BoundarySealForArchetype( *Target.Definition, Arguments.Archetype); if (TargetTopWorldZ <= TargetBottomWorldZ) { OutError = TEXT("The target strate has no positive voxel-space height."); return false; } DiffLayer = TStrongObjectPtr( NewObject(GetTransientPackage(), NAME_None, RF_Transient)); Generator = TStrongObjectPtr( NewObject(GetTransientPackage(), NAME_None, RF_Transient)); Generator->InitializeSettings(Settings.Get()); Generator->SetStrateManager(Manager.Get()); Generator->SetDiffLayer(DiffLayer.Get()); Mesher = TStrongObjectPtr( NewObject(GetTransientPackage(), NAME_None, RF_Transient)); Mesher->SetGenerator(Generator.Get()); if (Settings->WorldRadiusVoxels != 0.0f || Generator->WorldRadiusVoxels != 0.0f || Settings->InterStrateGapChunks != 0) { OutError = TEXT( "Explorer invariant failed: WorldRadiusVoxels must stay 0 and lateral regions must stay gated off."); return false; } return true; } }; class FGeneratorDensitySampler final : public IVoxelStrateDensitySampler { public: explicit FGeneratorDensitySampler(const UVoxelGenerator& InGenerator) : Generator(InGenerator) { } virtual float SampleDensity(float WorldX, float WorldY, float WorldZ) const override { return Generator.GetDensityAt(WorldX, WorldY, WorldZ); } private: const UVoxelGenerator& Generator; }; struct FExploreRenderFrame { FString FileName; FString Purpose = TEXT("overview"); FString CameraSeedSource = TEXT("player_fit"); FVector CameraSeedPoseVoxels = FVector::ZeroVector; FVector CameraVoxels = FVector::ZeroVector; FVector TargetVoxels = FVector::ZeroVector; int32 HitPixels = 0; bool bScaleMarkerProjected = false; bool bFailureMarkersProjected = false; bool bPartial = false; }; struct FExploreRenderOutput { FString Status; FString RefusalReason; int32 Width = 0; int32 Height = 0; float FixedStepVoxels = 0.0f; float FixedStepMeters = 0.0f; int32 BisectionIterations = 0; float MaxDistanceVoxels = 0.0f; int64 EstimatedFixedDensitySamples = 0; int64 LegacyTwoViewEstimatedDensitySamples = 0; FString StepRationale; FString CameraSeedPolicy; int32 CameraSeedCount = 0; bool bAllCamerasPlayerFit = false; double MeshSeconds = 0.0; double RasterSeconds = 0.0; double FirstViewRasterSeconds = 0.0; double AdditionalViewRasterSeconds = 0.0; int32 RequestedViewpointCount = 8; bool bMeshComplete = false; double MeshAccelerationSeconds = 0.0; bool bTruncated = false; TArray Frames; }; struct FExploreWalkOutput { FString Status; FString RefusalReason; bool bHasArrival = false; bool bHasDeparture = false; int32 ArrivalPassageCount = 0; int32 DeparturePassageCount = 0; FVector ArrivalVoxels = FVector::ZeroVector; FVector DepartureVoxels = FVector::ZeroVector; FString WindowPolicy; FVoxelPlayerFitWalkReport Report; bool bOriginCheckAttempted = false; bool bOriginCheckAvailable = false; FString OriginCheckRefusalReason; FVoxelPlayerFitWalkReport OriginCheckReport; int32 ArrivalLandingRoomProbeCount = 0; int32 ArrivalLandingRoomPlayerFitProbeCount = 0; int32 ArrivalLandingRoomArrivalComponentProbeCount = 0; int32 DepartureLandingRoomProbeCount = 0; int32 DepartureLandingRoomPlayerFitProbeCount = 0; int32 DepartureLandingRoomArrivalComponentProbeCount = 0; // Captured by the one walk pass and consumed by render camera selection. These are internal // hand-off buffers, not a second source of truth for the field. FVoxelStrateSampleGrid SharedSampleGrid; TArray CameraSeedPoses; TArray CameraSeedSources; }; struct FExploreMeshMetrics { int32 VertexCount = 0; int32 TriangleCount = 0; int32 SharedEdgeCount = 0; int32 DihedralSampleCount = 0; int32 NormalComparisonTriangleCount = 0; float EdgeLengthP50Meters = 0.0f; float EdgeLengthP99Meters = 0.0f; float EdgeLengthMaxMeters = 0.0f; float DihedralP50Degrees = 0.0f; float DihedralP75Degrees = 0.0f; float DihedralP90Degrees = 0.0f; float DihedralP95Degrees = 0.0f; float DihedralP99Degrees = 0.0f; float DihedralFractionOver20Degrees = 0.0f; float DihedralFractionOver40Degrees = 0.0f; float FaceNormalVsMeanVertexNormalP50Degrees = 0.0f; float FaceNormalVsMeanVertexNormalP90Degrees = 0.0f; }; struct FExploreMeshEdgeKey { FIntVector First = FIntVector::ZeroValue; FIntVector Second = FIntVector::ZeroValue; bool operator==(const FExploreMeshEdgeKey& Other) const { return First == Other.First && Second == Other.Second; } }; FORCEINLINE uint32 GetTypeHash(const FExploreMeshEdgeKey& Key) { return HashCombine(GetTypeHash(Key.First), GetTypeHash(Key.Second)); } struct FExploreMeshEdgeIncident { int32 IncidentCount = 0; int32 FirstTriangle = INDEX_NONE; int32 SecondTriangle = INDEX_NONE; }; struct FExploreExportOutput { FString Status; FString RefusalReason; FString MeshFileName; FString ManifestFileName; FIntVector RegionOrigin = FIntVector::ZeroValue; int32 RegionSize = 0; int32 MesherTileCount = 0; int32 MesherTilesCompleted = 0; int64 EstimatedWorkingBytes = 0; int64 WorkingMemoryCapBytes = MaxExportWorkingBytes; int64 MeshArrayBytes = 0; int64 MeshFileSizeBytes = 0; int32 VertexCount = 0; int32 TriangleCount = 0; FExploreMeshMetrics SurfaceMetrics; double MeshSeconds = 0.0; bool bTruncated = false; }; struct FExploreRunOutput { FExploreRenderOutput Render; FExploreWalkOutput Walk; FExploreExportOutput Export; double BudgetSeconds = 0.0; double ElapsedSeconds = 0.0; bool bTruncated = false; FString TruncatedDuring; TArray CompletedModes; }; bool ValidateCanonicalTile(const FVoxelMeshData& MeshData, FString& OutError); bool MeasureExploreMesh( const FVoxelMeshData& MeshData, FExploreMeshMetrics& OutMetrics, FString& OutError); bool FindMouths( const UVoxelStrateManager& Manager, int32 TargetSlot, FVector& OutArrival, FVector& OutDeparture, int32& OutArrivalCount, int32& OutDepartureCount, const FVoxelPassage*& OutArrivalPassage, const FVoxelPassage*& OutDeparturePassage) { OutArrival = FVector::ZeroVector; OutDeparture = FVector::ZeroVector; OutArrivalCount = 0; OutDepartureCount = 0; OutArrivalPassage = nullptr; OutDeparturePassage = nullptr; for (const FVoxelPassage& Passage : Manager.GetPassages()) { if (Passage.LowerStrateIndex == TargetSlot && Passage.UpperStrateIndex + 1 == TargetSlot) { if (OutArrivalCount == 0) { OutArrival = Passage.LowerPoint; OutArrivalPassage = &Passage; } ++OutArrivalCount; } if (Passage.UpperStrateIndex == TargetSlot && Passage.LowerStrateIndex == TargetSlot + 1) { if (OutDepartureCount == 0) { OutDeparture = Passage.UpperPoint; OutDeparturePassage = &Passage; } ++OutDepartureCount; } } return OutArrivalCount > 0 && OutDepartureCount > 0; } struct FExploreCameraBasis { FVector Forward = FVector::ForwardVector; FVector Right = FVector::RightVector; FVector Up = FVector::UpVector; float Aspect = 1.0f; float TanHalfFov = 1.0f; }; bool BuildCameraBasis( const FVector& Position, const FVector& Target, float FieldOfViewDegrees, int32 Width, int32 Height, FExploreCameraBasis& OutBasis) { OutBasis.Forward = (Target - Position).GetSafeNormal(); if (OutBasis.Forward.IsNearlyZero()) { return false; } OutBasis.Right = FVector::CrossProduct(OutBasis.Forward, FVector::UpVector).GetSafeNormal(); if (OutBasis.Right.IsNearlyZero()) { OutBasis.Right = FVector::CrossProduct(OutBasis.Forward, FVector::RightVector).GetSafeNormal(); } OutBasis.Up = FVector::CrossProduct(OutBasis.Right, OutBasis.Forward).GetSafeNormal(); OutBasis.Aspect = static_cast(Width) / static_cast(Height); OutBasis.TanHalfFov = FMath::Tan(FMath::DegreesToRadians(FieldOfViewDegrees * 0.5f)); return OutBasis.Aspect > 0.0f && OutBasis.TanHalfFov > 0.0f; } void PutPixel(TArray& Pixels, int32 Width, int32 Height, int32 X, int32 Y, const FColor& Color) { if (X >= 0 && X < Width && Y >= 0 && Y < Height) { Pixels[X + Width * Y] = Color; } } void DrawLine( TArray& Pixels, int32 Width, int32 Height, int32 X0, int32 Y0, int32 X1, int32 Y1, const FColor& Color) { int32 DX = FMath::Abs(X1 - X0); const int32 SX = X0 < X1 ? 1 : -1; int32 DY = -FMath::Abs(Y1 - Y0); const int32 SY = Y0 < Y1 ? 1 : -1; int32 Error = DX + DY; for (;;) { PutPixel(Pixels, Width, Height, X0, Y0, Color); if (X0 == X1 && Y0 == Y1) { break; } const int32 DoubleError = 2 * Error; if (DoubleError >= DY) { Error += DY; X0 += SX; } if (DoubleError <= DX) { Error += DX; Y0 += SY; } } } bool ProjectPoint( const FVector& Point, const FVector& CameraPosition, const FExploreCameraBasis& Basis, int32 Width, int32 Height, FIntPoint& OutPixel) { const FVector Relative = Point - CameraPosition; const float Depth = FVector::DotProduct(Relative, Basis.Forward); if (Depth <= KINDA_SMALL_NUMBER) { return false; } const float NdcX = FVector::DotProduct(Relative, Basis.Right) / (Depth * Basis.Aspect * Basis.TanHalfFov); const float NdcY = FVector::DotProduct(Relative, Basis.Up) / (Depth * Basis.TanHalfFov); OutPixel.X = FMath::RoundToInt((NdcX * 0.5f + 0.5f) * static_cast(Width - 1)); OutPixel.Y = FMath::RoundToInt((1.0f - (NdcY * 0.5f + 0.5f)) * static_cast(Height - 1)); return NdcX >= -1.0f && NdcX <= 1.0f && NdcY >= -1.0f && NdcY <= 1.0f; } void DrawScaleMarker( TArray& Pixels, int32 Width, int32 Height, const FVector& CameraPosition, const FVector& Target, const FExploreCameraBasis& Basis, bool& bOutProjected) { constexpr float HumanHeightVoxels = 2.0f * FVoxelPlayerCapsuleConstants::HalfHeightVoxels; const FVector MarkerBase = FVector( Target.X, Target.Y, Target.Z - FVoxelPlayerCapsuleConstants::HalfHeightVoxels) - Basis.Right * 18.0f + Basis.Forward * 48.0f; const FVector MarkerTop = MarkerBase + FVector(0.0f, 0.0f, HumanHeightVoxels); FIntPoint BasePixel = FIntPoint::ZeroValue; FIntPoint TopPixel = FIntPoint::ZeroValue; bOutProjected = ProjectPoint( MarkerBase, CameraPosition, Basis, Width, Height, BasePixel) && ProjectPoint(MarkerTop, CameraPosition, Basis, Width, Height, TopPixel); if (!bOutProjected) { // The world-space marker is normally in frame by construction. Keep a visible fallback // for unusual camera/FOV overrides, while the JSON records that it was not projected. BasePixel = FIntPoint(12, Height - 12); TopPixel = FIntPoint(12, FMath::Max(12, Height - 12 - Height / 5)); } const FColor MarkerColor(255, 220, 32, 255); DrawLine(Pixels, Width, Height, BasePixel.X, BasePixel.Y, TopPixel.X, TopPixel.Y, MarkerColor); const int32 TickHalfWidth = 5; for (const FIntPoint& Tick : { BasePixel, TopPixel }) { DrawLine( Pixels, Width, Height, Tick.X - TickHalfWidth, Tick.Y, Tick.X + TickHalfWidth, Tick.Y, MarkerColor); } } bool SavePng( const FString& FilePath, const TArray& Pixels, int32 Width, int32 Height, FString& OutError) { IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked( FName(TEXT("ImageWrapper"))); TSharedPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper( EImageFormat::PNG); if (!ImageWrapper.IsValid() || !ImageWrapper->SetRaw( Pixels.GetData(), static_cast(Pixels.Num()) * sizeof(FColor), Width, Height, ERGBFormat::BGRA, 8)) { OutError = TEXT("ImageWrapper could not accept the BGRA render buffer."); return false; } const TArray64& Compressed = ImageWrapper->GetCompressed(100); if (!FFileHelper::SaveArrayToFile(Compressed, *FilePath)) { OutError = FString::Printf(TEXT("Could not write PNG '%s'."), *FilePath); return false; } return true; } FIntVector ChooseExploreMeshOrigin( const FExploreArguments& Arguments, const FExploreWalkOutput* CameraSeedWalk, int32 MeshSize, int32 TargetMiddleWorldZ) { FVector Centre(0.0f, 0.0f, static_cast(TargetMiddleWorldZ)); if (CameraSeedWalk != nullptr && CameraSeedWalk->CameraSeedPoses.Num() > 0) { Centre = CameraSeedWalk->CameraSeedPoses[0]; } const auto AlignTileOrigin = [MeshSize](float CentreCoordinate) { return FMath::FloorToInt( (CentreCoordinate - 0.5f * static_cast(MeshSize)) / static_cast(CHUNK_SIZE)) * CHUNK_SIZE; }; return FIntVector( AlignTileOrigin(Centre.X), AlignTileOrigin(Centre.Y), AlignTileOrigin(Centre.Z)); } bool AppendCanonicalMesh( FVoxelMeshData& Destination, const FVoxelMeshData& Source, FString& OutError) { if (!ValidateCanonicalTile(Source, OutError)) { return false; } const int32 VertexOffset = Destination.Vertices.Num(); for (const int32 Index : Source.Triangles) { if (Index < 0 || Index >= Source.Vertices.Num()) { OutError = TEXT("Canonical mesher returned an out-of-range triangle index."); return false; } } Destination.Vertices.Append(Source.Vertices); Destination.Normals.Append(Source.Normals); Destination.UVs.Append(Source.UVs); Destination.Colors.Append(Source.Colors); Destination.Triangles.Reserve(Destination.Triangles.Num() + Source.Triangles.Num()); for (const int32 Index : Source.Triangles) { Destination.Triangles.Add(Index + VertexOffset); } // A multi-tile aggregate interleaves each tile's ground/cap runs. The CPU rasterizer uses // flat geometry and the OBJ path is material-neutral, so there is deliberately no invented // global ceiling run here. Destination.NumCeilingTriangles = 0; return true; } bool EnsureExploreMesh( const FExploreArguments& Arguments, FExploreWorld& World, const FExploreWalkOutput* CameraSeedWalk, FExploreBudget& Budget, FString& OutError) { 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.ExploreMeshOrigin = ChooseExploreMeshOrigin( Arguments, CameraSeedWalk, World.ExploreMeshSize, FMath::FloorToInt(0.5f * static_cast( World.TargetBottomWorldZ + World.TargetTopWorldZ))); World.ExploreMeshSeconds = 0.0; if (Budget.ShouldStop(TEXT("mesh"))) { World.ExploreMeshError = TEXT("The wall-clock budget elapsed before meshing began."); OutError = World.ExploreMeshError; return false; } // This is the same canonical UVoxelMarchingCubesMesher used by export and runtime chunk // generation. Skirts are presentation geometry for LOD seams and would extend outside the // bounded explorer region, so the shared mesh uses the export setting. World.Mesher->bGenerateSkirts = false; const double MeshStartSeconds = FPlatformTime::Seconds(); if (Budget.ShouldStop(TEXT("mesh"))) { World.ExploreMeshError = TEXT("The wall-clock budget elapsed before mesh tiles began."); OutError = World.ExploreMeshError; World.ExploreMeshSeconds = FPlatformTime::Seconds() - MeshStartSeconds; return false; } // Generate each tile through the canonical mesher on worker threads, then append strictly in // Z/Y/X order. The mesher's scratch buffers and the generator's hot caches are thread-local; // the fixed merge order keeps the aggregate mesh and all downstream raster/export bytes // deterministic even though tile completion order is not. TArray TileMeshes; TileMeshes.SetNum(World.ExploreMeshTileCount); TArray TileErrors; TileErrors.SetNum(World.ExploreMeshTileCount); TArray TileCompleted; TileCompleted.Init(0, World.ExploreMeshTileCount); std::atomic bTileWorkCancelled(false); const double TileWorkDeadline = Budget.StartSeconds + Budget.LimitSeconds - 0.25; ParallelFor( World.ExploreMeshTileCount, [&](int32 LinearTileIndex) { if (bTileWorkCancelled.load(std::memory_order_relaxed) || FPlatformTime::Seconds() >= TileWorkDeadline) { bTileWorkCancelled.store(true, std::memory_order_relaxed); return; } 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, 1, CHUNK_SIZE); FString TileError; if (!ValidateCanonicalTile(TileMeshes[LinearTileIndex], TileError)) { TileErrors[LinearTileIndex] = MoveTemp(TileError); bTileWorkCancelled.store(true, std::memory_order_relaxed); return; } TileCompleted[LinearTileIndex] = 1; }); 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.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; } World.ExploreMeshSeconds = FPlatformTime::Seconds() - MeshStartSeconds; World.bExploreMeshComplete = true; OutError.Reset(); return true; } float ExploreAxisValue(const FVector& Value, int32 Axis) { switch (Axis) { case 1: return Value.Y; case 2: return Value.Z; default: return Value.X; } } FVector ExploreTriangleMin(const FVector& A, const FVector& B, const FVector& C) { return FVector( FMath::Min3(A.X, B.X, C.X), FMath::Min3(A.Y, B.Y, C.Y), FMath::Min3(A.Z, B.Z, C.Z)); } FVector ExploreTriangleMax(const FVector& A, const FVector& B, const FVector& C) { return FVector( FMath::Max3(A.X, B.X, C.X), FMath::Max3(A.Y, B.Y, C.Y), FMath::Max3(A.Z, B.Z, C.Z)); } bool EnsureExploreAcceleration( const FExploreArguments& Arguments, FExploreWorld& World, FExploreBudget& Budget, FString& OutError) { if (World.bExploreAccelerationBuilt) { OutError.Reset(); return true; } World.ExploreTriangles.Reset(); World.ExploreTriangleOrder.Reset(); World.ExploreBvhNodes.Reset(); World.ExploreBvhRoot = INDEX_NONE; World.ExploreAccelerationSeconds = 0.0; const double AccelerationStartSeconds = FPlatformTime::Seconds(); if (!World.bExploreMeshComplete) { OutError = TEXT("Cannot build mesh acceleration before the canonical mesh is complete."); return false; } if (Budget.ShouldStop(TEXT("mesh-acceleration"))) { OutError = TEXT("The wall-clock budget elapsed before mesh acceleration began."); return false; } const FVoxelMeshData& Mesh = World.ExploreMesh; if (Mesh.Triangles.Num() % 3 != 0) { OutError = TEXT("Canonical mesh triangle index data is not divisible by three."); return false; } World.ExploreTriangles.Reserve(Mesh.Triangles.Num() / 3); for (int32 TriangleIndex = 0; TriangleIndex + 2 < Mesh.Triangles.Num(); TriangleIndex += 3) { if ((TriangleIndex & 16383) == 0 && Budget.ShouldStop(TEXT("mesh-acceleration"))) { OutError = TEXT("Mesh acceleration was truncated before its triangle cache completed."); World.ExploreTriangles.Reset(); return false; } const int32 IndexA = Mesh.Triangles[TriangleIndex]; const int32 IndexB = Mesh.Triangles[TriangleIndex + 1]; const int32 IndexC = Mesh.Triangles[TriangleIndex + 2]; if (!Mesh.Vertices.IsValidIndex(IndexA) || !Mesh.Vertices.IsValidIndex(IndexB) || !Mesh.Vertices.IsValidIndex(IndexC)) { OutError = TEXT("Canonical mesh acceleration saw an out-of-range triangle index."); World.ExploreTriangles.Reset(); return false; } const FVector A = Mesh.Vertices[IndexA] / VOXEL_SIZE; const FVector B = Mesh.Vertices[IndexB] / VOXEL_SIZE; const FVector C = Mesh.Vertices[IndexC] / VOXEL_SIZE; if (!FMath::IsFinite(A.X) || !FMath::IsFinite(A.Y) || !FMath::IsFinite(A.Z) || !FMath::IsFinite(B.X) || !FMath::IsFinite(B.Y) || !FMath::IsFinite(B.Z) || !FMath::IsFinite(C.X) || !FMath::IsFinite(C.Y) || !FMath::IsFinite(C.Z)) { OutError = TEXT("Canonical mesh acceleration saw a non-finite vertex."); World.ExploreTriangles.Reset(); return false; } const FVector Cross = FVector::CrossProduct(B - A, C - A); if (Cross.SizeSquared() <= SMALL_NUMBER) { // Degenerate canonical triangles have no raster contribution. Keep them out of the // acceleration structure without changing the source mesh or its export semantics. continue; } FExploreMeshTriangle& Triangle = World.ExploreTriangles.AddDefaulted_GetRef(); Triangle.A = A; Triangle.B = B; Triangle.C = C; Triangle.NormalA = Mesh.Normals[IndexA].GetSafeNormal(); Triangle.NormalB = Mesh.Normals[IndexB].GetSafeNormal(); Triangle.NormalC = Mesh.Normals[IndexC].GetSafeNormal(); Triangle.Min = ExploreTriangleMin(A, B, C); Triangle.Max = ExploreTriangleMax(A, B, C); Triangle.Centre = (A + B + C) / 3.0f; Triangle.Normal = Cross.GetSafeNormal(); } if (World.ExploreTriangles.Num() == 0) { // A valid empty mesh is still a complete mesh render: every pixel remains the explicit // background colour. The root sentinel lets the rasterizer handle this deterministically. World.ExploreAccelerationSeconds = FPlatformTime::Seconds() - AccelerationStartSeconds; World.bExploreAccelerationBuilt = true; OutError.Reset(); return true; } World.ExploreTriangleOrder.SetNumUninitialized(World.ExploreTriangles.Num()); for (int32 TriangleIndex = 0; TriangleIndex < World.ExploreTriangleOrder.Num(); ++TriangleIndex) { World.ExploreTriangleOrder[TriangleIndex] = TriangleIndex; } World.ExploreBvhNodes.Reserve(World.ExploreTriangles.Num() / 4 + 2); bool bBuildFailed = false; TFunction BuildNode; BuildNode = [&](int32 Start, int32 Count) -> int32 { if (bBuildFailed || Count <= 0) { return INDEX_NONE; } if (Budget.ShouldStop(TEXT("mesh-acceleration"))) { bBuildFailed = true; return INDEX_NONE; } const int32 NodeIndex = World.ExploreBvhNodes.AddDefaulted(); FExploreMeshBvhNode Node; Node.Min = FVector(FLT_MAX, FLT_MAX, FLT_MAX); Node.Max = FVector(-FLT_MAX, -FLT_MAX, -FLT_MAX); FVector CentroidMin = FVector(FLT_MAX, FLT_MAX, FLT_MAX); FVector CentroidMax = FVector(-FLT_MAX, -FLT_MAX, -FLT_MAX); for (int32 Offset = 0; Offset < Count; ++Offset) { const int32 TriangleIndex = World.ExploreTriangleOrder[Start + Offset]; const FExploreMeshTriangle& Triangle = World.ExploreTriangles[TriangleIndex]; Node.Min.X = FMath::Min(Node.Min.X, Triangle.Min.X); Node.Min.Y = FMath::Min(Node.Min.Y, Triangle.Min.Y); Node.Min.Z = FMath::Min(Node.Min.Z, Triangle.Min.Z); Node.Max.X = FMath::Max(Node.Max.X, Triangle.Max.X); Node.Max.Y = FMath::Max(Node.Max.Y, Triangle.Max.Y); Node.Max.Z = FMath::Max(Node.Max.Z, Triangle.Max.Z); CentroidMin.X = FMath::Min(CentroidMin.X, Triangle.Centre.X); CentroidMin.Y = FMath::Min(CentroidMin.Y, Triangle.Centre.Y); CentroidMin.Z = FMath::Min(CentroidMin.Z, Triangle.Centre.Z); CentroidMax.X = FMath::Max(CentroidMax.X, Triangle.Centre.X); CentroidMax.Y = FMath::Max(CentroidMax.Y, Triangle.Centre.Y); CentroidMax.Z = FMath::Max(CentroidMax.Z, Triangle.Centre.Z); } constexpr int32 LeafTriangleCount = 8; if (Count <= LeafTriangleCount) { Node.FirstTriangle = Start; Node.TriangleCount = Count; World.ExploreBvhNodes[NodeIndex] = Node; return NodeIndex; } const FVector CentroidExtent = CentroidMax - CentroidMin; int32 SplitAxis = 0; if (CentroidExtent.Y > CentroidExtent.X && CentroidExtent.Y >= CentroidExtent.Z) { SplitAxis = 1; } else if (CentroidExtent.Z > CentroidExtent.X && CentroidExtent.Z > CentroidExtent.Y) { SplitAxis = 2; } int32* OrderBegin = World.ExploreTriangleOrder.GetData() + Start; int32* OrderEnd = OrderBegin + Count; std::sort( OrderBegin, OrderEnd, [&](int32 LeftIndex, int32 RightIndex) { const float LeftValue = ExploreAxisValue( World.ExploreTriangles[LeftIndex].Centre, SplitAxis); const float RightValue = ExploreAxisValue( World.ExploreTriangles[RightIndex].Centre, SplitAxis); if (LeftValue != RightValue) { return LeftValue < RightValue; } return LeftIndex < RightIndex; }); const int32 LeftCount = Count / 2; const int32 RightCount = Count - LeftCount; const int32 LeftNode = BuildNode(Start, LeftCount); const int32 RightNode = BuildNode(Start + LeftCount, RightCount); if (LeftNode == INDEX_NONE || RightNode == INDEX_NONE) { bBuildFailed = true; return INDEX_NONE; } Node.Left = LeftNode; Node.Right = RightNode; World.ExploreBvhNodes[NodeIndex] = Node; return NodeIndex; }; World.ExploreBvhRoot = BuildNode(0, World.ExploreTriangles.Num()); if (bBuildFailed || World.ExploreBvhRoot == INDEX_NONE || Budget.bTruncated) { World.ExploreTriangles.Reset(); World.ExploreTriangleOrder.Reset(); World.ExploreBvhNodes.Reset(); World.ExploreBvhRoot = INDEX_NONE; OutError = TEXT("Mesh acceleration was truncated before its BVH completed."); return false; } World.ExploreAccelerationSeconds = FPlatformTime::Seconds() - AccelerationStartSeconds; World.bExploreAccelerationBuilt = true; OutError.Reset(); return true; } bool ExploreRayAabb( const FVector& Origin, const FVector& Direction, const FVector& BoundsMin, const FVector& BoundsMax, float MaxDistance, float& OutNear) { float NearDistance = 0.0f; float FarDistance = MaxDistance; for (int32 Axis = 0; Axis < 3; ++Axis) { const float OriginComponent = ExploreAxisValue(Origin, Axis); const float DirectionComponent = ExploreAxisValue(Direction, Axis); const float MinComponent = ExploreAxisValue(BoundsMin, Axis); const float MaxComponent = ExploreAxisValue(BoundsMax, Axis); if (FMath::Abs(DirectionComponent) <= KINDA_SMALL_NUMBER) { if (OriginComponent < MinComponent || OriginComponent > MaxComponent) { return false; } continue; } const float InverseDirection = 1.0f / DirectionComponent; float AxisNear = (MinComponent - OriginComponent) * InverseDirection; float AxisFar = (MaxComponent - OriginComponent) * InverseDirection; if (AxisNear > AxisFar) { Swap(AxisNear, AxisFar); } NearDistance = FMath::Max(NearDistance, AxisNear); FarDistance = FMath::Min(FarDistance, AxisFar); if (NearDistance > FarDistance) { return false; } } OutNear = NearDistance; return true; } bool ExploreRayTriangle( const FVector& Origin, const FVector& Direction, const FExploreMeshTriangle& Triangle, float MaxDistance, float& OutDistance, float& OutU, float& OutV) { const FVector Edge1 = Triangle.B - Triangle.A; const FVector Edge2 = Triangle.C - Triangle.A; const FVector Perpendicular = FVector::CrossProduct(Direction, Edge2); const float Determinant = FVector::DotProduct(Edge1, Perpendicular); if (FMath::Abs(Determinant) <= KINDA_SMALL_NUMBER) { return false; } const float InverseDeterminant = 1.0f / Determinant; const FVector ToOrigin = Origin - Triangle.A; const float U = FVector::DotProduct(ToOrigin, Perpendicular) * InverseDeterminant; if (U < 0.0f || U > 1.0f) { return false; } const FVector CrossOrigin = FVector::CrossProduct(ToOrigin, Edge1); const float V = FVector::DotProduct(Direction, CrossOrigin) * InverseDeterminant; if (V < 0.0f || U + V > 1.0f) { return false; } const float Distance = FVector::DotProduct(Edge2, CrossOrigin) * InverseDeterminant; if (Distance <= 0.01f || Distance >= MaxDistance) { return false; } OutDistance = Distance; OutU = U; OutV = V; return true; } bool RasterizeExploreMesh( const FExploreArguments& Arguments, FExploreWorld& World, const FVector& CameraPosition, const FVector& Target, FExploreBudget& Budget, TArray& OutPixels, int32& OutHitPixels, bool& bOutTruncated) { OutPixels.Init(FColor(12, 18, 28, 255), Arguments.RenderWidth * Arguments.RenderHeight); OutHitPixels = 0; bOutTruncated = false; if (!World.bExploreAccelerationBuilt) { FString AccelerationError; if (!EnsureExploreAcceleration(Arguments, World, Budget, AccelerationError)) { if (Budget.bTruncated) { bOutTruncated = true; return true; } return false; } } FExploreCameraBasis Basis; if (!BuildCameraBasis( CameraPosition, Target, 70.0f, Arguments.RenderWidth, Arguments.RenderHeight, Basis)) { return false; } const FVector KeyLightDirection = FVector(-0.35f, -0.45f, 0.82f).GetSafeNormal(); const FVector FillLightDirection = FVector(0.65f, 0.30f, 0.70f).GetSafeNormal(); constexpr float AmbientFill = 0.62f; constexpr float KeyLightStrength = 0.50f; constexpr float FillLightStrength = 0.24f; for (int32 PixelY = 0; PixelY < Arguments.RenderHeight; ++PixelY) { if (Budget.ShouldStop(TEXT("raster"))) { bOutTruncated = true; return true; } for (int32 PixelX = 0; PixelX < Arguments.RenderWidth; ++PixelX) { const float NormalizedX = (2.0f * (static_cast(PixelX) + 0.5f) / static_cast(Arguments.RenderWidth)) - 1.0f; const float NormalizedY = 1.0f - (2.0f * (static_cast(PixelY) + 0.5f) / static_cast(Arguments.RenderHeight)); const FVector Direction = ( Basis.Forward + Basis.Right * (NormalizedX * Basis.Aspect * Basis.TanHalfFov) + Basis.Up * (NormalizedY * Basis.TanHalfFov)).GetSafeNormal(); float ClosestDistance = Arguments.RenderMaxDistanceVoxels; int32 ClosestTriangleIndex = INDEX_NONE; float ClosestU = 0.0f; float ClosestV = 0.0f; int32 NodeStack[256]; int32 StackCount = 0; if (World.ExploreBvhRoot != INDEX_NONE) { NodeStack[StackCount++] = World.ExploreBvhRoot; } while (StackCount > 0) { const int32 NodeIndex = NodeStack[--StackCount]; if (!World.ExploreBvhNodes.IsValidIndex(NodeIndex)) { continue; } const FExploreMeshBvhNode& Node = World.ExploreBvhNodes[NodeIndex]; float NodeNear = 0.0f; if (!ExploreRayAabb( CameraPosition, Direction, Node.Min, Node.Max, ClosestDistance, NodeNear)) { continue; } if (Node.IsLeaf()) { for (int32 Offset = 0; Offset < Node.TriangleCount; ++Offset) { const int32 OrderIndex = Node.FirstTriangle + Offset; if (!World.ExploreTriangleOrder.IsValidIndex(OrderIndex)) { continue; } const int32 TriangleIndex = World.ExploreTriangleOrder[OrderIndex]; if (!World.ExploreTriangles.IsValidIndex(TriangleIndex)) { continue; } float TriangleDistance = 0.0f; float TriangleU = 0.0f; float TriangleV = 0.0f; if (ExploreRayTriangle( CameraPosition, Direction, World.ExploreTriangles[TriangleIndex], ClosestDistance, TriangleDistance, TriangleU, TriangleV)) { ClosestDistance = TriangleDistance; ClosestTriangleIndex = TriangleIndex; ClosestU = TriangleU; ClosestV = TriangleV; } } continue; } const int32 LeftNodeIndex = Node.Left; const int32 RightNodeIndex = Node.Right; float LeftNear = 0.0f; float RightNear = 0.0f; const bool bHitLeft = World.ExploreBvhNodes.IsValidIndex(LeftNodeIndex) && ExploreRayAabb( CameraPosition, Direction, World.ExploreBvhNodes[LeftNodeIndex].Min, World.ExploreBvhNodes[LeftNodeIndex].Max, ClosestDistance, LeftNear); const bool bHitRight = World.ExploreBvhNodes.IsValidIndex(RightNodeIndex) && ExploreRayAabb( CameraPosition, Direction, World.ExploreBvhNodes[RightNodeIndex].Min, World.ExploreBvhNodes[RightNodeIndex].Max, ClosestDistance, RightNear); if (bHitLeft && bHitRight) { if (LeftNear < RightNear) { if (StackCount + 2 <= UE_ARRAY_COUNT(NodeStack)) { NodeStack[StackCount++] = RightNodeIndex; NodeStack[StackCount++] = LeftNodeIndex; } } else if (StackCount + 2 <= UE_ARRAY_COUNT(NodeStack)) { NodeStack[StackCount++] = LeftNodeIndex; NodeStack[StackCount++] = RightNodeIndex; } } else if (bHitLeft && StackCount < UE_ARRAY_COUNT(NodeStack)) { NodeStack[StackCount++] = LeftNodeIndex; } else if (bHitRight && StackCount < UE_ARRAY_COUNT(NodeStack)) { NodeStack[StackCount++] = RightNodeIndex; } } if (ClosestTriangleIndex == INDEX_NONE) { continue; } const FExploreMeshTriangle& Triangle = World.ExploreTriangles[ClosestTriangleIndex]; FVector ShadingNormal = ( Triangle.NormalA * (1.0f - ClosestU - ClosestV) + Triangle.NormalB * ClosestU + Triangle.NormalC * ClosestV).GetSafeNormal(); if (ShadingNormal.IsNearlyZero()) { // The canonical mesher supplies non-zero gradient normals. Keep a defensive // fallback for malformed mesh data without reintroducing camera-facing flips. ShadingNormal = Triangle.Normal; } const float Key = FMath::Max( 0.0f, FVector::DotProduct(ShadingNormal, KeyLightDirection)); const float Fill = FMath::Max( 0.0f, FVector::DotProduct(ShadingNormal, FillLightDirection)); const float Brightness = FMath::Clamp( AmbientFill + KeyLightStrength * Key + FillLightStrength * Fill, 0.0f, 1.25f); const int32 PixelIndex = PixelX + Arguments.RenderWidth * PixelY; OutPixels[PixelIndex] = FColor( static_cast(FMath::Clamp(100.0f * Brightness, 0.0f, 255.0f)), static_cast(FMath::Clamp(128.0f * Brightness, 0.0f, 255.0f)), static_cast(FMath::Clamp(160.0f * Brightness, 0.0f, 255.0f)), 255); ++OutHitPixels; } } return true; } bool RunRender( const FExploreArguments& Arguments, FExploreWorld& World, const FExploreWalkOutput& CameraSeedWalk, FExploreRenderOutput& OutOutput, FExploreBudget& Budget) { OutOutput = FExploreRenderOutput(); OutOutput.Width = Arguments.RenderWidth; OutOutput.Height = Arguments.RenderHeight; OutOutput.FixedStepVoxels = Arguments.RenderStepVoxels; OutOutput.FixedStepMeters = Arguments.RenderStepVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters; OutOutput.BisectionIterations = 0; OutOutput.MaxDistanceVoxels = Arguments.RenderMaxDistanceVoxels; OutOutput.EstimatedFixedDensitySamples = 0; OutOutput.LegacyTwoViewEstimatedDensitySamples = static_cast(Arguments.RenderWidth) * static_cast(Arguments.RenderHeight) * 3ll * static_cast(FMath::Max( 1, FMath::CeilToInt( Arguments.RenderMaxDistanceVoxels / Arguments.RenderStepVoxels))) * 2ll; OutOutput.RequestedViewpointCount = 8; OutOutput.StepRationale = TEXT( "The renderer meshes the bounded region once with UVoxelMarchingCubesMesher, then CPU " "rasterises its canonical triangles through a deterministic BVH-assisted depth pass. " "-renderstep is retained only for legacy invocation compatibility; no render pixel samples " "the density field."); OutOutput.CameraSeedPolicy = TEXT( "Eight deterministic viewpoints are selected from the one shared player-fit mask. Their " "eye-height origins are density-checked, and their azimuths are rotated in fixed 45-degree " "increments so every image is from inside walkable space."); FString MeshError; if (!EnsureExploreMesh(Arguments, World, &CameraSeedWalk, Budget, MeshError)) { OutOutput.MeshSeconds = World.ExploreMeshSeconds; OutOutput.bMeshComplete = World.bExploreMeshComplete; if (Budget.bTruncated) { OutOutput.Status = TEXT("truncated"); OutOutput.bTruncated = true; OutOutput.RefusalReason = MeshError; } else { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = MeshError; } return false; } OutOutput.MeshSeconds = World.ExploreMeshSeconds; OutOutput.bMeshComplete = true; FString AccelerationError; if (!EnsureExploreAcceleration(Arguments, World, Budget, AccelerationError)) { OutOutput.MeshAccelerationSeconds = World.ExploreAccelerationSeconds; if (Budget.bTruncated) { OutOutput.Status = TEXT("truncated"); OutOutput.bTruncated = true; OutOutput.RefusalReason = AccelerationError; } else { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = AccelerationError; } return false; } OutOutput.MeshAccelerationSeconds = World.ExploreAccelerationSeconds; const double RasterStartSeconds = FPlatformTime::Seconds(); TArray, TInlineAllocator<8>> CameraSeeds; const auto AddCameraSeed = [&](bool bAvailable, const FVector& Position, const TCHAR* Source) { if (!bAvailable || !FMath::IsFinite(Position.X) || !FMath::IsFinite(Position.Y) || !FMath::IsFinite(Position.Z)) { return; } // The walk mask is authoritative, but this guard catches stale/corrupt report data before // a render can silently start inside rock. const float Density = World.Generator->GetDensityAt(Position.X, Position.Y, Position.Z); if (!FMath::IsFinite(Density) || !(Density > 0.0f)) { return; } for (const TPair& Existing : CameraSeeds) { if (Existing.Key.Equals(Position, 0.001f)) { return; } } CameraSeeds.Emplace(Position, FString(Source)); }; for (int32 SeedIndex = 0; SeedIndex < CameraSeedWalk.CameraSeedPoses.Num(); ++SeedIndex) { const FString Source = CameraSeedWalk.CameraSeedSources.IsValidIndex(SeedIndex) ? CameraSeedWalk.CameraSeedSources[SeedIndex] : FString::Printf(TEXT("player_fit_%02d"), SeedIndex); AddCameraSeed( true, CameraSeedWalk.CameraSeedPoses[SeedIndex], *Source); if (CameraSeeds.Num() >= OutOutput.RequestedViewpointCount) { break; } } if (CameraSeeds.Num() == 0) { OutOutput.Status = TEXT("refused"); OutOutput.RefusalReason = TEXT( "The player-fit seed pass produced no density-positive pose; refusing an inside-rock camera."); return false; } // A small room can contain fewer than eight distinct lattice cells. Reusing the same valid // player-fit origin with another deterministic angle is still a valid viewpoint and keeps // the contract (all camera origins are inside walkable space) without fabricating positions. while (CameraSeeds.Num() < OutOutput.RequestedViewpointCount) { const TPair& Anchor = CameraSeeds[0]; CameraSeeds.Emplace( Anchor.Key, FString::Printf(TEXT("%s_angle_%02d"), *Anchor.Value, CameraSeeds.Num())); } OutOutput.CameraSeedCount = CameraSeeds.Num(); OutOutput.bAllCamerasPlayerFit = true; FVector RouteFocus = FVector::ZeroVector; int32 RouteFocusCount = 0; if (CameraSeedWalk.bHasArrival) { RouteFocus += CameraSeedWalk.ArrivalVoxels; ++RouteFocusCount; } if (CameraSeedWalk.bHasDeparture) { RouteFocus += CameraSeedWalk.DepartureVoxels; ++RouteFocusCount; } if (RouteFocusCount > 0) { RouteFocus /= static_cast(RouteFocusCount); } else { RouteFocus = CameraSeeds[0].Key; } for (int32 ViewIndex = 0; ViewIndex < CameraSeeds.Num(); ++ViewIndex) { if (Budget.ShouldStop(TEXT("render"))) { OutOutput.Status = TEXT("truncated"); OutOutput.bTruncated = true; OutOutput.RefusalReason = TEXT("The wall-clock budget stopped viewpoint generation."); return false; } const FVector CameraSeedPose = CameraSeeds[ViewIndex].Key; const FVector CameraPosition = CameraSeedPose + FVector::UpVector * (FVoxelPlayerCapsuleConstants::HalfHeightVoxels - 0.5f); const float CameraDensity = World.Generator->GetDensityAt( CameraPosition.X, CameraPosition.Y, CameraPosition.Z); if (!FMath::IsFinite(CameraDensity) || !(CameraDensity > 0.0f)) { OutOutput.Status = TEXT("refused"); OutOutput.RefusalReason = TEXT( "The eye-height offset of a player-fit camera entered solid density."); return false; } FVector LookDirection = RouteFocus - CameraPosition; LookDirection.Z = 0.0f; LookDirection.Normalize(); if (LookDirection.IsNearlyZero()) { LookDirection = FVector( ViewIndex == 1 ? -0.6f : 0.8f, ViewIndex == 2 ? 0.7f : -0.4f, 0.18f).GetSafeNormal(); } // Keep the camera at the fitted pose while changing the azimuth for each viewpoint. LookDirection = LookDirection.RotateAngleAxis( 45.0f * static_cast(ViewIndex), FVector::UpVector).GetSafeNormal(); const FVector Target = CameraPosition + LookDirection * 96.0f + FVector::UpVector * 8.0f; const double ViewStartSeconds = FPlatformTime::Seconds(); TArray Pixels; int32 HitPixels = 0; bool bRasterTruncated = false; if (!RasterizeExploreMesh( Arguments, World, CameraPosition, Target, Budget, Pixels, HitPixels, bRasterTruncated)) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = TEXT("Could not construct a mesh raster camera."); return false; } FExploreCameraBasis Basis; if (!BuildCameraBasis( CameraPosition, Target, 70.0f, Arguments.RenderWidth, Arguments.RenderHeight, Basis)) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = TEXT("Could not construct a render camera basis."); return false; } bool bScaleMarkerProjected = false; DrawScaleMarker( Pixels, Arguments.RenderWidth, Arguments.RenderHeight, CameraPosition, Target, Basis, bScaleMarkerProjected); FExploreRenderFrame& Frame = OutOutput.Frames.AddDefaulted_GetRef(); Frame.FileName = FString::Printf(TEXT("render_%02d.png"), ViewIndex); Frame.Purpose = CameraSeeds[ViewIndex].Value; Frame.CameraSeedSource = CameraSeeds[ViewIndex].Value; Frame.CameraSeedPoseVoxels = CameraSeedPose; Frame.CameraVoxels = CameraPosition; Frame.TargetVoxels = Target; Frame.HitPixels = HitPixels; Frame.bScaleMarkerProjected = bScaleMarkerProjected; Frame.bPartial = bRasterTruncated; FString Error; if (!SavePng( FPaths::Combine(Arguments.OutDirectory, Frame.FileName), Pixels, Arguments.RenderWidth, Arguments.RenderHeight, Error)) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = Error; return false; } const double ViewSeconds = FPlatformTime::Seconds() - ViewStartSeconds; if (ViewIndex == 0) { OutOutput.FirstViewRasterSeconds = ViewSeconds; } if (bRasterTruncated) { OutOutput.Status = TEXT("truncated"); OutOutput.bTruncated = true; OutOutput.RefusalReason = TEXT("The wall-clock budget stopped triangle rasterisation."); break; } } OutOutput.MeshSeconds = World.ExploreMeshSeconds; OutOutput.RasterSeconds = FPlatformTime::Seconds() - RasterStartSeconds; const int32 AdditionalViewCount = FMath::Max(0, OutOutput.Frames.Num() - 1); OutOutput.AdditionalViewRasterSeconds = AdditionalViewCount > 0 ? (OutOutput.RasterSeconds - OutOutput.FirstViewRasterSeconds) / static_cast(AdditionalViewCount) : 0.0; if (!OutOutput.bTruncated) { OutOutput.Status = TEXT("ok"); } return !OutOutput.bTruncated; } void DrawPointMarker( TArray& Pixels, int32 Width, int32 Height, const FVector& Point, const FVector& CameraPosition, const FExploreCameraBasis& Basis, const FColor& Color, bool& bOutProjected) { FIntPoint Pixel = FIntPoint::ZeroValue; bOutProjected = ProjectPoint( Point, CameraPosition, Basis, Width, Height, Pixel); if (!bOutProjected) { return; } constexpr int32 MarkerRadius = 7; DrawLine( Pixels, Width, Height, Pixel.X - MarkerRadius, Pixel.Y, Pixel.X + MarkerRadius, Pixel.Y, Color); DrawLine( Pixels, Width, Height, Pixel.X, Pixel.Y - MarkerRadius, Pixel.X, Pixel.Y + MarkerRadius, Color); DrawLine( Pixels, Width, Height, Pixel.X - 4, Pixel.Y - 4, Pixel.X + 4, Pixel.Y + 4, Color); DrawLine( Pixels, Width, Height, Pixel.X - 4, Pixel.Y + 4, Pixel.X + 4, Pixel.Y - 4, Color); } bool RunFailureBoundaryRender( const FExploreArguments& Arguments, FExploreWorld& World, const FExploreWalkOutput& Walk, FExploreRenderOutput& InOutOutput, FExploreBudget& Budget) { const FVoxelPlayerFitWalkReport& Report = Walk.Report; if (!Report.bHasAgentFinalPosition || !Report.bHasTargetComponentNearestCell) { return false; } const FVector AgentFinal = Report.AgentFinalVoxels; const FVector TargetComponentCell = Report.TargetComponentNearestVoxels; const FVector LastReached = Report.LastReachedVoxels; const bool bHaveLastReached = Report.bHasLastReachedPosition; const FVector Focus = (AgentFinal + TargetComponentCell) * 0.5f; const FVector Segment = TargetComponentCell - AgentFinal; const FVector SegmentDirection = Segment.GetSafeNormal(); if (SegmentDirection.IsNearlyZero()) { return false; } FVector ViewSide = FVector::CrossProduct(SegmentDirection, FVector::UpVector).GetSafeNormal(); if (ViewSide.IsNearlyZero()) { ViewSide = FVector::CrossProduct(SegmentDirection, FVector::RightVector).GetSafeNormal(); } const float SegmentLength = Segment.Size(); const float ViewSideOffset = FMath::Min(64.0f, FMath::Max(16.0f, 0.25f * SegmentLength)); // Failure-focus views are still required to originate from the fitted graph. The old // implementation nudged the camera four voxels toward/away from the boundary and could put // the viewpoint in the very rock that this diagnostic is meant to explain. const FVector CameraCandidates[] = { AgentFinal, bHaveLastReached ? LastReached : AgentFinal, }; const FVector ViewTargets[] = { Focus + ViewSide * ViewSideOffset, Focus - ViewSide * ViewSideOffset, }; for (int32 ViewIndex = 0; ViewIndex < UE_ARRAY_COUNT(ViewTargets); ++ViewIndex) { if (Budget.ShouldStop(TEXT("failure-focus-render"))) { InOutOutput.Status = TEXT("truncated"); InOutOutput.bTruncated = true; InOutOutput.RefusalReason = TEXT( "The wall-clock budget stopped failure-focus viewpoint generation."); return false; } const FVector CameraSeedPose = CameraCandidates[ViewIndex]; const FVector CameraPosition = CameraSeedPose + FVector::UpVector * (FVoxelPlayerCapsuleConstants::HalfHeightVoxels - 0.5f); const float CameraDensity = World.Generator->GetDensityAt( CameraPosition.X, CameraPosition.Y, CameraPosition.Z); if (!FMath::IsFinite(CameraDensity) || !(CameraDensity > 0.0f)) { UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeExplore] refusing failure-focus camera %d: fitted pose is not air."), ViewIndex); return false; } const FVector Target = ViewTargets[ViewIndex]; TArray Pixels; int32 HitPixels = 0; bool bRasterTruncated = false; if (!RasterizeExploreMesh( Arguments, World, CameraPosition, Target, Budget, Pixels, HitPixels, bRasterTruncated)) { InOutOutput.Status = TEXT("error"); InOutOutput.RefusalReason = TEXT("Could not construct failure-focus mesh raster."); return false; } FExploreCameraBasis Basis; if (!BuildCameraBasis( CameraPosition, Target, 70.0f, Arguments.RenderWidth, Arguments.RenderHeight, Basis)) { return false; } bool bAgentProjected = false; bool bTargetProjected = false; bool bLastReachedProjected = false; DrawPointMarker( Pixels, Arguments.RenderWidth, Arguments.RenderHeight, AgentFinal, CameraPosition, Basis, FColor(242, 72, 72, 255), bAgentProjected); DrawPointMarker( Pixels, Arguments.RenderWidth, Arguments.RenderHeight, TargetComponentCell, CameraPosition, Basis, FColor(72, 242, 112, 255), bTargetProjected); if (bHaveLastReached) { DrawPointMarker( Pixels, Arguments.RenderWidth, Arguments.RenderHeight, LastReached, CameraPosition, Basis, FColor(242, 176, 56, 255), bLastReachedProjected); } FIntPoint AgentPixel = FIntPoint::ZeroValue; FIntPoint TargetPixel = FIntPoint::ZeroValue; if (ProjectPoint( AgentFinal, CameraPosition, Basis, Arguments.RenderWidth, Arguments.RenderHeight, AgentPixel) && ProjectPoint( TargetComponentCell, CameraPosition, Basis, Arguments.RenderWidth, Arguments.RenderHeight, TargetPixel)) { DrawLine( Pixels, Arguments.RenderWidth, Arguments.RenderHeight, AgentPixel.X, AgentPixel.Y, TargetPixel.X, TargetPixel.Y, FColor(244, 214, 72, 255)); } FExploreRenderFrame& Frame = InOutOutput.Frames.AddDefaulted_GetRef(); Frame.FileName = FString::Printf(TEXT("failure_boundary_%02d.png"), ViewIndex); Frame.Purpose = TEXT("agent_final_red_target_component_nearest_green_last_reached_orange"); Frame.CameraSeedSource = bHaveLastReached && ViewIndex == 1 ? TEXT("last_reached_player_fit") : TEXT("agent_final_player_fit"); Frame.CameraSeedPoseVoxels = CameraSeedPose; Frame.CameraVoxels = CameraPosition; Frame.TargetVoxels = Target; Frame.HitPixels = HitPixels; Frame.bFailureMarkersProjected = bAgentProjected && bTargetProjected && (!bHaveLastReached || bLastReachedProjected); Frame.bPartial = bRasterTruncated; bool bScaleMarkerProjected = false; DrawScaleMarker( Pixels, Arguments.RenderWidth, Arguments.RenderHeight, CameraPosition, Target, Basis, bScaleMarkerProjected); Frame.bScaleMarkerProjected = bScaleMarkerProjected; FString Error; if (!SavePng( FPaths::Combine(Arguments.OutDirectory, Frame.FileName), Pixels, Arguments.RenderWidth, Arguments.RenderHeight, Error)) { InOutOutput.Status = TEXT("error"); InOutOutput.RefusalReason = Error; return false; } if (bRasterTruncated) { InOutOutput.Status = TEXT("truncated"); InOutOutput.bTruncated = true; InOutOutput.RefusalReason = TEXT( "The wall-clock budget stopped failure-focus rasterisation."); return false; } } return true; } bool RunWalk( const FExploreArguments& Arguments, const FExploreWorld& World, FExploreWalkOutput& OutOutput, FExploreBudget& Budget) { OutOutput = FExploreWalkOutput(); OutOutput.WindowPolicy = NeedsOriginInWindow(Arguments.Archetype) ? TEXT("origin-inclusive target-strate passage-envelope AABB + margin") : TEXT("target-strate passage-envelope AABB + margin"); const int64 EstimatedWorkingBytes = static_cast(Arguments.MaxWalkCells) * EstimatedWalkBytesPerCell; if (EstimatedWorkingBytes > MaxWalkWorkingBytes) { OutOutput.Status = TEXT("refused"); OutOutput.RefusalReason = FString::Printf( TEXT("Refused before sampling: estimated walk memory %lld bytes exceeds cap %lld bytes."), static_cast(EstimatedWorkingBytes), static_cast(MaxWalkWorkingBytes)); return false; } if (Budget.ShouldStop(TEXT("walk"))) { OutOutput.Status = TEXT("truncated"); OutOutput.RefusalReason = TEXT("The wall-clock budget elapsed before walk sampling."); return false; } const FVoxelPassage* ArrivalPassage = nullptr; const FVoxelPassage* DeparturePassage = nullptr; if (!FindMouths( *World.Manager, Arguments.Slot, OutOutput.ArrivalVoxels, OutOutput.DepartureVoxels, OutOutput.ArrivalPassageCount, OutOutput.DeparturePassageCount, ArrivalPassage, DeparturePassage)) { OutOutput.Status = TEXT("refused"); OutOutput.RefusalReason = TEXT( "The target slot did not have both one-step chain mouths in the generated passage list."); return false; } OutOutput.bHasArrival = true; OutOutput.bHasDeparture = true; // Temporary seed-14 probe while diagnosing the fitted VerticalShafts junction. This is // intentionally narrow and will be removed after the floor/tree seam is corrected. if (Arguments.Archetype == ECaveGeneratorType::VerticalShafts && Arguments.Seed == 14) { const FVector ProbeA = OutOutput.ArrivalVoxels; const FVector ProbeB(-100.8f, -26.9f, ProbeA.Z); for (int32 Index = 0; Index <= 10; ++Index) { const float T = static_cast(Index) / 10.0f; const FVector P = FMath::Lerp(ProbeA, ProbeB, T); const float Dm704 = World.Generator->GetDensityAt(P.X, P.Y, -704.0f); const float Dm702 = World.Generator->GetDensityAt(P.X, P.Y, -702.0f); const float Dm7005 = World.Generator->GetDensityAt(P.X, P.Y, -700.5f); const float Dm700 = World.Generator->GetDensityAt(P.X, P.Y, -700.0f); const float Dm6995 = World.Generator->GetDensityAt(P.X, P.Y, -699.5f); const float Dm697 = World.Generator->GetDensityAt(P.X, P.Y, -697.0f); float MinBody = FLT_MAX; int32 NumSolidBody = 0; int32 SolidOffsetX = 0; int32 SolidOffsetY = 0; int32 SolidBodyRow = -1; for (int32 OffsetY = -1; OffsetY <= 1; ++OffsetY) { for (int32 OffsetX = -1; OffsetX <= 1; ++OffsetX) { for (int32 BodyRow = 0; BodyRow < 8; ++BodyRow) { const float BodyDensity = World.Generator->GetDensityAt( P.X + static_cast(OffsetX), P.Y + static_cast(OffsetY), -699.5f + static_cast(BodyRow)); if (BodyDensity < MinBody) { MinBody = BodyDensity; SolidOffsetX = OffsetX; SolidOffsetY = OffsetY; SolidBodyRow = BodyRow; } NumSolidBody += BodyDensity <= 0.0f ? 1 : 0; } } } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore][TemporaryShaftProbe] t=%.2f xy=(%.2f,%.2f) " "d[-704,-702,-700.5,-700,-699.5,-697]=[%.3f,%.3f,%.3f,%.3f,%.3f,%.3f] " "bodyMin=%.3f bodySolid=%d/72 firstMinOffset=(%d,%d) row=%d"), T, P.X, P.Y, Dm704, Dm702, Dm7005, Dm700, Dm6995, Dm697, MinBody, NumSolidBody, SolidOffsetX, SolidOffsetY, SolidBodyRow); } } FVoxelStrateMeasureSettings Settings; Settings.SampleStep = 1; Settings.MaxCells = Arguments.MaxWalkCells; Settings.MaxRouteRetries = 16; Settings.CoverPointA = FVector2D(OutOutput.ArrivalVoxels.X, OutOutput.ArrivalVoxels.Y); Settings.CoverPointB = FVector2D(OutOutput.DepartureVoxels.X, OutOutput.DepartureVoxels.Y); Settings.CoverMarginVoxels = FMath::CeilToFloat( Settings.PlayerCapsuleRadiusVoxels) + 2.0f; // A mouth-AABB is not a landing window: the landing door and its local apron can legitimately // leave that box before the player reaches the next room. Expand the same two-point box just // enough to contain the selected production mouth envelope. This is still a bounded, // deterministic measurement window; the central inter-strate descent belongs to the adjacent // strata and is not folded into this intra-strate walk question. const float WindowMinX = FMath::Min(OutOutput.ArrivalVoxels.X, OutOutput.DepartureVoxels.X); const float WindowMaxX = FMath::Max(OutOutput.ArrivalVoxels.X, OutOutput.DepartureVoxels.X); const float WindowMinY = FMath::Min(OutOutput.ArrivalVoxels.Y, OutOutput.DepartureVoxels.Y); const float WindowMaxY = FMath::Max(OutOutput.ArrivalVoxels.Y, OutOutput.DepartureVoxels.Y); const auto IncludeTargetPassageEnvelope = [&]( const FVoxelPassage* Passage, const FVoxelPassageLanding& Landing) { if (Passage == nullptr) { return; } const auto IncludeXY = [&](const FVector& Point, float Radius) { if (!FMath::IsFinite(Point.X) || !FMath::IsFinite(Point.Y) || !FMath::IsFinite(Point.Z) || !FMath::IsFinite(Radius)) { return; } const float AbsRadius = FMath::Abs(Radius); // Include control points whose centreline belongs to the measured strate. A control // point just outside the boundary belongs to the adjacent strate's descent; pulling // that whole switchback into this bounded walk box can multiply the sampled volume // without improving the intra-strate mouth-to-mouth question. if (Point.Z < static_cast(World.TargetBottomWorldZ) || Point.Z > static_cast(World.TargetTopWorldZ)) { return; } const float ExcessX = FMath::Max( 0.0f, FMath::Max( WindowMinX - (Point.X + AbsRadius), (Point.X - AbsRadius) - WindowMaxX)); const float ExcessY = FMath::Max( 0.0f, FMath::Max( WindowMinY - (Point.Y + AbsRadius), (Point.Y - AbsRadius) - WindowMaxY)); Settings.CoverMarginVoxels = FMath::Max( Settings.CoverMarginVoxels, FMath::Max(ExcessX, ExcessY)); }; IncludeXY( Landing.StandingPoint, Landing.HalfWidth + VoxelPassageGeometry::LandingCarveBlendVoxels); // The first/last three points are the production mouth, apron, and first slope section. // Do not pull the whole inter-strate switchback into the bounded target-strate test. const int32 NumLocalPoints = FMath::Min(3, Passage->ControlPoints.Num()); const bool bUseUpperEnd = &Landing == &Passage->UpperLanding; const int32 FirstPoint = bUseUpperEnd ? 0 : FMath::Max(0, Passage->ControlPoints.Num() - NumLocalPoints); const int32 LastPoint = bUseUpperEnd ? NumLocalPoints : Passage->ControlPoints.Num(); for (int32 PointIndex = FirstPoint; PointIndex < LastPoint; ++PointIndex) { const float Radius = Passage->ControlRadii.IsValidIndex(PointIndex) ? Passage->ControlRadii[PointIndex] : Passage->Radius; IncludeXY(Passage->ControlPoints[PointIndex], Radius + VoxelPassageGeometry::LandingCarveBlendVoxels); } }; IncludeTargetPassageEnvelope( ArrivalPassage, ArrivalPassage != nullptr ? ArrivalPassage->LowerLanding : FVoxelPassageLanding()); IncludeTargetPassageEnvelope( DeparturePassage, DeparturePassage != nullptr ? DeparturePassage->UpperLanding : FVoxelPassageLanding()); Settings.bIncludeOriginInCoverWindow = NeedsOriginInWindow(Arguments.Archetype); TArray LandingRoomProbePoints; auto AppendLandingRoomProbes = [&LandingRoomProbePoints]( const FVoxelPassageLanding& Landing) { const float ProbeOffset = FMath::Min( 2.0f, FMath::Max(0.0f, Landing.HalfWidth - 2.0f)); LandingRoomProbePoints.Add(Landing.StandingPoint); LandingRoomProbePoints.Add( Landing.StandingPoint + FVector(ProbeOffset, 0.0f, 0.0f)); LandingRoomProbePoints.Add( Landing.StandingPoint + FVector(-ProbeOffset, 0.0f, 0.0f)); LandingRoomProbePoints.Add( Landing.StandingPoint + FVector(0.0f, ProbeOffset, 0.0f)); LandingRoomProbePoints.Add( Landing.StandingPoint + FVector(0.0f, -ProbeOffset, 0.0f)); }; if (ArrivalPassage != nullptr) { AppendLandingRoomProbes(ArrivalPassage->LowerLanding); } if (DeparturePassage != nullptr) { AppendLandingRoomProbes(DeparturePassage->UpperLanding); } const FGeneratorDensitySampler Sampler(*World.Generator); if (!VF_MeasurePlayerFitWalkWithSampler( Sampler, World.TargetBottomWorldZ, World.TargetTopWorldZ, World.TargetBoundarySealThickness, OutOutput.ArrivalVoxels, OutOutput.DepartureVoxels, Settings, OutOutput.Report, &LandingRoomProbePoints, &OutOutput.SharedSampleGrid)) { OutOutput.Status = TEXT("refused"); OutOutput.RefusalReason = OutOutput.Report.RefusalReason; return false; } // Camera seeds are selected from this exact captured mask. The first valid fit cell anchors // the bounded render mesh; subsequent cells are kept local to that anchor so the mesh region // contains every requested viewpoint rather than silently rendering a distant empty box. OutOutput.CameraSeedPoses.Reset(); OutOutput.CameraSeedSources.Reset(); FVector CameraAnchor = FVector::ZeroVector; bool bHaveCameraAnchor = false; const auto AddCameraSeed = [&](const FVector& Position, const TCHAR* Source) { if (!FMath::IsFinite(Position.X) || !FMath::IsFinite(Position.Y) || !FMath::IsFinite(Position.Z) || !OutOutput.SharedSampleGrid.ContainsPoint(Position)) { return; } const float Step = static_cast(OutOutput.SharedSampleGrid.SampleStep); const int32 X = FMath::Clamp( FMath::FloorToInt((Position.X - OutOutput.SharedSampleGrid.MinX) / Step), 0, OutOutput.SharedSampleGrid.NumX - 1); const int32 Y = FMath::Clamp( FMath::FloorToInt((Position.Y - OutOutput.SharedSampleGrid.MinY) / Step), 0, OutOutput.SharedSampleGrid.NumY - 1); const int32 Z = FMath::Clamp( FMath::FloorToInt((Position.Z - OutOutput.SharedSampleGrid.MinZ) / Step), 0, OutOutput.SharedSampleGrid.NumZ - 1); const int32 Cell = OutOutput.SharedSampleGrid.Index(X, Y, Z); if (!OutOutput.SharedSampleGrid.PlayerFitMask.IsValidIndex(Cell) || OutOutput.SharedSampleGrid.PlayerFitMask[Cell] == 0u) { return; } if (!bHaveCameraAnchor) { CameraAnchor = Position; bHaveCameraAnchor = true; } if (FVector::DistSquared(Position, CameraAnchor) > FMath::Square(40.0f)) { return; } for (const FVector& Existing : OutOutput.CameraSeedPoses) { if (Existing.Equals(Position, 0.001f)) { return; } } OutOutput.CameraSeedPoses.Add(Position); OutOutput.CameraSeedSources.Add( Source != nullptr ? FString(Source) : FString::Printf(TEXT("player_fit_%02d"), OutOutput.CameraSeedPoses.Num() - 1)); }; AddCameraSeed(OutOutput.ArrivalVoxels, TEXT("arrival_player_fit")); AddCameraSeed(OutOutput.DepartureVoxels, TEXT("departure_player_fit")); if (OutOutput.Report.bHasLastReachedPosition) { AddCameraSeed( OutOutput.Report.LastReachedVoxels, TEXT("last_reached_player_fit")); } if (OutOutput.Report.bHasAgentFinalPosition) { AddCameraSeed( OutOutput.Report.AgentFinalVoxels, TEXT("agent_final_player_fit")); } if (!bHaveCameraAnchor) { for (int32 Cell = 0; Cell < OutOutput.SharedSampleGrid.PlayerFitMask.Num(); ++Cell) { if (OutOutput.SharedSampleGrid.PlayerFitMask[Cell] == 0u) { continue; } const int32 Plane = OutOutput.SharedSampleGrid.NumX * OutOutput.SharedSampleGrid.NumY; const int32 Z = Cell / Plane; const int32 InPlane = Cell - Z * Plane; const int32 Y = InPlane / OutOutput.SharedSampleGrid.NumX; const int32 X = InPlane - Y * OutOutput.SharedSampleGrid.NumX; AddCameraSeed( FVector( OutOutput.SharedSampleGrid.MinX + (static_cast(X) + 0.5f) * OutOutput.SharedSampleGrid.SampleStep, OutOutput.SharedSampleGrid.MinY + (static_cast(Y) + 0.5f) * OutOutput.SharedSampleGrid.SampleStep, OutOutput.SharedSampleGrid.MinZ + (static_cast(Z) + 0.5f) * OutOutput.SharedSampleGrid.SampleStep), TEXT("player_fit_grid")); if (bHaveCameraAnchor) { break; } } } if (bHaveCameraAnchor) { const int32 NumCandidates = OutOutput.SharedSampleGrid.PlayerFitMask.Num(); for (int32 Cell = 0; Cell < NumCandidates && OutOutput.CameraSeedPoses.Num() < 8; ++Cell) { if (OutOutput.SharedSampleGrid.PlayerFitMask[Cell] == 0u) { continue; } const int32 Plane = OutOutput.SharedSampleGrid.NumX * OutOutput.SharedSampleGrid.NumY; const int32 Z = Cell / Plane; const int32 InPlane = Cell - Z * Plane; const int32 Y = InPlane / OutOutput.SharedSampleGrid.NumX; const int32 X = InPlane - Y * OutOutput.SharedSampleGrid.NumX; AddCameraSeed( FVector( OutOutput.SharedSampleGrid.MinX + (static_cast(X) + 0.5f) * OutOutput.SharedSampleGrid.SampleStep, OutOutput.SharedSampleGrid.MinY + (static_cast(Y) + 0.5f) * OutOutput.SharedSampleGrid.SampleStep, OutOutput.SharedSampleGrid.MinZ + (static_cast(Z) + 0.5f) * OutOutput.SharedSampleGrid.SampleStep), TEXT("player_fit_grid")); } } // The origin observation is now a view over the shared primary grid. A second, expanded // walk would defeat the one-grid contract; callers that need origin coverage must request an // origin-inclusive archetype window, which is already selected before the single sample pass. OutOutput.bOriginCheckAttempted = true; if (OutOutput.Report.bOriginColumnInSampledWindow) { OutOutput.OriginCheckReport = OutOutput.Report; OutOutput.bOriginCheckAvailable = true; } else { OutOutput.OriginCheckRefusalReason = TEXT( "Origin is outside the shared primary sample window; no second grid was sampled."); } const int32 ArrivalProbeCount = ArrivalPassage != nullptr ? 5 : 0; const int32 DepartureProbeCount = DeparturePassage != nullptr ? 5 : 0; const int32 LandingProbeCount = ArrivalProbeCount + DepartureProbeCount; OutOutput.ArrivalLandingRoomProbeCount = ArrivalProbeCount; OutOutput.DepartureLandingRoomProbeCount = DepartureProbeCount; for (int32 ProbeIndex = 0; ProbeIndex < FMath::Min(LandingProbeCount, OutOutput.Report.ComponentProbePlayerFit.Num()); ++ProbeIndex) { const bool bPlayerFit = OutOutput.Report.ComponentProbePlayerFit[ProbeIndex] != 0u; const bool bInArrivalComponent = OutOutput.Report.ComponentProbeInStartComponent.IsValidIndex(ProbeIndex) && OutOutput.Report.ComponentProbeInStartComponent[ProbeIndex] != 0u; if (ProbeIndex < ArrivalProbeCount) { OutOutput.ArrivalLandingRoomPlayerFitProbeCount += bPlayerFit ? 1 : 0; OutOutput.ArrivalLandingRoomArrivalComponentProbeCount += bInArrivalComponent ? 1 : 0; } else { OutOutput.DepartureLandingRoomPlayerFitProbeCount += bPlayerFit ? 1 : 0; OutOutput.DepartureLandingRoomArrivalComponentProbeCount += bInArrivalComponent ? 1 : 0; } } if (Budget.ShouldStop(TEXT("walk"))) { OutOutput.Status = TEXT("truncated"); OutOutput.RefusalReason = TEXT("The wall-clock budget elapsed after walk sampling."); return false; } OutOutput.Status = TEXT("ok"); return true; } int64 EstimateExportWorkingBytes(int32 RegionSize) { const int64 TileCells = static_cast(CHUNK_SIZE) * static_cast(CHUNK_SIZE) * static_cast(CHUNK_SIZE); const int64 TileGridSamples = static_cast(CHUNK_SIZE + 3) * static_cast(CHUNK_SIZE + 3) * static_cast(CHUNK_SIZE + 3); // This preflight covers one canonical tile's scalar grid, edge map, mesh arrays, and temporary // triangle lists. The run-level canonical aggregate is retained separately so render and // export consume the same mesh. RegionSize is still accepted here to keep the call site's // contract explicit. (void)RegionSize; return TileGridSamples * static_cast(sizeof(float)) + TileCells * 448ll; } bool WriteUtf8(FArchive& Archive, const FString& Text) { FTCHARToUTF8 Converted(*Text); if (Converted.Length() > 0) { Archive.Serialize(const_cast(Converted.Get()), Converted.Length()); } return !Archive.IsError(); } bool ValidateCanonicalTile(const FVoxelMeshData& MeshData, FString& OutError) { if (MeshData.Vertices.Num() != MeshData.Normals.Num() || MeshData.Vertices.Num() != MeshData.UVs.Num() || MeshData.Vertices.Num() != MeshData.Colors.Num() || MeshData.Triangles.Num() % 3 != 0) { OutError = TEXT("Canonical mesher returned non-parallel mesh arrays."); return false; } return true; } bool ExploreMetricVectorIsFinite(const FVector& Value) { return FMath::IsFinite(Value.X) && FMath::IsFinite(Value.Y) && FMath::IsFinite(Value.Z); } FIntVector ExploreMetricWeldPosition(const FVector& PositionCm) { // Mesh positions are Unreal centimetres. 0.01 cm is deliberately much smaller than a // marching-cubes cell and only absorbs independent-tile floating-point round-off. constexpr float WeldQuantumCm = 0.01f; return FIntVector( FMath::RoundToInt(PositionCm.X / WeldQuantumCm), FMath::RoundToInt(PositionCm.Y / WeldQuantumCm), FMath::RoundToInt(PositionCm.Z / WeldQuantumCm)); } FExploreMeshEdgeKey ExploreMetricMakeEdgeKey( const FVector& PositionA, const FVector& PositionB) { const FIntVector A = ExploreMetricWeldPosition(PositionA); const FIntVector B = ExploreMetricWeldPosition(PositionB); const bool bAFirst = A.X < B.X || (A.X == B.X && (A.Y < B.Y || (A.Y == B.Y && A.Z <= B.Z))); return bAFirst ? FExploreMeshEdgeKey{A, B} : FExploreMeshEdgeKey{B, A}; } float ExploreMetricPercentile(const TArray& SortedValues, float Quantile) { if (SortedValues.Num() == 0) { return 0.0f; } const float Position = FMath::Clamp(Quantile, 0.0f, 1.0f) * static_cast(SortedValues.Num() - 1); const int32 Lower = FMath::FloorToInt(Position); const int32 Upper = FMath::Min(Lower + 1, SortedValues.Num() - 1); return FMath::Lerp(SortedValues[Lower], SortedValues[Upper], Position - static_cast(Lower)); } bool MeasureExploreMesh( const FVoxelMeshData& MeshData, FExploreMeshMetrics& OutMetrics, FString& OutError) { OutMetrics = FExploreMeshMetrics(); if (!ValidateCanonicalTile(MeshData, OutError)) { return false; } const int32 NumTriangles = MeshData.Triangles.Num() / 3; OutMetrics.VertexCount = MeshData.Vertices.Num(); OutMetrics.TriangleCount = NumTriangles; TArray EdgeLengthsMeters; TArray DihedralAnglesDegrees; TArray FaceNormalDivergencesDegrees; EdgeLengthsMeters.Reserve(NumTriangles * 3); DihedralAnglesDegrees.Reserve(NumTriangles * 2); FaceNormalDivergencesDegrees.Reserve(NumTriangles); TArray FaceNormals; FaceNormals.Init(FVector::ZeroVector, NumTriangles); TArray ValidFaceNormals; ValidFaceNormals.Init(0, NumTriangles); TMap EdgeIncidents; EdgeIncidents.Reserve(FMath::Max(1, NumTriangles * 2)); const auto AddEdgeIncident = [&EdgeIncidents]( const FVector& PositionA, const FVector& PositionB, int32 TriangleIndex) { const FExploreMeshEdgeKey Key = ExploreMetricMakeEdgeKey(PositionA, PositionB); if (Key.First == Key.Second) { return; } FExploreMeshEdgeIncident& Incident = EdgeIncidents.FindOrAdd(Key); if (Incident.IncidentCount == 0) { Incident.FirstTriangle = TriangleIndex; } else if (Incident.IncidentCount == 1) { Incident.SecondTriangle = TriangleIndex; } Incident.IncidentCount = FMath::Min(Incident.IncidentCount + 1, 3); }; for (int32 TriangleIndex = 0; TriangleIndex < NumTriangles; ++TriangleIndex) { const int32 Base = TriangleIndex * 3; const int32 IndexA = MeshData.Triangles[Base]; const int32 IndexB = MeshData.Triangles[Base + 1]; const int32 IndexC = MeshData.Triangles[Base + 2]; if (!MeshData.Vertices.IsValidIndex(IndexA) || !MeshData.Vertices.IsValidIndex(IndexB) || !MeshData.Vertices.IsValidIndex(IndexC) || !MeshData.Normals.IsValidIndex(IndexA) || !MeshData.Normals.IsValidIndex(IndexB) || !MeshData.Normals.IsValidIndex(IndexC)) { OutError = TEXT("Surface metric measurement saw an out-of-range mesh index."); return false; } const FVector& A = MeshData.Vertices[IndexA]; const FVector& B = MeshData.Vertices[IndexB]; const FVector& C = MeshData.Vertices[IndexC]; if (!ExploreMetricVectorIsFinite(A) || !ExploreMetricVectorIsFinite(B) || !ExploreMetricVectorIsFinite(C)) { OutError = TEXT("Surface metric measurement saw a non-finite vertex."); return false; } EdgeLengthsMeters.Add(FVector::Dist(A, B) * 0.01f); EdgeLengthsMeters.Add(FVector::Dist(B, C) * 0.01f); EdgeLengthsMeters.Add(FVector::Dist(C, A) * 0.01f); const FVector Cross = FVector::CrossProduct(B - A, C - A); if (Cross.SizeSquared() > SMALL_NUMBER) { const FVector MeanVertexNormal = ( MeshData.Normals[IndexA] + MeshData.Normals[IndexB] + MeshData.Normals[IndexC]).GetSafeNormal(); if (ExploreMetricVectorIsFinite(MeanVertexNormal) && !MeanVertexNormal.IsNearlyZero()) { // The canonical marching-cubes table is emitted with the winding required by // the runtime mesh path, while the density-gradient normals point solid-to-air. // Orient the geometric face normal to that same outward convention before // measuring the relief divergence; otherwise a winding convention change would // report ~180 degrees for an otherwise smooth surface. FVector FaceNormal = Cross.GetSafeNormal(); if (FVector::DotProduct(FaceNormal, MeanVertexNormal) < 0.0f) { FaceNormal *= -1.0f; } FaceNormals[TriangleIndex] = FaceNormal; ValidFaceNormals[TriangleIndex] = 1; const float Dot = FMath::Clamp( FVector::DotProduct(FaceNormal, MeanVertexNormal), -1.0f, 1.0f); FaceNormalDivergencesDegrees.Add( FMath::RadiansToDegrees(FMath::Acos(Dot))); ++OutMetrics.NormalComparisonTriangleCount; } } AddEdgeIncident(A, B, TriangleIndex); AddEdgeIncident(B, C, TriangleIndex); AddEdgeIncident(C, A, TriangleIndex); } for (const TPair& Pair : EdgeIncidents) { const FExploreMeshEdgeIncident& Incident = Pair.Value; if (Incident.IncidentCount != 2) { continue; } ++OutMetrics.SharedEdgeCount; if (!ValidFaceNormals.IsValidIndex(Incident.FirstTriangle) || !ValidFaceNormals.IsValidIndex(Incident.SecondTriangle) || !ValidFaceNormals[Incident.FirstTriangle] || !ValidFaceNormals[Incident.SecondTriangle]) { continue; } const float Dot = FMath::Clamp( FVector::DotProduct( FaceNormals[Incident.FirstTriangle], FaceNormals[Incident.SecondTriangle]), -1.0f, 1.0f); DihedralAnglesDegrees.Add(FMath::RadiansToDegrees(FMath::Acos(Dot))); } EdgeLengthsMeters.Sort(); DihedralAnglesDegrees.Sort(); FaceNormalDivergencesDegrees.Sort(); OutMetrics.DihedralSampleCount = DihedralAnglesDegrees.Num(); OutMetrics.EdgeLengthP50Meters = ExploreMetricPercentile(EdgeLengthsMeters, 0.50f); OutMetrics.EdgeLengthP99Meters = ExploreMetricPercentile(EdgeLengthsMeters, 0.99f); OutMetrics.EdgeLengthMaxMeters = EdgeLengthsMeters.Num() > 0 ? EdgeLengthsMeters.Last() : 0.0f; OutMetrics.DihedralP50Degrees = ExploreMetricPercentile(DihedralAnglesDegrees, 0.50f); OutMetrics.DihedralP75Degrees = ExploreMetricPercentile(DihedralAnglesDegrees, 0.75f); OutMetrics.DihedralP90Degrees = ExploreMetricPercentile(DihedralAnglesDegrees, 0.90f); OutMetrics.DihedralP95Degrees = ExploreMetricPercentile(DihedralAnglesDegrees, 0.95f); OutMetrics.DihedralP99Degrees = ExploreMetricPercentile(DihedralAnglesDegrees, 0.99f); if (DihedralAnglesDegrees.Num() > 0) { int32 Over20 = 0; int32 Over40 = 0; for (const float Angle : DihedralAnglesDegrees) { Over20 += Angle > 20.0f ? 1 : 0; Over40 += Angle > 40.0f ? 1 : 0; } OutMetrics.DihedralFractionOver20Degrees = static_cast(Over20) / static_cast(DihedralAnglesDegrees.Num()); OutMetrics.DihedralFractionOver40Degrees = static_cast(Over40) / static_cast(DihedralAnglesDegrees.Num()); } OutMetrics.FaceNormalVsMeanVertexNormalP50Degrees = ExploreMetricPercentile(FaceNormalDivergencesDegrees, 0.50f); OutMetrics.FaceNormalVsMeanVertexNormalP90Degrees = ExploreMetricPercentile(FaceNormalDivergencesDegrees, 0.90f); OutError.Reset(); return true; } bool WriteObjHeader(FArchive& Archive) { bool bOk = WriteUtf8(Archive, TEXT("# VoxelForge authoritative marching-cubes mesh\n")); bOk = bOk && WriteUtf8(Archive, TEXT("# Positions are metres; UVs retain canonical voxel-space values; source winding is preserved.\n")); bOk = bOk && WriteUtf8(Archive, TEXT("o VoxelForgeExplore\n")); return bOk && !Archive.IsError(); } bool WriteObjTile( FArchive& Archive, const FVoxelMeshData& MeshData, int32 VertexOffset, FString& OutError) { if (!ValidateCanonicalTile(MeshData, OutError)) { return false; } bool bOk = true; for (const FVector& Vertex : MeshData.Vertices) { bOk = bOk && WriteUtf8(Archive, FString::Printf( TEXT("v %.9f %.9f %.9f\n"), Vertex.X / 100.0, Vertex.Y / 100.0, Vertex.Z / 100.0)); } for (const FVector2D& UV : MeshData.UVs) { bOk = bOk && WriteUtf8(Archive, FString::Printf( TEXT("vt %.9f %.9f\n"), UV.X, UV.Y)); } for (const FVector& Normal : MeshData.Normals) { bOk = bOk && WriteUtf8(Archive, FString::Printf( TEXT("vn %.9f %.9f %.9f\n"), Normal.X, Normal.Y, Normal.Z)); } const int32 NumTriangles = MeshData.Triangles.Num() / 3; const int32 FirstCeilingTriangle = FMath::Clamp( NumTriangles - MeshData.NumCeilingTriangles, 0, NumTriangles); if (NumTriangles > 0) { bOk = bOk && WriteUtf8(Archive, TEXT("usemtl VoxelGround\n")); } for (int32 Triangle = 0; Triangle < NumTriangles; ++Triangle) { if (Triangle == FirstCeilingTriangle) { bOk = bOk && WriteUtf8(Archive, TEXT("usemtl VoxelCeiling\n")); } const int32 Base = Triangle * 3; const int32 SourceA = MeshData.Triangles[Base]; const int32 SourceB = MeshData.Triangles[Base + 1]; const int32 SourceC = MeshData.Triangles[Base + 2]; if (SourceA < 0 || SourceB < 0 || SourceC < 0 || SourceA >= MeshData.Vertices.Num() || SourceB >= MeshData.Vertices.Num() || SourceC >= MeshData.Vertices.Num()) { OutError = TEXT("Canonical mesher returned an out-of-range triangle index."); return false; } const int32 A = SourceA + 1 + VertexOffset; const int32 B = SourceB + 1 + VertexOffset; const int32 C = SourceC + 1 + VertexOffset; bOk = bOk && WriteUtf8(Archive, FString::Printf( TEXT("f %d/%d/%d %d/%d/%d %d/%d/%d\n"), A, A, A, B, B, B, C, C, C)); } if (!bOk || Archive.IsError()) { OutError = TEXT("Could not write a canonical OBJ tile."); return false; } return true; } bool WriteObjMesh( FArchive& Archive, const FVoxelMeshData& MeshData, FExploreBudget& Budget, FString& OutError, bool& bOutTruncated, int32& OutVerticesWritten, int32& OutTrianglesWritten) { bOutTruncated = false; OutVerticesWritten = 0; OutTrianglesWritten = 0; if (!ValidateCanonicalTile(MeshData, OutError)) { return false; } bool bOk = true; for (int32 VertexIndex = 0; VertexIndex < MeshData.Vertices.Num(); ++VertexIndex) { if ((VertexIndex & 255) == 0 && Budget.ShouldStop(TEXT("export-write"))) { bOutTruncated = true; return true; } const FVector& Vertex = MeshData.Vertices[VertexIndex]; bOk = bOk && WriteUtf8(Archive, FString::Printf( TEXT("v %.9f %.9f %.9f\n"), Vertex.X / 100.0, Vertex.Y / 100.0, Vertex.Z / 100.0)); ++OutVerticesWritten; } for (int32 UVIndex = 0; UVIndex < MeshData.UVs.Num(); ++UVIndex) { if ((UVIndex & 255) == 0 && Budget.ShouldStop(TEXT("export-write"))) { bOutTruncated = true; return true; } const FVector2D& UV = MeshData.UVs[UVIndex]; bOk = bOk && WriteUtf8(Archive, FString::Printf( TEXT("vt %.9f %.9f\n"), UV.X, UV.Y)); } for (int32 NormalIndex = 0; NormalIndex < MeshData.Normals.Num(); ++NormalIndex) { if ((NormalIndex & 255) == 0 && Budget.ShouldStop(TEXT("export-write"))) { bOutTruncated = true; return true; } const FVector& Normal = MeshData.Normals[NormalIndex]; bOk = bOk && WriteUtf8(Archive, FString::Printf( TEXT("vn %.9f %.9f %.9f\n"), Normal.X, Normal.Y, Normal.Z)); } const int32 NumTriangles = MeshData.Triangles.Num() / 3; if (NumTriangles > 0) { bOk = bOk && WriteUtf8(Archive, TEXT("usemtl VoxelGround\n")); } for (int32 Triangle = 0; Triangle < NumTriangles; ++Triangle) { if ((Triangle & 255) == 0 && Budget.ShouldStop(TEXT("export-write"))) { bOutTruncated = true; return true; } const int32 Base = Triangle * 3; const int32 A = MeshData.Triangles[Base]; const int32 B = MeshData.Triangles[Base + 1]; const int32 C = MeshData.Triangles[Base + 2]; if (A < 0 || B < 0 || C < 0 || A >= MeshData.Vertices.Num() || B >= MeshData.Vertices.Num() || C >= MeshData.Vertices.Num()) { OutError = TEXT("Canonical aggregate mesh returned an out-of-range triangle index."); return false; } bOk = bOk && WriteUtf8(Archive, FString::Printf( TEXT("f %d/%d/%d %d/%d/%d %d/%d/%d\n"), A + 1, A + 1, A + 1, B + 1, B + 1, B + 1, C + 1, C + 1, C + 1)); ++OutTrianglesWritten; } if (!bOk || Archive.IsError()) { OutError = TEXT("Could not write the canonical aggregate OBJ."); return false; } return true; } void WriteJsonVector(FExploreJsonWriter& Writer, const TCHAR* Key, const FVector& Value, float Scale) { Writer.WriteObjectStart(Key); Writer.WriteValue(TEXT("x"), static_cast(Value.X) * Scale); Writer.WriteValue(TEXT("y"), static_cast(Value.Y) * Scale); Writer.WriteValue(TEXT("z"), static_cast(Value.Z) * Scale); Writer.WriteObjectEnd(); } void WriteJsonIntVector(FExploreJsonWriter& Writer, const TCHAR* Key, const FIntVector& Value) { Writer.WriteObjectStart(Key); Writer.WriteValue(TEXT("x"), Value.X); Writer.WriteValue(TEXT("y"), Value.Y); Writer.WriteValue(TEXT("z"), Value.Z); Writer.WriteObjectEnd(); } void WriteJsonBounds( FExploreJsonWriter& Writer, const TCHAR* Key, const FIntVector& Origin, int32 Size, float VoxelToMeters) { Writer.WriteObjectStart(Key); Writer.WriteObjectStart(TEXT("min")); Writer.WriteValue(TEXT("x"), Origin.X); Writer.WriteValue(TEXT("y"), Origin.Y); Writer.WriteValue(TEXT("z"), Origin.Z); Writer.WriteObjectEnd(); Writer.WriteObjectStart(TEXT("max_exclusive")); Writer.WriteValue(TEXT("x"), Origin.X + Size); Writer.WriteValue(TEXT("y"), Origin.Y + Size); Writer.WriteValue(TEXT("z"), Origin.Z + Size); Writer.WriteObjectEnd(); Writer.WriteValue(TEXT("units"), TEXT("voxels")); Writer.WriteValue(TEXT("voxel_size_m"), static_cast(VoxelToMeters)); Writer.WriteObjectEnd(); } void WriteSurfaceReliefMetrics( FExploreJsonWriter& Writer, const FExploreMeshMetrics& Metrics) { Writer.WriteObjectStart(TEXT("surface_relief_metrics")); Writer.WriteValue(TEXT("vertex_count"), Metrics.VertexCount); Writer.WriteValue(TEXT("triangle_count"), Metrics.TriangleCount); Writer.WriteValue(TEXT("shared_edge_count"), Metrics.SharedEdgeCount); Writer.WriteValue(TEXT("dihedral_sample_count"), Metrics.DihedralSampleCount); Writer.WriteValue(TEXT("normal_comparison_triangle_count"), Metrics.NormalComparisonTriangleCount); Writer.WriteValue(TEXT("vertex_weld_quantum_cm"), 0.01); Writer.WriteObjectStart(TEXT("triangle_edge_length_m")); Writer.WriteValue(TEXT("p50"), static_cast(Metrics.EdgeLengthP50Meters)); Writer.WriteValue(TEXT("p99"), static_cast(Metrics.EdgeLengthP99Meters)); Writer.WriteValue(TEXT("max"), static_cast(Metrics.EdgeLengthMaxMeters)); Writer.WriteObjectEnd(); Writer.WriteObjectStart(TEXT("dihedral_angle_degrees")); Writer.WriteValue(TEXT("p50"), static_cast(Metrics.DihedralP50Degrees)); Writer.WriteValue(TEXT("p75"), static_cast(Metrics.DihedralP75Degrees)); Writer.WriteValue(TEXT("p90"), static_cast(Metrics.DihedralP90Degrees)); Writer.WriteValue(TEXT("p95"), static_cast(Metrics.DihedralP95Degrees)); Writer.WriteValue(TEXT("p99"), static_cast(Metrics.DihedralP99Degrees)); Writer.WriteValue(TEXT("fraction_over_20_degrees"), static_cast(Metrics.DihedralFractionOver20Degrees)); Writer.WriteValue(TEXT("fraction_over_40_degrees"), static_cast(Metrics.DihedralFractionOver40Degrees)); Writer.WriteObjectEnd(); Writer.WriteObjectStart(TEXT("face_normal_vs_mean_vertex_normal_degrees")); Writer.WriteValue(TEXT("p50"), static_cast(Metrics.FaceNormalVsMeanVertexNormalP50Degrees)); Writer.WriteValue(TEXT("p90"), static_cast(Metrics.FaceNormalVsMeanVertexNormalP90Degrees)); Writer.WriteObjectEnd(); Writer.WriteObjectEnd(); } FString BuildManifestJson( const FExploreArguments& Arguments, const FExploreExportOutput& Export) { FString Json; TSharedRef Writer = TJsonWriterFactory>::Create(&Json); Writer->WriteObjectStart(); Writer->WriteValue(TEXT("schema_version"), 3); 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); 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("truncated"), Export.bTruncated); Writer->WriteValue(TEXT("seed"), Arguments.Seed); Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype)); Writer->WriteValue(TEXT("slot"), Arguments.Slot); Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack); Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride); if (Arguments.bSurfaceRoughnessOverride) { Writer->WriteValue(TEXT("surface_roughness"), static_cast(Arguments.SurfaceRoughness)); } WriteJsonIntVector(*Writer, TEXT("region_origin_voxels"), Export.RegionOrigin); WriteJsonBounds(*Writer, TEXT("region_bounds_voxels"), Export.RegionOrigin, Export.RegionSize, 0.25f); Writer->WriteObjectStart(TEXT("region_bounds_metres")); Writer->WriteObjectStart(TEXT("min")); Writer->WriteValue(TEXT("x"), static_cast(Export.RegionOrigin.X) * 0.25); Writer->WriteValue(TEXT("y"), static_cast(Export.RegionOrigin.Y) * 0.25); Writer->WriteValue(TEXT("z"), static_cast(Export.RegionOrigin.Z) * 0.25); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("max_exclusive")); Writer->WriteValue(TEXT("x"), static_cast(Export.RegionOrigin.X + Export.RegionSize) * 0.25); Writer->WriteValue(TEXT("y"), static_cast(Export.RegionOrigin.Y + Export.RegionSize) * 0.25); Writer->WriteValue(TEXT("z"), static_cast(Export.RegionOrigin.Z + Export.RegionSize) * 0.25); Writer->WriteObjectEnd(); Writer->WriteValue(TEXT("units"), TEXT("metres")); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("player_dimensions")); Writer->WriteValue(TEXT("capsule_radius_m"), static_cast(FVoxelPlayerCapsuleConstants::RadiusCentimeters) / 100.0); Writer->WriteValue(TEXT("capsule_width_m"), static_cast(2.0f * FVoxelPlayerCapsuleConstants::RadiusCentimeters) / 100.0); Writer->WriteValue(TEXT("capsule_half_height_m"), static_cast(FVoxelPlayerCapsuleConstants::HalfHeightCentimeters) / 100.0); Writer->WriteValue(TEXT("capsule_height_m"), static_cast(2.0f * FVoxelPlayerCapsuleConstants::HalfHeightCentimeters) / 100.0); Writer->WriteValue(TEXT("max_step_height_m"), static_cast(FVoxelPlayerCapsuleConstants::MaxStepHeightMeters)); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("world_invariants")); Writer->WriteValue(TEXT("world_radius_voxels"), 0); Writer->WriteValue(TEXT("lateral_regions_enabled"), false); Writer->WriteValue(TEXT("diff_layer_mutated"), false); Writer->WriteObjectEnd(); Writer->WriteObjectEnd(); return Writer->Close() ? Json : FString(); } bool RunExport( const FExploreArguments& Arguments, FExploreWorld& World, const FExploreWalkOutput* SharedSampling, FExploreExportOutput& OutOutput, FExploreBudget& Budget) { OutOutput = FExploreExportOutput(); OutOutput.RegionSize = Arguments.ExportSize; OutOutput.EstimatedWorkingBytes = EstimateExportWorkingBytes(Arguments.ExportSize); if (OutOutput.EstimatedWorkingBytes > OutOutput.WorkingMemoryCapBytes) { OutOutput.Status = TEXT("refused"); OutOutput.RefusalReason = FString::Printf( TEXT("Refused before meshing: estimated working memory %lld bytes exceeds cap %lld bytes."), static_cast(OutOutput.EstimatedWorkingBytes), static_cast(OutOutput.WorkingMemoryCapBytes)); return false; } FString MeshError; const bool bMeshComplete = EnsureExploreMesh( Arguments, World, SharedSampling, Budget, MeshError); if (!bMeshComplete && !Budget.bTruncated) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = MeshError; return false; } OutOutput.RegionSize = World.ExploreMeshSize; OutOutput.MesherTileCount = World.ExploreMeshTileCount; OutOutput.MesherTilesCompleted = World.ExploreMeshTilesCompleted; OutOutput.RegionOrigin = World.ExploreMeshOrigin; FString Error; OutOutput.MeshFileName = TEXT("geometry.obj"); OutOutput.ManifestFileName = TEXT("manifest.json"); const FString MeshPath = FPaths::Combine(Arguments.OutDirectory, OutOutput.MeshFileName); TUniquePtr Archive(IFileManager::Get().CreateFileWriter(*MeshPath)); if (!Archive.IsValid()) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = FString::Printf(TEXT("Could not create OBJ '%s'."), *MeshPath); return false; } if (!WriteObjHeader(*Archive)) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = TEXT("Could not write the OBJ header."); return false; } if (!MeasureExploreMesh(World.ExploreMesh, OutOutput.SurfaceMetrics, Error)) { OutOutput.Status = TEXT("error"); OutOutput.RefusalReason = Error; return false; } OutOutput.MeshArrayBytes = static_cast(World.ExploreMesh.Vertices.Num()) * sizeof(FVector) + static_cast(World.ExploreMesh.Normals.Num()) * sizeof(FVector) + static_cast(World.ExploreMesh.UVs.Num()) * sizeof(FVector2D) + static_cast(World.ExploreMesh.Colors.Num()) * sizeof(FColor) + static_cast(World.ExploreMesh.Triangles.Num()) * sizeof(int32); OutOutput.VertexCount = World.ExploreMesh.Vertices.Num(); OutOutput.TriangleCount = World.ExploreMesh.Triangles.Num() / 3; OutOutput.MeshSeconds = World.ExploreMeshSeconds; 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.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 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; } if (!OutOutput.bTruncated) { OutOutput.Status = TEXT("ok"); } return !OutOutput.bTruncated; } void WritePlayerDimensions(FExploreJsonWriter& Writer, const TCHAR* Key) { Writer.WriteObjectStart(Key); Writer.WriteValue(TEXT("capsule_radius_m"), static_cast(FVoxelPlayerCapsuleConstants::RadiusCentimeters) / 100.0); Writer.WriteValue(TEXT("capsule_width_m"), static_cast(2.0f * FVoxelPlayerCapsuleConstants::RadiusCentimeters) / 100.0); Writer.WriteValue(TEXT("capsule_half_height_m"), static_cast(FVoxelPlayerCapsuleConstants::HalfHeightCentimeters) / 100.0); Writer.WriteValue(TEXT("capsule_height_m"), static_cast(2.0f * FVoxelPlayerCapsuleConstants::HalfHeightCentimeters) / 100.0); Writer.WriteValue(TEXT("max_step_height_m"), static_cast(FVoxelPlayerCapsuleConstants::MaxStepHeightMeters)); Writer.WriteValue(TEXT("walkable_floor_angle_degrees"), static_cast(FVoxelPlayerCapsuleConstants::WalkableFloorAngleDegrees)); Writer.WriteObjectEnd(); } FString BuildExploreJson( const FExploreArguments& Arguments, const FExploreWorld& World, const FExploreRunOutput& Output, bool bDeterminismAssertion, uint32 PayloadCrc32) { FString Json; TSharedRef Writer = TJsonWriterFactory>::Create(&Json); Writer->WriteObjectStart(); Writer->WriteValue(TEXT("schema_version"), 3); Writer->WriteValue(TEXT("tool"), TEXT("VoxelForgeExplore")); Writer->WriteValue(TEXT("read_only_generation"), true); Writer->WriteObjectStart(TEXT("input")); Writer->WriteValue(TEXT("seed"), Arguments.Seed); Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype)); Writer->WriteValue(TEXT("slot"), Arguments.Slot); Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack); Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride); if (Arguments.bSurfaceRoughnessOverride) { Writer->WriteValue(TEXT("surface_roughness"), static_cast(Arguments.SurfaceRoughness)); } 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")); Writer->WriteArrayEnd(); Writer->WriteValue(TEXT("failure_focus_render"), Arguments.bFailureFocusRender); Writer->WriteValue(TEXT("budget_minutes"), static_cast(Arguments.BudgetMinutes)); Writer->WriteValue(TEXT("out_directory"), Arguments.OutDirectory); Writer->WriteValue(TEXT("canonical_invocation"), FString::Printf( TEXT("UnrealEditor-Cmd VoxelM.uproject -run=VoxelForgeExplore -seed=%d -archetype=%s " "-slot=%d -opstack=%d -modes=%s -out=Saved/VoxelForge/Explore/"), Arguments.Seed, ArchetypeName(Arguments.Archetype), Arguments.Slot, Arguments.bUseOperatorStack ? 1 : 0, *Arguments.CanonicalModes())); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("run")); Writer->WriteValue(TEXT("budget_seconds"), Output.BudgetSeconds); Writer->WriteValue(TEXT("elapsed_seconds"), Output.ElapsedSeconds); Writer->WriteValue(TEXT("truncated"), Output.bTruncated); Writer->WriteValue(TEXT("truncated_during"), Output.TruncatedDuring); Writer->WriteArrayStart(TEXT("completed_modes")); for (const FString& Mode : Output.CompletedModes) { Writer->WriteValue(Mode); } Writer->WriteArrayEnd(); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("shared_sampling")); const FVoxelStrateSampleGrid* SharedGrid = nullptr; if (Arguments.bWalk || Arguments.bRender) { SharedGrid = &Output.Walk.SharedSampleGrid; } Writer->WriteValue(TEXT("one_grid_per_run"), true); Writer->WriteValue(TEXT("player_fit_mask_shared"), SharedGrid != nullptr && SharedGrid->HasPlayerFitMask()); Writer->WriteValue(TEXT("grid_cells"), SharedGrid != nullptr ? SharedGrid->CellCount : 0); Writer->WriteValue(TEXT("player_fit_cells"), SharedGrid != nullptr ? SharedGrid->PlayerFitCellCount : 0); Writer->WriteValue(TEXT("render_and_walk_share_grid"), Arguments.bRender && Arguments.bWalk); Writer->WriteValue(TEXT("export_uses_grid"), Arguments.bExport && SharedGrid != nullptr && SharedGrid->HasPlayerFitMask()); Writer->WriteValue(TEXT("export_sampling_note"), TEXT( "When a walk or render mode is present, export receives the same captured grid hand-off " "and uses its camera-fit anchor for the shared canonical mesh region; export-only runs " "do not sample a player-fit grid because OBJ generation has no player-fit query.")); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("world")); Writer->WriteValue(TEXT("voxel_size_m"), 0.25); Writer->WriteValue(TEXT("world_radius_voxels"), 0); Writer->WriteValue(TEXT("lateral_regions_enabled"), false); Writer->WriteValue(TEXT("diff_layer_mutated"), false); Writer->WriteValue(TEXT("target_strate_slot"), Arguments.Slot); Writer->WriteValue(TEXT("target_bottom_voxel_z"), World.TargetBottomWorldZ); Writer->WriteValue(TEXT("target_top_voxel_z_exclusive"), World.TargetTopWorldZ); Writer->WriteValue(TEXT("target_bottom_m"), static_cast(World.TargetBottomWorldZ) * 0.25); Writer->WriteValue(TEXT("target_top_m_exclusive"), static_cast(World.TargetTopWorldZ) * 0.25); Writer->WriteValue(TEXT("target_boundary_seal_thickness_voxels"), static_cast(World.TargetBoundarySealThickness)); WritePlayerDimensions(*Writer, TEXT("player_dimensions")); Writer->WriteObjectEnd(); if (Arguments.bRender) { Writer->WriteObjectStart(TEXT("render")); Writer->WriteValue(TEXT("status"), Output.Render.Status); Writer->WriteValue(TEXT("refusal_or_error"), Output.Render.RefusalReason); Writer->WriteValue(TEXT("width_px"), Output.Render.Width); Writer->WriteValue(TEXT("height_px"), Output.Render.Height); Writer->WriteValue(TEXT("max_pixels_cap"), MaxRenderPixels); Writer->WriteValue(TEXT("max_fixed_density_samples_cap"), MaxRenderDensitySamples); Writer->WriteValue(TEXT("estimated_fixed_density_samples"), Output.Render.EstimatedFixedDensitySamples); Writer->WriteValue(TEXT("fixed_step_voxels"), static_cast(Output.Render.FixedStepVoxels)); Writer->WriteValue(TEXT("fixed_step_m"), static_cast(Output.Render.FixedStepMeters)); Writer->WriteValue(TEXT("max_distance_voxels"), static_cast(Output.Render.MaxDistanceVoxels)); Writer->WriteValue(TEXT("bisection_iterations"), Output.Render.BisectionIterations); Writer->WriteValue(TEXT("human_marker_height_m"), 1.76); Writer->WriteValue(TEXT("step_rationale"), Output.Render.StepRationale); Writer->WriteValue(TEXT("camera_seed_policy"), Output.Render.CameraSeedPolicy); Writer->WriteValue(TEXT("camera_seed_count"), Output.Render.CameraSeedCount); Writer->WriteValue(TEXT("all_cameras_player_fit"), Output.Render.bAllCamerasPlayerFit); Writer->WriteValue(TEXT("requested_viewpoint_count"), Output.Render.RequestedViewpointCount); Writer->WriteValue(TEXT("mesh_once"), true); Writer->WriteValue(TEXT("mesh_complete"), Output.Render.bMeshComplete); Writer->WriteValue(TEXT("mesh_seconds"), Output.Render.MeshSeconds); Writer->WriteValue(TEXT("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 six-neighbour graph; stop at departure")); Writer->WriteValue(TEXT("player_fit_volume_scope"), TEXT( "bounded fitted route window; WorldRadiusVoxels=0 has no finite whole-world volume")); Writer->WriteValue(TEXT("max_cells_cap"), Arguments.MaxWalkCells); Writer->WriteValue(TEXT("working_memory_estimate_per_cell_bytes"), EstimatedWalkBytesPerCell); Writer->WriteValue(TEXT("estimated_working_bytes_at_cell_cap"), static_cast(Arguments.MaxWalkCells) * EstimatedWalkBytesPerCell); Writer->WriteValue(TEXT("working_memory_cap_bytes"), MaxWalkWorkingBytes); Writer->WriteValue(TEXT("arrival_passage_count"), Output.Walk.ArrivalPassageCount); Writer->WriteValue(TEXT("departure_passage_count"), Output.Walk.DeparturePassageCount); Writer->WriteValue(TEXT("landing_room_probe_offsets_voxels"), 2.0); Writer->WriteValue(TEXT("arrival_landing_room_probe_count"), Output.Walk.ArrivalLandingRoomProbeCount); Writer->WriteValue(TEXT("arrival_landing_room_player_fit_probe_count"), Output.Walk.ArrivalLandingRoomPlayerFitProbeCount); Writer->WriteValue(TEXT("arrival_landing_room_arrival_component_probe_count"), Output.Walk.ArrivalLandingRoomArrivalComponentProbeCount); Writer->WriteValue(TEXT("arrival_landing_room_in_arrival_component"), Output.Walk.ArrivalLandingRoomProbeCount > 0 && Output.Walk.ArrivalLandingRoomArrivalComponentProbeCount == Output.Walk.ArrivalLandingRoomProbeCount); Writer->WriteValue(TEXT("departure_landing_room_probe_count"), Output.Walk.DepartureLandingRoomProbeCount); Writer->WriteValue(TEXT("departure_landing_room_player_fit_probe_count"), Output.Walk.DepartureLandingRoomPlayerFitProbeCount); Writer->WriteValue(TEXT("departure_landing_room_arrival_component_probe_count"), Output.Walk.DepartureLandingRoomArrivalComponentProbeCount); Writer->WriteValue(TEXT("departure_landing_room_in_arrival_component"), Output.Walk.DepartureLandingRoomProbeCount > 0 && Output.Walk.DepartureLandingRoomArrivalComponentProbeCount == Output.Walk.DepartureLandingRoomProbeCount); if (Output.Walk.bHasArrival) { WriteJsonVector(*Writer, TEXT("arrival_mouth_voxels"), Output.Walk.ArrivalVoxels, 1.0f); WriteJsonVector(*Writer, TEXT("arrival_mouth_metres"), Output.Walk.ArrivalVoxels, 0.25f); } if (Output.Walk.bHasDeparture) { WriteJsonVector(*Writer, TEXT("departure_mouth_voxels"), Output.Walk.DepartureVoxels, 1.0f); WriteJsonVector(*Writer, TEXT("departure_mouth_metres"), Output.Walk.DepartureVoxels, 0.25f); } Writer->WriteValue(TEXT("connectivity_result"), ConnectivityResultName(Report.Result)); Writer->WriteValue(TEXT("connectivity_result_code"), static_cast(Report.Result)); Writer->WriteValue(TEXT("can_reach_departure"), Report.bCanReachDeparture); Writer->WriteValue(TEXT("start_snapped"), Report.bStartSnapped); Writer->WriteValue(TEXT("goal_snapped"), Report.bGoalSnapped); Writer->WriteValue(TEXT("route_retries"), Report.NumRouteRetries); Writer->WriteValue(TEXT("player_fit_volume_cells"), Report.PlayerFitVolumeCells); Writer->WriteValue(TEXT("reachable_player_fit_cells"), Report.ReachablePlayerFitCells); Writer->WriteValue(TEXT("reachable_player_fit_fraction"), static_cast(Report.ReachablePlayerFitFraction)); Writer->WriteValue(TEXT("sampled_num_x"), Report.SampledNumX); Writer->WriteValue(TEXT("sampled_num_y"), Report.SampledNumY); Writer->WriteValue(TEXT("sampled_num_z"), Report.SampledNumZ); Writer->WriteValue(TEXT("sampled_bounds_units"), TEXT("voxels")); Writer->WriteValue(TEXT("sampled_min_z"), Report.SampledMinZ); Writer->WriteValue(TEXT("sampled_max_z_exclusive"), Report.SampledMaxZ); Writer->WriteValue(TEXT("sampled_min_x"), static_cast(Report.SampledMinX)); Writer->WriteValue(TEXT("sampled_max_x_exclusive"), static_cast(Report.SampledMaxX)); Writer->WriteValue(TEXT("sampled_min_y"), static_cast(Report.SampledMinY)); Writer->WriteValue(TEXT("sampled_max_y_exclusive"), static_cast(Report.SampledMaxY)); Writer->WriteValue(TEXT("agent_graph_traversals"), Report.AgentGraphTraversals); Writer->WriteValue(TEXT("distance_travelled_m"), static_cast(Report.DistanceTravelledMeters)); Writer->WriteValue(TEXT("straight_line_m"), static_cast(Report.StraightLineMeters)); Writer->WriteValue(TEXT("tortuosity"), static_cast(Report.Tortuosity)); Writer->WriteValue(TEXT("dead_ends_encountered"), Report.DeadEndsEncountered); Writer->WriteValue(TEXT("dead_ends_per_100m"), static_cast(Report.DeadEndsPer100m)); Writer->WriteValue(TEXT("capsule_radius_m"), static_cast(Report.CapsuleRadiusMeters)); Writer->WriteValue(TEXT("capsule_width_m"), static_cast(Report.CapsuleWidthMeters)); Writer->WriteValue(TEXT("capsule_height_m"), static_cast(Report.CapsuleHeightMeters)); Writer->WriteValue(TEXT("narrow_gap_threshold_m"), static_cast(Report.NarrowGapThresholdMeters)); Writer->WriteValue(TEXT("narrow_gap_traversals"), Report.NarrowGapTraversals); Writer->WriteValue(TEXT("narrow_gap_fraction"), static_cast(Report.NarrowGapFraction)); Writer->WriteValue(TEXT("narrow_gap_events_per_100m"), static_cast(Report.NarrowGapEventsPer100m)); Writer->WriteValue(TEXT("narrow_gap_definition"), Report.NarrowGapDefinition); Writer->WriteValue(TEXT("player_fit_components"), Report.PlayerFitComponents); Writer->WriteValue(TEXT("largest_player_fit_component_cells"), Report.LargestPlayerFitComponentCells); Writer->WriteValue(TEXT("arrival_component_cells"), Report.ArrivalComponentCells); Writer->WriteValue(TEXT("departure_component_cells"), Report.DepartureComponentCells); Writer->WriteValue(TEXT("mouth_component_gap_voxels"), static_cast(Report.MouthComponentGapVoxels)); Writer->WriteValue(TEXT("mouth_component_gap_m"), static_cast(Report.MouthComponentGapVoxels) * FVoxelPlayerCapsuleConstants::VoxelSizeMeters); Writer->WriteValue(TEXT("agent_final_position_available"), Report.bHasAgentFinalPosition); if (Report.bHasAgentFinalPosition) { WriteJsonVector(*Writer, TEXT("agent_final_position_voxels"), Report.AgentFinalVoxels, 1.0f); WriteJsonVector(*Writer, TEXT("agent_final_position_metres"), Report.AgentFinalVoxels, 0.25f); } Writer->WriteValue(TEXT("last_reached_position_available"), Report.bHasLastReachedPosition); if (Report.bHasLastReachedPosition) { WriteJsonVector(*Writer, TEXT("last_reached_position_voxels"), Report.LastReachedVoxels, 1.0f); WriteJsonVector(*Writer, TEXT("last_reached_position_metres"), Report.LastReachedVoxels, 0.25f); } Writer->WriteValue(TEXT("target_component_nearest_cell_available"), Report.bHasTargetComponentNearestCell); if (Report.bHasTargetComponentNearestCell) { WriteJsonVector(*Writer, TEXT("target_component_nearest_cell_voxels"), Report.TargetComponentNearestVoxels, 1.0f); WriteJsonVector(*Writer, TEXT("target_component_nearest_cell_metres"), Report.TargetComponentNearestVoxels, 0.25f); } Writer->WriteValue(TEXT("agent_to_target_component_gap_voxels"), static_cast(Report.AgentToTargetComponentGapVoxels)); Writer->WriteValue(TEXT("agent_to_target_component_gap_m"), static_cast(Report.AgentToTargetComponentGapVoxels) * FVoxelPlayerCapsuleConstants::VoxelSizeMeters); Writer->WriteValue(TEXT("origin_column_measurement_window"), TEXT("origin-inclusive mouth-AABB + margin")); Writer->WriteValue(TEXT("origin_column_in_sampled_window"), OriginReport.bOriginColumnInSampledWindow); Writer->WriteValue(TEXT("origin_column_has_player_fit"), OriginReport.bOriginColumnHasPlayerFit); Writer->WriteValue(TEXT("origin_column_player_fit_cells"), OriginReport.OriginColumnPlayerFitCells); Writer->WriteValue(TEXT("origin_column_reachable_from_arrival"), OriginReport.bOriginColumnReachable); Writer->WriteValue(TEXT("reachable_set_to_origin_column_voxels"), static_cast(OriginReport.ReachableSetToOriginColumnVoxels)); Writer->WriteValue(TEXT("reachable_set_to_origin_column_m"), static_cast(OriginReport.ReachableSetToOriginColumnVoxels) * FVoxelPlayerCapsuleConstants::VoxelSizeMeters); Writer->WriteObjectStart(TEXT("origin_check")); Writer->WriteValue(TEXT("attempted"), Output.Walk.bOriginCheckAttempted); Writer->WriteValue(TEXT("available"), Output.Walk.bOriginCheckAvailable); Writer->WriteValue(TEXT("refusal_or_error"), Output.Walk.OriginCheckRefusalReason); if (Output.Walk.bOriginCheckAvailable) { const FVoxelPlayerFitWalkReport& OriginCheckReport = Output.Walk.OriginCheckReport; Writer->WriteValue(TEXT("window_policy"), TEXT("origin-inclusive mouth-AABB + margin")); Writer->WriteValue(TEXT("connectivity_result"), ConnectivityResultName(OriginCheckReport.Result)); Writer->WriteValue(TEXT("can_reach_departure"), OriginCheckReport.bCanReachDeparture); Writer->WriteValue(TEXT("sampled_num_x"), OriginCheckReport.SampledNumX); Writer->WriteValue(TEXT("sampled_num_y"), OriginCheckReport.SampledNumY); Writer->WriteValue(TEXT("sampled_num_z"), OriginCheckReport.SampledNumZ); Writer->WriteValue(TEXT("sampled_min_x"), static_cast(OriginCheckReport.SampledMinX)); Writer->WriteValue(TEXT("sampled_max_x_exclusive"), static_cast(OriginCheckReport.SampledMaxX)); Writer->WriteValue(TEXT("sampled_min_y"), static_cast(OriginCheckReport.SampledMinY)); Writer->WriteValue(TEXT("sampled_max_y_exclusive"), static_cast(OriginCheckReport.SampledMaxY)); Writer->WriteValue(TEXT("sampled_min_z"), OriginCheckReport.SampledMinZ); Writer->WriteValue(TEXT("sampled_max_z_exclusive"), OriginCheckReport.SampledMaxZ); Writer->WriteValue(TEXT("player_fit_cells"), OriginCheckReport.PlayerFitVolumeCells); Writer->WriteValue(TEXT("reachable_player_fit_cells"), OriginCheckReport.ReachablePlayerFitCells); Writer->WriteValue(TEXT("reachable_player_fit_fraction"), static_cast(OriginCheckReport.ReachablePlayerFitFraction)); Writer->WriteValue(TEXT("player_fit_components"), OriginCheckReport.PlayerFitComponents); Writer->WriteValue(TEXT("arrival_component_cells"), OriginCheckReport.ArrivalComponentCells); Writer->WriteValue(TEXT("departure_component_cells"), OriginCheckReport.DepartureComponentCells); Writer->WriteValue(TEXT("mouth_component_gap_voxels"), static_cast(OriginCheckReport.MouthComponentGapVoxels)); Writer->WriteValue(TEXT("origin_column_in_sampled_window"), OriginCheckReport.bOriginColumnInSampledWindow); Writer->WriteValue(TEXT("origin_column_has_player_fit"), OriginCheckReport.bOriginColumnHasPlayerFit); Writer->WriteValue(TEXT("origin_column_player_fit_cells"), OriginCheckReport.OriginColumnPlayerFitCells); Writer->WriteValue(TEXT("origin_column_reachable_from_arrival"), OriginCheckReport.bOriginColumnReachable); Writer->WriteValue(TEXT("reachable_set_to_origin_column_voxels"), static_cast(OriginCheckReport.ReachableSetToOriginColumnVoxels)); Writer->WriteValue(TEXT("reachable_set_to_origin_column_m"), static_cast(OriginCheckReport.ReachableSetToOriginColumnVoxels) * FVoxelPlayerCapsuleConstants::VoxelSizeMeters); } Writer->WriteObjectEnd(); Writer->WriteObjectEnd(); } if (Arguments.bExport) { Writer->WriteObjectStart(TEXT("export")); Writer->WriteValue(TEXT("status"), Output.Export.Status); Writer->WriteValue(TEXT("refusal_or_error"), Output.Export.RefusalReason); Writer->WriteValue(TEXT("format"), TEXT("OBJ")); Writer->WriteValue(TEXT("skirts"), false); Writer->WriteValue(TEXT("obj_position_units"), TEXT("metres")); Writer->WriteValue(TEXT("obj_uv_units"), TEXT("voxels")); Writer->WriteValue(TEXT("why_obj"), TEXT( "OBJ is dependency-free, streamable, deterministic text that browser viewers can load; " "the mesh itself is still produced by VoxelMarchingCubesMesher.")); Writer->WriteValue(TEXT("mesh_file"), Output.Export.MeshFileName); Writer->WriteValue(TEXT("manifest_file"), Output.Export.ManifestFileName); WriteJsonIntVector(*Writer, TEXT("region_origin_voxels"), Output.Export.RegionOrigin); WriteJsonBounds(*Writer, TEXT("region_bounds_voxels"), Output.Export.RegionOrigin, Output.Export.RegionSize, 0.25f); Writer->WriteValue(TEXT("region_size_cells_per_axis"), Output.Export.RegionSize); Writer->WriteValue(TEXT("canonical_mesher_tile_cells"), CHUNK_SIZE); Writer->WriteValue(TEXT("canonical_mesher_tile_count"), Output.Export.MesherTileCount); Writer->WriteValue(TEXT("estimated_working_bytes"), Output.Export.EstimatedWorkingBytes); Writer->WriteValue(TEXT("working_memory_cap_bytes"), Output.Export.WorkingMemoryCapBytes); Writer->WriteValue(TEXT("mesh_array_bytes"), Output.Export.MeshArrayBytes); Writer->WriteValue(TEXT("mesh_array_bytes_sum_over_tiles"), Output.Export.MeshArrayBytes); Writer->WriteValue(TEXT("mesh_file_size_bytes"), Output.Export.MeshFileSizeBytes); Writer->WriteValue(TEXT("vertex_count"), Output.Export.VertexCount); Writer->WriteValue(TEXT("triangle_count"), Output.Export.TriangleCount); WriteSurfaceReliefMetrics(*Writer, Output.Export.SurfaceMetrics); Writer->WriteValue(TEXT("mesh_seconds"), Output.Export.MeshSeconds); Writer->WriteValue(TEXT("truncated"), Output.Export.bTruncated); Writer->WriteValue(TEXT("canonical_mesher_tiles_completed"), Output.Export.MesherTilesCompleted); 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(); } } // namespace UVoxelForgeExploreCommandlet::UVoxelForgeExploreCommandlet() { IsClient = false; IsServer = false; IsEditor = true; LogToConsole = true; } int32 UVoxelForgeExploreCommandlet::Main(const FString& Params) { 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; } if (Arguments.bProfileDensity) { VoxelDensityProfile::Reset(); VoxelDensityProfile::SetEnabled(true); } if (!IFileManager::Get().MakeDirectory(*Arguments.OutDirectory, true)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] Could not create output directory '%s'."), *Arguments.OutDirectory); return 1; } FExploreBudget Budget( MainStartSeconds, static_cast(Arguments.BudgetMinutes) * 60.0); const double SetupStartSeconds = FPlatformTime::Seconds(); FExploreWorld World; if (!World.Build(Arguments, Error)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] %s"), *Error); return 1; } const double SetupSeconds = FPlatformTime::Seconds() - SetupStartSeconds; 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->StrateContentCutMinLevel : 9; for (int32 Level = 0; Level <= 4; ++Level) { // Match AVoxelWorld::LoadTile exactly: the probe is a production tile at this LOD, // including the configured coarse sample count. Using 1<= FullResClipLevels. const int32 Extent = CHUNK_SIZE << Level; const int32 Cells = Level < FullRes ? CHUNK_SIZE : CoarseCells; const int32 Step = FMath::Max(1, Extent / Cells); VoxelDensityProfile::Reset(); VoxelDensityProfile::SetEnabled(true); const double Start = FPlatformTime::Seconds(); const bool bBanded = Level >= CutMin; const FVoxelMeshData Mesh = World.Mesher->GenerateMesh( ProbeOrigin, Step, Cells, nullptr, bBanded ? World.TargetBottomWorldZ : INT32_MIN, bBanded ? World.TargetTopWorldZ - 1 : INT32_MAX); const double Seconds = FPlatformTime::Seconds() - Start; const VoxelDensityProfile::FSnapshot Profile = VoxelDensityProfile::Snapshot(); const int32 DensityIndex = static_cast(VoxelDensityProfile::EBucket::GetDensityAt); const uint64 Calls = Profile.Calls[DensityIndex]; const uint64 Lookups = Profile.Counters[static_cast(VoxelDensityProfile::ECounter::TunnelCacheLookup)]; const uint64 Hits = Profile.Counters[static_cast(VoxelDensityProfile::ECounter::TunnelCacheHit)]; const uint64 SdfBuilds = Profile.Counters[static_cast(VoxelDensityProfile::ECounter::SdfCacheBuild)]; const double DensityUs = static_cast(Profile.Cycles[DensityIndex]) * FPlatformTime::GetSecondsPerCycle64() * 1.0e6; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeLODProfile] level=%d step=%d cells=%d banded=%d seconds=%.6f density_calls=%llu " "density_us_per_call=%.3f sdf_builds=%llu cache_lookups=%llu cache_hits=%llu cache_misses=%llu " "hit_rate=%.3f vertices=%d triangles=%d mesh_empty=%d"), Level, Step, Cells, bBanded ? 1 : 0, Seconds, static_cast(Calls), Calls > 0 ? DensityUs / static_cast(Calls) : 0.0, static_cast(SdfBuilds), static_cast(Lookups), static_cast(Hits), static_cast(Profile.Counters[static_cast(VoxelDensityProfile::ECounter::TunnelCacheMiss)]), Lookups > 0 ? static_cast(Hits) / static_cast(Lookups) : 0.0, Mesh.Vertices.Num(), Mesh.Triangles.Num() / 3, Mesh.IsEmpty() ? 1 : 0); } VoxelDensityProfile::SetEnabled(false); } FExploreRunOutput Output; if (Budget.ShouldStop(TEXT("setup"))) { UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeExplore] setup consumed the wall-clock budget; no mode was started.")); } bool bRequestedModeFailed = false; FExploreWalkOutput RenderSeedWalk; const FExploreWalkOutput* CameraSeedWalk = nullptr; if (Arguments.bWalk) { const double Start = FPlatformTime::Seconds(); const bool bWalkOk = RunWalk(Arguments, World, Output.Walk, Budget); if (!bWalkOk && !Budget.bTruncated) { bRequestedModeFailed = true; } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] walk %.3fs (%s)"), FPlatformTime::Seconds() - Start, *Output.Walk.Status); CameraSeedWalk = &Output.Walk; if (bWalkOk && !Budget.bTruncated) { Budget.CompleteMode(TEXT("walk")); } } else if (Arguments.bRender) { // 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); if (!bWalkOk && !Budget.bTruncated) { bRequestedModeFailed = true; } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] render camera seed walk %.3fs (%s)"), FPlatformTime::Seconds() - Start, *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; } 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); } // 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; // 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; } 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.bProfileDensity) { const VoxelDensityProfile::FSnapshot Profile = VoxelDensityProfile::Snapshot(); const double CycleToMicroseconds = FPlatformTime::GetSecondsPerCycle64() * 1.0e6; const uint64 DensityCalls = Profile.Calls[static_cast(VoxelDensityProfile::EBucket::GetDensityAt)]; const uint64 DensityCycles = Profile.Cycles[static_cast(VoxelDensityProfile::EBucket::GetDensityAt)]; const double DensityTotalUs = static_cast(DensityCycles) * CycleToMicroseconds; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDensityProfile] total calls=%llu total_us=%.3f us_per_call=%.6f"), static_cast(DensityCalls), DensityTotalUs, DensityCalls > 0 ? DensityTotalUs / static_cast(DensityCalls) : 0.0); const uint64 CacheTotalStaticBytes = Profile.TunnelCacheStaticBytes + Profile.RoomGraphCacheStaticBytes; const uint64 CacheTotalDynamicBytes = Profile.TunnelCacheDynamicBytes + Profile.RoomGraphCacheDynamicBytes; const uint64 CacheTotalBytes = CacheTotalStaticBytes + CacheTotalDynamicBytes; const uint64 CacheWorkers = FMath::Max( Profile.TunnelCacheWorkers, Profile.RoomGraphCacheWorkers); const uint64 CacheValidEntries = Profile.TunnelCacheValidEntries + Profile.RoomGraphCacheValidEntries; const uint64 CacheEntryBytes = Profile.TunnelCacheEntryBytes + Profile.RoomGraphCacheEntryBytes; const uint64 CacheCapacityEntries = Profile.TunnelCacheCapacityEntries + Profile.RoomGraphCacheCapacityEntries; const uint64 CacheLargestEntryBytes = FMath::Max( Profile.TunnelCacheLargestEntryBytes, Profile.RoomGraphCacheLargestEntryBytes); const uint64 CacheLargestWorkerBytes = FMath::Max( Profile.TunnelCacheLargestWorkerBytes, Profile.RoomGraphCacheLargestWorkerBytes); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCacheMemory] workers=%llu capacity_entries=%llu valid_entries=%llu " "static_bytes=%llu dynamic_bytes=%llu total_bytes=%llu " "avg_valid_entry_bytes=%.1f largest_entry_bytes=%llu " "avg_worker_bytes=%.1f largest_worker_bytes=%llu " "tunnel_avg_entry_bytes=%.1f tunnel_largest_entry_bytes=%llu " "tunnel_largest_worker_valid=%llu " "roomgraph_avg_entry_bytes=%.1f roomgraph_largest_entry_bytes=%llu " "roomgraph_largest_worker_valid=%llu " "tunnel_workers=%llu tunnel_capacity=%llu tunnel_valid=%llu " "tunnel_static=%llu tunnel_dynamic=%llu roomgraph_workers=%llu " "roomgraph_capacity=%llu roomgraph_valid=%llu roomgraph_static=%llu " "roomgraph_dynamic=%llu"), static_cast(CacheWorkers), static_cast(CacheCapacityEntries), static_cast(CacheValidEntries), static_cast(CacheTotalStaticBytes), static_cast(CacheTotalDynamicBytes), static_cast(CacheTotalBytes), CacheValidEntries > 0 ? static_cast(CacheEntryBytes) / static_cast(CacheValidEntries) : 0.0, static_cast(CacheLargestEntryBytes), CacheWorkers > 0 ? static_cast(CacheTotalBytes) / static_cast(CacheWorkers) : 0.0, static_cast(CacheLargestWorkerBytes), Profile.TunnelCacheValidEntries > 0 ? static_cast(Profile.TunnelCacheEntryBytes) / static_cast(Profile.TunnelCacheValidEntries) : 0.0, static_cast(Profile.TunnelCacheLargestEntryBytes), static_cast(Profile.TunnelCacheLargestWorkerValidEntries), Profile.RoomGraphCacheValidEntries > 0 ? static_cast(Profile.RoomGraphCacheEntryBytes) / static_cast(Profile.RoomGraphCacheValidEntries) : 0.0, static_cast(Profile.RoomGraphCacheLargestEntryBytes), static_cast(Profile.RoomGraphCacheLargestWorkerValidEntries), static_cast(Profile.TunnelCacheWorkers), static_cast(Profile.TunnelCacheCapacityEntries), static_cast(Profile.TunnelCacheValidEntries), static_cast(Profile.TunnelCacheStaticBytes), static_cast(Profile.TunnelCacheDynamicBytes), static_cast(Profile.RoomGraphCacheWorkers), static_cast(Profile.RoomGraphCacheCapacityEntries), static_cast(Profile.RoomGraphCacheValidEntries), static_cast(Profile.RoomGraphCacheStaticBytes), static_cast(Profile.RoomGraphCacheDynamicBytes)); const VoxelDensityProfile::EBucket DensityPhases[] = { VoxelDensityProfile::EBucket::DensityPrologue, VoxelDensityProfile::EBucket::DensityCore, VoxelDensityProfile::EBucket::DensityDisturbances, VoxelDensityProfile::EBucket::DensityStructuralPosts, VoxelDensityProfile::EBucket::DensityBoundarySeal, VoxelDensityProfile::EBucket::DensityDiffLayer, VoxelDensityProfile::EBucket::DensityTail, }; uint64 DensityAttributionCycles = 0; uint64 DensityAttributionCalls = 0; for (const VoxelDensityProfile::EBucket Phase : DensityPhases) { const int32 PhaseIndex = static_cast(Phase); DensityAttributionCycles += Profile.Cycles[PhaseIndex]; DensityAttributionCalls += Profile.Calls[PhaseIndex]; } const double DensityAttributionUs = static_cast(DensityAttributionCycles) * CycleToMicroseconds; const double DensityUnattributedUs = DensityTotalUs - DensityAttributionUs; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDensityProfile] attribution_sum_us=%.3f unattributed_us=%.3f " "coverage=%.2f%% phase_calls=%llu"), DensityAttributionUs, DensityUnattributedUs, DensityTotalUs > 0.0 ? DensityAttributionUs * 100.0 / DensityTotalUs : 0.0, static_cast(DensityAttributionCalls)); for (int32 Index = 0; Index < VoxelDensityProfile::CounterCount; ++Index) { const VoxelDensityProfile::ECounter Counter = static_cast(Index); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDensityProfile] counter=%s value=%llu per_density_call=%.3f"), VoxelDensityProfile::CounterName(Counter), static_cast(Profile.Counters[Index]), DensityCalls > 0 ? static_cast(Profile.Counters[Index]) / static_cast(DensityCalls) : 0.0); } for (int32 Index = 0; Index < VoxelDensityProfile::BucketCount; ++Index) { const uint64 Calls = Profile.Calls[Index]; if (Calls == 0 || Index == static_cast(VoxelDensityProfile::EBucket::GetDensityAt)) { continue; } const double TotalUs = static_cast(Profile.Cycles[Index]) * CycleToMicroseconds; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDensityProfile] op=%s calls=%llu total_us=%.3f us_per_call=%.6f"), VoxelDensityProfile::BucketName(static_cast(Index)), static_cast(Calls), TotalUs, TotalUs / static_cast(Calls)); } const VoxelDensityProfile::EBucket MesherBuckets[] = { VoxelDensityProfile::EBucket::MesherDensityGrid, VoxelDensityProfile::EBucket::MesherCellClassification, VoxelDensityProfile::EBucket::MesherGradientNormals, VoxelDensityProfile::EBucket::MesherVertexInterpolation, VoxelDensityProfile::EBucket::MesherStreamBuilding, VoxelDensityProfile::EBucket::MesherOther, }; uint64 MesherAttributionCycles = 0; uint64 MesherAttributionCalls = 0; for (const VoxelDensityProfile::EBucket Bucket : MesherBuckets) { const int32 BucketIndex = static_cast(Bucket); MesherAttributionCycles += Profile.Cycles[BucketIndex]; MesherAttributionCalls += Profile.Calls[BucketIndex]; } const int32 MesherTotalIndex = static_cast( VoxelDensityProfile::EBucket::MesherGenerateMesh); const double MesherTotalUs = static_cast( Profile.Cycles[MesherTotalIndex]) * CycleToMicroseconds; const double MesherAttributionUs = static_cast( MesherAttributionCycles) * CycleToMicroseconds; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeMesherProfile] total_calls=%llu total_us=%.3f " "attribution_sum_us=%.3f unattributed_us=%.3f coverage=%.2f%% " "phase_calls=%llu"), static_cast(Profile.Calls[MesherTotalIndex]), MesherTotalUs, MesherAttributionUs, MesherTotalUs - MesherAttributionUs, MesherTotalUs > 0.0 ? MesherAttributionUs * 100.0 / MesherTotalUs : 0.0, static_cast(MesherAttributionCalls)); VoxelDensityProfile::SetEnabled(false); } return bRequestedModeFailed ? 2 : 0; }