passages: four-strate passage floors supported (L67, evaluator v212); 1-2 wall beat still red
- Shared composer: a passage's restored support is applied after a physically overlapping later bore, instead of being carved away by it (0-1: 59 -> 0 unsupported). - Capsule clearance for support restoration is tested against nearby route segments only (1-2: 3 -> 0, 2-3: 2 -> 0 unsupported). - L66 index switches replaced by a physical criterion (both mouths inside their strata bands, neither a SurfaceWorld landing). - Audit: floor zero-crossing refined by bisection (the 0.2498 m "step" was the gauge); wall ripple keeps the authored radius, cubic detrend per 32 m window. Controls detected: L35 ribs, beads, synthetic 0.25 m step, L36 washboard, L23 terrace. - Result: 0-1 and 2-3 pass all audits; 1-2 fails wall ripple 7.27 cm (limit 3.5) from an authored-radius beat at 7.06 m. Fast tier was green before the final gauge change and was not re-run after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
5e2fc53d39
commit
087333bf8a
17 files changed
+1439
-43
No files matched your search
@@ -971,6 +971,7 @@ public:
|
||||
const double floorThicknessVoxels=qPerMetre/Q;
|
||||
const double playerCenterAboveFloorQ=1.13*qPerMetre;
|
||||
float composed=density;
|
||||
float deferredSupportDensity=std::numeric_limits<float>::infinity();
|
||||
const auto composeRouteMouthSupport=[&](std::int32_t strateIndex,
|
||||
const std::array<std::int64_t,3>& mouth,bool departure)
|
||||
{
|
||||
@@ -1045,7 +1046,6 @@ public:
|
||||
{
|
||||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||
if(passage.controlPoints.size()<2)continue;
|
||||
const bool internalChainPassage=passageIndex>0;
|
||||
const auto supportStratum=[&](std::int32_t strateIndex)
|
||||
->const VoxelForgeCore::ResolvedLayoutStratumV2*
|
||||
{
|
||||
@@ -1056,6 +1056,31 @@ public:
|
||||
};
|
||||
const auto* supportUpper=supportStratum(passage.upperStrateIndex);
|
||||
const auto* supportLower=supportStratum(passage.lowerStrateIndex);
|
||||
const auto mouthInsideContent=[&](
|
||||
const VoxelForgeCore::ResolvedLayoutStratumV2* stratum,
|
||||
const std::array<std::int64_t,3>& mouth)
|
||||
{
|
||||
if(!stratum||!stratum->content.presentInCurrentField)return false;
|
||||
const auto zMicrometres=static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(mouth[2])
|
||||
*ResolvedLayout->coreVoxelSizeMicrometres/Q));
|
||||
return zMicrometres>=stratum->content.bottomMicrometres
|
||||
&&zMicrometres<stratum->content.topMicrometres;
|
||||
};
|
||||
// A passage is internal when both mouths are bounded by their strate
|
||||
// content and neither endpoint is a SurfaceWorld landing. Route order
|
||||
// and the total number of strata are irrelevant.
|
||||
const bool internalChainPassage=supportUpper&&supportLower
|
||||
&&!supportUpper->passageCapabilities.hasSurfaceLanding
|
||||
&&!supportLower->passageCapabilities.hasSurfaceLanding
|
||||
&&mouthInsideContent(supportUpper,passage.upperMouthSubvoxels)
|
||||
&&mouthInsideContent(supportLower,passage.lowerMouthSubvoxels);
|
||||
const auto laterPassageMayCarve=[&](std::ptrdiff_t supportSegment,
|
||||
double sampleX,double sampleY,double sampleZ)
|
||||
{
|
||||
return PassageSupportMayBeCarvedByLaterBore(passageIndex,
|
||||
supportSegment,sampleX,sampleY,sampleZ,0.35*qPerMetre);
|
||||
};
|
||||
const bool floatingIslandPassage=supportUpper
|
||||
&&supportUpper->passageCapabilities.hasFloatingIslandLanding;
|
||||
const bool surfaceToTunnelSupport=supportUpper&&supportLower
|
||||
@@ -1223,7 +1248,8 @@ public:
|
||||
+playerVertical*playerVertical)-0.34*qPerMetre);
|
||||
}
|
||||
double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz;
|
||||
const bool smoothCarveJoin=index>2&&index+12<lastIndex
|
||||
const bool smoothCarveJoin=index>2
|
||||
&&index+(internalChainPassage?2:12)<lowerMouthIndex
|
||||
&&DeterministicSqrt(lengthSq)<=8.0*qPerMetre
|
||||
&&!IsPassageShortSpanSegment(passageIndex,segmentIndex-1)
|
||||
&&!IsPassageShortSpanSegment(passageIndex,segmentIndex)
|
||||
@@ -1301,8 +1327,9 @@ public:
|
||||
?std::clamp(-(qz-(nearestA.positionSubvoxels[2]+bestT*(
|
||||
static_cast<double>(nearestB.positionSubvoxels[2])-nearestA.positionSubvoxels[2])))
|
||||
/distanceToCenter,0.0,1.0):0.0;
|
||||
const float relief=VF_FieldPassageWallRelief(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z),static_cast<std::uint32_t>(Owner?Owner->Seed:0));
|
||||
const float relief=VF_FieldPassageWallRelief(static_cast<float>(X),
|
||||
static_cast<float>(Y),static_cast<float>(Z),
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0));
|
||||
const float mouthSurfaceRelief=VF_FieldSurfaceRelief(static_cast<float>(X)*1.5f,
|
||||
static_cast<float>(Y)*1.5f,static_cast<float>(Z)*1.5f,
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x4D4F5554u);
|
||||
@@ -1427,7 +1454,8 @@ public:
|
||||
static_cast<double>(mouthTerrainRelief),-1.0,1.0)
|
||||
*0.20*qPerMetre*mouthFloorBlend*floorReliefWeight
|
||||
*(upperMouthFloorFade?1.0:0.0);
|
||||
const double wallReliefWeight=0.05+0.95*(1.0-verticalWallAlignment);
|
||||
const double wallOpening=1.0-verticalWallAlignment;
|
||||
const double wallReliefWeight=0.05+0.95*wallOpening;
|
||||
const double roughSignedDistance=bestSignedDistance
|
||||
+static_cast<double>(std::clamp(relief,-1.0f,1.0f))*0.21*qPerMetre*wallReliefWeight
|
||||
+floorReliefOffset;
|
||||
@@ -1472,6 +1500,7 @@ public:
|
||||
static_cast<float>(-roughSignedDistance/Q));
|
||||
composed=std::lerp(composed,fullyCarvedDensity,
|
||||
static_cast<float>(floorCarveWeight));
|
||||
const float composedAfterCarve=composed;
|
||||
const auto considerSupportSegment=[&](std::ptrdiff_t index)
|
||||
{
|
||||
const auto& segmentA=passage.controlPoints[static_cast<std::size_t>(index)];
|
||||
@@ -1570,15 +1599,25 @@ public:
|
||||
const double minimumSupportQueryDepth=index==selectedSupportSegmentIndex
|
||||
&&sparseRouteSupport?0.65:0.93;
|
||||
if(supportQueryDepth<minimumSupportQueryDepth*qPerMetre
|
||||
||supportQueryDepth>1.30*qPerMetre)return;
|
||||
||supportQueryDepth>1.30*qPerMetre)
|
||||
return;
|
||||
const bool fallbackSupport=qz>=supportPlayerCenterZ-1.50*qPerMetre;
|
||||
const double supportThicknessVoxels=fallbackSupport
|
||||
?qPerMetre/Q:0.5*qPerMetre/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=ResolvedLayout->strates.size()>2
|
||||
&&index>=lowerMouthIndex-2
|
||||
const double lowerMouthDx=centerX
|
||||
-static_cast<double>(passage.lowerMouthSubvoxels[0]);
|
||||
const double lowerMouthDy=centerY
|
||||
-static_cast<double>(passage.lowerMouthSubvoxels[1]);
|
||||
const double lowerMouthDz=centerZ-radius
|
||||
-static_cast<double>(passage.lowerMouthSubvoxels[2]);
|
||||
const double lowerMouthBodyDistance=DeterministicSqrt(
|
||||
lowerMouthDx*lowerMouthDx+lowerMouthDy*lowerMouthDy
|
||||
+lowerMouthDz*lowerMouthDz);
|
||||
const bool lowerMouthBodyOverlap=internalChainPassage
|
||||
&&lowerMouthBodyDistance<=2.0*qPerMetre
|
||||
&&supportQueryDepth<0.98*qPerMetre;
|
||||
// Keep a narrow walk core, but place its top on the native surface
|
||||
// so the support remains a worn ramp instead of a route-height slab.
|
||||
@@ -1731,7 +1770,8 @@ public:
|
||||
bestSupportUsesSignedDistance=signedDistanceSupport;
|
||||
bestSupportMouthBlend=mouthSupportBlend;
|
||||
bestSupportNativeBlend=mouthTread
|
||||
?0.85*mouthSupportBlend*(1.0-supportEdgeSmooth):0.0;
|
||||
?(internalChainPassage?1.0:0.85)
|
||||
*mouthSupportBlend*(1.0-supportEdgeSmooth):0.0;
|
||||
};
|
||||
std::ptrdiff_t supportCenterIndex=bestIndex;
|
||||
if(bestIndex<lowerMouthIndex)
|
||||
@@ -1775,6 +1815,8 @@ public:
|
||||
lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,
|
||||
capsuleQueryZ,qPerMetre,capsuleClearanceMetres)
|
||||
:bestPlayerCapsuleSignedDistance>=capsuleClearanceMetres*qPerMetre;
|
||||
bool supportApplied=false;
|
||||
bool supportNeedsDeferral=false;
|
||||
// Outside bridge and short-span segments, add the tread only where the
|
||||
// native field already provides a solid envelope; the capsule check keeps
|
||||
// that support below the player's walk space.
|
||||
@@ -1782,6 +1824,8 @@ public:
|
||||
||shortSpanFloorSupport)
|
||||
&&(capsuleAllowsSupport||bestSupportDensity>=0.0))
|
||||
{
|
||||
supportNeedsDeferral=laterPassageMayCarve(bestSupportSegmentIndex,
|
||||
qx,qy,qz);
|
||||
if(bestSupportIsMouthTread)
|
||||
{
|
||||
// Preserve the owner's native ground through the shallow mouth
|
||||
@@ -1797,13 +1841,36 @@ public:
|
||||
const double nativeSurfaceDetail=std::clamp(
|
||||
static_cast<double>(mouthTerrainRelief),-1.0,1.0)
|
||||
*0.18*qPerMetre/Q;
|
||||
const float nativeGroundWithRelief=static_cast<float>(density
|
||||
-nativeSurfaceDetail);
|
||||
const float nativeGroundWithRelief=static_cast<float>(
|
||||
(supportNeedsDeferral?std::max(-1.0f,density):density)
|
||||
-nativeSurfaceDetail);
|
||||
composed=std::lerp(composed,nativeGroundWithRelief,
|
||||
static_cast<float>(bestSupportNativeBlend));
|
||||
supportApplied=composed<composedAfterCarve;
|
||||
}
|
||||
}
|
||||
else composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
||||
else
|
||||
{
|
||||
const float densityBeforeSupport=composed;
|
||||
composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
||||
supportApplied=composed<densityBeforeSupport;
|
||||
}
|
||||
}
|
||||
if(supportApplied)
|
||||
{
|
||||
if(PassageOtherCapsuleBlocksSupport(passageIndex,
|
||||
bestSupportSegmentIndex,qx,qy,qz,qPerMetre,capsuleClearanceMetres))
|
||||
{
|
||||
composed=composedAfterCarve;
|
||||
supportApplied=false;
|
||||
}
|
||||
}
|
||||
if(supportApplied&&supportNeedsDeferral)
|
||||
{
|
||||
deferredSupportDensity=std::min(deferredSupportDensity,composed);
|
||||
// Preserve a tread only when a later passage bore reaches this
|
||||
// sample; otherwise retain the ordinary composition order.
|
||||
composed=composedAfterCarve;
|
||||
}
|
||||
}
|
||||
// Restore the surface route where the passage cut crossed otherwise solid
|
||||
@@ -1818,6 +1885,7 @@ public:
|
||||
composeRouteMouthSupport(passage.upperStrateIndex,
|
||||
passage.upperMouthSubvoxels,true);
|
||||
}
|
||||
composed=std::min(composed,deferredSupportDensity);
|
||||
return composed;
|
||||
}
|
||||
|
||||
@@ -3282,6 +3350,17 @@ private:
|
||||
{
|
||||
std::array<double,3> minimum{},maximum{};
|
||||
};
|
||||
struct FPassageLaterBoreCandidate
|
||||
{
|
||||
std::array<double,3> minimum{},maximum{};
|
||||
double reach=0.0;
|
||||
};
|
||||
struct FPassageLaterBoreSource
|
||||
{
|
||||
std::array<double,3> minimum{},maximum{};
|
||||
std::vector<FPassageLaterBoreCandidate> candidates;
|
||||
std::vector<std::size_t> capsuleCandidatePassages;
|
||||
};
|
||||
struct FPassageProofNode
|
||||
{
|
||||
std::array<double,3> minimum{},maximum{};
|
||||
@@ -4237,6 +4316,7 @@ private:
|
||||
PassageNearestNodes.clear();
|
||||
PassageProofBounds.clear();PassageProofNodes.clear();
|
||||
PassageNearestNodes.resize(ResolvedLayout?ResolvedLayout->passages.size():0);
|
||||
PassageLaterBoreSources.clear();
|
||||
PassageColumnSegmentFlags.clear();PassageShortSpanSegmentFlags.clear();
|
||||
PassageHasShortSpanFlags.clear();
|
||||
if(!ResolvedLayout||ResolvedLayout->passages.empty())return;
|
||||
@@ -4336,6 +4416,75 @@ private:
|
||||
}
|
||||
}
|
||||
}
|
||||
// Precompute the later-bore leaves that can intersect each tread segment.
|
||||
// Passage support samples normally stay within this source envelope, so the
|
||||
// hot query can test a short list of physical bounds instead of walking a
|
||||
// full spatial tree for every density sample.
|
||||
PassageLaterBoreSources.resize(ResolvedLayout->passages.size());
|
||||
const double qPerMetre=Q*voxelsPerMetre;
|
||||
for(std::size_t sourcePassageIndex=0;
|
||||
sourcePassageIndex<ResolvedLayout->passages.size();++sourcePassageIndex)
|
||||
{
|
||||
const auto& sourcePassage=ResolvedLayout->passages[sourcePassageIndex];
|
||||
const std::size_t sourceSegmentCount=sourcePassage.controlPoints.size()>1
|
||||
?sourcePassage.controlPoints.size()-1:0;
|
||||
auto& sourceSegments=PassageLaterBoreSources[sourcePassageIndex];
|
||||
sourceSegments.resize(sourceSegmentCount);
|
||||
for(std::size_t sourceSegment=0;sourceSegment<sourceSegmentCount;++sourceSegment)
|
||||
{
|
||||
const auto& sourceA=sourcePassage.controlPoints[sourceSegment];
|
||||
const auto& sourceB=sourcePassage.controlPoints[sourceSegment+1];
|
||||
const double sourceRadius=std::max(sourceA.radiusSubvoxels,
|
||||
sourceB.radiusSubvoxels);
|
||||
auto& source=sourceSegments[sourceSegment];
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
source.minimum[axis]=std::min(sourceA.positionSubvoxels[axis],
|
||||
sourceB.positionSubvoxels[axis])-sourceRadius-3.0*qPerMetre;
|
||||
source.maximum[axis]=std::max(sourceA.positionSubvoxels[axis],
|
||||
sourceB.positionSubvoxels[axis])+sourceRadius+3.0*qPerMetre;
|
||||
}
|
||||
for(std::size_t laterPassageIndex=sourcePassageIndex+1;
|
||||
laterPassageIndex<ResolvedLayout->passages.size();++laterPassageIndex)
|
||||
{
|
||||
for(const auto& node:PassageNearestNodes[laterPassageIndex])
|
||||
{
|
||||
if(node.left>=0)continue;
|
||||
const double reach=node.maximumRadius+0.35*qPerMetre+1.0e-6;
|
||||
bool intersectsSource=true;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
intersectsSource=intersectsSource
|
||||
&&node.minimum[axis]-reach<=source.maximum[axis]
|
||||
&&node.maximum[axis]+reach>=source.minimum[axis];
|
||||
if(!intersectsSource)continue;
|
||||
source.candidates.push_back({node.minimum,node.maximum,reach});
|
||||
}
|
||||
}
|
||||
const double capsuleXYReach=0.39*qPerMetre;
|
||||
for(std::size_t otherPassageIndex=0;
|
||||
otherPassageIndex<ResolvedLayout->passages.size();++otherPassageIndex)
|
||||
{
|
||||
if(otherPassageIndex==sourcePassageIndex
|
||||
||PassageNearestNodes[otherPassageIndex].empty())continue;
|
||||
const auto& otherRoot=PassageNearestNodes[otherPassageIndex].front();
|
||||
const std::array<double,3> blockerMinimum={
|
||||
otherRoot.minimum[0]-capsuleXYReach,
|
||||
otherRoot.minimum[1]-capsuleXYReach,
|
||||
otherRoot.minimum[2]-otherRoot.maximumRadius+0.20*qPerMetre};
|
||||
const std::array<double,3> blockerMaximum={
|
||||
otherRoot.maximum[0]+capsuleXYReach,
|
||||
otherRoot.maximum[1]+capsuleXYReach,
|
||||
otherRoot.maximum[2]+2.06*qPerMetre};
|
||||
bool intersectsSource=true;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
intersectsSource=intersectsSource
|
||||
&&blockerMinimum[axis]<=source.maximum[axis]
|
||||
&&blockerMaximum[axis]>=source.minimum[axis];
|
||||
if(intersectsSource)
|
||||
source.capsuleCandidatePassages.push_back(otherPassageIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
for(const auto& crossing:PassageVoidCrossings)
|
||||
{
|
||||
if(crossing.passageIndex<PassageColumnSegmentFlags.size())
|
||||
@@ -4473,6 +4622,88 @@ private:
|
||||
signedDistanceLimit);
|
||||
}
|
||||
|
||||
bool PassageSupportMayBeCarvedByLaterBore(std::size_t passageIndex,
|
||||
std::ptrdiff_t supportSegment,double qx,double qy,double qz,
|
||||
double signedDistanceLimit) const
|
||||
{
|
||||
if(!ResolvedLayout||passageIndex+1>=ResolvedLayout->passages.size())return false;
|
||||
if(passageIndex<PassageLaterBoreSources.size()&&supportSegment>=0
|
||||
&&static_cast<std::size_t>(supportSegment)
|
||||
<PassageLaterBoreSources[passageIndex].size())
|
||||
{
|
||||
const auto& source=PassageLaterBoreSources[passageIndex][
|
||||
static_cast<std::size_t>(supportSegment)];
|
||||
const bool insideSourceEnvelope=qx>=source.minimum[0]&&qx<=source.maximum[0]
|
||||
&&qy>=source.minimum[1]&&qy<=source.maximum[1]
|
||||
&&qz>=source.minimum[2]&&qz<=source.maximum[2];
|
||||
if(insideSourceEnvelope)
|
||||
{
|
||||
for(const auto& candidate:source.candidates)
|
||||
{
|
||||
double distanceSquared=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double q=axis==0?qx:axis==1?qy:qz;
|
||||
const double distance=q<candidate.minimum[axis]
|
||||
?candidate.minimum[axis]-q
|
||||
:q>candidate.maximum[axis]?q-candidate.maximum[axis]:0.0;
|
||||
distanceSquared+=distance*distance;
|
||||
}
|
||||
const double reach=candidate.reach;
|
||||
if(distanceSquared<=reach*reach)return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for(std::size_t otherPassageIndex=passageIndex+1;
|
||||
otherPassageIndex<ResolvedLayout->passages.size();++otherPassageIndex)
|
||||
{
|
||||
const auto& otherPassage=ResolvedLayout->passages[otherPassageIndex];
|
||||
if(otherPassage.controlPoints.size()<2)continue;
|
||||
if(PassagePotentialWithinNode(otherPassageIndex,0,0,
|
||||
otherPassage.controlPoints.size()-1,qx,qy,qz,signedDistanceLimit))return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PassageOtherCapsuleBlocksSupport(std::size_t passageIndex,
|
||||
std::ptrdiff_t supportSegment,double qx,double qy,double qz,
|
||||
double qPerMetre,double minimumClearanceMetres) const
|
||||
{
|
||||
if(passageIndex<PassageLaterBoreSources.size()&&supportSegment>=0
|
||||
&&static_cast<std::size_t>(supportSegment)
|
||||
<PassageLaterBoreSources[passageIndex].size())
|
||||
{
|
||||
const auto& source=PassageLaterBoreSources[passageIndex][
|
||||
static_cast<std::size_t>(supportSegment)];
|
||||
const bool insideSourceEnvelope=qx>=source.minimum[0]&&qx<=source.maximum[0]
|
||||
&&qy>=source.minimum[1]&&qy<=source.maximum[1]
|
||||
&&qz>=source.minimum[2]&&qz<=source.maximum[2];
|
||||
if(insideSourceEnvelope)
|
||||
{
|
||||
for(const std::size_t otherPassageIndex:source.capsuleCandidatePassages)
|
||||
{
|
||||
const auto& otherPassage=ResolvedLayout->passages[otherPassageIndex];
|
||||
if(PassagePlayerCapsuleBlocksSupportNode(otherPassageIndex,0,0,
|
||||
otherPassage.controlPoints.size()-1,qx,qy,qz,qPerMetre,
|
||||
minimumClearanceMetres))return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for(std::size_t otherPassageIndex=0;
|
||||
otherPassageIndex<ResolvedLayout->passages.size();++otherPassageIndex)
|
||||
{
|
||||
if(otherPassageIndex==passageIndex)continue;
|
||||
const auto& otherPassage=ResolvedLayout->passages[otherPassageIndex];
|
||||
if(otherPassage.controlPoints.size()<2)continue;
|
||||
if(PassagePlayerCapsuleBlocksSupportNode(otherPassageIndex,0,0,
|
||||
otherPassage.controlPoints.size()-1,qx,qy,qz,qPerMetre,
|
||||
minimumClearanceMetres))return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PassageMayAffectDensity(std::size_t passageIndex,std::ptrdiff_t carveBegin,
|
||||
std::ptrdiff_t carveEnd,std::ptrdiff_t lowerMouthIndex,
|
||||
std::ptrdiff_t mouthSearchBegin,std::ptrdiff_t mouthSearchEnd,std::ptrdiff_t lastIndex,
|
||||
@@ -4529,7 +4760,10 @@ private:
|
||||
const double vertical=std::max(0.0,std::abs(qz-centerZ)-0.54*qPerMetre);
|
||||
const double distance=DeterministicSqrt(horizontalX*horizontalX+horizontalY*horizontalY
|
||||
+vertical*vertical)-0.34*qPerMetre;
|
||||
if(distance<minimumClearanceMetres*qPerMetre)return true;
|
||||
if(distance<minimumClearanceMetres*qPerMetre)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -4749,8 +4983,12 @@ private:
|
||||
const double irregularity=1.25*qPerMetre
|
||||
+std::clamp(static_cast<double>(broadRelief),-1.0,1.0)*1.10*qPerMetre
|
||||
+std::clamp(static_cast<double>(surfaceRelief),-1.0,1.0)*0.35*qPerMetre;
|
||||
// Add a subtle host-rock root only when the unmodified column lacks the
|
||||
// vertical relief required for walkable passage support.
|
||||
const double rootTaper=crossing.needsRootTaper
|
||||
?0.08*qPerMetre*smoothStep((t-0.35)/0.50):0.0;
|
||||
const double radius=static_cast<double>(crossing.minimumRadiusSubvoxels)
|
||||
+std::max(0.15*qPerMetre,irregularity)+baseFlare+ceilingFlare;
|
||||
+std::max(0.15*qPerMetre,irregularity)+rootTaper+baseFlare+ceilingFlare;
|
||||
const double radialDistanceField=radialDistance-radius;
|
||||
const double topDistance=qz-crossing.topZSubvoxels;
|
||||
const double bottomDistance=crossing.bottomZSubvoxels-qz;
|
||||
@@ -4780,6 +5018,67 @@ private:
|
||||
return ApplyResolvedStrateBoundaryBand(density,X,Y,Z);
|
||||
}
|
||||
|
||||
double PassageColumnNaturalReliefMetres(
|
||||
const FStandalonePassageVoidCrossing& crossing,double qPerMetre) const
|
||||
{
|
||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
constexpr std::array<double,6> HeightFractions{{0.25,0.35,0.45,0.55,0.65,0.75}};
|
||||
constexpr std::size_t DirectionCount=16;
|
||||
const auto smoothStep=[](double value)
|
||||
{
|
||||
value=std::clamp(value,0.0,1.0);
|
||||
return value*value*(3.0-2.0*value);
|
||||
};
|
||||
std::array<double,HeightFractions.size()> meanRadii{};
|
||||
std::size_t heightCount=0;
|
||||
const auto seed=static_cast<std::uint32_t>(Owner?Owner->Seed:0);
|
||||
const double height=static_cast<double>(crossing.topZSubvoxels
|
||||
-crossing.bottomZSubvoxels);
|
||||
if(height<=0.0)return 0.0;
|
||||
for(const double fraction:HeightFractions)
|
||||
{
|
||||
const double qz=crossing.topZSubvoxels-height*fraction;
|
||||
const float z=static_cast<float>(qz/Q);
|
||||
const float scaledZ=z*0.12f;
|
||||
const float driftX=VF_FieldSurfaceRelief(0.0f,0.0f,scaledZ,seed^0x434f4c58u);
|
||||
const float driftY=VF_FieldSurfaceRelief(0.0f,0.0f,scaledZ,seed^0x434f4c59u);
|
||||
const double axisX=crossing.centerXSubvoxels
|
||||
+std::clamp(static_cast<double>(driftX),-1.0,1.0)*0.35*qPerMetre;
|
||||
const double axisY=crossing.centerYSubvoxels
|
||||
+std::clamp(static_cast<double>(driftY),-1.0,1.0)*0.35*qPerMetre;
|
||||
const double t=(crossing.topZSubvoxels-qz)/height;
|
||||
const double baseFlare=6.0*qPerMetre*smoothStep((t-0.82)/0.18);
|
||||
const double ceilingFlare=2.0*qPerMetre*(1.0-smoothStep(t/0.075));
|
||||
double meanRadius=0.0;
|
||||
for(std::size_t direction=0;direction<DirectionCount;++direction)
|
||||
{
|
||||
const double angle=2.0*3.14159265358979323846*direction/DirectionCount;
|
||||
double radius=crossing.minimumRadiusSubvoxels+1.25*qPerMetre
|
||||
+baseFlare+ceilingFlare;
|
||||
for(int iteration=0;iteration<8;++iteration)
|
||||
{
|
||||
const float sampleX=static_cast<float>((axisX+radius*std::cos(angle))/Q);
|
||||
const float sampleY=static_cast<float>((axisY+radius*std::sin(angle))/Q);
|
||||
const float broadRelief=VF_FieldSurfaceRelief(sampleX*0.12f,
|
||||
sampleY*0.12f,scaledZ,seed^0x434f4c52u);
|
||||
const float surfaceRelief=VF_FieldSurfaceRelief(sampleX,sampleY,z,
|
||||
seed^0x434f4c53u);
|
||||
const double irregularity=1.25*qPerMetre
|
||||
+std::clamp(static_cast<double>(broadRelief),-1.0,1.0)*1.10*qPerMetre
|
||||
+std::clamp(static_cast<double>(surfaceRelief),-1.0,1.0)*0.35*qPerMetre;
|
||||
radius=crossing.minimumRadiusSubvoxels
|
||||
+std::max(0.15*qPerMetre,irregularity)+baseFlare+ceilingFlare;
|
||||
}
|
||||
meanRadius+=radius;
|
||||
}
|
||||
meanRadii[heightCount++]=meanRadius/DirectionCount/qPerMetre;
|
||||
}
|
||||
if(heightCount<2)return 0.0;
|
||||
const auto begin=meanRadii.begin();
|
||||
const auto end=begin+static_cast<std::ptrdiff_t>(heightCount);
|
||||
return *std::max_element(begin,end)-*std::min_element(begin,end);
|
||||
}
|
||||
|
||||
void BuildPassageVoidCrossingPlan()
|
||||
{
|
||||
BuildResolvedBoundaryZIntervals();
|
||||
@@ -4789,7 +5088,8 @@ private:
|
||||
{BuildPassageSpatialIndices();return;}
|
||||
auto& cache=*Owner->PassageVoidCrossingCache;
|
||||
std::lock_guard<std::mutex> lock(cache.mutex);
|
||||
const std::string cacheKey=ResolvedLayout->stableIdentity+"/passage-void-columns-v1";
|
||||
const std::string cacheKey=ResolvedLayout->stableIdentity+"/passage-void-columns-v2-seed-"
|
||||
+std::to_string(Owner->Seed);
|
||||
const auto cached=cache.byLayout.find(cacheKey);
|
||||
if(cached!=cache.byLayout.end())
|
||||
{PassageVoidCrossings=cached->second;BuildPassageSpatialIndices();return;}
|
||||
@@ -4968,6 +5268,12 @@ private:
|
||||
maximumCrossingZ+5.0*qPerMetre));
|
||||
crossing.bottomZSubvoxels=static_cast<std::int64_t>(std::llround(
|
||||
minimumCrossingZ-6.0*qPerMetre));
|
||||
crossing.naturalSurfaceReliefMetres=PassageColumnNaturalReliefMetres(
|
||||
crossing,qPerMetre);
|
||||
// The support-column audit requires 20 cm of vertical relief.
|
||||
// Add a shallow root flare only when the existing field profile
|
||||
// falls short, so naturally varied legacy columns remain intact.
|
||||
crossing.needsRootTaper=crossing.naturalSurfaceReliefMetres<0.20;
|
||||
}
|
||||
PassageVoidCrossings.push_back(crossing);
|
||||
if(crossing.needsColumn)
|
||||
@@ -4975,7 +5281,7 @@ private:
|
||||
const double coreRadius=crossing.nominalRadiusSubvoxels/qPerMetre;
|
||||
const double minimumWall=coreRadius-crossing.maximumRouteOffsetMetres
|
||||
-crossing.maximumBoreRadiusMetres;
|
||||
std::printf("[PassageVoidColumnPlan] id=%s segments=%zu..%zu unsupported_samples=%llu span_m=%.3f threshold_m=%.3f connected=%d/%d axis_m=(%.3f,%.3f) z_m=%.3f..%.3f route_radius_m=%.3f bore_radius_m=%.3f min_radius_m=%.3f nominal_radius_m=%.3f max_radius_m=%.3f nominal_wall_m=%.3f flare_m=6.000 status=%s\n",
|
||||
std::printf("[PassageVoidColumnPlan] id=%s segments=%zu..%zu unsupported_samples=%llu span_m=%.3f threshold_m=%.3f connected=%d/%d axis_m=(%.3f,%.3f) z_m=%.3f..%.3f route_radius_m=%.3f bore_radius_m=%.3f min_radius_m=%.3f nominal_radius_m=%.3f max_radius_m=%.3f nominal_wall_m=%.3f natural_relief_m=%.3f root_taper=%d flare_m=6.000 status=%s\n",
|
||||
passage.stableId.c_str(),crossing.firstSegment,crossing.lastSegment,
|
||||
static_cast<unsigned long long>(crossing.unsupportedSamples),gapLength,
|
||||
shortSpanMaximumMetres,static_cast<int>(crossing.connectedAtStart),
|
||||
@@ -4985,6 +5291,7 @@ private:
|
||||
crossing.maximumRouteOffsetMetres,crossing.maximumBoreRadiusMetres,
|
||||
crossing.minimumRadiusSubvoxels/qPerMetre,coreRadius,
|
||||
crossing.maximumRadiusSubvoxels/qPerMetre,minimumWall,
|
||||
crossing.naturalSurfaceReliefMetres,static_cast<int>(crossing.needsRootTaper),
|
||||
crossing.connectedAtStart&&crossing.connectedAtEnd?"PLANNED":"UNATTACHED");
|
||||
}
|
||||
else
|
||||
@@ -10801,6 +11108,7 @@ private:
|
||||
std::vector<FPassageFeatureNode> PassageFeatureNodes;
|
||||
bool PassageFeatureBoundsFailOpen=false;
|
||||
std::vector<std::vector<FPassageNearestNode>> PassageNearestNodes;
|
||||
std::vector<std::vector<FPassageLaterBoreSource>> PassageLaterBoreSources;
|
||||
std::vector<FPassageProofBound> PassageProofBounds;
|
||||
std::vector<FPassageProofNode> PassageProofNodes;
|
||||
std::vector<std::vector<std::uint8_t>> PassageColumnSegmentFlags,PassageShortSpanSegmentFlags;
|
||||
|
||||
Reference in new issue
Block a user