feat(tunnels): floor authored at BUILD time, room clips tunnel, climb tier added

The round the owner asked for, with three corrections from Codex that are worth more
than the features.

WHAT LANDED
- The complete tunnel floor profile is authored into FCachedTunnel during
  BuildChunkCache, not derived per sample. This was his point: "it should be done at
  creation time, since it knows its destination."
- Room floor ownership blends/clips the tunnel floor at mouths -- his A > B boolean.
- The traversal validator gained a climb tier, structured exactly as agreed:
    bWalkOnlyCanReachDeparture / bClimbCanReachDeparture kept SEPARATE, with
    WalkOnly / Climb / MineRequired cell counts reported alongside.
  The comment in the header states the contract: "The primordial capability contract
  is walk-only... climb is reported as a separate tier and never makes the hard
  connectivity verdict pass." That is the distinction we wanted: reporting tiers, not
  permission.
- Floor controls exposed under Cave Morphology|Tunnels|Floor.

⚠️ CORRECTION 1 -- LARGE LEDGES ARE NOT CAPABILITY-SAFE YET, so they stay opt-in.
    default (preference 0)   fit 20,804   walk reach 10,884   0 ledges
    preference 2             fit 20,765   walk reach 10,712   360 ledges
  Ledges cost 172 walk-reachable cells. Codex kept preference 0 (the old staircase) as
  the default rather than shipping the owner's preferred look at the price of walkable
  space, and led its report with that. Correct call.

⚠️ CORRECTION 2 -- THE BACKSTOP STILL CANNOT GO. Support/room fallback counters were
  zero, but the structural protection clamp fired on 31,017 of 31,017 authored floor
  samples -- 100%. So the shape is still not correct by construction; the clamp is
  doing the work. Instrumenting it was the right move: we now have a number instead of
  an argument.

⚠️ CORRECTION 3 -- THE "X + 2A" CLEARANCE BUDGET IS NOT IMPLEMENTABLE AS I SPECIFIED.
  Interval propagation bounds FIELD VALUES, not SPATIAL DISPLACEMENT. Deriving A needs
  a proven Lipschitz/influence bound for every post-operation, which we do not have.
  Both the owner and I assumed the interval work already gave us this; it does not.
  The idea is still right, the prerequisite is a displacement bound.

MEASURED BY ME: build 0 errors; export 1.365 / 1.357 / 1.420 s (checkpoint was
1.321 / 1.357), flat within noise; 153,346 triangles.

ALSO MEASURED THIS ROUND
- Edit cost: 8 affected chunks, 195.5 ms total -- generation 195.471 ms, apply 0.059,
  diff 0.020, queue 0.001. It is full level-0 regeneration; a dirty-mesh path needs
  partial section support that is unavailable without modifying RMC.
- Streaming caps: effective generation cap 14, pending max 14, CAP HIT. Mesh applies
  per frame 4 (not hit), 2 ms apply budget (not hit), unloads 6 (not hit).
  ⇒ The generation core cap is what binds during startup fill.
- Peak export RSS 1.70 GB (was 19.5 GB before the memory work).
- LOD4 empty outliers gone. LOD0 not solved: 4,600 empty tiles at 3 us median, but 11
  mixed-classifier fallbacks over 100 ms, max 554 ms -- verdict=Mixed, then the full
  mesher, producing no triangles.

⚠️ UNVERIFIED AND CONCERNING: the reported in-game meshed times are far worse than
  412a7ad's -- p50 498.8 ms vs 115 ms, p95 1.227 s vs 410 ms, max 1.974 s vs 844 ms.
  The samples differ (9,336 ready events vs 841 requests, different traversal, possibly
  queue wait included), and the export path is flat, so I do not know whether this is a
  real regression or a different measurement. I have NOT reproduced it myself. It is
  the first thing the next round must settle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-11 04:25:45 +02:00
1 parent e91f1c44bb
commit 8e42d6d807
15 files changed
+706 -95

No files matched your search

+119 -23
View File
@@ -586,6 +586,8 @@ namespace
static_cast<float>(B.Y - A.Y)).Size();
Profile.StartFloorZ = FloorA;
Profile.EndFloorZ = FloorB;
Profile.NaturalStartFloorZ = FloorA;
Profile.NaturalEndFloorZ = FloorB;
Profile.NumSteps = 0;
if (bGlobalTerrace && GlobalLedgeCount > 0)
@@ -606,7 +608,10 @@ namespace
Profile.EndFloorZ = FloorAtControl[0]
+ TotalDelta * static_cast<float>(EndLevel)
/ static_cast<float>(GlobalLedgeCount);
Profile.NumSteps = EndLevel > StartLevel ? 1 : 0;
// A ledge is a level transition in either direction. The old one-sided test
// accidentally left descending tunnels as a ramp, which is the exact opposite
// of the authored "few large drops" policy.
Profile.NumSteps = EndLevel != StartLevel ? 1 : 0;
}
else if (Tunnel.bTunnelFloorTerracingEnabled
&& PreferredLedges == 0
@@ -634,6 +639,10 @@ namespace
if (VoxelDensityProfile::AreCountersEnabled())
{
uint64 AuthoredLedges = 0;
uint64 LedgesBelow2 = 0;
uint64 Ledges2To4 = 0;
uint64 Ledges4To8 = 0;
uint64 Ledges8Plus = 0;
for (const FTunnelFloorSegmentProfile& Profile : OutProfiles)
{
if (Profile.NumSteps > 0)
@@ -643,6 +652,14 @@ namespace
// in both cases.
AuthoredLedges += bGlobalTerrace
? 1u : static_cast<uint64>(Profile.NumSteps);
const float HeightPerLedge = FMath::Abs(
Profile.EndFloorZ - Profile.StartFloorZ)
/ static_cast<float>(bGlobalTerrace
? 1 : FMath::Max(Profile.NumSteps, 1));
if (HeightPerLedge < 2.0f) { ++LedgesBelow2; }
else if (HeightPerLedge < 4.0f) { ++Ledges2To4; }
else if (HeightPerLedge < 8.0f) { ++Ledges4To8; }
else { ++Ledges8Plus; }
}
}
VoxelDensityProfile::AddCounter(
@@ -653,6 +670,18 @@ namespace
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelFloorProfileLedges,
AuthoredLedges);
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelFloorProfileLedgesBelow2Voxels,
LedgesBelow2);
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelFloorProfileLedges2To4Voxels,
Ledges2To4);
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelFloorProfileLedges4To8Voxels,
Ledges4To8);
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelFloorProfileLedges8PlusVoxels,
Ledges8Plus);
}
}
@@ -803,6 +832,11 @@ namespace
}
else
{
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelFloorProfileFallbacks);
}
const float ProfileT = VF_TunnelFloorProfileT(
A, RadiusA, B, RadiusB, T);
Terms.BaseFloorZ = FMath::Lerp(
@@ -824,15 +858,13 @@ namespace
|| RoomFloorOwnership->RoomHash == Tunnel.FloorRoomHashB);
if (bOwnsThisTunnelMouth)
{
// A raised tunnel apron is the demonstrated failure mode. Lower it toward the room
// floor, but never raise an existing tunnel floor into solid space: that one-sided
// ownership rule is conservative for the capability contract and leaves a lower
// tunnel to be handled by the room's own floor union.
const float OwnedFloorZ = FMath::Min(
Terms.BaseFloorZ, RoomFloorOwnership->FloorZ);
// The room is A and the corridor is B: inside the finite mouth hand-off, A owns the
// floor regardless of which endpoint happened to be higher. The old one-sided Min
// only removed a raised tunnel apron; a lower tunnel still won the SmoothMin union
// and left a lowered shelf in the chamber. Interpolate to the room floor in the
// authored apron so both representations agree at the room boundary.
Terms.BaseFloorZ = FMath::Lerp(
Terms.BaseFloorZ,
OwnedFloorZ,
Terms.BaseFloorZ, RoomFloorOwnership->FloorZ,
FMath::Clamp(RoomFloorOwnership->Weight, 0.0f, 1.0f));
// The room's floor relief is already included in FloorZ. Fade the tunnel relief out
// with the ownership blend, so the transition cannot stack two independent floors.
@@ -954,8 +986,6 @@ namespace
const FVector& B = ControlPoints[SegmentIndex + 1];
const float RadiusA = ControlRadii[SegmentIndex];
const float RadiusB = ControlRadii[SegmentIndex + 1];
const float SegmentSDF = VoxelSDF::TaperedCapsule(
Position, A, B, RadiusA, RadiusB);
float T = 0.0f;
float HorizontalDistanceSquared = FLT_MAX;
@@ -965,7 +995,9 @@ namespace
{
// A vertical/degenerate XY segment has no finite floor projection. Retain the
// capsule for air, but never invent a support surface there.
Result.SDF = FMath::Min(Result.SDF, SegmentSDF);
Result.SDF = FMath::Min(
Result.SDF,
VoxelSDF::TaperedCapsule(Position, A, B, RadiusA, RadiusB));
continue;
}
@@ -973,6 +1005,22 @@ namespace
A, RadiusA, B, RadiusB,
T, SegmentIndex, NumSegments, Tunnel, FloorProfiles,
RoomFloorOwnership);
const FTunnelFloorSegmentProfile* Profile = nullptr;
if (FloorProfiles != nullptr
&& FloorProfiles->Num() == NumSegments
&& FloorProfiles->IsValidIndex(SegmentIndex))
{
Profile = &(*FloorProfiles)[SegmentIndex];
}
const float NaturalFloorZ = Profile != nullptr
? FMath::Lerp(
Profile->NaturalStartFloorZ,
Profile->NaturalEndFloorZ,
FMath::Clamp(T, 0.0f, 1.0f))
: FMath::Lerp(
VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA)),
VoxelPassageGeometry::TunnelFloorZ(B, FMath::Abs(RadiusB)),
FMath::Clamp(T, 0.0f, 1.0f));
const float SupportRadius = FMath::Max(
FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f,
VoxelPassageGeometry::PlayerRadiusVoxels);
@@ -984,6 +1032,38 @@ namespace
Tunnel.FloorReliefFrequency,
ReliefBound);
// The corridor is authored as one shape: translate this tapered capsule so its
// bottom follows the immutable floor profile. A zero shift is deliberately kept on
// the original call, preserving the natural path exactly. The provisional shift
// excludes relief only long enough to decide whether the bounded relief can matter;
// the final shift below includes the actual cached column value.
// Preference 0 is the compatibility profile: keep its established floor field and
// its established capsule so the default archetype does not silently change. A
// positive ledge preference is an explicit authored-shape opt-in; there the baked
// profile owns both the floor and the arch and the capsule follows it at evaluation.
const bool bUseAuthoredProfile = bApplyFloorCut
&& Profile != nullptr
&& Tunnel.TunnelFloorLedgeCountPreference > 0;
const bool bAnchorCapsule = bUseAuthoredProfile
&& FMath::IsFinite(NaturalFloorZ);
const auto EvaluateAnchoredCapsule = [
&Position, &A, &B, RadiusA, RadiusB, NaturalFloorZ, bAnchorCapsule]
(float FloorZ) -> float
{
const float Shift = FloorZ - NaturalFloorZ;
if (!bAnchorCapsule
|| !FMath::IsFinite(Shift)
|| FMath::Abs(Shift) <= KINDA_SMALL_NUMBER)
{
return VoxelSDF::TaperedCapsule(
Position, A, B, RadiusA, RadiusB);
}
const FVector Offset(0.0f, 0.0f, Shift);
return VoxelSDF::TaperedCapsule(
Position, A + Offset, B + Offset, RadiusA, RadiusB);
};
float SegmentSDF = EvaluateAnchoredCapsule(FloorTerms.BaseFloorZ);
bool bEvaluateRelief = bApplyFloorCut;
if (bEvaluateRelief && bHasReliefBound)
{
@@ -1018,6 +1098,15 @@ namespace
const float FloorZ = VF_TunnelFloorFromTerms(
Position, Tunnel, FloorTerms,
CacheIdentity, TunnelIndex, bEvaluateRelief, ReliefColumn);
// Relief belongs to the authored floor as well as to its SmoothMax cut. Re-evaluate
// the translated arch only when the column relief actually changed its anchor; most
// samples retain the cheap provisional result.
if (bAnchorCapsule
&& FMath::IsFinite(FloorZ)
&& FMath::Abs(FloorZ - FloorTerms.BaseFloorZ) > KINDA_SMALL_NUMBER)
{
SegmentSDF = EvaluateAnchoredCapsule(FloorZ);
}
const float SegmentResultSDF = bApplyFloorCut
? VoxelSDF::SmoothMax(
SegmentSDF, FloorZ - static_cast<float>(Position.Z), FloorBlend)
@@ -5301,6 +5390,11 @@ void VoxelCaveMorphology::BuildChunkCache(
CT.ControlPoints, CT.ControlRadii, CT, CT.FloorProfiles);
VF_BuildTunnelFloorProfile(
CT.WorldControlPoints, CT.WorldControlRadii, CT, CT.WorldFloorProfiles);
CT.bHasCompleteFloorProfile = CT.FloorProfiles.Num() == CT.ControlPoints.Num() - 1
&& CT.FloorProfiles.Num() > 0;
CT.bHasCompleteWorldFloorProfile = CT.WorldFloorProfiles.Num()
== CT.WorldControlPoints.Num() - 1
&& CT.WorldFloorProfiles.Num() > 0;
// The floor hand-off is a short apron around each destination, not a second floor
// that owns the whole room. Bake the same bounded region in both coordinate spaces;
// this keeps the room/tunnel boolean deterministic even though the cave warp is 3D.
@@ -5995,15 +6089,15 @@ float VoxelCaveMorphology::EvaluateSDFCached(
}
// The corridor is a swept floor cut: each tapered capsule segment is intersected with
// its interpolated floor half-space before the tunnel union is blended into rooms. The
// room wins naturally in this union (SmoothMin): a tunnel floor is a solid half-space,
// so it cannot replace the room's air above the room floor. Do not lower the SDF floor
// from a proximity-only mouth test; a warped query can be near an endpoint without being
// inside that endpoint's room, and changing it there loses capability.
// its build-time floor profile before the tunnel union is blended into rooms. The room's
// finite mouth ownership is also passed into that same shape evaluation; this is the SDF
// side of A > B, while the world-space post keeps the disturbance path consistent.
const FVFRoomFloorOwnership RoomMouthOwnership =
VF_FindSDFTunnelMouthOwnership(Pos, Tunnel, BlendK);
const float TunnelSDF = VF_EvaluateSweptTunnel(
Pos, Tunnel, /*bWorldChain=*/false, BlendK,
/*bApplyFloorCut=*/Tunnel.bTunnelFloorEnabled, &Cache, TunnelIdx,
nullptr).SDF;
&RoomMouthOwnership).SDF;
MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK);
};
@@ -6134,7 +6228,7 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
TunnelIdx, *SupportColumn, FloorZ, SupportMinZ, SupportMaxZ)
: VF_ProjectSweptTunnelFloor(
Pos, Tunnel, /*bWorldChain=*/true, FloorZ, SupportRadius,
&Cache, TunnelIdx);
&Cache, TunnelIdx, &RoomMouthOwnership);
bRaisedTunnelFloor = bRoomOwnsThisTunnel
&& bProjectedFloor
&& FloorZ > RoomMouthOwnership.FloorZ
@@ -6177,7 +6271,7 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
const FVFTunnelShapeEvaluation TunnelShape = VF_EvaluateSweptTunnel(
Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
/*bApplyFloorCut=*/Tunnel.bTunnelFloorEnabled && !bRaisedTunnelFloor,
&Cache, TunnelIdx, nullptr);
&Cache, TunnelIdx, &RoomMouthOwnership);
Result.SDF = FMath::Min(Result.SDF, TunnelShape.SDF);
};
@@ -6246,7 +6340,7 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
float SupportRadius = 0.0f;
if (VF_ProjectSweptTunnelFloor(
Pos, Tunnel, bHasWorldChain, ProjectedFloorZ, SupportRadius,
&Cache, TunnelIdx)
&Cache, TunnelIdx, &RoomMouthOwnership)
&& ProjectedFloorZ > RoomMouthOwnership.FloorZ + KINDA_SMALL_NUMBER)
{
// Only a raised tunnel apron is clipped. If the tunnel is lower, the room's
@@ -6259,7 +6353,7 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
return VF_EvaluateSweptTunnel(
Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
/*bApplyFloorCut=*/Tunnel.bTunnelFloorEnabled, &Cache, TunnelIdx,
nullptr).bSupportFloor;
&RoomMouthOwnership).bSupportFloor;
};
bool bSupport = false;
@@ -6308,11 +6402,13 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
continue;
}
const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx];
const FVFRoomFloorOwnership RoomMouthOwnership =
VF_FindWorldTunnelMouthOwnership(ColumnPosition, Tunnel, Cache.SDFBlendRadius);
float FloorZ = 0.0f;
float SupportRadius = 0.0f;
if (!VF_ProjectSweptTunnelFloor(
ColumnPosition, Tunnel, /*bWorldChain=*/true,
FloorZ, SupportRadius, &Cache, TunnelIdx))
FloorZ, SupportRadius, &Cache, TunnelIdx, &RoomMouthOwnership))
{
continue;
}
@@ -4226,6 +4226,35 @@ namespace
}
InOut.Sdf = CaveSDF;
// The swept floor is part of the cached tunnel shape, but the common structural tail
// used to discover the same band only after all detail ops had run. Publish that
// build-time ownership now so the stack can preserve the authored support through its
// remaining modifiers without a second floor-writing pass. TryGet... below reuses the
// exact same cached result for the generator's post-disturbance hand-off.
const FTunnelSupportFloorColumn* SupportColumn = GetSupportColumn(
S.LastWorldPosition);
S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, GetCache(), SupportColumn);
S.LastTunnelCoreEvaluationPosition = S.LastWorldPosition;
S.bLastTunnelCoreWorldEvaluationValid = true;
if (S.LastTunnelCoreWorldEvaluation.bSupportFloor
|| S.LastTunnelCoreWorldEvaluation.bRoomFloor)
{
const float StructuralFloorMinimumDensity =
FMath::Max(P.BaseDensity * 2.0f, 1.0f);
if (FMath::IsFinite(StructuralFloorMinimumDensity)
&& StructuralFloorMinimumDensity > 0.0f)
{
InOut.bProtectedStructuralFloor = true;
InOut.StructuralFloorMinimumDensity = StructuralFloorMinimumDensity;
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelAuthoredFloorSamples);
}
}
}
}
//---------------------------------------------------------------------
@@ -416,9 +416,17 @@ namespace VoxelDensityProfile
case ECounter::TunnelSupportFloorChecks:return TEXT("TunnelSupportFloorChecks");
case ECounter::TunnelSupportFloorBackstopFires:return TEXT("TunnelSupportFloorBackstopFires");
case ECounter::TunnelRoomFloorBackstopFires:return TEXT("TunnelRoomFloorBackstopFires");
case ECounter::TunnelAuthoredFloorSamples:return TEXT("TunnelAuthoredFloorSamples");
case ECounter::TunnelAuthoredFloorProtectionClamps:return TEXT("TunnelAuthoredFloorProtectionClamps");
case ECounter::TunnelAuthoredFloorDisturbanceSkips:return TEXT("TunnelAuthoredFloorDisturbanceSkips");
case ECounter::TunnelFloorProfileFallbacks:return TEXT("TunnelFloorProfileFallbacks");
case ECounter::TunnelFloorProfileBuilds:return TEXT("TunnelFloorProfileBuilds");
case ECounter::TunnelFloorProfileSegments:return TEXT("TunnelFloorProfileSegments");
case ECounter::TunnelFloorProfileLedges:return TEXT("TunnelFloorProfileLedges");
case ECounter::TunnelFloorProfileLedgesBelow2Voxels:return TEXT("TunnelFloorProfileLedgesBelow2Voxels");
case ECounter::TunnelFloorProfileLedges2To4Voxels:return TEXT("TunnelFloorProfileLedges2To4Voxels");
case ECounter::TunnelFloorProfileLedges4To8Voxels:return TEXT("TunnelFloorProfileLedges4To8Voxels");
case ECounter::TunnelFloorProfileLedges8PlusVoxels:return TEXT("TunnelFloorProfileLedges8PlusVoxels");
case ECounter::PassageCandidates: return TEXT("PassageCandidates");
case ECounter::PassageEvaluated: return TEXT("PassageEvaluated");
case ECounter::MesherBlockTests: return TEXT("MesherBlockTests");
+86 -16
View File
@@ -295,7 +295,8 @@ static FORCEINLINE void ApplyOriginLandingFloor(float& Density,
// it works uniformly for every generator type. Stays inside the seal bands so it can
// never breach a strate boundary. All features are hash-placed and deterministic.
static void ApplyDisturbances(float& MC, float X, float Y, float Z,
const FStrateDisturbanceParams& D, uint32 Seed, bool bProtectVerticalShaftAir)
const FStrateDisturbanceParams& D, uint32 Seed, bool bProtectVerticalShaftAir,
bool bProtectAuthoredTunnelFloor)
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::ApplyDisturbances);
@@ -303,6 +304,20 @@ static void ApplyDisturbances(float& MC, float X, float Y, float Z,
const float InnerBot = D.StrateBottomWorldZ + D.BoundarySealThickness;
if (Z <= InnerBot || Z >= InnerTop) return;
// A tunnel floor is now authored into the cached shape with its clearance budget. The generic
// disturbance layer is deliberately outside that shape, so it must not reopen or bridge the
// finite support band after the shape has proved it. This is an ownership hand-off, not a
// per-sample natural-floor validation.
if (bProtectAuthoredTunnelFloor)
{
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelAuthoredFloorDisturbanceSkips);
}
return;
}
const float Solid = D.BaseDensity * 2.0f;
const float Blend = 3.0f;
const FVector P(X, Y, Z);
@@ -1410,6 +1425,43 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
}
DensityCoreTimer.End();
// Resolve the graph's authored floor before the disturbance pass. The operator-stack
// source has already computed this exact result while publishing its sample; native
// tunnel generation uses the prepared immutable cache. Keeping the result here means the
// disturbance layer can respect the shape-level floor and the common tail can reuse the
// same query instead of discovering a floor only after it has been damaged.
FTunnelCoreWorldEvaluation PreDisturbanceTunnelCore;
bool bHavePreDisturbanceTunnelCore = false;
if (CP_UseOpStack)
{
bHavePreDisturbanceTunnelCore = ActiveOpStack->TryGetLastTunnelCoreWorldEvaluation(
PreDisturbanceTunnelCore);
}
if (!bHavePreDisturbanceTunnelCore && ActiveTunnelCoreCache->bValid)
{
const FTunnelSupportFloorColumn* SupportColumn = nullptr;
FTunnelSupportFloorColumn EmptySupportColumn;
const bool bIntegerXY =
WorldX == FMath::FloorToFloat(WorldX)
&& WorldY == FMath::FloorToFloat(WorldY);
if (bIntegerXY)
{
const int32 IX = FMath::FloorToInt(WorldX);
const int32 IY = FMath::FloorToInt(WorldY);
SupportColumn = FindTunnelSupportColumn(
ActiveTunnelCoreCache->Cache, IX, IY, EmptySupportColumn);
}
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCorePosts);
PreDisturbanceTunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, SupportColumn);
bHavePreDisturbanceTunnelCore = true;
}
const bool bProtectAuthoredTunnelFloor = bHavePreDisturbanceTunnelCore
&& (PreDisturbanceTunnelCore.bSupportFloor
|| PreDisturbanceTunnelCore.bRoomFloor);
// Disturbance layer (the "wow" post-process) — cached params, MC convention. The
// vertical shaft tree opens a walkable route before this shared pass, so a bridge/ridge
// must not refill it. Both native and operator-stack paths use the same marker helper.
@@ -1418,7 +1470,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
const bool bProtectVerticalShaftAir =
VoxelPassageGeometry::VerticalShaftConnectorAirMarker();
ApplyDisturbances(Result, WorldX, WorldY, WorldZ, CP_Dist, (uint32)Seed,
bProtectVerticalShaftAir);
bProtectVerticalShaftAir, bProtectAuthoredTunnelFloor);
DensityDisturbancesTimer.End();
// A disturbance is allowed to add visual detail, but it must not refill the landing's
@@ -1504,7 +1556,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
StrateManager->ApplyPassageStructuralPostsMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
CP_Dist.BoundarySealThickness);
CP_Dist.BoundarySealThickness, bProtectAuthoredTunnelFloor);
// Disturbance features are authored as a generic MC-space post and may add a ridge or
// bridge over a graph tunnel. Reassert the native cached tunnel core here, after every
@@ -1513,8 +1565,11 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
if (ActiveTunnelCoreCache->bValid || CP_UseOpStack)
{
FTunnelCoreWorldEvaluation TunnelCore;
bool bHaveTunnelCore = CP_UseOpStack
&& ActiveOpStack->TryGetLastTunnelCoreWorldEvaluation(TunnelCore);
bool bHaveTunnelCore = bHavePreDisturbanceTunnelCore;
if (bHaveTunnelCore)
{
TunnelCore = PreDisturbanceTunnelCore;
}
if (!bHaveTunnelCore && ActiveTunnelCoreCache->bValid)
{
const FTunnelSupportFloorColumn* SupportColumn = nullptr;
@@ -1540,33 +1595,43 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
if (bHaveTunnelCore)
{
const bool bTunnelSupportFloor = TunnelCore.bSupportFloor;
if (TunnelCore.bRoomFloor)
if (TunnelCore.bRoomFloor && !bProtectAuthoredTunnelFloor)
{
const float StructuralSolidDensity =
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f);
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelRoomFloorBackstopFires);
if (Result > 0.0f)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelRoomFloorBackstopFires);
}
}
// A tunnel that penetrates a room cannot reassert its own floor through the
// room's floor. The morphology query marks only the finite room support band;
// preserve it after the generic post-disturbance air backstop.
Result = FMath::Min(
Result,
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
StructuralSolidDensity);
}
// The swept capsule owns the relief profile, while this finite support band is
// the conservative post-disturbance backstop that keeps terrain modifiers from
// removing the only player support at a mouth or a narrow cross-section.
if (bTunnelSupportFloor)
if (bTunnelSupportFloor && !bProtectAuthoredTunnelFloor)
{
const float StructuralSolidDensity =
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f);
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorBackstopFires);
if (Result > 0.0f)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorBackstopFires);
}
}
Result = FMath::Min(
Result,
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
StructuralSolidDensity);
}
const float CoreSDF = TunnelCore.SDF;
if (!bTunnelSupportFloor
@@ -2746,12 +2811,17 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
// operator-stack path; its support band follows the same relieved swept floor.
if (bTunnelSupportFloor)
{
const float StructuralSolidDensity =
FMath::Max(Params.BaseDensity * 2.0f, 1.0f);
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorBackstopFires);
if (Density < 0.0f)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorBackstopFires);
}
}
Density = FMath::Max(Density, FMath::Max(Params.BaseDensity * 2.0f, 1.0f));
Density = FMath::Max(Density, StructuralSolidDensity);
}
const float TunnelCoreSDF = (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f)
? VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(WorldX, WorldY, WorldZ, SDFCache)
@@ -1946,7 +1946,8 @@ static bool VF_IsPassageRoomFloor(
void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const
float BaseDensity, float SealThickness,
bool bProtectAuthoredTunnelFloor) const
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageStructuralPosts);
@@ -2009,7 +2010,12 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
}
}
if (!bAnyPassageFloor && MinLandingSDF < FLT_MAX)
// A graph tunnel floor is authored by the room-graph source with its complete clearance
// budget. A neighbouring inter-strate landing must not reopen that finite support band; its
// carve remains valid everywhere else. Legacy callers leave this false and retain the old
// passage-owned behavior.
if (!bProtectAuthoredTunnelFloor
&& !bAnyPassageFloor && MinLandingSDF < FLT_MAX)
{
float InternalDensity = -Density;
VF_ApplyPassageLandingCarving(
@@ -2022,7 +2028,7 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
Density = FMath::Min(Density, -BaseDensity);
}
if (bWalkableAir)
if (bWalkableAir && !bProtectAuthoredTunnelFloor)
{
Density = FMath::Max(
Density, BaseDensity * 2.0f + SealThickness + 4.0f);
+204 -42
View File
@@ -474,6 +474,15 @@ namespace VoxelStrateMeasurePrivate
bool bClimb = false;
};
// Walk is the hard capability contract. The climb policy is used only for the diagnostic
// tier; keeping it explicit at every graph operation prevents a future caller from silently
// turning a climbable ledge into a walk-only connectivity pass.
enum class EPlayerTraversalPolicy : uint8
{
WalkOnly,
WalkAndClimb,
};
bool IsEligiblePlayerFitCell(
const FSampleGrid& Grid,
const TArray<uint8>& PlayerFitMask,
@@ -507,8 +516,6 @@ namespace VoxelStrateMeasurePrivate
}
const bool bFirstIsHigher = FirstSupportHeight > SecondSupportHeight;
const int32 LowerColumnX = bFirstIsHigher ? SecondX : FirstX;
const int32 LowerColumnY = bFirstIsHigher ? SecondY : FirstY;
const int32 HigherColumnX = bFirstIsHigher ? FirstX : SecondX;
const int32 HigherColumnY = bFirstIsHigher ? FirstY : SecondY;
const float LowerHeight = FMath::Min(FirstSupportHeight, SecondSupportHeight);
@@ -521,18 +528,19 @@ namespace VoxelStrateMeasurePrivate
}
// A climb edge must meet a solid riser, not connect two independently supported
// floating poses across an empty gap. Exclude the top support cell itself: that cell is
// present for every supported platform and is not evidence of a climbable face.
const int32 FirstRiserZ = FMath::Max(
0,
FMath::FloorToInt((LowerHeight - Grid.MinZ) / Step));
const int32 LastRiserZ = FMath::Min(
Grid.NumZ - 1,
FMath::CeilToInt((HigherHeight - Grid.MinZ) / Step) - 2);
for (int32 Z = FirstRiserZ; Z <= LastRiserZ; ++Z)
// floating poses across an empty gap. Test the actual vertical interval between the two
// support surfaces in the higher column. The old floor/ceil/-2 indexing skipped the only
// solid sample for short quantised ledges and was especially brittle when the support
// heights came from scalar interpolation rather than an integer cell boundary.
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
{
if (Grid.Air[Grid.Index(HigherColumnX, HigherColumnY, Z)] == 0u
|| Grid.Air[Grid.Index(LowerColumnX, LowerColumnY, Z)] == 0u)
const float SampleZ = Grid.MinZ + (static_cast<float>(Z) + 0.5f) * Step;
if (SampleZ + KINDA_SMALL_NUMBER < LowerHeight
|| SampleZ >= HigherHeight - KINDA_SMALL_NUMBER)
{
continue;
}
if (Grid.Air[Grid.Index(HigherColumnX, HigherColumnY, Z)] == 0u)
{
return true;
}
@@ -547,7 +555,8 @@ namespace VoxelStrateMeasurePrivate
const FVoxelStrateMeasureSettings* Settings,
int32 First,
int32 Second,
bool& bOutClimb)
bool& bOutClimb,
EPlayerTraversalPolicy Policy = EPlayerTraversalPolicy::WalkAndClimb)
{
bOutClimb = false;
if (!IsEligiblePlayerFitCell(Grid, PlayerFitMask, First)
@@ -605,7 +614,8 @@ namespace VoxelStrateMeasurePrivate
{
return true;
}
if (!Settings->bPlayerClimbingEnabled
if (Policy == EPlayerTraversalPolicy::WalkOnly
|| !Settings->bPlayerClimbingEnabled
|| HeightDelta > ClimbHeightVoxels + KINDA_SMALL_NUMBER)
{
return false;
@@ -622,7 +632,8 @@ namespace VoxelStrateMeasurePrivate
const TArray<float>* PlayerFitSupportHeights,
const FVoxelStrateMeasureSettings* Settings,
int32 Cell,
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>>& OutNeighbours)
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>>& OutNeighbours,
EPlayerTraversalPolicy Policy = EPlayerTraversalPolicy::WalkAndClimb)
{
OutNeighbours.Reset();
if (Cell < 0 || Cell >= Grid.CellCount)
@@ -643,7 +654,7 @@ namespace VoxelStrateMeasurePrivate
bool bClimb = false;
if (IsPlayerTraversalEdge(
Grid, PlayerFitMask, PlayerFitSupportHeights, Settings,
Cell, Neighbor, bClimb))
Cell, Neighbor, bClimb, Policy))
{
OutNeighbours.Add({ Neighbor, bClimb });
}
@@ -658,7 +669,8 @@ namespace VoxelStrateMeasurePrivate
if (Z + 1 < Grid.NumZ) TryAdd(Grid.Index(X, Y, Z + 1));
if (Z > 0) TryAdd(Grid.Index(X, Y, Z - 1));
if (PlayerFitSupportHeights == nullptr || Settings == nullptr
if (Policy == EPlayerTraversalPolicy::WalkOnly
|| PlayerFitSupportHeights == nullptr || Settings == nullptr
|| !Settings->bPlayerClimbingEnabled)
{
return;
@@ -695,8 +707,8 @@ namespace VoxelStrateMeasurePrivate
const int32 Neighbor = Grid.Index(TargetX, TargetY, TargetZ);
bool bClimb = false;
if (IsPlayerTraversalEdge(
Grid, PlayerFitMask, PlayerFitSupportHeights, Settings,
Cell, Neighbor, bClimb)
Grid, PlayerFitMask, PlayerFitSupportHeights, Settings,
Cell, Neighbor, bClimb, Policy)
&& bClimb)
{
OutNeighbours.Add({ Neighbor, true });
@@ -717,7 +729,8 @@ namespace VoxelStrateMeasurePrivate
TArray<int64>* OutComponentCells,
const TArray<uint8>* EligibilityMask = nullptr,
const TArray<float>* PlayerFitSupportHeights = nullptr,
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr)
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr,
EPlayerTraversalPolicy TraversalPolicy = EPlayerTraversalPolicy::WalkAndClimb)
{
OutComponents.SetNumUninitialized(Grid.CellCount);
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
@@ -780,7 +793,7 @@ namespace VoxelStrateMeasurePrivate
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>> Neighbours;
AppendPlayerTraversalNeighbours(
Grid, *EligibilityMask, PlayerFitSupportHeights,
TraversalSettings, Current, Neighbours);
TraversalSettings, Current, Neighbours, TraversalPolicy);
for (const FPlayerTraversalNeighbour& Neighbour : Neighbours)
{
Visit(Neighbour.Cell);
@@ -1727,7 +1740,8 @@ namespace VoxelStrateMeasurePrivate
nullptr,
&PlayerFit,
&PlayerFitSupportHeights,
&Settings);
&Settings,
EPlayerTraversalPolicy::WalkOnly);
InOutMetrics.NumTraversableComponents = NumComponents;
InOutMetrics.LargestTraversableComponentCells = LargestComponentCells;
InOutMetrics.TraversableComponentShare = InOutMetrics.NumPlayerFitCells > 0
@@ -2234,7 +2248,8 @@ namespace VoxelStrateMeasurePrivate
FCoarseEdge& OutFailedEdge,
const TArray<uint8>* EligibilityMask = nullptr,
const TArray<float>* PlayerFitSupportHeights = nullptr,
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr)
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr,
EPlayerTraversalPolicy TraversalPolicy = EPlayerTraversalPolicy::WalkAndClimb)
{
OutFailedEdge = FCoarseEdge();
@@ -2262,7 +2277,8 @@ namespace VoxelStrateMeasurePrivate
bool bClimb = false;
if (!IsPlayerTraversalEdge(
Grid, *EligibilityMask, PlayerFitSupportHeights,
TraversalSettings, Path[PathIndex - 1], Path[PathIndex], bClimb))
TraversalSettings, Path[PathIndex - 1], Path[PathIndex], bClimb,
TraversalPolicy))
{
OutFailedEdge = MakeCoarseEdge(
Path[PathIndex - 1], Path[PathIndex]);
@@ -2344,7 +2360,8 @@ namespace VoxelStrateMeasurePrivate
TArray<int32>& OutPath,
const TArray<uint8>* EligibilityMask = nullptr,
const TArray<float>* PlayerFitSupportHeights = nullptr,
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr)
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr,
EPlayerTraversalPolicy TraversalPolicy = EPlayerTraversalPolicy::WalkAndClimb)
{
const auto IsEligible = [&, EligibilityMask](int32 Cell)
{
@@ -2396,7 +2413,7 @@ namespace VoxelStrateMeasurePrivate
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>> Neighbours;
AppendPlayerTraversalNeighbours(
Grid, *EligibilityMask, PlayerFitSupportHeights,
TraversalSettings, Current, Neighbours);
TraversalSettings, Current, Neighbours, TraversalPolicy);
for (const FPlayerTraversalNeighbour& Neighbour : Neighbours)
{
Visit(Neighbour.Cell);
@@ -2454,7 +2471,8 @@ namespace VoxelStrateMeasurePrivate
int32 MaxRouteRetries,
const TArray<uint8>* EligibilityMask = nullptr,
const TArray<float>* PlayerFitSupportHeights = nullptr,
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr)
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr,
EPlayerTraversalPolicy TraversalPolicy = EPlayerTraversalPolicy::WalkAndClimb)
{
OutStart = -1;
OutGoal = -1;
@@ -2493,7 +2511,7 @@ namespace VoxelStrateMeasurePrivate
{
if (!FindCoarsePath(
Grid, OutStart, OutGoal, BlockedEdges, Path, EligibilityMask,
PlayerFitSupportHeights, TraversalSettings))
PlayerFitSupportHeights, TraversalSettings, TraversalPolicy))
{
// This is a negative verdict only after all edges excluded by earlier
// full-resolution failures leave no coarse route at this resolution.
@@ -2506,7 +2524,7 @@ namespace VoxelStrateMeasurePrivate
FCoarseEdge FailedEdge;
if (FullResolutionPathIsAir(
Generator, Sampler, Grid, AVoxel, BVoxel, Path, FailedEdge,
EligibilityMask, PlayerFitSupportHeights, TraversalSettings))
EligibilityMask, PlayerFitSupportHeights, TraversalSettings, TraversalPolicy))
{
return EVoxelConnectivityResult::Connected;
}
@@ -2579,7 +2597,8 @@ namespace VoxelStrateMeasurePrivate
TArray<int32>* OutComponents = nullptr,
int32* OutNumComponents = nullptr,
const TArray<float>* PlayerFitSupportHeights = nullptr,
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr)
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr,
EPlayerTraversalPolicy TraversalPolicy = EPlayerTraversalPolicy::WalkAndClimb)
{
OutDiagnostics = FVoxelConnectivityDiagnostics();
if (OutComponents != nullptr)
@@ -2620,7 +2639,8 @@ namespace VoxelStrateMeasurePrivate
nullptr,
EligibilityMask,
PlayerFitSupportHeights,
TraversalSettings);
TraversalSettings,
TraversalPolicy);
if (OutComponents != nullptr)
{
@@ -2680,6 +2700,115 @@ namespace VoxelStrateMeasurePrivate
OutDiagnostics.GoalToStartComponentDistanceCells = ComponentDistances[1];
}
// The hard capability graph is deliberately flooded with walking edges only. This second
// pass contracts each walk-only component to one node and adds only the explicitly supported
// climb edges between nodes. It gives the reporting tiers without allowing a climb to turn a
// failed primordial walk-only verdict into a pass.
void BuildClimbTierFromWalkComponents(
const FSampleGrid& Grid,
const TArray<uint8>& PlayerFitMask,
const TArray<float>& PlayerFitSupportHeights,
const FVoxelStrateMeasureSettings& Settings,
const TArray<int32>& Components,
int32 NumComponents,
int32 StartCell,
int32 GoalCell,
int64 PlayerFitCells,
int64& OutWalkOnlyReachableCells,
int64& OutClimbReachableCells,
int64& OutMineRequiredCells,
bool& bOutClimbCanReach,
int64& OutClimbEdges)
{
OutWalkOnlyReachableCells = 0;
OutClimbReachableCells = 0;
OutMineRequiredCells = 0;
bOutClimbCanReach = false;
OutClimbEdges = 0;
if (NumComponents <= 0
|| Components.Num() != Grid.CellCount
|| !Components.IsValidIndex(StartCell)
|| !Components.IsValidIndex(GoalCell))
{
return;
}
const int32 StartComponent = Components[StartCell];
const int32 GoalComponent = Components[GoalCell];
if (StartComponent < 0 || StartComponent >= NumComponents
|| GoalComponent < 0 || GoalComponent >= NumComponents)
{
return;
}
TArray<TArray<int32>> CellsByComponent;
CellsByComponent.SetNum(NumComponents);
for (int32 Cell = 0; Cell < Grid.CellCount; ++Cell)
{
const int32 Component = Components[Cell];
if (Component >= 0 && Component < NumComponents
&& IsEligiblePlayerFitCell(Grid, PlayerFitMask, Cell))
{
CellsByComponent[Component].Add(Cell);
}
}
OutWalkOnlyReachableCells = CellsByComponent[StartComponent].Num();
TArray<uint8> ReachedComponents;
ReachedComponents.Init(0u, NumComponents);
TArray<int32> Queue;
Queue.Reserve(NumComponents);
ReachedComponents[StartComponent] = 1u;
Queue.Add(StartComponent);
for (int32 QueueIndex = 0; QueueIndex < Queue.Num(); ++QueueIndex)
{
const int32 Component = Queue[QueueIndex];
for (const int32 Cell : CellsByComponent[Component])
{
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>> Neighbours;
AppendPlayerTraversalNeighbours(
Grid,
PlayerFitMask,
&PlayerFitSupportHeights,
&Settings,
Cell,
Neighbours,
EPlayerTraversalPolicy::WalkAndClimb);
for (const FPlayerTraversalNeighbour& Neighbour : Neighbours)
{
if (!Neighbour.bClimb || !Components.IsValidIndex(Neighbour.Cell))
{
continue;
}
++OutClimbEdges;
const int32 NeighbourComponent = Components[Neighbour.Cell];
if (NeighbourComponent >= 0 && NeighbourComponent < NumComponents
&& NeighbourComponent != Component
&& ReachedComponents[NeighbourComponent] == 0u)
{
ReachedComponents[NeighbourComponent] = 1u;
Queue.Add(NeighbourComponent);
}
}
}
}
for (int32 Component = 0; Component < NumComponents; ++Component)
{
if (ReachedComponents[Component] != 0u)
{
OutClimbReachableCells += CellsByComponent[Component].Num();
}
}
bOutClimbCanReach = ReachedComponents[GoalComponent] != 0u;
OutMineRequiredCells = FMath::Max<int64>(
0,
PlayerFitCells - OutClimbReachableCells);
}
EVoxelConnectivityResult QueryConnectivity(
const UVoxelGenerator* Generator,
const UVoxelStrateManager* Manager,
@@ -2809,7 +2938,10 @@ namespace VoxelStrateMeasurePrivate
Settings.MaxRouteRetries,
bPlayerFitRestricted ? &PlayerFitMask : nullptr,
bPlayerFitRestricted ? &PlayerFitSupportHeights : nullptr,
bPlayerFitRestricted ? &Settings : nullptr);
bPlayerFitRestricted ? &Settings : nullptr,
bPlayerFitRestricted
? EPlayerTraversalPolicy::WalkOnly
: EPlayerTraversalPolicy::WalkAndClimb);
if (OutDiagnostics != nullptr)
{
PopulateConnectivityDiagnostics(
@@ -2825,7 +2957,10 @@ namespace VoxelStrateMeasurePrivate
nullptr,
nullptr,
bPlayerFitRestricted ? &PlayerFitSupportHeights : nullptr,
bPlayerFitRestricted ? &Settings : nullptr);
bPlayerFitRestricted ? &Settings : nullptr,
bPlayerFitRestricted
? EPlayerTraversalPolicy::WalkOnly
: EPlayerTraversalPolicy::WalkAndClimb);
if (bPlayerFitRestricted &&
(Result == EVoxelConnectivityResult::StartCellNotPlayerFit
|| Result == EVoxelConnectivityResult::GoalCellNotPlayerFit))
@@ -2861,11 +2996,12 @@ namespace VoxelStrateMeasurePrivate
const TArray<uint8>& PlayerFitMask,
const TArray<float>* PlayerFitSupportHeights,
const FVoxelStrateMeasureSettings* Settings,
int32 Cell)
int32 Cell,
EPlayerTraversalPolicy Policy = EPlayerTraversalPolicy::WalkAndClimb)
{
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>> Neighbours;
AppendPlayerTraversalNeighbours(
Grid, PlayerFitMask, PlayerFitSupportHeights, Settings, Cell, Neighbours);
Grid, PlayerFitMask, PlayerFitSupportHeights, Settings, Cell, Neighbours, Policy);
int32 Count = 0;
for (const FPlayerTraversalNeighbour& Neighbour : Neighbours)
{
@@ -2938,6 +3074,7 @@ namespace VoxelStrateMeasurePrivate
const TArray<uint8>& PlayerFitMask,
const TArray<float>* PlayerFitSupportHeights,
const FVoxelStrateMeasureSettings* TraversalSettings,
EPlayerTraversalPolicy TraversalPolicy,
int32 Start,
int32 Goal,
float NarrowGapThresholdVoxels,
@@ -2977,7 +3114,7 @@ namespace VoxelStrateMeasurePrivate
if (PlayerFitSupportHeights != nullptr && TraversalSettings != nullptr
&& IsPlayerTraversalEdge(
Grid, PlayerFitMask, PlayerFitSupportHeights, TraversalSettings,
From, To, bClimb)
From, To, bClimb, TraversalPolicy)
&& bClimb)
{
++InOutReport.ClimbTraversals;
@@ -3012,7 +3149,7 @@ namespace VoxelStrateMeasurePrivate
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>> Neighbours;
AppendPlayerTraversalNeighbours(
Grid, PlayerFitMask, PlayerFitSupportHeights, TraversalSettings,
Current, Neighbours);
Current, Neighbours, TraversalPolicy);
bool bAdvanced = false;
while (NextDirection.Last() < Neighbours.Num())
{
@@ -3043,7 +3180,7 @@ namespace VoxelStrateMeasurePrivate
if (Current != Start && Current != Goal
&& CountPlayerFitNeighbours(
Grid, PlayerFitMask, PlayerFitSupportHeights,
TraversalSettings, Current) <= 1)
TraversalSettings, Current, TraversalPolicy) <= 1)
{
++InOutReport.DeadEndsEncountered;
}
@@ -3466,7 +3603,8 @@ bool VF_MeasurePlayerFitWalkWithSampler(
Settings.MaxRouteRetries,
&PlayerFitMask,
&PlayerFitSupportHeights,
&Settings);
&Settings,
EPlayerTraversalPolicy::WalkOnly);
FVoxelConnectivityDiagnostics Diagnostics;
TArray<int32> Components;
@@ -3484,7 +3622,8 @@ bool VF_MeasurePlayerFitWalkWithSampler(
&Components,
&NumComponents,
&PlayerFitSupportHeights,
&Settings);
&Settings,
EPlayerTraversalPolicy::WalkOnly);
OutReport.bValid = true;
OutReport.Result = Result;
@@ -3499,12 +3638,34 @@ bool VF_MeasurePlayerFitWalkWithSampler(
OutReport.SampledMinY = Grid.MinY;
OutReport.SampledMaxY = Grid.MaxY;
OutReport.PlayerFitVolumeCells = NumPlayerFitCells;
OutReport.ReachablePlayerFitCells = FMath::Max<int64>(
OutReport.WalkOnlyReachablePlayerFitCells = FMath::Max<int64>(
Diagnostics.StartComponentCells, 0);
OutReport.ReachablePlayerFitCells = OutReport.WalkOnlyReachablePlayerFitCells;
OutReport.bWalkOnlyCanReachDeparture = Result == EVoxelConnectivityResult::Connected;
OutReport.bCanReachDeparture = OutReport.bWalkOnlyCanReachDeparture;
BuildClimbTierFromWalkComponents(
Grid,
PlayerFitMask,
PlayerFitSupportHeights,
Settings,
Components,
NumComponents,
Start,
Goal,
NumPlayerFitCells,
OutReport.WalkOnlyReachablePlayerFitCells,
OutReport.ClimbReachablePlayerFitCells,
OutReport.MineRequiredPlayerFitCells,
OutReport.bClimbCanReachDeparture,
OutReport.ClimbEdges);
OutReport.ReachablePlayerFitFraction = OutReport.PlayerFitVolumeCells > 0
? static_cast<float>(static_cast<double>(OutReport.ReachablePlayerFitCells)
/ static_cast<double>(OutReport.PlayerFitVolumeCells))
: 0.0f;
OutReport.ClimbReachablePlayerFitFraction = OutReport.PlayerFitVolumeCells > 0
? static_cast<float>(static_cast<double>(OutReport.ClimbReachablePlayerFitCells)
/ static_cast<double>(OutReport.PlayerFitVolumeCells))
: 0.0f;
OutReport.PlayerFitComponents = NumComponents;
OutReport.LargestPlayerFitComponentCells = Diagnostics.LargestComponentCells;
OutReport.ArrivalComponentCells = Diagnostics.StartComponentCells;
@@ -3524,6 +3685,7 @@ bool VF_MeasurePlayerFitWalkWithSampler(
PlayerFitMask,
&PlayerFitSupportHeights,
&Settings,
EPlayerTraversalPolicy::WalkOnly,
Start,
Goal,
NarrowGapThresholdVoxels,
+162 -6
View File
@@ -31,6 +31,7 @@
#include "IImageWrapperModule.h"
#include "Modules/ModuleManager.h"
#include "Misc/FileHelper.h"
#include "Misc/CommandLine.h"
#include "Misc/Paths.h"
#include "HAL/IConsoleManager.h"
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
@@ -47,6 +48,18 @@ namespace
TEXT("voxel.ProfileTileGeneration"),
GVoxelForgeProfileTileGeneration,
TEXT("Log worker tile generation time with level/step/cell count."));
// Startup command-line cvars can arrive before the game world has spawned AVoxelWorld (or
// before its first tiles have become visible). Keep the diagnostic edit pending until Tick has
// a loaded tile; otherwise a startup measurement silently becomes a no-op or edits the wrong
// position.
int32 GVoxelForgePendingTestModification = 0;
float GVoxelForgePendingModificationRadius = 3.0f;
float GVoxelForgePendingModificationStrength = 10.0f;
bool GVoxelForgeTestModificationCommandLineConsumed = false;
bool GVoxelForgeStreamingBudgetReported = false;
bool GVoxelForgeGenerationCapHitReported = false;
int32 GVoxelForgeMaxPendingTilesObserved = 0;
}
AVoxelWorld::AVoxelWorld()
@@ -1075,7 +1088,52 @@ void AVoxelWorld::Tick(float DeltaTime)
// Independent of the density volume (self-guards on OrbLightMPC); this is the replacement path.
UpdateOrbLightMPC();
}
// The diagnostic harness uses an explicit command-line token rather than a console command:
// Unreal processes startup ExecCmds before this runtime module's console registrations are
// guaranteed to exist. Reading the command line here is order-independent, and the actual edit
// still waits for a loaded tile and goes through the ordinary modification path below.
if (!GVoxelForgeTestModificationCommandLineConsumed)
{
GVoxelForgeTestModificationCommandLineConsumed = true;
const FString CommandLine(FCommandLine::Get());
if (CommandLine.Contains(TEXT("-voxel.TestModification=1"), ESearchCase::IgnoreCase))
{
GVoxelForgePendingModificationRadius = 3.0f;
GVoxelForgePendingModificationStrength = 10.0f;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeTestModification] queued radius=%.2f strength=%.2f"),
GVoxelForgePendingModificationRadius,
GVoxelForgePendingModificationStrength);
GVoxelForgePendingTestModification = 1;
}
}
ProcessPendingChunks();
// Complete the one-shot diagnostic request only after the player's level-0 centre tile is
// loaded and idle. Waiting for any coarse tile would take the async-neighbour branch and would
// not measure the normal synchronous centre remesh that makes a local edit feel expensive.
FVector ModificationPosition = PlayerLastPos;
if (ModificationPosition == FVector::ZeroVector)
{
ModificationPosition = GetActorLocation();
}
const FVoxelTileKey ModificationCenterTile(
WorldToChunkCoord(WorldToLocalCm(ModificationPosition)), 0);
if (GVoxelForgePendingTestModification != 0
&& DiffLayer
&& LoadedTiles.Contains(ModificationCenterTile)
&& !PendingTiles.Contains(ModificationCenterTile))
{
GVoxelForgePendingTestModification = 0;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeTestModification] applying position=(%.2f,%.2f,%.2f) radius=%.2f strength=%.2f"),
ModificationPosition.X, ModificationPosition.Y, ModificationPosition.Z,
GVoxelForgePendingModificationRadius, GVoxelForgePendingModificationStrength);
CarveAtPosition(
ModificationPosition,
GVoxelForgePendingModificationRadius,
GVoxelForgePendingModificationStrength);
}
ProcessUnloadQueue();
#if ENABLE_DRAW_DEBUG
@@ -1322,6 +1380,9 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
{
if (!Generator || !Mesher || ShouldAbortWork()) return;
const bool bProfileModification = GVoxelForgeProfileTileGeneration != 0;
const uint64 SyncStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0;
const FIntVector OriginVoxels = Tile.OriginVoxels();
const int32 Cells = CHUNK_SIZE; // level 0 is always full-res (level 0 < FullResClipLevels)
const int32 Step = 1; // Extent(=CHUNK_SIZE) / Cells
@@ -1341,14 +1402,30 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
}
FChunkResult Result;
Result.RequestStartCycles = GVoxelForgeProfileTileGeneration != 0
Result.RequestStartCycles = bProfileModification
? FPlatformTime::Cycles64() : 0;
const uint64 GenerateStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0;
GenerateTileResult(Tile, OriginVoxels, Step, Cells, GenerationEpoch, /*bWantCapture*/ false,
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
/*bSheetTile*/ false, /*SheetChunkZ*/ 0,
/*Hole*/ 0, 0, 0, 0, Result); // hole unused (not a sheet tile)
const double GenerateSeconds = bProfileModification
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - GenerateStartCycles)
: 0.0;
const uint64 ApplyStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0;
ApplyTileResult(Result);
if (bProfileModification)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeSyncRemeshProfile] tile=(%d,%d,%d) empty=%d generation=%.6f "
"apply=%.6f total=%.6f"),
Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z,
(Result.bEmpty || !Result.Streams) ? 1 : 0,
GenerateSeconds,
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ApplyStartCycles),
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - SyncStartCycles));
}
}
void AVoxelWorld::ProcessUnloadQueue()
@@ -1717,18 +1794,46 @@ bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector&
int32 AVoxelWorld::GetMaxConcurrentTasks() const
{
// T2.d — the asset value, capped to the spare LOGICAL cores. BackgroundNormal priority
// (see LoadTile) already stops gen from starving the frame; this cap stops a flat 16 from
// thrashing context switches on small CPUs where 16 > the machine's spare parallelism.
// T2.d — both sides of the cap are authored in Voxel|Streaming. BackgroundNormal priority
// (see LoadTile) already stops gen from starving the frame; this cap stops a flat task count
// from thrashing context switches on small CPUs where it exceeds spare parallelism.
const int32 Asset = Settings ? Settings->MaxConcurrentTasks : 16;
const int32 SpareCores = FMath::Max(2, FPlatformMisc::NumberOfCoresIncludingHyperthreads() - 2);
return FMath::Clamp(Asset, 1, SpareCores);
const int32 LogicalCores = FMath::Max(
1, FPlatformMisc::NumberOfCoresIncludingHyperthreads());
const int32 AutomaticCap = FMath::Max(2, LogicalCores - 2);
const int32 AuthoredCoreCap = Settings ? Settings->MaxGenerationWorkerCores : 0;
const int32 CoreCap = AuthoredCoreCap > 0
? FMath::Clamp(AuthoredCoreCap, 1, LogicalCores)
: AutomaticCap;
return FMath::Clamp(Asset, 1, CoreCap);
}
void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
{
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_UpdateChunks);
const int32 MaxTasks = GetMaxConcurrentTasks();
if (GVoxelForgeProfileTileGeneration != 0
&& !GVoxelForgeStreamingBudgetReported)
{
const int32 LogicalCores = FMath::Max(
1, FPlatformMisc::NumberOfCoresIncludingHyperthreads());
const int32 AutomaticCap = FMath::Max(2, LogicalCores - 2);
const int32 AssetCap = Settings ? Settings->MaxConcurrentTasks : 16;
const int32 AuthoredCoreCap = Settings ? Settings->MaxGenerationWorkerCores : 0;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStreamingBudget] logical_cores=%d automatic_core_cap=%d "
"authored_core_cap=%d asset_task_cap=%d effective_generation_cap=%d "
"max_mesh_applies=%d max_apply_ms=%.3f max_unloads=%d"),
LogicalCores,
AutomaticCap,
AuthoredCoreCap,
AssetCap,
MaxTasks,
Settings ? Settings->MaxMeshAppliesPerFrame : 4,
Settings ? Settings->MaxMeshApplyMilliseconds : 2.0f,
Settings ? Settings->MaxUnloadsPerFrame : 6);
GVoxelForgeStreamingBudgetReported = true;
}
const FIntVector CenterChunk = WorldToChunkCoord(WorldToLocalCm(CenterPosition)); // player's level-0 tile
CurrentCenterChunk = CenterChunk;
@@ -2013,6 +2118,24 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
++Submitted;
}
if (GVoxelForgeProfileTileGeneration != 0
&& PendingTiles.Num() > GVoxelForgeMaxPendingTilesObserved)
{
GVoxelForgeMaxPendingTilesObserved = PendingTiles.Num();
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStreamingBudget] pending_max=%d effective=%d submitted=%d"),
GVoxelForgeMaxPendingTilesObserved, MaxTasks, Submitted);
}
if (GVoxelForgeProfileTileGeneration != 0
&& PendingTiles.Num() >= MaxTasks
&& !GVoxelForgeGenerationCapHitReported)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStreamingBudget] generation_cap_hit=1 pending=%d effective=%d"),
PendingTiles.Num(), MaxTasks);
GVoxelForgeGenerationCapHitReported = true;
}
if (Submitted == 0 && PendingTiles.Num() == 0 && BandRemeshQueue.Num() == 0 && DirtyRemeshQueue.Num() == 0)
{
// Everything desired is loaded → load-before-unload is satisfied: drop the
@@ -2036,6 +2159,14 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
const int32 MaxTasks = GetMaxConcurrentTasks(); // T2.d — core-clamped
if (PendingTiles.Num() >= MaxTasks)
{
if (GVoxelForgeProfileTileGeneration != 0
&& !GVoxelForgeGenerationCapHitReported)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStreamingBudget] generation_cap_hit=1 pending=%d effective=%d"),
PendingTiles.Num(), MaxTasks);
GVoxelForgeGenerationCapHitReported = true;
}
return; // Budget full — wait for a task to finish.
}
PendingTiles.Add(Tile);
@@ -2727,7 +2858,13 @@ void AVoxelWorld::FillAtPosition(FVector Position, float Radius, float Strength)
void AVoxelWorld::ApplyModification(const FVoxelModification& Modification)
{
if (!DiffLayer) return;
const bool bProfileModification = GVoxelForgeProfileTileGeneration != 0;
const uint64 ModificationStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0;
const uint64 DiffStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0;
TArray<FIntVector> AffectedChunks = DiffLayer->ApplyModification(Modification);
const double DiffSeconds = bProfileModification
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - DiffStartCycles)
: 0.0;
// INSTANT DIG FEEL — synchronously re-mesh the level-0 tile the brush CENTRE sits in, so the hole
// appears THIS frame right where the player is looking. Neighbour tiles (brush edge) re-mesh async
@@ -2743,11 +2880,30 @@ void AVoxelWorld::ApplyModification(const FVoxelModification& Modification)
&& LoadedTiles.Contains(CenterTile)
&& !PendingTiles.Contains(CenterTile))
{
const uint64 SyncStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0;
SyncRemeshTile(CenterTile);
if (bProfileModification)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeModificationProfile] center_sync=%.6f"),
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - SyncStartCycles));
}
bSyncedCenter = true;
}
const uint64 QueueStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0;
RemeshDirtyChunks(AffectedChunks, bSyncedCenter ? &CenterTile : nullptr);
if (bProfileModification)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeModificationProfile] affected=%d diff=%.6f queue=%.6f total=%.6f "
"center_synced=%d"),
AffectedChunks.Num(),
DiffSeconds,
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - QueueStartCycles),
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ModificationStartCycles),
bSyncedCenter ? 1 : 0);
}
// Remove decorations inside the modified volume so grass doesn't float over a dug hole (or bury under a
// fill). Instant + flicker-free (only the affected instances go); the placer already skips carved columns
@@ -629,6 +629,11 @@ struct FTunnelFloorSegmentProfile
{
float StartFloorZ = 0.0f;
float EndFloorZ = 0.0f;
// The unmodified tapered-capsule bottom at the same two control points. Keeping these
// alongside the authored floor lets evaluation translate the arch to the baked floor without
// re-deriving a second profile (and makes the build-time floor/arch relationship explicit).
float NaturalStartFloorZ = 0.0f;
float NaturalEndFloorZ = 0.0f;
float ReliefScale = 0.0f;
int32 NumSteps = 0;
};
@@ -657,6 +662,8 @@ struct FCachedTunnel
// malformed/legacy caches fall back to the old local evaluator.
TArray<FTunnelFloorSegmentProfile> FloorProfiles;
TArray<FTunnelFloorSegmentProfile> WorldFloorProfiles;
bool bHasCompleteFloorProfile = false;
bool bHasCompleteWorldFloorProfile = false;
// The corridor floor is a swept SmoothMax cut, not a later slab. These are copied from the
// same strate fields used by room floors; the seed is derived once from the tunnel pair hash.
float FloorReliefStrength = 0.0f;
@@ -334,6 +334,13 @@ struct FVoxelOpSample
// placées quand aucune primitive n'atteint ce voxel).
// FLT_MAX = "no surface nearby" — the initial state, and the early-out placed sources use.
float Sdf = FLT_MAX;
// The room-graph source can author a finite tunnel/room floor band while it is building the
// shape. This structural metadata is not a third composable field: it tells the stack that
// later detail operators must not turn the already-authored support back into air. Carrying it
// with the sample keeps the rule true even when a detail op is unaware of the room graph.
bool bProtectedStructuralFloor = false;
float StructuralFloorMinimumDensity = 0.0f;
};
/**
@@ -183,6 +183,23 @@ public:
VoxelDensityProfile::FScopedTimer ProfileTimer(Entry.ProfileName);
Entry.Op->Eval(WorldX, WorldY, WorldZ, S);
}
// The floor profile is authored by the room-graph source, before any detail op runs. Keep
// that source-level clearance invariant through the complete stack. This is intentionally
// one final clamp, rather than a per-op query or a support-column revalidation: the source
// has already proved the current sample belongs to its finite authored floor band.
if (S.bProtectedStructuralFloor
&& FMath::IsFinite(S.StructuralFloorMinimumDensity)
&& S.StructuralFloorMinimumDensity > 0.0f)
{
if (S.Density < S.StructuralFloorMinimumDensity
&& VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelAuthoredFloorProtectionClamps);
}
S.Density = FMath::Max(S.Density, S.StructuralFloorMinimumDensity);
}
return S;
}
@@ -98,9 +98,17 @@ namespace VoxelDensityProfile
TunnelSupportFloorChecks,
TunnelSupportFloorBackstopFires,
TunnelRoomFloorBackstopFires,
TunnelAuthoredFloorSamples,
TunnelAuthoredFloorProtectionClamps,
TunnelAuthoredFloorDisturbanceSkips,
TunnelFloorProfileFallbacks,
TunnelFloorProfileBuilds,
TunnelFloorProfileSegments,
TunnelFloorProfileLedges,
TunnelFloorProfileLedgesBelow2Voxels,
TunnelFloorProfileLedges2To4Voxels,
TunnelFloorProfileLedges4To8Voxels,
TunnelFloorProfileLedges8PlusVoxels,
PassageCandidates,
PassageEvaluated,
MesherBlockTests,
+9 -2
View File
@@ -46,6 +46,13 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 MaxConcurrentTasks = 16;
// Optional hard cap for the generation worker budget. 0 keeps the safe automatic cap of
// logical cores minus two; a positive value makes that cap visible and authorable beside the
// task budget instead of hiding it in AVoxelWorld. The effective task count is still the
// minimum of this value and MaxConcurrentTasks.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0"))
int32 MaxGenerationWorkerCores = 0;
// Nombre max de meshes appliqués (upload GPU) par frame. Seuls les vrais applies
// comptent (chunks vides/périmés se vident gratuitement). À 32³ chaque apply est léger
// (~0.1 ms) — tunable en live sur l'asset : montez (8-16) si le remplissage traîne,
@@ -282,12 +289,12 @@ public:
// frame. A region is applied once ALL its cells have finished marching, in one batched AddInstances
// per mesh; this throttles that burst. The expensive ray-march runs async on workers. (Named "Cells"
// for asset back-compat — the apply unit is now a region of DecorationRegionSizeCells^2 cells.)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1"))
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "1"))
int32 MaxDecorationCellsPerFrame = 2;
// Max decoration ray-march tasks in flight at once. Keeps decoration marching from crowding the
// mesh-gen workers (which are the streaming bottleneck). 0 disables decorations entirely.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "0"))
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0"))
int32 MaxConcurrentDecorationTasks = 4;
//=========================================================================
@@ -398,9 +398,10 @@ public:
void ApplyPassageTunnelAirMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Apply the landing/tunnel MC backstop in one cached passage scan after disturbances. */
/** Apply the landing/tunnel MC post in one cached passage scan after disturbances. */
void ApplyPassageStructuralPostsMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
float BaseDensity, float SealThickness,
bool bProtectAuthoredTunnelFloor = false) const;
/** Re-assert a landing floor after the landing air pass, still before the final XY seal. */
void ApplyPassageLandingFloorMC(float& Density, float WorldX, float WorldY, float WorldZ,
@@ -311,10 +311,23 @@ struct VOXELFORGE_API FVoxelPlayerFitWalkReport
int32 NumRouteRetries = 0;
bool bClimbingEnabled = false;
float MaxClimbHeightMeters = 0.0f;
// The primordial capability contract is walk-only. The existing reachable fields below
// therefore remain walk-only; climb is reported as a separate tier and never makes the hard
// connectivity verdict pass.
bool bWalkOnlyCanReachDeparture = false;
bool bClimbCanReachDeparture = false;
int64 PlayerFitVolumeCells = 0;
int64 ReachablePlayerFitCells = 0;
int64 WalkOnlyReachablePlayerFitCells = 0;
int64 ClimbReachablePlayerFitCells = 0;
int64 ClimbEdges = 0;
// Fit poses outside the walk+climb reachable set need an edit/mining action to become
// reachable. This is a reporting remainder, not a promise that arbitrary mining can reach
// every such pose without a separate excavation model.
int64 MineRequiredPlayerFitCells = 0;
float ReachablePlayerFitFraction = 0.0f;
float ClimbReachablePlayerFitFraction = 0.0f;
int32 SampledNumX = 0;
int32 SampledNumY = 0;
int32 SampledNumZ = 0;
@@ -4261,7 +4261,7 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("refusal_or_error"), Output.Walk.RefusalReason);
Writer->WriteValue(TEXT("window_policy"), Output.Walk.WindowPolicy);
Writer->WriteValue(TEXT("agent_policy"), TEXT(
"fixed-order depth-first walk over the exact VF player-fit graph; six-neighbour movement plus supported climb edges; stop at departure"));
"fixed-order depth-first walk over the exact VF player-fit graph; walk-only six-neighbour movement; climb is reported separately and never authorizes the result; stop at departure"));
Writer->WriteValue(TEXT("player_fit_volume_scope"), TEXT(
"bounded fitted route window; WorldRadiusVoxels=0 has no finite whole-world volume"));
Writer->WriteValue(TEXT("max_cells_cap"), Arguments.MaxWalkCells);
@@ -4305,6 +4305,8 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("connectivity_result"), ConnectivityResultName(Report.Result));
Writer->WriteValue(TEXT("connectivity_result_code"), static_cast<int32>(Report.Result));
Writer->WriteValue(TEXT("can_reach_departure"), Report.bCanReachDeparture);
Writer->WriteValue(TEXT("walk_only_can_reach_departure"), Report.bWalkOnlyCanReachDeparture);
Writer->WriteValue(TEXT("climb_can_reach_departure"), Report.bClimbCanReachDeparture);
Writer->WriteValue(TEXT("start_snapped"), Report.bStartSnapped);
Writer->WriteValue(TEXT("goal_snapped"), Report.bGoalSnapped);
Writer->WriteValue(TEXT("route_retries"), Report.NumRouteRetries);
@@ -4313,6 +4315,15 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("player_fit_volume_cells"), Report.PlayerFitVolumeCells);
Writer->WriteValue(TEXT("reachable_player_fit_cells"), Report.ReachablePlayerFitCells);
Writer->WriteValue(TEXT("reachable_player_fit_fraction"), static_cast<double>(Report.ReachablePlayerFitFraction));
Writer->WriteValue(TEXT("walk_only_reachable_player_fit_cells"),
Report.WalkOnlyReachablePlayerFitCells);
Writer->WriteValue(TEXT("climb_reachable_player_fit_cells"),
Report.ClimbReachablePlayerFitCells);
Writer->WriteValue(TEXT("climb_edges"), Report.ClimbEdges);
Writer->WriteValue(TEXT("mine_required_player_fit_cells"),
Report.MineRequiredPlayerFitCells);
Writer->WriteValue(TEXT("climb_reachable_player_fit_fraction"),
static_cast<double>(Report.ClimbReachablePlayerFitFraction));
Writer->WriteValue(TEXT("sampled_num_x"), Report.SampledNumX);
Writer->WriteValue(TEXT("sampled_num_y"), Report.SampledNumY);
Writer->WriteValue(TEXT("sampled_num_z"), Report.SampledNumZ);
@@ -4391,6 +4402,10 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("window_policy"), TEXT("origin-inclusive mouth-AABB + margin"));
Writer->WriteValue(TEXT("connectivity_result"), ConnectivityResultName(OriginCheckReport.Result));
Writer->WriteValue(TEXT("can_reach_departure"), OriginCheckReport.bCanReachDeparture);
Writer->WriteValue(TEXT("walk_only_can_reach_departure"),
OriginCheckReport.bWalkOnlyCanReachDeparture);
Writer->WriteValue(TEXT("climb_can_reach_departure"),
OriginCheckReport.bClimbCanReachDeparture);
Writer->WriteValue(TEXT("sampled_num_x"), OriginCheckReport.SampledNumX);
Writer->WriteValue(TEXT("sampled_num_y"), OriginCheckReport.SampledNumY);
Writer->WriteValue(TEXT("sampled_num_z"), OriginCheckReport.SampledNumZ);
@@ -4404,6 +4419,15 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("reachable_player_fit_cells"), OriginCheckReport.ReachablePlayerFitCells);
Writer->WriteValue(TEXT("reachable_player_fit_fraction"),
static_cast<double>(OriginCheckReport.ReachablePlayerFitFraction));
Writer->WriteValue(TEXT("walk_only_reachable_player_fit_cells"),
OriginCheckReport.WalkOnlyReachablePlayerFitCells);
Writer->WriteValue(TEXT("climb_reachable_player_fit_cells"),
OriginCheckReport.ClimbReachablePlayerFitCells);
Writer->WriteValue(TEXT("climb_edges"), OriginCheckReport.ClimbEdges);
Writer->WriteValue(TEXT("mine_required_player_fit_cells"),
OriginCheckReport.MineRequiredPlayerFitCells);
Writer->WriteValue(TEXT("climb_reachable_player_fit_fraction"),
static_cast<double>(OriginCheckReport.ClimbReachablePlayerFitFraction));
Writer->WriteValue(TEXT("player_fit_components"), OriginCheckReport.PlayerFitComponents);
Writer->WriteValue(TEXT("arrival_component_cells"), OriginCheckReport.ArrivalComponentCells);
Writer->WriteValue(TEXT("departure_component_cells"), OriginCheckReport.DepartureComponentCells);