wip(L25, side branch): FI helix enclosed in a 13 m rock root; support never created in open air
Fixes the L24 ribbon (owner request). NOT merged: the open-air support gate leaves ~4,000 unsupported samples on every SurfaceWorld-recipe passage style (TN -> SW), a walkability regression. L26 fixes that on this branch. See Linux25/REPORT.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
This commit is contained in:
1 parent
cb9236c265
commit
ef789aaa00
14 files changed
+662
-110
No files matched your search
@@ -110,6 +110,9 @@ public:
|
||||
const auto composeRouteMouthSupport=[&](std::int32_t strateIndex,
|
||||
const std::array<std::int64_t,3>& mouth,bool departure)
|
||||
{
|
||||
// Support is allowed to reinforce native solid terrain only. Without this
|
||||
// guard the route-point tread stamps a floating shelf into an open stratum.
|
||||
if(!(density<0.0f))return;
|
||||
const double mouthDx=qx-static_cast<double>(mouth[0]);
|
||||
const double mouthDy=qy-static_cast<double>(mouth[1]);
|
||||
const double routeSupportReach=13.0*qPerMetre;
|
||||
@@ -159,26 +162,26 @@ public:
|
||||
std::clamp(Z-floorVoxels,-floorThickness,0.0)));
|
||||
}
|
||||
};
|
||||
// Seed the route floor first. The bore carve below reopens any overlap at the
|
||||
// mouth, then applies its own lower tread, so neither shape blocks the capsule.
|
||||
// Keep the native surface route connected to the entrance. Passage carving
|
||||
// then removes only the parts that overlap the tunnel, whose own tread owns
|
||||
// the interior ramp.
|
||||
for(const auto& passage:ResolvedLayout->passages)
|
||||
{
|
||||
composeRouteMouthSupport(passage.upperStrateIndex,passage.upperMouthSubvoxels,true);
|
||||
// The lower mouth emerges into its native terrain. Keep the passage tread
|
||||
// support, but do not stamp short, flat route-anchor shelves over that cave mouth.
|
||||
}
|
||||
composeRouteMouthSupport(passage.upperStrateIndex,
|
||||
passage.upperMouthSubvoxels,true);
|
||||
for(const auto& passage:ResolvedLayout->passages)
|
||||
{
|
||||
if(passage.controlPoints.size()<2)continue;
|
||||
const double placementRadiusQ=static_cast<double>(passage.placementRadiusSubvoxels)
|
||||
+static_cast<double>(passage.midRadiusSubvoxels);
|
||||
if(qx*qx+qy*qy>placementRadiusQ*placementRadiusQ)continue;
|
||||
if(qx*qx+qy*qy>placementRadiusQ*placementRadiusQ)
|
||||
continue;
|
||||
const double firstFloorQ=passage.controlPoints.front().positionSubvoxels[2]
|
||||
-passage.controlPoints.front().radiusSubvoxels+playerCenterAboveFloorQ;
|
||||
const double lastFloorQ=passage.controlPoints.back().positionSubvoxels[2]
|
||||
-passage.controlPoints.back().radiusSubvoxels+playerCenterAboveFloorQ;
|
||||
if(qz>firstFloorQ+2.0*passage.mouthRadiusSubvoxels
|
||||
||qz<lastFloorQ-2.0*passage.mouthRadiusSubvoxels)continue;
|
||||
||qz<lastFloorQ-2.0*passage.mouthRadiusSubvoxels)
|
||||
continue;
|
||||
const auto floorProfile=[&](const auto& point)
|
||||
{return point.positionSubvoxels[2]-point.radiusSubvoxels+playerCenterAboveFloorQ;};
|
||||
const std::ptrdiff_t lastIndex=static_cast<std::ptrdiff_t>(passage.controlPoints.size())-1;
|
||||
@@ -237,21 +240,6 @@ public:
|
||||
const auto& nearestB=passage.controlPoints[static_cast<std::size_t>(bestIndex+1)];
|
||||
const double nearestRadius=nearestA.radiusSubvoxels+bestT*(
|
||||
static_cast<double>(nearestB.radiusSubvoxels)-nearestA.radiusSubvoxels);
|
||||
const double nearestDx=static_cast<double>(nearestB.positionSubvoxels[0])
|
||||
-nearestA.positionSubvoxels[0];
|
||||
const double nearestDy=static_cast<double>(nearestB.positionSubvoxels[1])
|
||||
-nearestA.positionSubvoxels[1];
|
||||
const double nearestHorizontalSq=nearestDx*nearestDx+nearestDy*nearestDy;
|
||||
const double supportPathT=nearestHorizontalSq>0.0
|
||||
?std::clamp(((qx-nearestA.positionSubvoxels[0])*nearestDx
|
||||
+(qy-nearestA.positionSubvoxels[1])*nearestDy)/nearestHorizontalSq,0.0,1.0)
|
||||
:bestT;
|
||||
const double supportPathRadius=nearestA.radiusSubvoxels+supportPathT*(
|
||||
static_cast<double>(nearestB.radiusSubvoxels)-nearestA.radiusSubvoxels);
|
||||
const double supportPathPlayerCenterZ=nearestA.positionSubvoxels[2]+supportPathT*(
|
||||
static_cast<double>(nearestB.positionSubvoxels[2])-nearestA.positionSubvoxels[2])
|
||||
-supportPathRadius+playerCenterAboveFloorQ;
|
||||
const double nearestPlayerCenterZ=supportPathPlayerCenterZ;
|
||||
const double distanceToCenter=std::max(1.0e-9,bestSignedDistance+nearestRadius);
|
||||
const double verticalWallAlignment=distanceToCenter>1.0e-9
|
||||
?std::clamp(-(qz-(nearestA.positionSubvoxels[2]+bestT*(
|
||||
@@ -265,11 +253,6 @@ public:
|
||||
// Positive density is air; rough rock is cut with full side/ceiling detail and
|
||||
// gentler relief on the gravity-facing wall so the supported tread stays open.
|
||||
composed=std::max(composed,static_cast<float>(-roughSignedDistance/Q));
|
||||
// At tight bends, keep the tread tied to the player-center profile selected
|
||||
// by horizontal path position so vertical probe offsets cannot shift the ramp.
|
||||
const bool fallbackSupport=qz>=nearestPlayerCenterZ-1.50*qPerMetre;
|
||||
const double supportThicknessVoxels=fallbackSupport
|
||||
?qPerMetre/Q:0.5*qPerMetre/Q;
|
||||
double bestSupportDistanceSquared=std::numeric_limits<double>::infinity();
|
||||
const auto considerSupportSegment=[&](std::ptrdiff_t index)
|
||||
{
|
||||
@@ -293,21 +276,28 @@ public:
|
||||
static_cast<double>(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]);
|
||||
const double radius=segmentA.radiusSubvoxels+supportT*(
|
||||
static_cast<double>(segmentB.radiusSubvoxels)-segmentA.radiusSubvoxels);
|
||||
const double supportPlayerCenterZ=centerZ-radius+playerCenterAboveFloorQ;
|
||||
if(fallbackSupport)
|
||||
{
|
||||
const double supportProfileError=std::abs(
|
||||
supportPlayerCenterZ-nearestPlayerCenterZ);
|
||||
if(supportProfileError>0.06*qPerMetre)return;
|
||||
}
|
||||
const double horizontalX=qx-centerX,horizontalY=qy-centerY;
|
||||
const double horizontalDistance=DeterministicSqrt(horizontalX*horizontalX
|
||||
+horizontalY*horizontalY);
|
||||
const double supportPlayerCenterZ=centerZ-radius+playerCenterAboveFloorQ;
|
||||
const double supportQueryDepth=supportPlayerCenterZ-qz;
|
||||
// Support belongs just below the avatar's foot plane. Queries within
|
||||
// the capsule body, or a neighbouring coil's overlapping bore, must
|
||||
// not turn that tread into a ceiling or mid-air shelf.
|
||||
if(supportQueryDepth<0.93*qPerMetre||supportQueryDepth>1.30*qPerMetre)return;
|
||||
const bool fallbackSupport=qz>=supportPlayerCenterZ-1.50*qPerMetre;
|
||||
const double supportThicknessVoxels=fallbackSupport
|
||||
?qPerMetre/Q:0.5*qPerMetre/Q;
|
||||
const double floorQ=centerZ-radius+floorOffset*qPerMetre;
|
||||
const double floorVoxels=floorQ/Q;
|
||||
// Keep the supported ramp to a comfortable walking width. Extending a
|
||||
// full bore-radius shelf across overlapping helix coils can block the
|
||||
// capsule on the neighbouring turn.
|
||||
const bool lowerMouthBodyOverlap=index>=lowerMouthIndex-2
|
||||
&&supportQueryDepth<0.98*qPerMetre;
|
||||
const double supportHalfWidth=std::min(radius,
|
||||
(lowerMouthBodyOverlap?0.90:1.25)*qPerMetre);
|
||||
if(Z<floorVoxels-supportThicknessVoxels||Z>floorVoxels+1.0/Q)return;
|
||||
const double supportHalfWidth=fallbackSupport?radius
|
||||
:std::max(0.0,radius+2.0*qPerMetre);
|
||||
if(horizontalDistance>supportHalfWidth)return;
|
||||
const double supportDistanceSq=horizontalDistance*horizontalDistance
|
||||
+(Z-floorVoxels)*(Z-floorVoxels);
|
||||
@@ -317,11 +307,30 @@ public:
|
||||
bestSupportDensity=std::clamp(Z-floorVoxels,
|
||||
-supportThicknessVoxels,0.0);
|
||||
};
|
||||
const std::ptrdiff_t supportBegin=std::max<std::ptrdiff_t>(0,bestIndex-1);
|
||||
const std::ptrdiff_t supportEnd=std::min<std::ptrdiff_t>(lastIndex-1,bestIndex+1);
|
||||
std::ptrdiff_t supportCenterIndex=bestIndex;
|
||||
if(bestIndex<lowerMouthIndex)
|
||||
{
|
||||
// A helix can have multiple centerlines at one XY. Select the tread
|
||||
// whose floor is just below this density query, not the closest wall.
|
||||
const double supportProfileTarget=qz+0.93*qPerMetre;
|
||||
const auto supportProfile=std::lower_bound(passage.controlPoints.begin(),mainEnd,
|
||||
supportProfileTarget,[&](const auto& point,double value)
|
||||
{return floorProfile(point)>value;});
|
||||
const std::ptrdiff_t profileIndex=std::clamp<std::ptrdiff_t>(
|
||||
supportProfile-passage.controlPoints.begin(),0,lowerMouthIndex);
|
||||
// At floor height the nearest 3D centerline can belong to the next
|
||||
// coil below. The descending floor profile identifies the coil that
|
||||
// owns this tread, even when it is more than eight control points away.
|
||||
supportCenterIndex=profileIndex;
|
||||
}
|
||||
const std::ptrdiff_t supportBegin=std::max<std::ptrdiff_t>(0,supportCenterIndex-1);
|
||||
const std::ptrdiff_t supportEnd=std::min<std::ptrdiff_t>(lastIndex-1,
|
||||
supportCenterIndex+1);
|
||||
for(std::ptrdiff_t index=supportBegin;index<=supportEnd;++index)
|
||||
considerSupportSegment(index);
|
||||
if(hasSupportFloor)
|
||||
// Keep route treads inside the native rock envelope. Carving may reopen the
|
||||
// floor above this point, but a support cannot create a new surface in air.
|
||||
if(hasSupportFloor&&density<0.0f)
|
||||
composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
||||
}
|
||||
return composed;
|
||||
|
||||
Reference in new issue
Block a user