wip(assembler): round L36 -- tunnel floor without washboard; FI mouth without shards (evaluator v23)
The walk tread was built per segment as a one-sided density clamp, so its crossing with the native field moved at segment joins (L35: 2.25-2.48 cm washboard). Treads now overlap up to 2 m as bounded signed-distance slabs (0.06 cm residual). The FI mouth's crater detail and tread met without a smooth join; v23 filters the local field, drops thin isolated solids and smooth-unions the tread with the terrain. New floor ripple and mouth fragment/crease audits with L35 as failing controls. 24/24 passage rows, 30/30 determinism, 16/16 invariants, LOD0-8 rings; cost within noise. 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
ba017077d1
commit
8be1c4d9ef
27 files changed
+1787
-55
No files matched your search
@@ -727,8 +727,9 @@ public:
|
||||
}
|
||||
if(routeCount<2)return;
|
||||
const std::size_t lastSegment=routeCount-1;
|
||||
const double floorThickness=1000000.0/std::max<std::uint32_t>(1,
|
||||
ResolvedLayout->coreVoxelSizeMicrometres);
|
||||
constexpr double MouthSupportBlendMetres=1.5;
|
||||
const double mouthBlendWidth=MouthSupportBlendMetres*qPerMetre;
|
||||
double mouthSupportDistance=std::numeric_limits<double>::infinity();
|
||||
for(std::size_t index=0;index<lastSegment;++index)
|
||||
{
|
||||
const auto& a=*route[index];const auto& b=*route[index+1];
|
||||
@@ -743,12 +744,25 @@ public:
|
||||
?a.values[3]+t*(static_cast<double>(b.values[3])-a.values[3]):0.0;
|
||||
const double halfWidth=std::max(0.75*qPerMetre,routeRadius);
|
||||
const double horizontalX=qx-centerX,horizontalY=qy-centerY;
|
||||
if(horizontalX*horizontalX+horizontalY*horizontalY>halfWidth*halfWidth)continue;
|
||||
const double floorVoxels=(centerZ-1.20*qPerMetre)/Q;
|
||||
if(Z<floorVoxels-floorThickness||Z>floorVoxels+1.0/Q)continue;
|
||||
composed=std::min(composed,static_cast<float>(
|
||||
std::clamp(Z-floorVoxels,-floorThickness,0.0)));
|
||||
const double horizontalDistance=DeterministicSqrt(
|
||||
horizontalX*horizontalX+horizontalY*horizontalY);
|
||||
const double floorQ=centerZ-1.20*qPerMetre;
|
||||
const double sideDistance=horizontalDistance-halfWidth;
|
||||
const double topDistance=qz-floorQ;
|
||||
const double bottomDistance=floorQ-qPerMetre-qz;
|
||||
const double verticalDistance=std::max(topDistance,bottomDistance);
|
||||
if(sideDistance>mouthBlendWidth||verticalDistance>mouthBlendWidth)continue;
|
||||
const double outside=DeterministicSqrt(
|
||||
std::max(sideDistance,0.0)*std::max(sideDistance,0.0)
|
||||
+std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0));
|
||||
const double inside=std::min(std::max(sideDistance,verticalDistance),0.0);
|
||||
const double signedDistance=outside+inside;
|
||||
mouthSupportDistance=std::min(mouthSupportDistance,signedDistance);
|
||||
}
|
||||
if(std::isfinite(mouthSupportDistance))
|
||||
composed=SmoothMinDistance(composed,
|
||||
static_cast<float>(mouthSupportDistance/Q),
|
||||
static_cast<float>(mouthBlendWidth/Q));
|
||||
};
|
||||
// Keep the native surface route connected to the entrance. Passage carving
|
||||
// then removes only the parts that overlap the tunnel, whose own tread owns
|
||||
@@ -765,6 +779,19 @@ public:
|
||||
{
|
||||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||
if(passage.controlPoints.size()<2)continue;
|
||||
const auto supportStratum=[&](std::int32_t strateIndex)
|
||||
->const VoxelForgeCore::ResolvedLayoutStratumV2*
|
||||
{
|
||||
const auto it=std::find_if(ResolvedLayout->strates.begin(),
|
||||
ResolvedLayout->strates.end(),[strateIndex](const auto& stratum)
|
||||
{return stratum.strateIndex==strateIndex;});
|
||||
return it==ResolvedLayout->strates.end()?nullptr:&*it;
|
||||
};
|
||||
const auto* supportUpper=supportStratum(passage.upperStrateIndex);
|
||||
const auto* supportLower=supportStratum(passage.lowerStrateIndex);
|
||||
const bool surfaceToTunnelSupport=supportUpper&&supportLower
|
||||
&&supportUpper->generatorKind=="SurfaceWorld"
|
||||
&&supportLower->generatorKind=="TunnelNetwork";
|
||||
const double firstFloorQ=passage.controlPoints.front().positionSubvoxels[2]
|
||||
-passage.controlPoints.front().radiusSubvoxels+playerCenterAboveFloorQ;
|
||||
const double lastFloorQ=passage.controlPoints.back().positionSubvoxels[2]
|
||||
@@ -880,7 +907,20 @@ public:
|
||||
std::ptrdiff_t bestScanIndex=std::numeric_limits<std::ptrdiff_t>::max();
|
||||
std::ptrdiff_t bestSupportSegmentIndex=0;
|
||||
bool hasSupportFloor=false;
|
||||
double bestSupportDensity=0.0;
|
||||
double bestSupportDistanceSquared=std::numeric_limits<double>::infinity();
|
||||
double bestSupportDensity=std::numeric_limits<double>::infinity();
|
||||
double bestSupportFloorQ=0.0;
|
||||
bool bestSupportUsesSignedDistance=false;
|
||||
std::ptrdiff_t selectedSupportSegmentIndex=-1;
|
||||
const double lowerMouthDx=qx-passage.lowerMouthSubvoxels[0];
|
||||
const double lowerMouthDy=qy-passage.lowerMouthSubvoxels[1];
|
||||
const double lowerMouthDz=qz-passage.lowerMouthSubvoxels[2];
|
||||
const double lowerMouthDistance=DeterministicSqrt(
|
||||
lowerMouthDx*lowerMouthDx+lowerMouthDy*lowerMouthDy
|
||||
+lowerMouthDz*lowerMouthDz)/qPerMetre;
|
||||
const double lowerMouthT=std::clamp((20.0-lowerMouthDistance)/12.0,0.0,1.0);
|
||||
const double lowerMouthBlend=lowerMouthT*lowerMouthT
|
||||
*(3.0-2.0*lowerMouthT);
|
||||
const auto considerCarveSegment=[&](std::size_t segmentIndex,int scanPhase)
|
||||
{
|
||||
const std::ptrdiff_t index=static_cast<std::ptrdiff_t>(segmentIndex);
|
||||
@@ -992,7 +1032,7 @@ public:
|
||||
/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 double wallReliefWeight=1.0-0.80*verticalWallAlignment;
|
||||
const double wallReliefWeight=0.05+0.95*(1.0-verticalWallAlignment);
|
||||
const double roughSignedDistance=bestSignedDistance
|
||||
+static_cast<double>(std::clamp(relief,-1.0f,1.0f))*0.21*qPerMetre*wallReliefWeight;
|
||||
if(IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(bestIndex)))
|
||||
@@ -1004,38 +1044,48 @@ 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));
|
||||
double bestSupportDistanceSquared=std::numeric_limits<double>::infinity();
|
||||
const auto considerSupportSegment=[&](std::ptrdiff_t index)
|
||||
{
|
||||
const auto& segmentA=passage.controlPoints[static_cast<std::size_t>(index)];
|
||||
const auto& segmentB=passage.controlPoints[static_cast<std::size_t>(index+1)];
|
||||
const double dx=static_cast<double>(segmentB.positionSubvoxels[0])-segmentA.positionSubvoxels[0];
|
||||
const double dy=static_cast<double>(segmentB.positionSubvoxels[1])-segmentA.positionSubvoxels[1];
|
||||
const double dz=static_cast<double>(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2];
|
||||
const double radiusA=segmentA.radiusSubvoxels;
|
||||
const double radiusB=segmentB.radiusSubvoxels;
|
||||
const bool entryMouthShoulder=index<=2;
|
||||
// Keep the tread within the probe band and below the capsule's
|
||||
// lowest sampled point.
|
||||
const double floorOffset=IsPassageShortSpanSegment(passageIndex,
|
||||
const bool bridgeDeck=IsPassageShortSpanSegment(passageIndex,
|
||||
static_cast<std::size_t>(index))
|
||||
||IsPassageColumnSegment(passageIndex,static_cast<std::size_t>(index))
|
||||
||IsPassageColumnSegment(passageIndex,static_cast<std::size_t>(index));
|
||||
// Keep a small capsule margin at regular treads and restore most of
|
||||
// the nominal height smoothly as a tread approaches its landing.
|
||||
double floorOffset=bridgeDeck
|
||||
?PassageBridgeDeckOffsetMetres(passageIndex,static_cast<std::size_t>(index))
|
||||
:0.20;
|
||||
:std::lerp(0.15,0.20,lowerMouthBlend);
|
||||
const double horizontalLengthSq=dx*dx+dy*dy;
|
||||
const bool overlapSupportJoin=index+12<lastIndex
|
||||
&&!IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(index))
|
||||
&&(index==0||(!IsPassageShortSpanSegment(passageIndex,
|
||||
static_cast<std::size_t>(index-1))));
|
||||
const bool smoothSupportJoin=index>0&&overlapSupportJoin
|
||||
&&!IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(index+1));
|
||||
const double projection=horizontalLengthSq>0.0
|
||||
?((qx-segmentA.positionSubvoxels[0])*dx
|
||||
+(qy-segmentA.positionSubvoxels[1])*dy)/horizontalLengthSq:bestT;
|
||||
// Extend tread segments by a fixed distance through route knots. A
|
||||
// fractional extension made the support width change with segment length.
|
||||
const double overlapFraction=overlapSupportJoin&&horizontalLengthSq>0.0
|
||||
?std::min(0.5,2.0*qPerMetre/DeterministicSqrt(horizontalLengthSq)):0.0;
|
||||
const double supportT=horizontalLengthSq>0.0
|
||||
?std::clamp(((qx-segmentA.positionSubvoxels[0])*dx
|
||||
+(qy-segmentA.positionSubvoxels[1])*dy)/horizontalLengthSq,0.0,1.0)
|
||||
:bestT;
|
||||
?std::clamp(projection,-overlapFraction,1.0+overlapFraction):bestT;
|
||||
const double centerX=segmentA.positionSubvoxels[0]+supportT*dx;
|
||||
const double centerY=segmentA.positionSubvoxels[1]+supportT*dy;
|
||||
double centerZ=segmentA.positionSubvoxels[2]+supportT*(
|
||||
static_cast<double>(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]);
|
||||
double radius=segmentA.radiusSubvoxels+supportT*(
|
||||
static_cast<double>(segmentB.radiusSubvoxels)-segmentA.radiusSubvoxels);
|
||||
const bool smoothSupportJoin=index>0&&index+12<lastIndex
|
||||
&&!IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(index-1))
|
||||
&&!IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(index))
|
||||
&&!IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(index+1));
|
||||
if(smoothSupportJoin)
|
||||
{
|
||||
const auto& p0=passage.controlPoints[static_cast<std::size_t>(index-1)];
|
||||
@@ -1063,7 +1113,15 @@ public:
|
||||
// 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;
|
||||
// Sparse authored routes use the full signed distance around the top;
|
||||
// dense routes retain the legacy one-sided tread density.
|
||||
const double supportSegmentLength=DeterministicSqrt(
|
||||
horizontalLengthSq+dz*dz)/qPerMetre;
|
||||
const bool sparseRouteSupport=supportSegmentLength>1.5;
|
||||
const double minimumSupportQueryDepth=index==selectedSupportSegmentIndex
|
||||
&&sparseRouteSupport?0.65:0.93;
|
||||
if(supportQueryDepth<minimumSupportQueryDepth*qPerMetre
|
||||
||supportQueryDepth>1.30*qPerMetre)return;
|
||||
const bool fallbackSupport=qz>=supportPlayerCenterZ-1.50*qPerMetre;
|
||||
const double supportThicknessVoxels=fallbackSupport
|
||||
?qPerMetre/Q:0.5*qPerMetre/Q;
|
||||
@@ -1072,12 +1130,8 @@ public:
|
||||
// capsule on the neighbouring turn.
|
||||
const bool lowerMouthBodyOverlap=index>=lowerMouthIndex-2
|
||||
&&supportQueryDepth<0.98*qPerMetre;
|
||||
const double supportEdgeNoise=entryMouthShoulder?std::clamp(
|
||||
static_cast<double>(VF_FieldPassageGroundGrain(static_cast<float>(X),
|
||||
static_cast<float>(Y),
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0))),-1.0,1.0):0.0;
|
||||
const double supportHalfWidthMetres=lowerMouthBodyOverlap?0.90
|
||||
:entryMouthShoulder?1.20+0.20*supportEdgeNoise:1.25;
|
||||
:entryMouthShoulder?1.20:1.25;
|
||||
const double supportHalfWidth=std::min(radius,
|
||||
supportHalfWidthMetres*qPerMetre);
|
||||
// Keep the walk core within a 0.50 m span with shallow relief, then
|
||||
@@ -1095,30 +1149,50 @@ public:
|
||||
const double localSupportThicknessVoxels=entryMouthShoulder
|
||||
?supportThicknessVoxels*(1.0-0.72*supportEdgeSmooth)
|
||||
:supportThicknessVoxels;
|
||||
const double supportEdgeDepth=entryMouthShoulder
|
||||
?0.35+0.15*supportEdgeNoise:0.35;
|
||||
constexpr double supportEdgeDepth=0.35;
|
||||
const double floorQ=centerZ-radius+floorOffset*qPerMetre
|
||||
+supportEdgeNoise*(horizontalDistance>0.60*qPerMetre
|
||||
?0.35:0.14)*qPerMetre
|
||||
-supportEdgeSmooth*supportEdgeDepth*qPerMetre;
|
||||
const double floorVoxels=floorQ/Q;
|
||||
if(Z<floorVoxels-localSupportThicknessVoxels||Z>floorVoxels+1.0/Q)return;
|
||||
const double supportPositiveRangeVoxels=sparseRouteSupport
|
||||
?0.35*qPerMetre/Q:1.0/Q;
|
||||
if(Z<floorVoxels-localSupportThicknessVoxels
|
||||
||Z>floorVoxels+supportPositiveRangeVoxels)return;
|
||||
if(horizontalDistance>supportHalfWidth)return;
|
||||
const double supportDistanceSq=horizontalDistance*horizontalDistance
|
||||
const double supportDistanceSquared=horizontalDistance*horizontalDistance
|
||||
+(Z-floorVoxels)*(Z-floorVoxels);
|
||||
if(supportDistanceSq>=bestSupportDistanceSquared)return;
|
||||
bestSupportDistanceSquared=supportDistanceSq;
|
||||
if(supportDistanceSquared>=bestSupportDistanceSquared)return;
|
||||
bestSupportDistanceSquared=supportDistanceSquared;
|
||||
hasSupportFloor=true;
|
||||
bestSupportSegmentIndex=index;
|
||||
bestSupportDensity=std::clamp(Z-floorVoxels,
|
||||
-localSupportThicknessVoxels,0.0);
|
||||
if(sparseRouteSupport)
|
||||
{
|
||||
// Sparse support profiles need both sides of a signed distance
|
||||
// so the sampled zero crossing stays on the tread top.
|
||||
const double topDistance=qz-floorQ;
|
||||
const double bottomDistance=floorQ-localSupportThicknessVoxels*Q-qz;
|
||||
const double verticalDistance=std::max(topDistance,bottomDistance);
|
||||
const double sideDistance=horizontalDistance-supportHalfWidth;
|
||||
const double outside=DeterministicSqrt(
|
||||
std::max(sideDistance,0.0)*std::max(sideDistance,0.0)
|
||||
+std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0));
|
||||
const double inside=std::min(std::max(sideDistance,verticalDistance),0.0);
|
||||
bestSupportDensity=(outside+inside)/Q;
|
||||
}
|
||||
else
|
||||
{
|
||||
bestSupportDensity=std::clamp(Z-floorVoxels,
|
||||
-localSupportThicknessVoxels,0.0);
|
||||
}
|
||||
bestSupportFloorQ=floorQ;
|
||||
bestSupportUsesSignedDistance=sparseRouteSupport;
|
||||
};
|
||||
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 double supportProfileOffset=surfaceToTunnelSupport?1.13:0.93;
|
||||
const double supportProfileTarget=qz+supportProfileOffset*qPerMetre;
|
||||
const auto supportProfile=std::lower_bound(passage.controlPoints.begin(),mainEnd,
|
||||
supportProfileTarget,[&](const auto& point,double value)
|
||||
{return floorProfile(point)>value;});
|
||||
@@ -1132,6 +1206,8 @@ public:
|
||||
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);
|
||||
selectedSupportSegmentIndex=supportCenterIndex>0
|
||||
?supportCenterIndex-1:supportCenterIndex;
|
||||
for(std::ptrdiff_t index=supportBegin;index<=supportEnd;++index)
|
||||
considerSupportSegment(index);
|
||||
// Keep route treads inside the native rock envelope. Carving may reopen the
|
||||
@@ -1142,13 +1218,24 @@ public:
|
||||
const bool shortSpanFloorSupport=hasSupportFloor
|
||||
&&IsPassageShortSpanSegment(passageIndex,
|
||||
static_cast<std::size_t>(bestSupportSegmentIndex));
|
||||
const bool capsuleAllowsSupport=passageTileScopeMatches
|
||||
const double capsuleClearanceMetres=bestSupportUsesSignedDistance?0.045:0.05;
|
||||
const double capsuleQueryZ=bestSupportUsesSignedDistance?bestSupportFloorQ:qz;
|
||||
const bool supportCapsuleProfileAvailable=passageIndex<PassageNearestNodes.size()
|
||||
&&!PassageNearestNodes[passageIndex].empty();
|
||||
const bool capsuleAllowsSupport=(passageTileScopeMatches
|
||||
||(bestSupportUsesSignedDistance&&supportCapsuleProfileAvailable))
|
||||
?!PassagePlayerCapsuleBlocksSupport(passageIndex,carveBegin,carveEnd,
|
||||
lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,qz,qPerMetre)
|
||||
:bestPlayerCapsuleSignedDistance>=0.05*qPerMetre;
|
||||
lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,
|
||||
capsuleQueryZ,qPerMetre,capsuleClearanceMetres)
|
||||
:bestPlayerCapsuleSignedDistance>=capsuleClearanceMetres*qPerMetre;
|
||||
// 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.
|
||||
if(hasSupportFloor&&(density<0.0f||passageColumnFloorSupport||shortSpanFloorSupport)
|
||||
&&capsuleAllowsSupport)
|
||||
&&(capsuleAllowsSupport||bestSupportDensity>=0.0))
|
||||
{
|
||||
composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
||||
}
|
||||
}
|
||||
return composed;
|
||||
}
|
||||
@@ -2990,7 +3077,8 @@ private:
|
||||
}
|
||||
|
||||
bool PassagePlayerCapsuleBlocksSupportNode(std::size_t passageIndex,int nodeIndex,
|
||||
std::size_t first,std::size_t end,double qx,double qy,double qz,double qPerMetre) const
|
||||
std::size_t first,std::size_t end,double qx,double qy,double qz,double qPerMetre,
|
||||
double minimumClearanceMetres) const
|
||||
{
|
||||
if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return false;
|
||||
const auto& nodes=PassageNearestNodes[passageIndex];
|
||||
@@ -3026,26 +3114,26 @@ 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<0.05*qPerMetre)return true;
|
||||
if(distance<minimumClearanceMetres*qPerMetre)return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return PassagePlayerCapsuleBlocksSupportNode(passageIndex,node.left,first,end,
|
||||
qx,qy,qz,qPerMetre)
|
||||
qx,qy,qz,qPerMetre,minimumClearanceMetres)
|
||||
||PassagePlayerCapsuleBlocksSupportNode(passageIndex,node.right,first,end,
|
||||
qx,qy,qz,qPerMetre);
|
||||
qx,qy,qz,qPerMetre,minimumClearanceMetres);
|
||||
}
|
||||
|
||||
bool PassagePlayerCapsuleBlocksSupport(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,
|
||||
double qx,double qy,double qz,double qPerMetre) const
|
||||
double qx,double qy,double qz,double qPerMetre,double minimumClearanceMetres) const
|
||||
{
|
||||
const auto blocked=[&](std::ptrdiff_t first,std::ptrdiff_t last)
|
||||
{
|
||||
return first<=last&&PassagePlayerCapsuleBlocksSupportNode(passageIndex,0,
|
||||
static_cast<std::size_t>(first),static_cast<std::size_t>(last+1),
|
||||
qx,qy,qz,qPerMetre);
|
||||
qx,qy,qz,qPerMetre,minimumClearanceMetres);
|
||||
};
|
||||
return blocked(carveBegin,carveEnd)
|
||||
||blocked(0,std::min<std::ptrdiff_t>(8,lowerMouthIndex)-1)
|
||||
@@ -4429,7 +4517,159 @@ private:
|
||||
const VoxelForgeCore::FloatingIslandFieldV1 Field(
|
||||
Plan->manifest.recipe,Plan->manifest.plan);
|
||||
if(!Field.isValid())return false;
|
||||
std::int64_t Density=Field.densitySubvoxels(ToFixed(X),ToFixed(Y),ToFixed(Z));
|
||||
const std::int64_t qx=ToFixed(X),qy=ToFixed(Y),qz=ToFixed(Z);
|
||||
std::int64_t Density=Field.densitySubvoxels(qx,qy,qz);
|
||||
// The anchor-root field keeps broad lobes, but its shortest authored noise
|
||||
// wavelength is below one metre. Filter that detail only around a FloatingIslands
|
||||
// passage mouth so Marching Cubes does not expose voxel-sized chips on the crater
|
||||
// wall. The broad root profile is unchanged outside this local mouth envelope.
|
||||
double mouthFilterWeight=0.0;
|
||||
const double qToMetres=static_cast<double>(ResolvedLayout->coreVoxelSizeMicrometres)
|
||||
/(static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)*1000000.0);
|
||||
const double densityMetres=std::abs(static_cast<double>(Density))*qToMetres;
|
||||
if(densityMetres<4.0)
|
||||
{
|
||||
bool foundPassageMouth=false;
|
||||
for(const auto& passage:ResolvedLayout->passages)
|
||||
{
|
||||
if(passage.upperStrateIndex!=StrateIndex)continue;
|
||||
foundPassageMouth=true;
|
||||
const double dx=(static_cast<double>(qx)-passage.upperMouthSubvoxels[0])*qToMetres;
|
||||
const double dy=(static_cast<double>(qy)-passage.upperMouthSubvoxels[1])*qToMetres;
|
||||
const double dz=(static_cast<double>(qz)-passage.upperMouthSubvoxels[2])*qToMetres;
|
||||
const double distance=std::sqrt(dx*dx+dy*dy+dz*dz);
|
||||
constexpr double FilterCoreRadiusMetres=8.5;
|
||||
constexpr double FilterOuterRadiusMetres=12.5;
|
||||
const double fade=std::clamp((FilterOuterRadiusMetres-distance)
|
||||
/(FilterOuterRadiusMetres-FilterCoreRadiusMetres),0.0,1.0);
|
||||
const double weight=fade*fade*(3.0-2.0*fade);
|
||||
mouthFilterWeight=std::max(mouthFilterWeight,weight);
|
||||
}
|
||||
if(!foundPassageMouth)
|
||||
{
|
||||
// The passage-free native control retains the stratum route anchors.
|
||||
// Use its terminal route point as the same local mouth filter origin so
|
||||
// both native and composed fields receive the same root smoothing.
|
||||
const auto stratum=std::find_if(ResolvedLayout->strates.begin(),
|
||||
ResolvedLayout->strates.end(),[StrateIndex](const auto& value)
|
||||
{return value.strateIndex==StrateIndex;});
|
||||
if(stratum!=ResolvedLayout->strates.end())
|
||||
{
|
||||
const VoxelForgeCore::ResolvedLayoutAnchorV2* terminal=nullptr;
|
||||
for(const auto& anchor:stratum->anchors)
|
||||
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
||||
terminal=&anchor;
|
||||
if(terminal)
|
||||
{
|
||||
const double dx=static_cast<double>(qx-terminal->values[0])*qToMetres;
|
||||
const double dy=static_cast<double>(qy-terminal->values[1])*qToMetres;
|
||||
const double dz=static_cast<double>(qz-terminal->values[2])*qToMetres;
|
||||
const double distance=std::sqrt(dx*dx+dy*dy+dz*dz);
|
||||
constexpr double FilterCoreRadiusMetres=8.5;
|
||||
constexpr double FilterOuterRadiusMetres=12.5;
|
||||
const double fade=std::clamp((FilterOuterRadiusMetres-distance)
|
||||
/(FilterOuterRadiusMetres-FilterCoreRadiusMetres),0.0,1.0);
|
||||
mouthFilterWeight=fade*fade*(3.0-2.0*fade);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(mouthFilterWeight>0.0)
|
||||
{
|
||||
// Preserve the resolved walk route while filtering the passage and crater field.
|
||||
const double queryX=static_cast<double>(qx)*qToMetres;
|
||||
const double queryY=static_cast<double>(qy)*qToMetres;
|
||||
const double queryZ=static_cast<double>(qz)*qToMetres;
|
||||
const auto stratum=std::find_if(ResolvedLayout->strates.begin(),
|
||||
ResolvedLayout->strates.end(),[StrateIndex](const auto& value)
|
||||
{return value.strateIndex==StrateIndex;});
|
||||
if(stratum!=ResolvedLayout->strates.end())
|
||||
{
|
||||
const VoxelForgeCore::ResolvedLayoutAnchorV2* previous=nullptr;
|
||||
for(const auto& anchor:stratum->anchors)
|
||||
{
|
||||
if(anchor.kind!="route_point"||anchor.values.size()<3)continue;
|
||||
if(previous)
|
||||
{
|
||||
const double ax=static_cast<double>(previous->values[0])*qToMetres;
|
||||
const double ay=static_cast<double>(previous->values[1])*qToMetres;
|
||||
const double az=static_cast<double>(previous->values[2])*qToMetres;
|
||||
const double dx=(static_cast<double>(anchor.values[0])
|
||||
-previous->values[0])*qToMetres;
|
||||
const double dy=(static_cast<double>(anchor.values[1])
|
||||
-previous->values[1])*qToMetres;
|
||||
const double lengthSquared=dx*dx+dy*dy;
|
||||
const double t=lengthSquared>0.0?std::clamp(
|
||||
((queryX-ax)*dx+(queryY-ay)*dy)/lengthSquared,0.0,1.0):0.0;
|
||||
const double centerX=ax+t*dx,centerY=ay+t*dy;
|
||||
const double centerZ=az+t*(static_cast<double>(anchor.values[2])
|
||||
-previous->values[2])*qToMetres;
|
||||
const double supportRadius=previous->values.size()>=4
|
||||
?static_cast<double>(previous->values[3])*qToMetres:0.75;
|
||||
const double floorZ=centerZ-1.20;
|
||||
const double protectedRadius=supportRadius+0.75;
|
||||
if((queryX-centerX)*(queryX-centerX)
|
||||
+(queryY-centerY)*(queryY-centerY)
|
||||
<protectedRadius*protectedRadius
|
||||
&&std::abs(queryZ-floorZ)<1.25)
|
||||
{
|
||||
mouthFilterWeight=0.0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
previous=&anchor;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(mouthFilterWeight>0.0)
|
||||
{
|
||||
constexpr std::int64_t FilterStep=8*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
std::int64_t weightedSum=0;
|
||||
for(int dz=-1;dz<=1;++dz)
|
||||
for(int dy=-1;dy<=1;++dy)
|
||||
for(int dx=-1;dx<=1;++dx)
|
||||
{
|
||||
const std::int64_t weight=(dx==0?2:1)*(dy==0?2:1)*(dz==0?2:1);
|
||||
const std::int64_t sample=(dx==0&&dy==0&&dz==0)?Density:
|
||||
Field.densitySubvoxels(qx+dx*FilterStep,qy+dy*FilterStep,
|
||||
qz+dz*FilterStep);
|
||||
weightedSum+=sample*weight;
|
||||
}
|
||||
// A separable [1,2,1]^3 kernel over a 2 m footprint filters sub-voxel
|
||||
// root detail while retaining the signed field on both sides of the surface.
|
||||
const double filtered=static_cast<double>(weightedSum)/64.0;
|
||||
const std::int64_t filteredDensity=static_cast<std::int64_t>(std::llround(
|
||||
std::lerp(static_cast<double>(Density),filtered,mouthFilterWeight)));
|
||||
Density=filteredDensity;
|
||||
// Marching Cubes can retain an isolated solid with no two-metre axis
|
||||
// thickness as a handful of disconnected triangles. Remove only shallow
|
||||
// negative samples surrounded by air at a one-metre probe distance; a
|
||||
// walk floor or wall has solid material on at least one side of this test.
|
||||
constexpr double FragmentProbeMetres=1.0;
|
||||
const std::int64_t fragmentStep=static_cast<std::int64_t>(std::llround(
|
||||
FragmentProbeMetres/qToMetres));
|
||||
if(mouthFilterWeight>=0.25&&Density<0
|
||||
&&static_cast<double>(-Density)*qToMetres<FragmentProbeMetres)
|
||||
{
|
||||
bool surroundedByAir=true;
|
||||
std::int64_t minimumAirDensity=std::numeric_limits<std::int64_t>::max();
|
||||
for(std::size_t axis=0;axis<3&&surroundedByAir;++axis)
|
||||
for(const int sign:{-1,1})
|
||||
{
|
||||
std::array<std::int64_t,3> sample{qx,qy,qz};
|
||||
sample[axis]+=sign*fragmentStep;
|
||||
const std::int64_t neighbor=Field.densitySubvoxels(
|
||||
sample[0],sample[1],sample[2]);
|
||||
if(neighbor<=0)
|
||||
{
|
||||
surroundedByAir=false;
|
||||
break;
|
||||
}
|
||||
minimumAirDensity=std::min(minimumAirDensity,neighbor);
|
||||
}
|
||||
if(surroundedByAir)Density=std::max(Density,minimumAirDensity);
|
||||
}
|
||||
}
|
||||
const std::int64_t Bottom=Plan->manifest.recipe.layoutMinimumZSubvoxels;
|
||||
const std::int64_t Top=Plan->manifest.recipe.layoutMaximumZSubvoxels;
|
||||
const std::int64_t Seal=Plan->manifest.recipe.boundarySealSubvoxels;
|
||||
|
||||
Reference in new issue
Block a user