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:
1 parent
e91f1c44bb
commit
8e42d6d807
15 files changed
+706
-95
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user