wip(assembler): round L35 -- smooth passage walls without regular ribs (evaluator v21)
The shared passage carve took the minimum of piecewise-linear capsules (scallops at joins) and added wall relief on regular 8 m / 4 m octaves, which read as evenly spaced ribs. Interior sweep joins are now Catmull-Rom smoothed and the relief uses irregular rotated 43/29/19 m scales; the natural wobble remains. New axial ripple audit (0.125 m radius samples, 0.5-8 m periods): beaded control and L34 geometry fail, all 24 passage rows pass. FI mouth tread apron tapered. 30/30 determinism, 16/16 invariants, ring audits LOD0-8 pass; 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
49e29fcd36
commit
ba017077d1
28 files changed
+1727
-17
No files matched your search
@@ -251,7 +251,7 @@ namespace VoxelForgeCore
|
||||
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2;
|
||||
// Version 14 adds shared passage columns; version 16 smooths owner Gradient faces;
|
||||
// version 20 restores full Gradient amplitude and uses a continuous signed-distance band union.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 20;
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 21;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
|
||||
@@ -965,7 +965,32 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
const double ledge=std::min<double>(ringRadius/4,
|
||||
static_cast<double>(cfg?std::max<std::int64_t>(0,
|
||||
cfg->cascadeLedgeSubvoxels):0));
|
||||
radius-=ledge*(0.5-0.5*std::cos(angle*steps));
|
||||
// Give each turn a seeded, shallow angular profile so the same cosine
|
||||
// scallop cannot repeat at full ledge depth on every revolution.
|
||||
const std::uint32_t controlCount=std::clamp(steps*2u,8u,
|
||||
segments/2u);
|
||||
const double coordinate=angle*controlCount
|
||||
/(2.0*3.14159265358979323846);
|
||||
const std::uint32_t control0=static_cast<std::uint32_t>(std::floor(coordinate));
|
||||
const std::uint32_t control1=(control0+1u)%controlCount;
|
||||
const double blend=coordinate-std::floor(coordinate);
|
||||
const auto controlValue=[&](std::uint32_t control)
|
||||
{
|
||||
const std::uint32_t salt=0x43415343u
|
||||
^(loopIndex*0x9e3779b9u)^(control*0x85ebca6bu);
|
||||
const std::uint32_t bits=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||
upper.strateIndex,lower.strateIndex,connection,salt)>>40);
|
||||
return 2.0*static_cast<double>(bits)/16777215.0-1.0;
|
||||
};
|
||||
const double smooth=blend*blend*(3.0-2.0*blend);
|
||||
const double irregular=std::lerp(controlValue(control0),
|
||||
controlValue(control1),smooth);
|
||||
const std::uint32_t loopHash=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||
upper.strateIndex,lower.strateIndex,connection,
|
||||
0x4c454447u+loopIndex)>>40);
|
||||
const double amplitude=0.60+0.40
|
||||
*static_cast<double>(loopHash)/16777215.0;
|
||||
radius-=ledge*amplitude*(0.24+0.10*irregular);
|
||||
}
|
||||
if(pathStyle!=1u&&anchoredIslandPassage&&passageAnchor)
|
||||
{
|
||||
|
||||
@@ -911,7 +911,34 @@ public:
|
||||
DeterministicSqrt(playerDx*playerDx+playerDy*playerDy
|
||||
+playerVertical*playerVertical)-0.34*qPerMetre);
|
||||
}
|
||||
const double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz;
|
||||
double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz;
|
||||
const bool smoothCarveJoin=index>2&&index+12<lastIndex
|
||||
&&DeterministicSqrt(lengthSq)<=8.0*qPerMetre
|
||||
&&!IsPassageShortSpanSegment(passageIndex,segmentIndex-1)
|
||||
&&!IsPassageShortSpanSegment(passageIndex,segmentIndex)
|
||||
&&!IsPassageShortSpanSegment(passageIndex,segmentIndex+1);
|
||||
if(smoothCarveJoin)
|
||||
{
|
||||
const auto& p0=passage.controlPoints[static_cast<std::size_t>(index-1)];
|
||||
const auto& p3=passage.controlPoints[static_cast<std::size_t>(index+2)];
|
||||
constexpr double SmoothJoinWeight=1.0;
|
||||
const double t2=t*t,t3=t2*t;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double v0=static_cast<double>(p0.positionSubvoxels[axis]);
|
||||
const double v1=static_cast<double>(a.positionSubvoxels[axis]);
|
||||
const double v2=static_cast<double>(b.positionSubvoxels[axis]);
|
||||
const double v3=static_cast<double>(p3.positionSubvoxels[axis]);
|
||||
const double curve=0.5*((2.0*v1)+(-v0+v2)*t
|
||||
+(2.0*v0-5.0*v1+4.0*v2-v3)*t2
|
||||
+(-v0+3.0*v1-3.0*v2+v3)*t3);
|
||||
const double line=axis==0?cx:axis==1?cy:cz;
|
||||
const double blended=line+(curve-line)*SmoothJoinWeight;
|
||||
if(axis==0)cx=blended;
|
||||
else if(axis==1)cy=blended;
|
||||
else cz=blended;
|
||||
}
|
||||
}
|
||||
const double ex=qx-cx,ey=qy-cy,ez=qz-cz;
|
||||
const double radius=static_cast<double>(a.radiusSubvoxels)
|
||||
+t*(b.radiusSubvoxels-a.radiusSubvoxels);
|
||||
@@ -963,11 +990,11 @@ 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_FieldSurfaceRelief(static_cast<float>(X),static_cast<float>(Y),
|
||||
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 roughSignedDistance=bestSignedDistance
|
||||
+static_cast<double>(std::clamp(relief,-1.0f,1.0f))*0.24*qPerMetre*wallReliefWeight;
|
||||
+static_cast<double>(std::clamp(relief,-1.0f,1.0f))*0.21*qPerMetre*wallReliefWeight;
|
||||
if(IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(bestIndex)))
|
||||
{
|
||||
const double bridgeDistance=PassageBridgeSignedDistance(passageIndex,
|
||||
@@ -986,6 +1013,7 @@ public:
|
||||
const double dy=static_cast<double>(segmentB.positionSubvoxels[1])-segmentA.positionSubvoxels[1];
|
||||
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,
|
||||
@@ -1000,10 +1028,33 @@ public:
|
||||
:bestT;
|
||||
const double centerX=segmentA.positionSubvoxels[0]+supportT*dx;
|
||||
const double centerY=segmentA.positionSubvoxels[1]+supportT*dy;
|
||||
const double centerZ=segmentA.positionSubvoxels[2]+supportT*(
|
||||
double centerZ=segmentA.positionSubvoxels[2]+supportT*(
|
||||
static_cast<double>(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]);
|
||||
const double radius=segmentA.radiusSubvoxels+supportT*(
|
||||
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)];
|
||||
const auto& p3=passage.controlPoints[static_cast<std::size_t>(index+2)];
|
||||
const double t2=supportT*supportT,t3=t2*supportT;
|
||||
const auto interpolate=[&](double v0,double v1,double v2,double v3)
|
||||
{
|
||||
return 0.5*((2.0*v1)+(-v0+v2)*supportT
|
||||
+(2.0*v0-5.0*v1+4.0*v2-v3)*t2
|
||||
+(-v0+3.0*v1-3.0*v2+v3)*t3);
|
||||
};
|
||||
constexpr double SmoothSupportHeightWeight=0.25;
|
||||
centerZ+=(interpolate(p0.positionSubvoxels[2],segmentA.positionSubvoxels[2],
|
||||
segmentB.positionSubvoxels[2],p3.positionSubvoxels[2])-centerZ)
|
||||
*SmoothSupportHeightWeight;
|
||||
radius+=(interpolate(p0.radiusSubvoxels,segmentA.radiusSubvoxels,
|
||||
segmentB.radiusSubvoxels,p3.radiusSubvoxels)-radius)
|
||||
*SmoothSupportHeightWeight;
|
||||
}
|
||||
const double horizontalX=qx-centerX,horizontalY=qy-centerY;
|
||||
const double horizontalDistance=DeterministicSqrt(horizontalX*horizontalX
|
||||
+horizontalY*horizontalY);
|
||||
@@ -1016,16 +1067,42 @@ public:
|
||||
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 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;
|
||||
const double supportHalfWidth=std::min(radius,
|
||||
(lowerMouthBodyOverlap?0.90:1.25)*qPerMetre);
|
||||
if(Z<floorVoxels-supportThicknessVoxels||Z>floorVoxels+1.0/Q)return;
|
||||
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 supportEdgeBlendMetres=std::min(
|
||||
entryMouthShoulder?0.85:0.35,
|
||||
std::max(0.0,supportHalfWidth/qPerMetre-supportEdgeCoreMetres));
|
||||
const double supportEdgeT=supportEdgeBlendMetres>1.0e-6
|
||||
?std::clamp((horizontalDistance
|
||||
-(supportHalfWidth-supportEdgeBlendMetres*qPerMetre))
|
||||
/(supportEdgeBlendMetres*qPerMetre),0.0,1.0):0.0;
|
||||
const double supportEdgeSmooth=supportEdgeT*supportEdgeT
|
||||
*(3.0-2.0*supportEdgeT);
|
||||
const double localSupportThicknessVoxels=entryMouthShoulder
|
||||
?supportThicknessVoxels*(1.0-0.72*supportEdgeSmooth)
|
||||
:supportThicknessVoxels;
|
||||
const double supportEdgeDepth=entryMouthShoulder
|
||||
?0.35+0.15*supportEdgeNoise: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;
|
||||
if(horizontalDistance>supportHalfWidth)return;
|
||||
const double supportDistanceSq=horizontalDistance*horizontalDistance
|
||||
+(Z-floorVoxels)*(Z-floorVoxels);
|
||||
@@ -1034,7 +1111,7 @@ public:
|
||||
hasSupportFloor=true;
|
||||
bestSupportSegmentIndex=index;
|
||||
bestSupportDensity=std::clamp(Z-floorVoxels,
|
||||
-supportThicknessVoxels,0.0);
|
||||
-localSupportThicknessVoxels,0.0);
|
||||
};
|
||||
std::ptrdiff_t supportCenterIndex=bestIndex;
|
||||
if(bestIndex<lowerMouthIndex)
|
||||
@@ -4541,9 +4618,10 @@ private:
|
||||
const float StrataRelief=1.35f*(Relief+0.5f*OwnerRelief);
|
||||
const float CaveRelief=1.00f*Relief;
|
||||
const float FloorRelief=0.55f*Relief;
|
||||
// Passage walls keep the broad coherent relief bands but omit the shortest
|
||||
// octave, which makes repeated rings on the authored shafts.
|
||||
const float PassageRelief=0.75f*PassageBandRelief;
|
||||
// The shared passage sweep supplies its own irregular relief. Keep only a
|
||||
// small trace of the terrain value-noise bands so their cell edges cannot
|
||||
// dominate the passage wall and read as repeated rings.
|
||||
const float PassageRelief=0.10f*PassageBandRelief;
|
||||
return StrataWeight*StrataRelief+CaveWeight*CaveRelief
|
||||
+FloorWeight*FloorRelief+Passage*PassageRelief;
|
||||
}
|
||||
|
||||
@@ -331,6 +331,28 @@ VF_FIELD_INLINE float VF_FieldPerlin3D(float x,float y,float z)
|
||||
return VF_FieldPerlinLerp(y0,y1,sw);
|
||||
}
|
||||
|
||||
VF_FIELD_INLINE float VF_FieldPassageWallRelief(float x,float y,float z,VF_FieldU32 seed)
|
||||
{
|
||||
// Use long, non-commensurate rotated scales. The shared passage wall keeps broad,
|
||||
// uneven rock relief without a short wavelength that can read as repeated rings.
|
||||
const float broad=VF_FieldValueNoise(x+17.0f,y-23.0f,z+11.0f,43,
|
||||
seed^0xB5297A4Du);
|
||||
const float mid=VF_FieldValueNoise(y-7.0f,z+19.0f,x-31.0f,29,
|
||||
seed^0x63D83595u);
|
||||
const float fine=VF_FieldValueNoise(z+5.0f,x+29.0f,y-13.0f,19,
|
||||
seed^0xA511E9B3u);
|
||||
return 0.50f*broad+0.35f*mid+0.15f*fine;
|
||||
}
|
||||
|
||||
VF_FIELD_INLINE float VF_FieldPassageGroundGrain(float x,float y,VF_FieldU32 seed)
|
||||
{
|
||||
const float broad=VF_FieldValueNoise(x+3.25f,y-7.5f,0.0f,8,
|
||||
seed^0xC2B2AE35u);
|
||||
const float fine=VF_FieldValueNoise(y+11.75f,x-5.0f,0.0f,5,
|
||||
seed^0x27D4EB2Fu);
|
||||
return 0.65f*broad+0.35f*fine;
|
||||
}
|
||||
|
||||
VF_FIELD_INLINE float VF_FieldOwnerFractal(float x,float y,float z,VF_FieldI32 octaves,
|
||||
VF_FieldI32 mode)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user