wip(assembler): round L37 -- FI mouth carve-fade fix, no raised bridge (style 0); research R1
The passage mouth fade multiplied the signed carve distance by zero, which turned native rock into an open surface (the reason earlier rounds stamped walk pads at mouths); it now blends between the uncarved field and the full signed carve. The FI style-0 route followed a lifted grade envelope over the crater (the "bridge"); it now runs on the rim and follows the native surface. New built-shape and crease audits with thresholds frozen before the fix (L36 and box controls fail). 24/24 passage rows, 30/30 determinism, 16/16 invariants, 18/18 interface/ring. Unreal: one recipe setter (uncompiled). Research R1 (read-only, web): ranked proposals for natural walkable mouths, floor-limited carve first. 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
8be1c4d9ef
commit
3e443bf81c
33 files changed
+2767
-86
No files matched your search
@@ -94,6 +94,9 @@ public:
|
||||
}
|
||||
bool hasMouthSupport(std::size_t passageIndex) const
|
||||
{return passageIndex<passageMouthSupports_.size()&&passageMouthSupports_[passageIndex]!=0;}
|
||||
bool hasAnyMouthSupport() const
|
||||
{return std::any_of(passageMouthSupports_.begin(),passageMouthSupports_.end(),
|
||||
[](std::uint8_t value){return value!=0;});}
|
||||
bool hasRouteFeatureOverlap() const{return routeFeatureOverlap_;}
|
||||
bool hasColumnZOverlap() const{return columnZOverlap_;}
|
||||
bool mayContainResolvedBoundaryBand() const{return mayContainResolvedBoundaryBand_;}
|
||||
@@ -683,23 +686,36 @@ public:
|
||||
{
|
||||
if(!ResolvedLayout||ResolvedLayout->passages.empty()||!std::isfinite(X)
|
||||
||!std::isfinite(Y)||!std::isfinite(Z))return density;
|
||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
const double qPerMetre=Q*1000000.0/
|
||||
std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres);
|
||||
const double qx=static_cast<double>(X)*Q,qy=static_cast<double>(Y)*Q,
|
||||
qz=static_cast<double>(Z)*Q;
|
||||
const auto* passageTileQueries=ActivePassageTileQueries;
|
||||
const bool passageTileScopeMatches=passageTileQueries
|
||||
&&passageTileQueries->matches(*this);
|
||||
bool queryNearUpperMouthSupport=false;
|
||||
for(const auto& passage:ResolvedLayout->passages)
|
||||
{
|
||||
const double dx=qx-static_cast<double>(passage.upperMouthSubvoxels[0]);
|
||||
const double dy=qy-static_cast<double>(passage.upperMouthSubvoxels[1]);
|
||||
const double dz=qz-static_cast<double>(passage.upperMouthSubvoxels[2]);
|
||||
const double reach=15.0*qPerMetre;
|
||||
if(dx*dx+dy*dy<=reach*reach&&std::abs(dz)<=8.0*qPerMetre)
|
||||
{queryNearUpperMouthSupport=true;break;}
|
||||
}
|
||||
if(passageTileScopeMatches&&density>=0.0f
|
||||
&&!passageTileQueries->hasRouteFeatureOverlap()
|
||||
&&!passageTileQueries->hasColumnZOverlap())return density;
|
||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
const double qx=static_cast<double>(X)*Q,qy=static_cast<double>(Y)*Q,qz=static_cast<double>(Z)*Q;
|
||||
const double qPerMetre=Q*1000000.0/std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres);
|
||||
&&!passageTileQueries->hasColumnZOverlap()
|
||||
&&!passageTileQueries->hasAnyMouthSupport()
|
||||
&&!queryNearUpperMouthSupport)return density;
|
||||
const double floorThicknessVoxels=qPerMetre/Q;
|
||||
const double playerCenterAboveFloorQ=1.13*qPerMetre;
|
||||
float composed=density;
|
||||
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.
|
||||
// Surface-route treads may reinforce native solid terrain only.
|
||||
if(!(density<0.0f))return;
|
||||
const double mouthDx=qx-static_cast<double>(mouth[0]);
|
||||
const double mouthDy=qy-static_cast<double>(mouth[1]);
|
||||
@@ -727,7 +743,7 @@ public:
|
||||
}
|
||||
if(routeCount<2)return;
|
||||
const std::size_t lastSegment=routeCount-1;
|
||||
constexpr double MouthSupportBlendMetres=1.5;
|
||||
constexpr double MouthSupportBlendMetres=0.45;
|
||||
const double mouthBlendWidth=MouthSupportBlendMetres*qPerMetre;
|
||||
double mouthSupportDistance=std::numeric_limits<double>::infinity();
|
||||
for(std::size_t index=0;index<lastSegment;++index)
|
||||
@@ -742,11 +758,13 @@ public:
|
||||
const double centerZ=a.values[2]+t*(static_cast<double>(b.values[2])-a.values[2]);
|
||||
const double routeRadius=a.values.size()>=4
|
||||
?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 halfWidth=std::clamp(routeRadius,
|
||||
0.42*qPerMetre,0.56*qPerMetre);
|
||||
const double horizontalX=qx-centerX,horizontalY=qy-centerY;
|
||||
const double horizontalDistance=DeterministicSqrt(
|
||||
horizontalX*horizontalX+horizontalY*horizontalY);
|
||||
const double floorQ=centerZ-1.20*qPerMetre;
|
||||
const double floorQ=centerZ-(VoxelForgeCore::WorldAssemblyEvaluatorVersion>=184
|
||||
?playerCenterAboveFloorQ+0.25*qPerMetre:1.20*qPerMetre);
|
||||
const double sideDistance=horizontalDistance-halfWidth;
|
||||
const double topDistance=qz-floorQ;
|
||||
const double bottomDistance=floorQ-qPerMetre-qz;
|
||||
@@ -756,25 +774,14 @@ public:
|
||||
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);
|
||||
mouthSupportDistance=std::min(mouthSupportDistance,outside+inside);
|
||||
}
|
||||
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
|
||||
// the interior ramp.
|
||||
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
||||
{
|
||||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||
const bool tileHasSupport=passageTileScopeMatches;
|
||||
if(!tileHasSupport||ActivePassageTileQueries->hasMouthSupport(passageIndex))
|
||||
composeRouteMouthSupport(passage.upperStrateIndex,
|
||||
passage.upperMouthSubvoxels,true);
|
||||
}
|
||||
// The connected passage tread continues the surface route into the tunnel.
|
||||
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
||||
{
|
||||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||
@@ -789,6 +796,8 @@ public:
|
||||
};
|
||||
const auto* supportUpper=supportStratum(passage.upperStrateIndex);
|
||||
const auto* supportLower=supportStratum(passage.lowerStrateIndex);
|
||||
const bool floatingIslandPassage=supportUpper
|
||||
&&supportUpper->generatorKind=="FloatingIslands";
|
||||
const bool surfaceToTunnelSupport=supportUpper&&supportLower
|
||||
&&supportUpper->generatorKind=="SurfaceWorld"
|
||||
&&supportLower->generatorKind=="TunnelNetwork";
|
||||
@@ -907,8 +916,11 @@ public:
|
||||
std::ptrdiff_t bestScanIndex=std::numeric_limits<std::ptrdiff_t>::max();
|
||||
std::ptrdiff_t bestSupportSegmentIndex=0;
|
||||
bool hasSupportFloor=false;
|
||||
bool bestSupportIsMouthTread=false;
|
||||
double bestSupportMouthBlend=1.0;
|
||||
double bestSupportDistanceSquared=std::numeric_limits<double>::infinity();
|
||||
double bestSupportDensity=std::numeric_limits<double>::infinity();
|
||||
double bestSupportNativeBlend=0.0;
|
||||
double bestSupportFloorQ=0.0;
|
||||
bool bestSupportUsesSignedDistance=false;
|
||||
std::ptrdiff_t selectedSupportSegmentIndex=-1;
|
||||
@@ -1032,18 +1044,175 @@ 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 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);
|
||||
const float mouthTerrainRelief=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);
|
||||
const float mouthTreadFieldRelief=VF_FieldSurfaceRelief(static_cast<float>(X)*0.75f,
|
||||
static_cast<float>(Y)*0.75f,static_cast<float>(Z)*0.75f,
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x54485244u);
|
||||
const auto routeSegmentLengthMetres=[&](std::ptrdiff_t routeIndex)
|
||||
{
|
||||
const auto& before=passage.controlPoints[static_cast<std::size_t>(routeIndex)];
|
||||
const auto& after=passage.controlPoints[static_cast<std::size_t>(routeIndex+1)];
|
||||
const double dx=static_cast<double>(after.positionSubvoxels[0])
|
||||
-before.positionSubvoxels[0];
|
||||
const double dy=static_cast<double>(after.positionSubvoxels[1])
|
||||
-before.positionSubvoxels[1];
|
||||
const double dz=static_cast<double>(after.positionSubvoxels[2])
|
||||
-before.positionSubvoxels[2];
|
||||
return DeterministicSqrt(dx*dx+dy*dy+dz*dz)/qPerMetre;
|
||||
};
|
||||
double distanceFromOpenMouth=std::numeric_limits<double>::infinity();
|
||||
if(bestIndex<=2)
|
||||
{
|
||||
distanceFromOpenMouth=bestT*routeSegmentLengthMetres(bestIndex);
|
||||
for(std::ptrdiff_t routeIndex=0;routeIndex<bestIndex;++routeIndex)
|
||||
distanceFromOpenMouth+=routeSegmentLengthMetres(routeIndex);
|
||||
}
|
||||
else if(bestIndex>=lowerMouthIndex-2&&bestIndex<=lowerMouthIndex+2)
|
||||
{
|
||||
if(bestIndex<lowerMouthIndex)
|
||||
{
|
||||
distanceFromOpenMouth=(1.0-bestT)*routeSegmentLengthMetres(bestIndex);
|
||||
for(std::ptrdiff_t routeIndex=bestIndex+1;
|
||||
routeIndex<lowerMouthIndex;++routeIndex)
|
||||
distanceFromOpenMouth+=routeSegmentLengthMetres(routeIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
distanceFromOpenMouth=bestT*routeSegmentLengthMetres(bestIndex);
|
||||
for(std::ptrdiff_t routeIndex=lowerMouthIndex;routeIndex<bestIndex;++routeIndex)
|
||||
distanceFromOpenMouth+=routeSegmentLengthMetres(routeIndex);
|
||||
}
|
||||
}
|
||||
double mouthEntryFloorAlignment=0.0;
|
||||
double mouthEntryPlanDistance=std::numeric_limits<double>::infinity();
|
||||
double mouthEntryFloorQ=0.0;
|
||||
bool upperMouthFloorFade=false;
|
||||
{
|
||||
// The large mouth bore overlaps several inner coils. Its global
|
||||
// nearest SDF can belong to a later turn even at the lip, so use the
|
||||
// projected entrance path to recover the local floor orientation.
|
||||
double prefixMetres=0.0;
|
||||
double nearestEntryPlanDistance=std::numeric_limits<double>::infinity();
|
||||
double projectedEntryDistance=std::numeric_limits<double>::infinity();
|
||||
for(std::ptrdiff_t routeIndex=0;
|
||||
routeIndex<std::min<std::ptrdiff_t>(3,lowerMouthIndex);++routeIndex)
|
||||
{
|
||||
const auto& before=passage.controlPoints[static_cast<std::size_t>(routeIndex)];
|
||||
const auto& after=passage.controlPoints[static_cast<std::size_t>(routeIndex+1)];
|
||||
const double ax=static_cast<double>(before.positionSubvoxels[0]);
|
||||
const double ay=static_cast<double>(before.positionSubvoxels[1]);
|
||||
const double az=static_cast<double>(before.positionSubvoxels[2]);
|
||||
const double dx=static_cast<double>(after.positionSubvoxels[0])-ax;
|
||||
const double dy=static_cast<double>(after.positionSubvoxels[1])-ay;
|
||||
const double dz=static_cast<double>(after.positionSubvoxels[2])-az;
|
||||
const double horizontalLengthSq=dx*dx+dy*dy;
|
||||
const double t=horizontalLengthSq>0.0?std::clamp(
|
||||
((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):0.0;
|
||||
double projectedX=ax+t*dx,projectedY=ay+t*dy;
|
||||
const double horizontalLength=DeterministicSqrt(horizontalLengthSq);
|
||||
if(horizontalLength>1.0e-6)
|
||||
{
|
||||
const double pathRelief=std::clamp(static_cast<double>(
|
||||
VF_FieldSurfaceRelief(static_cast<float>(projectedX/qPerMetre*1.5),
|
||||
static_cast<float>(projectedY/qPerMetre*1.5),
|
||||
static_cast<float>((az+t*dz)/qPerMetre*1.5),
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x4D4F5554u)),
|
||||
-1.0,1.0);
|
||||
const double lateralOffset=pathRelief*0.18*qPerMetre;
|
||||
projectedX-=dy/horizontalLength*lateralOffset;
|
||||
projectedY+=dx/horizontalLength*lateralOffset;
|
||||
}
|
||||
const double planDx=qx-projectedX,planDy=qy-projectedY;
|
||||
const double planDistance=DeterministicSqrt(planDx*planDx+planDy*planDy);
|
||||
if(planDistance<nearestEntryPlanDistance)
|
||||
{
|
||||
nearestEntryPlanDistance=planDistance;
|
||||
const double centerZ=az+t*dz;
|
||||
const double radius=before.radiusSubvoxels+t*(
|
||||
static_cast<double>(after.radiusSubvoxels)-before.radiusSubvoxels);
|
||||
mouthEntryFloorQ=centerZ-radius+0.20*qPerMetre;
|
||||
const double centerDistance=DeterministicSqrt(planDx*planDx
|
||||
+planDy*planDy+(qz-centerZ)*(qz-centerZ));
|
||||
mouthEntryFloorAlignment=centerDistance>1.0e-9
|
||||
?std::clamp(-(qz-centerZ)/centerDistance,0.0,1.0):0.0;
|
||||
projectedEntryDistance=prefixMetres
|
||||
+t*routeSegmentLengthMetres(routeIndex);
|
||||
}
|
||||
prefixMetres+=routeSegmentLengthMetres(routeIndex);
|
||||
}
|
||||
if(nearestEntryPlanDistance<=2.0*qPerMetre
|
||||
&&projectedEntryDistance<=8.0
|
||||
&&std::abs(qz-mouthEntryFloorQ)<=4.0*qPerMetre)
|
||||
{
|
||||
distanceFromOpenMouth=projectedEntryDistance;
|
||||
mouthEntryPlanDistance=nearestEntryPlanDistance/qPerMetre;
|
||||
upperMouthFloorFade=true;
|
||||
}
|
||||
}
|
||||
double mouthFloorBlend=0.0;
|
||||
if(std::isfinite(distanceFromOpenMouth))
|
||||
{
|
||||
const double fade=std::clamp(distanceFromOpenMouth/11.0,0.0,1.0);
|
||||
const double smooth=fade*fade*(3.0-2.0*fade);
|
||||
mouthFloorBlend=1.0-smooth;
|
||||
}
|
||||
const double floorReliefT=std::clamp((verticalWallAlignment-0.55)/0.45,0.0,1.0);
|
||||
const double floorReliefWeight=floorReliefT*floorReliefT
|
||||
*(3.0-2.0*floorReliefT);
|
||||
const double floorReliefOffset=std::clamp(
|
||||
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 roughSignedDistance=bestSignedDistance
|
||||
+static_cast<double>(std::clamp(relief,-1.0f,1.0f))*0.21*qPerMetre*wallReliefWeight;
|
||||
+static_cast<double>(std::clamp(relief,-1.0f,1.0f))*0.21*qPerMetre*wallReliefWeight
|
||||
+floorReliefOffset;
|
||||
if(IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(bestIndex)))
|
||||
{
|
||||
const double bridgeDistance=PassageBridgeSignedDistance(passageIndex,
|
||||
static_cast<std::size_t>(bestIndex),X,Y,Z);
|
||||
composed=std::min(composed,static_cast<float>(bridgeDistance/Q));
|
||||
}
|
||||
// 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));
|
||||
// Leave native crater ground at the lip. The bore opens through the walls
|
||||
// immediately, while its floor cut eases in along the passage centerline.
|
||||
double mouthCarveFade=1.0;
|
||||
if(upperMouthFloorFade)
|
||||
{
|
||||
constexpr double mouthCarveFadeMetres=8.0;
|
||||
mouthCarveFade=std::clamp(distanceFromOpenMouth/mouthCarveFadeMetres,
|
||||
0.0,1.0);
|
||||
mouthCarveFade=mouthCarveFade*mouthCarveFade*(3.0-2.0*mouthCarveFade);
|
||||
}
|
||||
const double floorAlignment=std::max(verticalWallAlignment,
|
||||
mouthEntryFloorAlignment)*std::max(verticalWallAlignment,
|
||||
mouthEntryFloorAlignment);
|
||||
double floorBelowWeight=0.0;
|
||||
if(upperMouthFloorFade)
|
||||
{
|
||||
const double belowFloor=(mouthEntryFloorQ-qz)/qPerMetre;
|
||||
const double supportRamp=std::clamp(belowFloor/0.15,0.0,1.0);
|
||||
const double supportFade=std::clamp((1.0-belowFloor)/0.35,0.0,1.0);
|
||||
const double rampSmooth=supportRamp*supportRamp*(3.0-2.0*supportRamp);
|
||||
const double fadeSmooth=supportFade*supportFade*(3.0-2.0*supportFade);
|
||||
floorBelowWeight=rampSmooth*fadeSmooth;
|
||||
}
|
||||
// Preserve native terrain only along a capsule-scale worn lane. The
|
||||
// former bore-wide alignment feather left a straight platform-width
|
||||
// bridge across the FI mouth.
|
||||
double mouthWalkLaneT=std::clamp((1.05-mouthEntryPlanDistance)/0.60,0.0,1.0);
|
||||
mouthWalkLaneT=mouthWalkLaneT*mouthWalkLaneT*(3.0-2.0*mouthWalkLaneT);
|
||||
const double mouthWalkLaneWeight=upperMouthFloorFade?mouthWalkLaneT:0.0;
|
||||
const double floorCarveWeight=1.0-(1.0-mouthCarveFade)
|
||||
*floorAlignment*floorAlignment*floorBelowWeight*mouthWalkLaneWeight;
|
||||
const float fullyCarvedDensity=std::max(composed,
|
||||
static_cast<float>(-roughSignedDistance/Q));
|
||||
composed=std::lerp(composed,fullyCarvedDensity,
|
||||
static_cast<float>(floorCarveWeight));
|
||||
const auto considerSupportSegment=[&](std::ptrdiff_t index)
|
||||
{
|
||||
const auto& segmentA=passage.controlPoints[static_cast<std::size_t>(index)];
|
||||
@@ -1080,8 +1249,8 @@ public:
|
||||
?std::min(0.5,2.0*qPerMetre/DeterministicSqrt(horizontalLengthSq)):0.0;
|
||||
const double supportT=horizontalLengthSq>0.0
|
||||
?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 centerX=segmentA.positionSubvoxels[0]+supportT*dx;
|
||||
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*(
|
||||
@@ -1105,6 +1274,25 @@ public:
|
||||
segmentB.radiusSubvoxels,p3.radiusSubvoxels)-radius)
|
||||
*SmoothSupportHeightWeight;
|
||||
}
|
||||
double treadRelief=0.0;
|
||||
if(entryMouthShoulder
|
||||
||(index>=lowerMouthIndex-2&&index<=lowerMouthIndex+2))
|
||||
{
|
||||
const double horizontalLength=DeterministicSqrt(horizontalLengthSq);
|
||||
if(horizontalLength>1.0e-6)
|
||||
{
|
||||
const double pathRelief=std::clamp(static_cast<double>(
|
||||
VF_FieldSurfaceRelief(static_cast<float>(centerX/qPerMetre*1.5),
|
||||
static_cast<float>(centerY/qPerMetre*1.5),
|
||||
static_cast<float>(centerZ/qPerMetre*1.5),
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x4D4F5554u)),
|
||||
-1.0,1.0);
|
||||
treadRelief=pathRelief;
|
||||
const double lateralOffset=pathRelief*0.18*qPerMetre;
|
||||
centerX-=dy/horizontalLength*lateralOffset;
|
||||
centerY+=dx/horizontalLength*lateralOffset;
|
||||
}
|
||||
}
|
||||
const double horizontalX=qx-centerX,horizontalY=qy-centerY;
|
||||
const double horizontalDistance=DeterministicSqrt(horizontalX*horizontalX
|
||||
+horizontalY*horizontalY);
|
||||
@@ -1117,7 +1305,9 @@ public:
|
||||
// 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 sparseRouteThreshold=VoxelForgeCore::WorldAssemblyEvaluatorVersion>=188
|
||||
&&floatingIslandPassage&&passage.style==0?1.0:1.5;
|
||||
const bool sparseRouteSupport=supportSegmentLength>sparseRouteThreshold;
|
||||
const double minimumSupportQueryDepth=index==selectedSupportSegmentIndex
|
||||
&&sparseRouteSupport?0.65:0.93;
|
||||
if(supportQueryDepth<minimumSupportQueryDepth*qPerMetre
|
||||
@@ -1130,15 +1320,75 @@ public:
|
||||
// capsule on the neighbouring turn.
|
||||
const bool lowerMouthBodyOverlap=index>=lowerMouthIndex-2
|
||||
&&supportQueryDepth<0.98*qPerMetre;
|
||||
const double supportHalfWidthMetres=lowerMouthBodyOverlap?0.90
|
||||
:entryMouthShoulder?1.20:1.25;
|
||||
const double supportHalfWidth=std::min(radius,
|
||||
// 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.
|
||||
const bool mouthTread=entryMouthShoulder
|
||||
&&(!floatingIslandPassage||passage.style==0);
|
||||
double distanceFromMouthMetres=std::numeric_limits<double>::infinity();
|
||||
if(entryMouthShoulder)
|
||||
{
|
||||
double prefixMetres=0.0;
|
||||
for(std::ptrdiff_t routeIndex=0;routeIndex<index;++routeIndex)
|
||||
{
|
||||
const auto& before=passage.controlPoints[
|
||||
static_cast<std::size_t>(routeIndex)];
|
||||
const auto& after=passage.controlPoints[
|
||||
static_cast<std::size_t>(routeIndex+1)];
|
||||
const double dx=static_cast<double>(after.positionSubvoxels[0])
|
||||
-before.positionSubvoxels[0];
|
||||
const double dy=static_cast<double>(after.positionSubvoxels[1])
|
||||
-before.positionSubvoxels[1];
|
||||
const double dz=static_cast<double>(after.positionSubvoxels[2])
|
||||
-before.positionSubvoxels[2];
|
||||
prefixMetres+=DeterministicSqrt(dx*dx+dy*dy+dz*dz)/qPerMetre;
|
||||
}
|
||||
distanceFromMouthMetres=std::max(0.0,
|
||||
prefixMetres+supportT*supportSegmentLength);
|
||||
}
|
||||
else if(lowerMouthBodyOverlap)
|
||||
{
|
||||
double suffixMetres=0.0;
|
||||
for(std::ptrdiff_t routeIndex=index+1;
|
||||
routeIndex<lowerMouthIndex;++routeIndex)
|
||||
{
|
||||
const auto& before=passage.controlPoints[
|
||||
static_cast<std::size_t>(routeIndex)];
|
||||
const auto& after=passage.controlPoints[
|
||||
static_cast<std::size_t>(routeIndex+1)];
|
||||
const double dx=static_cast<double>(after.positionSubvoxels[0])
|
||||
-before.positionSubvoxels[0];
|
||||
const double dy=static_cast<double>(after.positionSubvoxels[1])
|
||||
-before.positionSubvoxels[1];
|
||||
const double dz=static_cast<double>(after.positionSubvoxels[2])
|
||||
-before.positionSubvoxels[2];
|
||||
suffixMetres+=DeterministicSqrt(dx*dx+dy*dy+dz*dz)/qPerMetre;
|
||||
}
|
||||
distanceFromMouthMetres=std::max(0.0,
|
||||
suffixMetres+(1.0-supportT)*supportSegmentLength);
|
||||
}
|
||||
double mouthReliefT=std::clamp(distanceFromMouthMetres/8.0,0.0,1.0);
|
||||
mouthReliefT=mouthReliefT*mouthReliefT*(3.0-2.0*mouthReliefT);
|
||||
double mouthSupportT=std::clamp((distanceFromMouthMetres-0.25)/4.0,0.0,1.0);
|
||||
mouthSupportT=mouthSupportT*mouthSupportT*(3.0-2.0*mouthSupportT);
|
||||
const double mouthSupportWidth=std::lerp(0.82,1.00,mouthSupportT);
|
||||
const double supportHalfWidthMetres=mouthTread?mouthSupportWidth
|
||||
:lowerMouthBodyOverlap?0.90:1.25;
|
||||
double supportHalfWidth=std::min(radius,
|
||||
supportHalfWidthMetres*qPerMetre);
|
||||
// Keep the walk core within a 0.50 m span with shallow relief, then
|
||||
// taper the outer shoulder; capsule and probe widths remain supported.
|
||||
const double supportEdgeCoreMetres=entryMouthShoulder?0.60:0.90;
|
||||
const double shoulderWarp=mouthTread
|
||||
?std::clamp(static_cast<double>(mouthSurfaceRelief),-1.0,1.0)
|
||||
*0.12*qPerMetre:0.0;
|
||||
supportHalfWidth=mouthTread
|
||||
?std::clamp(supportHalfWidth+shoulderWarp,
|
||||
std::lerp(0.52,0.62,mouthSupportT)*qPerMetre,
|
||||
std::lerp(0.92,1.10,mouthSupportT)*qPerMetre)
|
||||
:std::max(0.0,supportHalfWidth+shoulderWarp);
|
||||
// Keep the walk core within the player footprint, then ease the outer
|
||||
// edge into native ground over a long enough shoulder to avoid a berm.
|
||||
const double supportEdgeCoreMetres=mouthTread?0.08
|
||||
:entryMouthShoulder?0.60:0.90;
|
||||
const double supportEdgeBlendMetres=std::min(
|
||||
entryMouthShoulder?0.85:0.35,
|
||||
mouthTread?1.40:entryMouthShoulder?0.85:0.35,
|
||||
std::max(0.0,supportHalfWidth/qPerMetre-supportEdgeCoreMetres));
|
||||
const double supportEdgeT=supportEdgeBlendMetres>1.0e-6
|
||||
?std::clamp((horizontalDistance
|
||||
@@ -1146,25 +1396,57 @@ public:
|
||||
/(supportEdgeBlendMetres*qPerMetre),0.0,1.0):0.0;
|
||||
const double supportEdgeSmooth=supportEdgeT*supportEdgeT
|
||||
*(3.0-2.0*supportEdgeT);
|
||||
const double mouthSupportThicknessVoxels=std::lerp(
|
||||
0.35*supportThicknessVoxels,supportThicknessVoxels,mouthSupportT);
|
||||
const double localSupportThicknessVoxels=entryMouthShoulder
|
||||
?supportThicknessVoxels*(1.0-0.72*supportEdgeSmooth)
|
||||
?mouthSupportThicknessVoxels*(1.0-0.72*supportEdgeSmooth)
|
||||
:supportThicknessVoxels;
|
||||
constexpr double supportEdgeDepth=0.35;
|
||||
const double floorQ=centerZ-radius+floorOffset*qPerMetre
|
||||
double mouthSupportBlend=1.0;
|
||||
if(mouthTread&&std::isfinite(distanceFromMouthMetres))
|
||||
{
|
||||
const double mouthBlendT=std::clamp(
|
||||
distanceFromMouthMetres/0.75,0.0,1.0);
|
||||
mouthSupportBlend=0.75+0.25*mouthBlendT*mouthBlendT
|
||||
*(3.0-2.0*mouthBlendT);
|
||||
}
|
||||
const double supportEdgeDepth=mouthTread?0.25:0.35;
|
||||
const double mouthFloorOffset=mouthTread
|
||||
?std::lerp(0.15,0.19,lowerMouthBlend):floorOffset;
|
||||
const double baseFloorQ=centerZ-radius+mouthFloorOffset*qPerMetre
|
||||
-supportEdgeSmooth*supportEdgeDepth*qPerMetre;
|
||||
const double nativeSurfaceQ=static_cast<double>(qz)
|
||||
-static_cast<double>(density)*Q;
|
||||
// Keep support near the route floor; visible mouth preservation below
|
||||
// blends the source field directly instead of extending this SDF top.
|
||||
const double nativeFieldOffset=std::clamp(nativeSurfaceQ-baseFloorQ,
|
||||
-0.80*qPerMetre,0.80*qPerMetre);
|
||||
const double nativeSurfaceBlend=mouthTread?1.0:1.0-mouthReliefT;
|
||||
const double nativeTreadOffset=std::clamp(
|
||||
nativeFieldOffset*nativeSurfaceBlend+floorReliefOffset
|
||||
-std::abs(treadRelief)*0.08*qPerMetre
|
||||
*(1.0-0.25*mouthReliefT),
|
||||
-0.80*qPerMetre,0.80*qPerMetre);
|
||||
const double floorQ=baseFloorQ
|
||||
+(mouthTread?std::clamp(nativeTreadOffset
|
||||
+std::clamp(static_cast<double>(mouthTreadFieldRelief),-1.0,1.0)
|
||||
*0.40*qPerMetre,-0.80*qPerMetre,0.0):0.0);
|
||||
const double floorVoxels=floorQ/Q;
|
||||
const double supportPositiveRangeVoxels=sparseRouteSupport
|
||||
const bool signedDistanceSupport=sparseRouteSupport||mouthTread;
|
||||
const double supportPositiveRangeVoxels=signedDistanceSupport
|
||||
?0.35*qPerMetre/Q:1.0/Q;
|
||||
if(Z<floorVoxels-localSupportThicknessVoxels
|
||||
||Z>floorVoxels+supportPositiveRangeVoxels)return;
|
||||
if(horizontalDistance>supportHalfWidth)return;
|
||||
const double supportQueryHalfWidth=supportHalfWidth
|
||||
+(mouthTread?0.45*qPerMetre:0.0);
|
||||
if(horizontalDistance>supportQueryHalfWidth)return;
|
||||
const double supportDistanceSquared=horizontalDistance*horizontalDistance
|
||||
+(Z-floorVoxels)*(Z-floorVoxels);
|
||||
if(supportDistanceSquared>=bestSupportDistanceSquared)return;
|
||||
bestSupportDistanceSquared=supportDistanceSquared;
|
||||
hasSupportFloor=true;
|
||||
bestSupportSegmentIndex=index;
|
||||
if(sparseRouteSupport)
|
||||
bestSupportIsMouthTread=mouthTread;
|
||||
if(signedDistanceSupport)
|
||||
{
|
||||
// Sparse support profiles need both sides of a signed distance
|
||||
// so the sampled zero crossing stays on the tread top.
|
||||
@@ -1172,11 +1454,10 @@ public:
|
||||
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;
|
||||
bestSupportDensity=SmoothMaxDistance(
|
||||
static_cast<float>(sideDistance/Q),
|
||||
static_cast<float>(verticalDistance/Q),
|
||||
entryMouthShoulder?0.80f:0.20f);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1184,7 +1465,10 @@ public:
|
||||
-localSupportThicknessVoxels,0.0);
|
||||
}
|
||||
bestSupportFloorQ=floorQ;
|
||||
bestSupportUsesSignedDistance=sparseRouteSupport;
|
||||
bestSupportUsesSignedDistance=signedDistanceSupport;
|
||||
bestSupportMouthBlend=mouthSupportBlend;
|
||||
bestSupportNativeBlend=mouthTread
|
||||
?0.85*mouthSupportBlend*(1.0-supportEdgeSmooth):0.0;
|
||||
};
|
||||
std::ptrdiff_t supportCenterIndex=bestIndex;
|
||||
if(bestIndex<lowerMouthIndex)
|
||||
@@ -1231,12 +1515,46 @@ public:
|
||||
// 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)
|
||||
if(hasSupportFloor&&(density<0.0f||passageColumnFloorSupport
|
||||
||shortSpanFloorSupport)
|
||||
&&(capsuleAllowsSupport||bestSupportDensity>=0.0))
|
||||
{
|
||||
composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
||||
if(bestSupportIsMouthTread)
|
||||
{
|
||||
// Preserve the owner's native ground through the shallow mouth
|
||||
// carve. Blending back the native field keeps its crater curvature
|
||||
// and noise. The capsule test keeps neighbouring coils open.
|
||||
const bool nativeRestoreCapsuleClear=density<0.0f
|
||||
&&bestSupportNativeBlend>0.0&&capsuleAllowsSupport
|
||||
&&!PassagePlayerCapsuleBlocksSupport(passageIndex,carveBegin,
|
||||
carveEnd,lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,
|
||||
lastIndex,qx,qy,qz,qPerMetre,capsuleClearanceMetres);
|
||||
if(nativeRestoreCapsuleClear)
|
||||
{
|
||||
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);
|
||||
composed=std::lerp(composed,nativeGroundWithRelief,
|
||||
static_cast<float>(bestSupportNativeBlend));
|
||||
}
|
||||
}
|
||||
else composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
||||
}
|
||||
}
|
||||
// Restore the surface route where the passage cut crossed otherwise solid
|
||||
// native ground. The support stays native-solid-only, so it cannot float.
|
||||
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
||||
{
|
||||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||
if(passage.controlPoints.size()<2)continue;
|
||||
const bool tileHasSupport=passageTileScopeMatches;
|
||||
if(!tileHasSupport
|
||||
||ActivePassageTileQueries->hasMouthSupport(passageIndex)))
|
||||
composeRouteMouthSupport(passage.upperStrateIndex,
|
||||
passage.upperMouthSubvoxels,true);
|
||||
}
|
||||
return composed;
|
||||
}
|
||||
|
||||
@@ -4519,6 +4837,7 @@ private:
|
||||
if(!Field.isValid())return false;
|
||||
const std::int64_t qx=ToFixed(X),qy=ToFixed(Y),qz=ToFixed(Z);
|
||||
std::int64_t Density=Field.densitySubvoxels(qx,qy,qz);
|
||||
const std::int64_t NativeDensity=Density;
|
||||
// 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
|
||||
@@ -4529,11 +4848,11 @@ private:
|
||||
const double densityMetres=std::abs(static_cast<double>(Density))*qToMetres;
|
||||
if(densityMetres<4.0)
|
||||
{
|
||||
bool foundPassageMouth=false;
|
||||
bool hasResolvedPassageMouth=false;
|
||||
for(const auto& passage:ResolvedLayout->passages)
|
||||
{
|
||||
if(passage.upperStrateIndex!=StrateIndex)continue;
|
||||
foundPassageMouth=true;
|
||||
hasResolvedPassageMouth=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;
|
||||
@@ -4545,7 +4864,7 @@ private:
|
||||
const double weight=fade*fade*(3.0-2.0*fade);
|
||||
mouthFilterWeight=std::max(mouthFilterWeight,weight);
|
||||
}
|
||||
if(!foundPassageMouth)
|
||||
if(!hasResolvedPassageMouth)
|
||||
{
|
||||
// The passage-free native control retains the stratum route anchors.
|
||||
// Use its terminal route point as the same local mouth filter origin so
|
||||
@@ -4574,12 +4893,20 @@ private:
|
||||
}
|
||||
}
|
||||
}
|
||||
if(mouthFilterWeight>0.0)
|
||||
double routeNativeBlend=0.0;
|
||||
const auto mouthPassage=std::find_if(ResolvedLayout->passages.begin(),
|
||||
ResolvedLayout->passages.end(),[StrateIndex](const auto& passage)
|
||||
{return passage.upperStrateIndex==StrateIndex;});
|
||||
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 bool outsideCraterMouth=mouthPassage==ResolvedLayout->passages.end()
|
||||
||std::hypot(queryX-mouthPassage->upperMouthSubvoxels[0]*qToMetres,
|
||||
queryY-mouthPassage->upperMouthSubvoxels[1]*qToMetres)>5.5;
|
||||
if(mouthFilterWeight>0.0&&outsideCraterMouth)
|
||||
{
|
||||
// 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;
|
||||
// Retain the native route only on the outer approach. The crater-mouth
|
||||
// interior stays with the filtered ground field and cannot become a bridge.
|
||||
const auto stratum=std::find_if(ResolvedLayout->strates.begin(),
|
||||
ResolvedLayout->strates.end(),[StrateIndex](const auto& value)
|
||||
{return value.strateIndex==StrateIndex;});
|
||||
@@ -4607,15 +4934,20 @@ private:
|
||||
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;
|
||||
}
|
||||
constexpr double WalkCoreRadiusMetres=0.45;
|
||||
constexpr double WearShoulderMetres=0.80;
|
||||
const double protectedRadius=std::min(WalkCoreRadiusMetres,
|
||||
supportRadius+0.10);
|
||||
const double routeDistance=std::hypot(queryX-centerX,queryY-centerY);
|
||||
const double radialT=std::clamp(
|
||||
(protectedRadius+WearShoulderMetres-routeDistance)
|
||||
/WearShoulderMetres,0.0,1.0);
|
||||
const double verticalT=std::clamp(
|
||||
(3.25-std::abs(queryZ-floorZ))/2.0,0.0,1.0);
|
||||
const double radialSmooth=radialT*radialT*(3.0-2.0*radialT);
|
||||
const double verticalSmooth=verticalT*verticalT*(3.0-2.0*verticalT);
|
||||
routeNativeBlend=std::max(routeNativeBlend,
|
||||
radialSmooth*verticalSmooth);
|
||||
}
|
||||
previous=&anchor;
|
||||
}
|
||||
@@ -4638,9 +4970,19 @@ private:
|
||||
// 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::int64_t filteredDensity=static_cast<std::int64_t>(std::llround(
|
||||
std::lerp(static_cast<double>(Density),filtered,mouthFilterWeight)));
|
||||
// Filtering detail at an open mouth may soften the surface, but it cannot
|
||||
// turn native air into a new solid shelf over the crater.
|
||||
if(NativeDensity>=0&&filteredDensity<0)filteredDensity=0;
|
||||
Density=filteredDensity;
|
||||
if(routeNativeBlend>0.0&&NativeDensity<0)
|
||||
{
|
||||
const double routePreserved=std::lerp(static_cast<double>(Density),
|
||||
static_cast<double>(std::min(Density,NativeDensity)),
|
||||
routeNativeBlend*0.60);
|
||||
Density=static_cast<std::int64_t>(std::llround(routePreserved));
|
||||
}
|
||||
// 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
|
||||
|
||||
Reference in new issue
Block a user