From 5ec09d1ff1705dbdebf9aa90b99172f199d4ea14 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Thu, 29 Oct 2026 18:09:46 +0100 Subject: [PATCH] passages: dense centripetal spline + open-air half-pipe attempt (L111, failed audits) Route resampled to ~1 m (310 -> 2,790 segments); open air writes a 0.75 m floor slab plus 1.0 m side walls, no roof. The 3D spline overshoots between planned route points, raising floor grades above 15 deg; a grade-limited variant left 2 unsupported samples on passage 0-1, and the shell/ ripple audits still model the old chord route. Stopped after two designs. Not merged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1 --- .../include/VoxelForgeCore/VoxelForgeCore.h | 5 +- Standalone/Core/src/ShapeWorldInternal.h | 371 ++++++--- .../WorldAssemblerM3/Linux111/REPORT.md | 42 + Standalone/Source/StreamBenchmarkPipeline.h | 762 ++++++++++-------- Standalone/Tools/verify/verify.py | 8 +- 5 files changed, 749 insertions(+), 439 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Linux111/REPORT.md diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index e546f79..d39d106 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -468,8 +468,9 @@ namespace VoxelForgeCore // Version 210 keeps bulk native-cave sampling on the scalar field path at strata edges. // Version 211 makes coordinate biasing in the shared cell hash unsigned, removing signed-overflow UB. // Version 212 balances generated entry routes against existing walk lanes and preserves their shared treads. - // Version 217 replaces passage supports with flat-floor hollow-shell geometry. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 217; + // Version 218 resamples passages on a smooth spline and uses open-air half-pipe shells. + // Version 218 sweeps passages around a dense smooth spline and limits exposed shells to a trough. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 218; enum class FieldChannel : std::uint32_t { diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index f6c049d..a40e2b5 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -1196,6 +1196,7 @@ public: constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); constexpr double FloorShellThicknessMetres=0.75; constexpr double WallReliefMetres=0.21; + constexpr double OpenAirSideWallHeightMetres=1.0; constexpr double MouthShellBlendMetres=5.0; const double qPerMetre=Q*1000000.0/ std::max(1,ResolvedLayout->coreVoxelSizeMicrometres); @@ -1264,7 +1265,7 @@ public: } const std::ptrdiff_t lastIndex=static_cast( - passage.controlPoints.size())-1; + PassageSegmentData[passageIndex].size())-1; double bestDistance=std::numeric_limits::infinity(); const auto visitSegment=[&](std::size_t segmentIndex,int) { @@ -1355,11 +1356,14 @@ public: const double floorBlend=std::clamp(floorDistanceMetres/0.25,0.0,1.0); const double smoothFloorBlend=floorBlend*floorBlend *(3.0-2.0*floorBlend); - const double mouthWallThickness=segmentIndex - =0.0 + &&wallElevationMetres<=OpenAirSideWallHeightMetres ?smoothShellFade*shellThickness:0.0; double floorExtensionFade=1.0; - if(segmentIndex>=constants.routeEndControlIndex) + if(!segment.routeSegment) { const double pastMouthMetres=std::max(0.0,alongMetres -constants.totalPathLengthMetres); @@ -1417,11 +1421,14 @@ public: const double floorBlend=std::clamp(floorDistanceMetres/0.25,0.0,1.0); const double smoothFloorBlend=floorBlend*floorBlend *(3.0-2.0*floorBlend); - const double mouthWallThickness=nearest.segmentIndex - =0.0 + &&wallElevationMetres<=OpenAirSideWallHeightMetres ?smoothShellFade*shellThickness:0.0; double floorExtensionFade=1.0; - if(nearest.segmentIndex>=constants.routeEndControlIndex) + if(!segment.routeSegment) { const double pastMouthMetres=std::max(0.0,alongMetres -constants.totalPathLengthMetres); @@ -1446,15 +1453,11 @@ public: // solid only in air, so it leaves host rock unchanged. Taking the union // before writing either polarity makes every tunnel interior beat every // overlapping tunnel shell. - // Positive native graph distance is authored tunnel interior. Blend it - // into the same air union before any passage shell can change its sign. - // This also preserves the cave interior across a shared-layer boundary - // blend, which may be solid on the passage's side of that interface. - if(nativeTunnelFieldDensity>=0.0f) + // Preserve native graph air inside the passage interior before any shell + // can change its sign. Outside that interior, the shell may cover host air. + if(nativeTunnelFieldDensity>=0.0f&&unionDistance<=0.0) { - const float passageAir=unionDistance<=0.0 - ?static_cast(-unionDistance/Q) - :std::numeric_limits::lowest(); + const float passageAir=static_cast(-unionDistance/Q); return std::max(nativeTunnelFieldDensity,passageAir); } if(nativeFloorSupport!=std::numeric_limits::lowest() @@ -3377,7 +3380,38 @@ private: double radiusA=0.0,radiusB=0.0,radiusDelta=0.0; double lengthSq=0.0,horizontalLengthSq=0.0; double lengthSubvoxels=0.0,horizontalLength=0.0; + bool routeSegment=false; }; + static double PassageCentripetalKnotSpan( + const std::array& a,const std::array& b) + { + const double dx=b[0]-a[0],dy=b[1]-a[1],dz=b[2]-a[2]; + return std::max(1.0e-6,DeterministicSqrt(DeterministicSqrt( + dx*dx+dy*dy+dz*dz))); + } + static std::array PassageCentripetalCatmullRom( + const std::array& p0,const std::array& p1, + const std::array& p2,const std::array& p3, + double t,double dt0,double dt1,double dt2) + { + std::array result{}; + const double t2=t*t,t3=t2*t; + for(std::size_t axis=0;axis<4;++axis) + { + const double m1=dt1*((p1[axis]-p0[axis])/dt0 + -(p2[axis]-p0[axis])/(dt0+dt1) + +(p2[axis]-p1[axis])/dt1); + const double m2=dt1*((p2[axis]-p1[axis])/dt1 + -(p3[axis]-p1[axis])/(dt1+dt2) + +(p3[axis]-p2[axis])/dt2); + result[axis]=(2.0*t3-3.0*t2+1.0)*p1[axis] + +(-2.0*t3+3.0*t2)*p2[axis] + +(t3-2.0*t2+t)*m1+(t3-t2)*m2; + } + result[3]=std::clamp(result[3],std::min(p1[3],p2[3]), + std::max(p1[3],p2[3])); + return result; + } static double PassageFlatFloorDepthSubvoxels( const FPassageComposerConstants& constants,std::size_t segmentIndex,double t, double radiusSubvoxels,double qPerMetre) @@ -3411,7 +3445,7 @@ private: const FPassageComposerConstants& constants,std::size_t segmentIndex,double floorT, const FPassageSegmentData& segment,double floorDepthSubvoxels,double qPerMetre) { - if(segmentIndex>=constants.routeEndControlIndex||qPerMetre<=0.0) + if(!segment.routeSegment||qPerMetre<=0.0) return segment.a[2]+floorT*segment.delta[2]; const double radiusMetres=(segment.radiusA+floorT*segment.radiusDelta)/qPerMetre; const double alongMetres=segment.authoredAlongMetres @@ -4222,13 +4256,11 @@ private: int BuildPassageNearestNode(std::size_t passageIndex,std::vector segments) { - const auto& points=ResolvedLayout->passages[passageIndex].controlPoints; FPassageNearestNode node; node.minimum.fill(std::numeric_limits::infinity()); node.maximum.fill(-std::numeric_limits::infinity()); for(const std::size_t segment:segments) { - const auto& a=points[segment];const auto& b=points[segment+1]; const auto& prepared=PassageSegmentData[passageIndex][segment]; for(std::size_t axis=0;axis<3;++axis) { @@ -4238,7 +4270,7 @@ private: prepared.a[axis],prepared.b[axis]}); } node.maximumRadius=std::max({node.maximumRadius, - static_cast(a.radiusSubvoxels),static_cast(b.radiusSubvoxels)}); + prepared.radiusA,prepared.radiusB}); } node.minimumSegment=*std::min_element(segments.begin(),segments.end()); node.maximumSegment=*std::max_element(segments.begin(),segments.end()); @@ -4258,8 +4290,8 @@ private: -PassageNearestNodes[passageIndex][nodeIndex].minimum[splitAxis])splitAxis=axis; const std::size_t middle=segments.size()/2; const auto center=[&](std::size_t segment) - {return static_cast(points[segment].positionSubvoxels[splitAxis]) - +static_cast(points[segment+1].positionSubvoxels[splitAxis]);}; + {const auto& prepared=PassageSegmentData[passageIndex][segment]; + return prepared.a[splitAxis]+prepared.b[splitAxis];}; std::nth_element(segments.begin(),segments.begin()+middle,segments.end(), [&](std::size_t a,std::size_t b) {const double ca=center(a),cb=center(b);return ca==cb?a( std::chrono::steady_clock::now()-preparationStart).count()); return; @@ -4508,7 +4540,9 @@ private: constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double qPerMetre=Q*1000000.0/ std::max(1,ResolvedLayout->coreVoxelSizeMicrometres); - std::size_t preparedSegments=0; + constexpr double PassageSegmentSpacingMetres=1.0; + constexpr std::size_t ArcLengthTableSteps=32; + std::size_t preparedSegments=0,controlSpans=0; for(std::size_t passageIndex=0;passageIndexpassages.size(); ++passageIndex) { @@ -4519,50 +4553,104 @@ private: PassageSegmentOffsets[passageIndex+1]=preparedSegments; continue; } - auto& segmentData=PassageSegmentData[passageIndex]; - segmentData.resize(passage.controlPoints.size()-1); const std::size_t routeEnd=std::clamp( passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1); - std::vector radiiMetres; - radiiMetres.reserve(routeEnd+1); - for(std::size_t pointIndex=0;pointIndex<=routeEnd;++pointIndex) - { - radiiMetres.push_back(static_cast( - passage.controlPoints[pointIndex].radiusSubvoxels)/qPerMetre); - } auto& constants=PassageComposerConstants[passageIndex]; constants.routeEndControlIndex=routeEnd; - std::vector authoredPathDistanceMetres( - passage.controlPoints.size(),0.0); - for(std::size_t pointIndex=1;pointIndex(b[0]-a[0])/qPerMetre; - const double dy=static_cast(b[1]-a[1])/qPerMetre; - const double dz=static_cast(b[2]-a[2])/qPerMetre; - authoredPathDistanceMetres[pointIndex]= - authoredPathDistanceMetres[pointIndex-1] - +DeterministicSqrt(dx*dx+dy*dy+dz*dz); + std::array a{},b{}; + double alongMetres=0.0,lengthMetres=0.0; + std::size_t span=0; + double tA=0.0,tB=1.0; + bool routeSegment=false; + }; + std::vector> controls; + controls.reserve(passage.controlPoints.size()); + for(const auto& point:passage.controlPoints) + controls.push_back({static_cast(point.positionSubvoxels[0]), + static_cast(point.positionSubvoxels[1]), + static_cast(point.positionSubvoxels[2]), + static_cast(point.radiusSubvoxels)}); + std::vector rawSegments; + rawSegments.reserve((passage.controlPoints.size()-1)*10); + std::vector> routeRadiusSamples; + routeRadiusSamples.push_back({0.0,controls.front()[3]/qPerMetre}); + double alongMetres=0.0,authoredTotalPathLengthMetres=0.0; + const auto curvePoint=[&](std::size_t span,double t) + { + const auto& p1=controls[span]; + const auto& p2=controls[span+1]; + const auto& p0=controls[span==0?0:span-1]; + const auto& p3=controls[std::min(span+2,controls.size()-1)]; + return PassageCentripetalCatmullRom(p0,p1,p2,p3,t, + PassageCentripetalKnotSpan(p0,p1), + PassageCentripetalKnotSpan(p1,p2), + PassageCentripetalKnotSpan(p2,p3)); + }; + for(std::size_t span=0;span+1 arcLength{}; + auto previous=curvePoint(span,0.0); + for(std::size_t step=1;step<=ArcLengthTableSteps;++step) + { + const auto current=curvePoint(span, + static_cast(step)/ArcLengthTableSteps); + const double dx=(current[0]-previous[0])/qPerMetre; + const double dy=(current[1]-previous[1])/qPerMetre; + const double dz=(current[2]-previous[2])/qPerMetre; + arcLength[step]=arcLength[step-1] + +DeterministicSqrt(dx*dx+dy*dy+dz*dz); + previous=current; + } + const double spanLength=arcLength.back(); + const std::size_t segmentCount=std::max(1, + static_cast(std::ceil( + spanLength/PassageSegmentSpacingMetres))); + auto a=curvePoint(span,0.0); + double aT=0.0; + for(std::size_t segment=1;segment<=segmentCount;++segment) + { + const double targetArc=spanLength*static_cast(segment) + /segmentCount; + const auto upper=std::lower_bound(arcLength.begin()+1, + arcLength.end(),targetArc); + const std::size_t upperIndex=static_cast( + upper-arcLength.begin()); + const std::size_t lowerIndex=upperIndex-1; + const double arcSpan=arcLength[upperIndex]-arcLength[lowerIndex]; + const double arcT=arcSpan>1.0e-12 + ?std::clamp((targetArc-arcLength[lowerIndex])/arcSpan,0.0,1.0):0.0; + const double t=(static_cast(lowerIndex)+arcT) + /ArcLengthTableSteps; + const auto b=curvePoint(span,t); + const double dx=(b[0]-a[0])/qPerMetre; + const double dy=(b[1]-a[1])/qPerMetre; + const double dz=(b[2]-a[2])/qPerMetre; + const double length=DeterministicSqrt(dx*dx+dy*dy+dz*dz); + rawSegments.push_back({a,b,alongMetres,length,span,aT,t,span::infinity(); double minimumRadius=std::numeric_limits::infinity(); - for(std::size_t pointIndex=0;pointIndex<=routeEnd;++pointIndex) + for(const auto& sample:routeRadiusSamples) { - const double radius=radiiMetres[pointIndex]; - minimumRadius=std::min(minimumRadius,radius); - const double along=authoredPathDistanceMetres[pointIndex]; - if(along>=10.0&&authoredTotalPathLengthMetres-along>=10.0) - minimumBodyRadius=std::min(minimumBodyRadius,radius); + minimumRadius=std::min(minimumRadius,sample[1]); + if(sample[0]>=10.0&&authoredTotalPathLengthMetres-sample[0]>=10.0) + minimumBodyRadius=std::min(minimumBodyRadius,sample[1]); } if(!std::isfinite(minimumBodyRadius))minimumBodyRadius=minimumRadius; - // One fixed flat-floor depth through the body. The authored grade - // constraint follows centreline-radius+0.23 m; shift the swept centreline - // where a taper changes radius so this floor keeps that same route. + // Keep one flat-floor depth through the body and ease it into both mouths. constants.floorDepthMetres=std::max(0.5,minimumBodyRadius-0.23); constants.upperMouthFloorDepthMetres=std::max(0.0, static_cast(passage.controlPoints.front().positionSubvoxels[2] @@ -4586,76 +4674,135 @@ private: constants.lowerMouthFloorDepthMetres=std::max(0.0, routeFloorMetres-nativeFloorMetres); } - const auto routeFloorDepth=[&](std::size_t pointIndex,double radius) + + struct FRouteFloorSample { - const double localRadiusDepth=std::max(0.0,radius-0.23); - const double along=authoredPathDistanceMetres[pointIndex]; - const double mouthDistance=std::max(0.0,std::min(along, - authoredTotalPathLengthMetres-along)); - const double linear=std::clamp(mouthDistance/5.0,0.0,1.0); - const double ramp=linear*linear*(3.0-2.0*linear); - const double target=along<=authoredTotalPathLengthMetres*0.5 - ?constants.upperMouthFloorDepthMetres - :constants.lowerMouthFloorDepthMetres; - const double mouthDepth=std::clamp(target,0.0,std::max(0.0,radius)); - return std::lerp(mouthDepth,localRadiusDepth,ramp); + std::array point{}; + double alongMetres=0.0,desiredFloorZMetres=0.0; }; - std::vector effectiveCenterZSubvoxels( - passage.controlPoints.size(),0.0); - for(std::size_t pointIndex=0;pointIndex routeFloorSamples; + routeFloorSamples.reserve(rawSegments.size()+1); + const auto appendRouteFloorSample=[&](const std::array& point, + double along) { - const double radius=pointIndex<=routeEnd - ?radiiMetres[pointIndex] - :static_cast(passage.controlPoints[pointIndex] - .radiusSubvoxels)/qPerMetre; - const double localTubeDepth=std::min(constants.floorDepthMetres, - std::max(0.0,radius-0.23)); - const double routeDepth=pointIndex<=routeEnd - ?routeFloorDepth(pointIndex,radius):std::max(0.0,radius-0.23); - effectiveCenterZSubvoxels[pointIndex]= - static_cast(passage.controlPoints[pointIndex].positionSubvoxels[2]) - +(-routeDepth+localTubeDepth)*qPerMetre; + const double radiusMetres=point[3]/qPerMetre; + const double routeDepth=PassageRouteFloorDepthMetres(constants,along, + radiusMetres); + routeFloorSamples.push_back({point,along,point[2]/qPerMetre-routeDepth}); + }; + for(const auto& raw:rawSegments) + { + if(!raw.routeSegment)break; + if(routeFloorSamples.empty())appendRouteFloorSample(raw.a,raw.alongMetres); + appendRouteFloorSample(raw.b,raw.alongMetres+raw.lengthMetres); } - for(std::size_t segmentIndex=0;segmentIndex1) { - const auto& a=passage.controlPoints[segmentIndex]; - const auto& b=passage.controlPoints[segmentIndex+1]; - FPassageSegmentData& data=segmentData[segmentIndex]; - data.authoredCenterZA=static_cast(a.positionSubvoxels[2]); - data.authoredCenterZDelta=static_cast( - b.positionSubvoxels[2]-a.positionSubvoxels[2]); - data.authoredAlongMetres=authoredPathDistanceMetres[segmentIndex]; - data.authoredLengthMetres=authoredPathDistanceMetres[segmentIndex+1] - -authoredPathDistanceMetres[segmentIndex]; + constexpr double MaximumWalkGradeTangent=0.2586407821; + std::vector horizontalGradeLimits(routeFloorSamples.size()-1); + std::vector remainingGradeAllowance(routeFloorSamples.size(),0.0); + for(std::size_t i=0;i+10;--i) + remainingGradeAllowance[i-1]=remainingGradeAllowance[i] + +horizontalGradeLimits[i-1]; + std::vector walkableFloor(routeFloorSamples.size()); + walkableFloor.front()=routeFloorSamples.front().desiredFloorZMetres; + walkableFloor.back()=routeFloorSamples.back().desiredFloorZMetres; + for(std::size_t i=1;i+1(a.positionSubvoxels[axis]); - data.b[axis]=static_cast(b.positionSubvoxels[axis]); + data.a[axis]=raw.a[axis]; + data.b[axis]=raw.b[axis]; if(axis==2) { - data.a[axis]=effectiveCenterZSubvoxels[segmentIndex]; - data.b[axis]=effectiveCenterZSubvoxels[segmentIndex+1]; + data.a[axis]=effectiveCenterZ(raw.a[2],raw.a[3],raw.alongMetres); + data.b[axis]=effectiveCenterZ(raw.b[2],raw.b[3],alongB); } data.delta[axis]=data.b[axis]-data.a[axis]; } - data.radiusA=static_cast(a.radiusSubvoxels); - data.radiusB=static_cast(b.radiusSubvoxels); - data.radiusDelta=static_cast(b.radiusSubvoxels)-a.radiusSubvoxels; const double dx=data.delta[0],dy=data.delta[1],dz=data.delta[2]; data.lengthSq=dx*dx+dy*dy+dz*dz; data.horizontalLengthSq=dx*dx+dy*dy; data.lengthSubvoxels=DeterministicSqrt(data.lengthSq); data.horizontalLength=DeterministicSqrt(data.horizontalLengthSq); + segmentData.push_back(data); ++preparedSegments; } constants.totalPathLengthMetres=authoredTotalPathLengthMetres; PassageSegmentOffsets[passageIndex+1]=preparedSegments; } if(tracePreparation) - std::printf("[L109PassagePrepare] passages=%zu segments=%zu bytes=%zu prepare_ms=%.3f\n", - PassageSegmentData.size(),preparedSegments, - preparedSegments*sizeof(FPassageSegmentData), + std::printf("[L111PassagePrepare] passages=%zu control_spans=%zu segments=%zu spacing_m=%.2f bytes=%zu prepare_ms=%.3f\n", + PassageSegmentData.size(),controlSpans,preparedSegments, + PassageSegmentSpacingMetres,preparedSegments*sizeof(FPassageSegmentData), std::chrono::duration( std::chrono::steady_clock::now()-preparationStart).count()); } @@ -4681,20 +4828,16 @@ private: for(std::size_t passageIndex=0;passageIndexpassages.size(); ++passageIndex) { - const auto& passage=ResolvedLayout->passages[passageIndex]; - const std::size_t segmentCount=passage.controlPoints.size()>1 - ?passage.controlPoints.size()-1:0; + const std::size_t segmentCount=PassageSegmentData[passageIndex].size(); if(segmentCount==0)continue; std::vector segmentIds(segmentCount); for(std::size_t i=0;i(a.radiusSubvoxels), - static_cast(b.radiusSubvoxels))/Q; + const double maximumRadius=std::max(prepared.radiusA, + prepared.radiusB)/Q; std::array routeMinimum{},routeMaximum{}; for(std::size_t axis=0;axis<3;++axis) { @@ -11286,6 +11429,7 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa { constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; constexpr double InfluenceMetres=4.0; + constexpr double OpenAirSideWallHeightMetres=1.0; if(!shape_||step<=0||cells<=0||!std::isfinite(passageCarveReachMetres_) ||std::abs(passageCarveReachMetres_-InfluenceMetres)>1.0e-9)return; @@ -11403,11 +11547,14 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa const double floorBlend=std::clamp(floorDistanceMetres/0.25,0.0,1.0); const double smoothFloorBlend=floorBlend*floorBlend *(3.0-2.0*floorBlend); - const double mouthWallThickness=candidate.segmentIndex - =0.0 + &&wallElevationMetres<=OpenAirSideWallHeightMetres ?smoothShellFade*shellThickness:0.0; double floorExtensionFade=1.0; - if(candidate.segmentIndex>=constants.routeEndControlIndex) + if(!segment.routeSegment) { const double pastMouthMetres=std::max(0.0,alongMetres -constants.totalPathLengthMetres); diff --git a/Standalone/Research/WorldAssemblerM3/Linux111/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux111/REPORT.md new file mode 100644 index 0000000..7099861 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux111/REPORT.md @@ -0,0 +1,42 @@ +# Linux111 report + +## Design + +Version 218 resamples passage controls with a centripetal Catmull–Rom curve at about 1 m spacing (310 control spans became 2,790 prepared segments). Open-air shells use a 0.75 m floor slab and side walls up to 1.0 m above the floor; there is no roof. Passage interior remains higher priority than shell. The second design constrained the floor route to a 14.5° grade ceiling. + +The per-tile candidate lists were not measured. The 310→2,790 figures are prepared route segment counts, not tile candidate list sizes. + +## Speed + +L109 medians below are the candidate column from the saved five-pair timing matrix. L111 timing was not run after the audits failed. + +| Stratum | L109 W8 near-ready (s) | L109 W12 near-ready (s) | L111 W8/W12 | +|---|---:|---:|---| +| SurfaceWorld | 0.737 | 0.761 | Not run | +| FloatingIslands | 0.911 | 0.863 | Not run | +| TunnelNetwork | 0.956 | 0.895 | Not run | +| VerticalShafts | 0.789 | 0.738 | Not run | + +## Audits + +- Clang standalone build completed. The second design removed all grade failures. Passage 0–1 still had 2 unsupported walk samples; passages 1–2 and 2–3 passed. The combined resolved-passage audit failed. +- Mouth-profile audits passed 3/3. Naturalness and ripple audits passed 2/3. +- The half-pipe shell audit passed 2/3; passage 0–1 reported 11,544 floor and 6,945 wall violations. A targeted trace showed at least one reported floor sample was 0.101 m inside the passage after relief, where interior correctly overrides the shell. The audit's smooth-route overlap model still disagreed at that point, so the remaining shell counts are not conclusive. +- Fast tier, Clang/GCC W1/W8/W12 hash matrix, readiness timing, and Unreal Linux build were not run. Canonical v218 hashes were not generated. + +## Captures + +No Linux111 capture images were generated. Mouths, 25/50/75% stations, open-air sections, and programmatically verified 4 m outside/interior close-ups remain outstanding. + +The candidate audit found these open-air sections: + +- passage 0–1: 6.408–16.782 m; 19.500–24.439 m; 1,044.046–1,498.708 m +- passage 1–2: 81.654–85.646 m; 104.092–104.829 m +- passage 2–3: 7.701–26.008 m; 372.697–377.363 m + +## Failed attempts + +1. Dense spline plus half-pipe: walk grades exceeded 15° and passage 0–1 failed shell, naturalness, and ripple checks. +2. Dense spline plus a grade-limited floor: grade failures cleared, but passage 0–1 retained 2 unsupported walk samples and its shell/naturalness/ripple audits still failed. Work stopped after this second design as requested. + +Logs: `Saved/Linux/l111/build-clang.log` and `Saved/Linux/l111/audit-owner.log`. diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index e322fd0..a860758 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -3,6 +3,7 @@ #include "OwnerWorldLoader.h" #include "StreamingWorld.h" #include "LinuxScreenSpaceGate.h" +#include "../Core/src/VoxelForgeFieldShared.h" #include #include @@ -2967,6 +2968,52 @@ namespace StandaloneStreamBenchmark const auto qToMetres=[&](std::int64_t q) {return static_cast(q)*World.CoreVoxelSizeMicrons /(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel*1000000.0);}; + const auto passageSplineControl=[&]( + const VoxelForgeCore::ResolvedLayoutPassageV1& passage,std::size_t index) + { + const auto& point=passage.controlPoints[index]; + return std::array{qToMetres(point.positionSubvoxels[0]), + qToMetres(point.positionSubvoxels[1]), + qToMetres(point.positionSubvoxels[2]), + qToMetres(point.radiusSubvoxels)}; + }; + const auto passageSplineKnotSpan=[](const std::array& a, + const std::array& b) + { + const double dx=b[0]-a[0],dy=b[1]-a[1],dz=b[2]-a[2]; + return std::max(1.0e-6,std::sqrt(std::sqrt(dx*dx+dy*dy+dz*dz))); + }; + const auto passageSplinePoint=[&]( + const VoxelForgeCore::ResolvedLayoutPassageV1& passage, + std::size_t span,double t) + { + const auto p0=passageSplineControl(passage,span==0?0:span-1); + const auto p1=passageSplineControl(passage,span); + const auto p2=passageSplineControl(passage,span+1); + const auto p3=passageSplineControl(passage, + std::min(span+2,passage.controlPoints.size()-1)); + const double dt0=passageSplineKnotSpan(p0,p1); + const double dt1=passageSplineKnotSpan(p1,p2); + const double dt2=passageSplineKnotSpan(p2,p3); + const double t2=t*t,t3=t2*t; + std::array result{}; + for(std::size_t axis=0;axis<4;++axis) + { + const double m1=dt1*((p1[axis]-p0[axis])/dt0 + -(p2[axis]-p0[axis])/(dt0+dt1) + +(p2[axis]-p1[axis])/dt1); + const double m2=dt1*((p2[axis]-p1[axis])/dt1 + -(p3[axis]-p1[axis])/(dt1+dt2) + +(p3[axis]-p2[axis])/dt2); + result[axis]=(2.0*t3-3.0*t2+1.0)*p1[axis] + +(-2.0*t3+3.0*t2)*p2[axis] + +(t3-2.0*t2+t)*m1+(t3-t2)*m2; + } + result[3]=std::clamp(result[3],std::min(p1[3],p2[3]), + std::max(p1[3],p2[3])); + return result; + }; + constexpr std::size_t PassageSplineArcSteps=32; const auto passageBodyFloorDepth=[&]( const VoxelForgeCore::ResolvedLayoutPassageV1& passage) { @@ -2976,12 +3023,19 @@ namespace StandaloneStreamBenchmark std::vector distances(routeEnd+1,0.0); for(std::size_t i=1;i<=routeEnd;++i) { - const auto& a=passage.controlPoints[i-1].positionSubvoxels; - const auto& b=passage.controlPoints[i].positionSubvoxels; - const double dx=qToMetres(b[0]-a[0]); - const double dy=qToMetres(b[1]-a[1]); - const double dz=qToMetres(b[2]-a[2]); - distances[i]=distances[i-1]+std::sqrt(dx*dx+dy*dy+dz*dz); + auto previous=passageSplinePoint(passage,i-1,0.0); + double spanLength=0.0; + for(std::size_t step=1;step<=PassageSplineArcSteps;++step) + { + const auto current=passageSplinePoint(passage,i-1, + static_cast(step)/PassageSplineArcSteps); + const double dx=current[0]-previous[0]; + const double dy=current[1]-previous[1]; + const double dz=current[2]-previous[2]; + spanLength+=std::sqrt(dx*dx+dy*dy+dz*dz); + previous=current; + } + distances[i]=distances[i-1]+spanLength; } double minimumRadius=std::numeric_limits::infinity(); double minimumBodyRadius=minimumRadius; @@ -3007,8 +3061,23 @@ namespace StandaloneStreamBenchmark const double lower=qToMetres( passage.controlPoints[routeEnd].positionSubvoxels[2] -passage.lowerMouthSubvoxels[2])+1.13; - return std::array{{std::max(0.0,upper), - std::max(0.0,lower)}}; + double lowerDepth=std::max(0.0,lower); + const auto nativeCaveMouth=std::find_if(ResolvedLayout->cavernEdges.begin(), + ResolvedLayout->cavernEdges.end(),[&](const auto& edge) + { + return edge.strateIndex==passage.lowerStrateIndex + &&edge.startAnchorStableId==passage.stableId+"/lower-mouth" + &&!edge.polylineMicrometres.empty(); + }); + if(nativeCaveMouth!=ResolvedLayout->cavernEdges.end()) + { + const double routeFloor=qToMetres( + passage.controlPoints[routeEnd].positionSubvoxels[2]); + const double nativeFloor=static_cast( + nativeCaveMouth->polylineMicrometres.front()[2])/1000000.0; + lowerDepth=std::max(0.0,routeFloor-nativeFloor); + } + return std::array{{std::max(0.0,upper),lowerDepth}}; }; const auto passageFloorDepthAt=[](double alongMetres,double totalMetres, double radiusMetres,double bodyDepthMetres,double upperMouthDepth, @@ -3040,12 +3109,19 @@ namespace StandaloneStreamBenchmark if(passage.controlPoints.size()<2)return distances; for(std::size_t i=1;i(step)/PassageSplineArcSteps); + const double dx=current[0]-previous[0]; + const double dy=current[1]-previous[1]; + const double dz=current[2]-previous[2]; + spanLength+=std::sqrt(dx*dx+dy*dy+dz*dz); + previous=current; + } + distances[i]=distances[i-1]+spanLength; } return distances; }; @@ -3063,76 +3139,234 @@ namespace StandaloneStreamBenchmark return qToMetres(point.positionSubvoxels[2])-routeFloorDepth +passageTubeDepthAt(radius,bodyDepth); }; + struct FPassageAuditRoutePoint + { + double x=0.0,y=0.0,z=0.0,radius=0.0,distance=0.0; + std::size_t segmentIndex=0; + }; + const auto passageSmoothRoute=[&]( + const VoxelForgeCore::ResolvedLayoutPassageV1& passage, + double bodyDepth,const std::array& mouthFloorDepths) + { + std::vector route; + if(passage.controlPoints.size()<2)return route; + const std::size_t routeEnd=std::clamp( + passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1); + constexpr double AuditRouteSpacingMetres=1.0; + auto previous=passageSplinePoint(passage,0,0.0); + route.push_back({previous[0],previous[1],previous[2],previous[3],0.0,0}); + double distance=0.0; + for(std::size_t span=0;span arcLength{}; + auto arcPrevious=passageSplinePoint(passage,span,0.0); + for(std::size_t step=1;step<=PassageSplineArcSteps;++step) + { + const auto current=passageSplinePoint(passage,span, + static_cast(step)/PassageSplineArcSteps); + const double dx=current[0]-arcPrevious[0]; + const double dy=current[1]-arcPrevious[1]; + const double dz=current[2]-arcPrevious[2]; + arcLength[step]=arcLength[step-1] + +std::sqrt(dx*dx+dy*dy+dz*dz); + arcPrevious=current; + } + const double spanLength=arcLength.back(); + const std::size_t steps=std::max(1, + static_cast(std::ceil( + spanLength/AuditRouteSpacingMetres))); + for(std::size_t sample=1;sample<=steps;++sample) + { + const double targetArc=spanLength*static_cast(sample)/steps; + const auto upper=std::lower_bound(arcLength.begin()+1, + arcLength.end(),targetArc); + const std::size_t upperIndex=static_cast( + upper-arcLength.begin()); + const std::size_t lowerIndex=upperIndex-1; + const double arcSpan=arcLength[upperIndex]-arcLength[lowerIndex]; + const double arcT=arcSpan>1.0e-12 + ?std::clamp((targetArc-arcLength[lowerIndex])/arcSpan,0.0,1.0):0.0; + const double t=(static_cast(lowerIndex)+arcT) + /PassageSplineArcSteps; + const auto current=passageSplinePoint(passage,span,t); + const double dx=current[0]-previous[0]; + const double dy=current[1]-previous[1]; + const double dz=current[2]-previous[2]; + distance+=std::sqrt(dx*dx+dy*dy+dz*dz); + route.push_back({current[0],current[1],current[2],current[3], + distance,span+1}); + previous=current; + } + } + const double totalLength=route.empty()?0.0:route.back().distance; + std::vector desiredFloor(route.size()); + std::vector horizontalGradeLimits(route.size()>0?route.size()-1:0); + std::vector remainingGradeAllowance(route.size(),0.0); + constexpr double MaximumWalkGradeTangent=0.2586407821; + for(std::size_t i=0;i0;--i) + remainingGradeAllowance[i-1]=remainingGradeAllowance[i] + +horizontalGradeLimits[i-1]; + if(route.size()>1) + { + std::vector walkableFloor(route.size()); + walkableFloor.front()=desiredFloor.front(); + walkableFloor.back()=desiredFloor.back(); + for(std::size_t i=1;i+1> allPassageRouteDistances; std::vector allPassageBodyFloorDepths; + std::vector> allPassageSmoothRoutes; allPassageRouteDistances.reserve(ResolvedLayout->passages.size()); allPassageBodyFloorDepths.reserve(ResolvedLayout->passages.size()); + allPassageSmoothRoutes.reserve(ResolvedLayout->passages.size()); for(const auto& passage:ResolvedLayout->passages) { allPassageRouteDistances.push_back(passageRouteDistances(passage)); - allPassageBodyFloorDepths.push_back(passageBodyFloorDepth(passage)); + const double bodyDepth=passageBodyFloorDepth(passage); + allPassageBodyFloorDepths.push_back(bodyDepth); + allPassageSmoothRoutes.push_back(passageSmoothRoute(passage,bodyDepth, + passageMouthFloorDepths(passage))); } + const auto interpolatePassageRoute=[]( + const std::vector& route,double distance) + { + if(route.empty())return FPassageAuditRoutePoint{}; + distance=std::clamp(distance,0.0,route.back().distance); + const auto upper=std::lower_bound(route.begin()+1,route.end(),distance, + [](const FPassageAuditRoutePoint& point,double value) + {return point.distance1.0e-9?(distance-a.distance)/span:0.0; + return FPassageAuditRoutePoint{ + std::lerp(a.x,b.x,t),std::lerp(a.y,b.y,t),std::lerp(a.z,b.z,t), + std::lerp(a.radius,b.radius,t),distance,b.segmentIndex}; + }; const auto passageInteriorWinsAt=[&](double xMetres,double yMetres, double zMetres,std::size_t ignoredPassage=std::numeric_limits::max(), std::size_t ignoredSegment=std::numeric_limits::max()) { constexpr double CornerBlendMetres=0.50; - // Relief moves the curved surface by up to 0.21 m. Samples in this - // band around another segment's boundary are ambiguous shell overlaps; - // the composed union resolves them locally, with interior air winning. - constexpr double OverlapBandMetres=0.21; + constexpr double WallReliefMetres=0.21; for(std::size_t passageIndex=0; passageIndexpassages.size();++passageIndex) { const auto& passage=ResolvedLayout->passages[passageIndex]; if(passage.controlPoints.size()<2)continue; - const std::size_t routeEnd=passage.controlPoints.size()-1; - const auto& distances=allPassageRouteDistances[passageIndex]; - const double totalLength=distances[std::clamp( - passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1)]; + const auto& route=allPassageSmoothRoutes[passageIndex]; + if(route.size()<2)continue; const double bodyDepth=allPassageBodyFloorDepths[passageIndex]; - const auto mouthFloorDepths=passageMouthFloorDepths(passage); - for(std::size_t segment=1;segment<=routeEnd;++segment) + double bestDistance=std::numeric_limits::infinity(); + double bestRoundDistance=0.0,bestCenterZ=0.0; + std::size_t bestSegment=std::numeric_limits::max(); + for(std::size_t segment=1;segment1.0e-12 - ?std::clamp(((xMetres-ax)*dx+(yMetres-ay)*dy) + ?std::clamp(((xMetres-a.x)*dx+(yMetres-a.y)*dy) /horizontalLengthSquared,0.0,1.0):t; - const double floorCenterZ=az+floorT*dz; - const double floorRadius=std::lerp(qToMetres(a.radiusSubvoxels), - qToMetres(b.radiusSubvoxels),floorT); - const double along=distances[segment-1] - +std::sqrt(lengthSquared)*floorT; + const double floorCenterZ=a.z+floorT*dz; + const double floorRadius=std::lerp(a.radius,b.radius,floorT); const double floorDepth=passageTubeDepthAt(floorRadius,bodyDepth); const double roundDistance=std::sqrt((xMetres-centerX)*(xMetres-centerX) +(yMetres-centerY)*(yMetres-centerY) +(zMetres-centerZ)*(zMetres-centerZ))-radius; - const double floorDistance=floorCenterZ-floorDepth-zMetres; + const double floorPlaneZ=floorCenterZ-floorDepth; + const double floorDistance=floorPlaneZ-zMetres; const double h=std::max(CornerBlendMetres- std::abs(roundDistance-floorDistance),0.0)/CornerBlendMetres; const double flatDistance=std::max(roundDistance,floorDistance) +h*h*h*(CornerBlendMetres/6.0); - if(flatDistance::max())continue; + const auto& a=route[bestSegment-1]; + const auto& b=route[bestSegment]; + const double dx=b.x-a.x,dy=b.y-a.y,dz=b.z-a.z; + const double lengthSquared=dx*dx+dy*dy+dz*dz; + if(lengthSquared<=1.0e-12)continue; + const double t=std::clamp(((xMetres-a.x)*dx+(yMetres-a.y)*dy + +(zMetres-a.z)*dz)/lengthSquared,0.0,1.0); + const double radius=std::lerp(a.radius,b.radius,t); + const double centerX=a.x+t*dx,centerY=a.y+t*dy; + const double centerZ=a.z+t*dz; + const double distanceToCenter=std::max(1.0e-9, + bestRoundDistance+radius); + const double verticalWallAlignment=std::clamp( + -(zMetres-centerZ)/distanceToCenter,0.0,1.0); + const double wallReliefWeight=0.05+0.95 + *(1.0-verticalWallAlignment); + const float relief=VF_FieldPassageWallRelief( + static_cast(xMetres/coreMetres), + static_cast(yMetres/coreMetres), + static_cast(zMetres/coreMetres), + static_cast(World.Seed)); + const double roughRoundDistance=bestRoundDistance + +static_cast(std::clamp(relief,-1.0f,1.0f)) + *WallReliefMetres*wallReliefWeight; + const auto& aFloor=route[bestSegment-1]; + const auto& bFloor=route[bestSegment]; + const double floorDx=bFloor.x-aFloor.x; + const double floorDy=bFloor.y-aFloor.y; + const double floorDz=bFloor.z-aFloor.z; + const double horizontalSquared=floorDx*floorDx+floorDy*floorDy; + const double floorT=horizontalSquared>1.0e-12 + ?std::clamp(((xMetres-aFloor.x)*floorDx + +(yMetres-aFloor.y)*floorDy)/horizontalSquared,0.0,1.0):t; + const double floorRadius=std::lerp(aFloor.radius,bFloor.radius,floorT); + const double floorDepth=passageTubeDepthAt(floorRadius,bodyDepth); + const double floorCenterZ=aFloor.z+floorT*floorDz; + const double floorDistance=floorCenterZ-floorDepth-zMetres; + const double blend=std::max(CornerBlendMetres- + std::abs(roughRoundDistance-floorDistance),0.0)/CornerBlendMetres; + const double shapedDistance=std::max(roughRoundDistance,floorDistance) + +blend*blend*blend*(CornerBlendMetres/6.0); + if(shapedDistance<=0.0)return true; } return false; }; @@ -3438,31 +3672,20 @@ namespace StandaloneStreamBenchmark >=PassageProfilePlateauMinimumMetres &&l23TerraceControlResult.second>=PassageProfileStepThresholdMetres; const auto auditPassageNaturalness=[&](const VoxelForgeCore::World& Field, - const VoxelForgeCore::ResolvedLayoutPassageV1& passage) + std::size_t passageIndex) { FPassageNaturalnessAudit audit; - const std::size_t end=std::min(passage.lowerMouthControlIndex, - passage.controlPoints.size()-1); - const auto routeDistances=passageRouteDistances(passage); - const double totalRouteLength=routeDistances.empty()?0.0: - routeDistances[end]; - const double bodyFloorDepth=passageBodyFloorDepth(passage); - const auto mouthFloorDepths=passageMouthFloorDepths(passage); + const auto& path=allPassageSmoothRoutes[passageIndex]; + if(path.size()<3)return audit; + const double bodyFloorDepth=allPassageBodyFloorDepths[passageIndex]; double flatRun=0.0; - for(std::size_t i=1;i<=end;++i) + for(std::size_t i=1;i1.0e-6&&std::abs(dz)/horizontal @@ -3471,28 +3694,31 @@ namespace StandaloneStreamBenchmark audit.maximumFlatRunMetres,flatRun);} else flatRun=0.0; } - if(end>=6) + if(path.size()>=8) { - const std::size_t sampleCount=std::min(64,end-4); + const std::size_t sampleCount=std::min(64,path.size()-4); double minimumRadius=std::numeric_limits::infinity(); double maximumRadius=-std::numeric_limits::infinity(); for(std::size_t sample=0;sample(1,sampleCount-1); - const auto& point=passage.controlPoints[index]; - const auto& before=passage.controlPoints[index-1]; - const auto& after=passage.controlPoints[index+1]; - const double tangentX=qToMetres(after.positionSubvoxels[0]-before.positionSubvoxels[0]); - const double tangentY=qToMetres(after.positionSubvoxels[1]-before.positionSubvoxels[1]); + const auto& point=path[index]; + const auto& before=path[index-1]; + const auto& after=path[index+1]; + const double tangentX=after.x-before.x; + const double tangentY=after.y-before.y; const double tangentLength=std::hypot(tangentX,tangentY); if(tangentLength<=1.0e-6){++audit.missingWallSamples;continue;} - const double radiusMetres=qToMetres(point.radiusSubvoxels); - const double sampleRadius=std::max(0.20,radiusMetres-0.18); - const double centerX=qToMetres(point.positionSubvoxels[0]); - const double centerY=qToMetres(point.positionSubvoxels[1]); - const double centerZ=passageEffectiveCenterZ(passage,index, - routeDistances,totalRouteLength,bodyFloorDepth,mouthFloorDepths); + constexpr double WallProbeHeightMetres=0.75; + const double floorDepth=passageTubeDepthAt(point.radius,bodyFloorDepth); + const double vertical=WallProbeHeightMetres-floorDepth; + const double wallRadiusSquared=point.radius*point.radius-vertical*vertical; + if(wallRadiusSquared<=0.0){audit.missingWallSamples+=2;continue;} + const double sampleRadius=std::max(0.20, + std::sqrt(wallRadiusSquared)-0.18); + const double centerX=point.x,centerY=point.y; + const double centerZ=point.z-floorDepth+WallProbeHeightMetres; const double nx=-tangentY/tangentLength,ny=tangentX/tangentLength; for(const double side:{-1.0,1.0}) { @@ -3523,7 +3749,6 @@ namespace StandaloneStreamBenchmark return audit; }; const auto auditPassageRipples=[&](const VoxelForgeCore::World& Field, - const VoxelForgeCore::ResolvedLayoutPassageV1& passage, std::size_t passageIndex) { FPassageRippleAudit audit; @@ -3539,33 +3764,16 @@ namespace StandaloneStreamBenchmark double x=0.0,y=0.0,z=0.0,radius=0.0,distance=0.0; std::size_t segmentIndex=0; }; - const std::size_t end=std::min(passage.lowerMouthControlIndex, - passage.controlPoints.size()-1); - if(end<2)return audit; - const auto routeDistances=passageRouteDistances(passage); - const double totalRouteLength=routeDistances.empty()?0.0: - routeDistances[end]; - const double bodyFloorDepth=passageBodyFloorDepth(passage); - const auto mouthFloorDepths=passageMouthFloorDepths(passage); + const auto& passage=ResolvedLayout->passages[passageIndex]; + const auto& smoothRoute=allPassageSmoothRoutes[passageIndex]; + if(smoothRoute.size()<2)return audit; + const double bodyFloorDepth=allPassageBodyFloorDepths[passageIndex]; std::vector path; - path.reserve(end+1); - for(std::size_t i=0;i<=end;++i) + path.reserve(smoothRoute.size()); + for(const auto& point:smoothRoute) { - const auto& point=passage.controlPoints[i]; - FPathPoint value{qToMetres(point.positionSubvoxels[0]), - qToMetres(point.positionSubvoxels[1]), - passageEffectiveCenterZ(passage,i,routeDistances, - totalRouteLength,bodyFloorDepth,mouthFloorDepths), - qToMetres(point.radiusSubvoxels),0.0,i==0?1:i}; - if(!path.empty()) - { - const auto& previous=path.back(); - value.distance=previous.distance+std::sqrt( - (value.x-previous.x)*(value.x-previous.x) - +(value.y-previous.y)*(value.y-previous.y) - +(value.z-previous.z)*(value.z-previous.z)); - } - path.push_back(value); + path.push_back({point.x,point.y,point.z,point.radius, + point.distance,point.segmentIndex}); } const double total=path.back().distance; if(totalpassages[passageIndex]; if(passage.controlPoints.size()<2)continue; - const std::size_t routeEnd=std::clamp( - passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1); - const auto& distances=allPassageRouteDistances[passageIndex]; - const double totalLength=distances[std::clamp( - passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1)]; const double bodyDepth=allPassageBodyFloorDepths[passageIndex]; - const auto mouthFloorDepths=passageMouthFloorDepths(passage); + const auto& path=allPassageSmoothRoutes[passageIndex]; + if(path.size()<2)continue; + const double totalLength=path.back().distance; double bestFlatDistance=std::numeric_limits::infinity(); double bestFloorDistance=0.0,bestMouthDistance=0.0; - for(std::size_t segment=0;segment1.0e-12 ?std::clamp(((xMetres-ax)*dx+(yMetres-ay)*dy) /horizontalSquared,0.0,1.0):t; - const double floorRadius=std::lerp(qToMetres(a.radiusSubvoxels), - qToMetres(b.radiusSubvoxels),floorT); - const double floorAlong=distances[segment] - +(distances[segment+1]-distances[segment])*floorT; - const double floorCenterZ=std::lerp( - qToMetres(a.positionSubvoxels[2]), - qToMetres(b.positionSubvoxels[2]),floorT) - -passageFloorDepthAt(floorAlong,totalLength,floorRadius, - bodyDepth,mouthFloorDepths[0],mouthFloorDepths[1]) - +passageTubeDepthAt(floorRadius,bodyDepth); - const double along=distances[segment] - +(distances[segment+1]-distances[segment])*t; + const double floorRadius=std::lerp(a.radius,b.radius,floorT); + const double floorCenterZ=std::lerp(a.z,b.z,floorT); + const double along=std::lerp(a.distance,b.distance,t); const double floorDepth=passageTubeDepthAt(floorRadius,bodyDepth); const double roundDistance=std::sqrt((xMetres-centerX)*(xMetres-centerX) +(yMetres-centerY)*(yMetres-centerY) @@ -4045,7 +4233,11 @@ namespace StandaloneStreamBenchmark const double fade=std::clamp(bestMouthDistance/5.0,0.0,1.0); const double smoothFade=fade*fade*(3.0-2.0*fade); const double floorBlend=smoothstep(bestFloorDistance/0.25); - const double thickness=0.75*std::max(smoothFade,floorBlend); + const double wallElevation=-bestFloorDistance; + const double wallThickness=wallElevation>=0.0&&wallElevation<=1.0 + ?smoothFade:0.0; + const double floorThickness=smoothFade*floorBlend; + const double thickness=0.75*std::max(wallThickness,floorThickness); if(bestFlatDistance+ReliefBoundMetres( - passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1); - const std::size_t geometryEnd=passage.controlPoints.size()-1; - const auto routeDistances=passageRouteDistances(passage); - const double totalRouteLength=routeDistances[std::clamp( - passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1)]; - const double bodyFloorDepth=passageBodyFloorDepth(passage); - const auto mouthFloorDepths=passageMouthFloorDepths(passage); - const auto expectedShellThickness=[&](double along,double floorDistance) + const auto& route=allPassageSmoothRoutes[passageIndex]; + if(route.size()<2)return audit; + const double totalRouteLength=route.back().distance; + const double bodyFloorDepth=allPassageBodyFloorDepths[passageIndex]; + constexpr std::array,5> floorOffsets{{ + {{0.0,0.0}},{{0.30,0.0}},{{-0.30,0.0}}, + {{0.0,0.30}},{{0.0,-0.30}}}}; + const auto expectedFloorThickness=[&](double along,double floorDistance) { const double mouthDistance=std::max(0.0,std::min(along, totalRouteLength-along)); @@ -4077,43 +4268,27 @@ namespace StandaloneStreamBenchmark const double floorBlend=std::clamp(floorDistance/0.25,0.0,1.0); const double smoothFloorBlend=floorBlend*floorBlend *(3.0-2.0*floorBlend); - const double extensionFade=std::clamp(1.0 - -(along-totalRouteLength)/5.0,0.0,1.0); - const double smoothExtensionFade=extensionFade*extensionFade - *(3.0-2.0*extensionFade); - return 0.75*std::max(smoothFade, - smoothFloorBlend*smoothExtensionFade); + return 0.75*smoothFade*smoothFloorBlend; }; - constexpr std::array,5> floorOffsets{{ - {{0.0,0.0}},{{0.30,0.0}},{{-0.30,0.0}},{{0.0,0.30}},{{0.0,-0.30}}}}; - for(std::size_t segment=1;segment<=geometryEnd;++segment) + for(std::size_t segment=1;segment(1, - static_cast(std::ceil(length/PassageShellAuditSpacingMetres))); - for(std::size_t sampleIndex=0;sampleIndex<=intervals;++sampleIndex) + static_cast(std::ceil( + length/PassageShellAuditSpacingMetres))); + for(std::size_t sampleIndex=segment==1?0:1; + sampleIndex<=intervals;++sampleIndex) { - if(segment>1&&sampleIndex==0)continue; const double t=static_cast(sampleIndex)/intervals; - const double centerX=ax+dx*t,centerY=ay+dy*t,centerZ=az+dz*t; - const double along=routeDistances[segment-1] - +authoredSegmentLength*t; - const double radius=std::lerp(qToMetres(a.radiusSubvoxels), - qToMetres(b.radiusSubvoxels),t); - if(std::min(along,totalRouteLength-along)>0.0) + const double centerX=a.x+dx*t,centerY=a.y+dy*t,centerZ=a.z+dz*t; + const double along=a.distance+alongLength*t; + const double radius=std::lerp(a.radius,b.radius,t); + if(std::min(along,totalRouteLength-along)>0.0) + { for(const auto& offset:floorOffsets) for(const double depth:PassageShellAuditDepthsMetres) { @@ -4121,44 +4296,30 @@ namespace StandaloneStreamBenchmark const double sampleX=centerX+offset[0]; const double sampleY=centerY+offset[1]; const double floorT=horizontalLengthSquared>1.0e-12 - ?std::clamp(((sampleX-ax)*dx+(sampleY-ay)*dy) + ?std::clamp(((sampleX-a.x)*dx+(sampleY-a.y)*dy) /horizontalLengthSquared,0.0,1.0):t; - const double floorAlong=routeDistances[segment-1] - +authoredSegmentLength*floorT; - const double floorRadius=std::lerp( - qToMetres(a.radiusSubvoxels), - qToMetres(b.radiusSubvoxels),floorT); - const double floorCenterZ=segment<=routeEnd - ?std::lerp(qToMetres(a.positionSubvoxels[2]), - qToMetres(b.positionSubvoxels[2]),floorT) - -passageFloorDepthAt(floorAlong,totalRouteLength, - floorRadius,bodyFloorDepth,mouthFloorDepths[0], - mouthFloorDepths[1]) - +passageTubeDepthAt(floorRadius,bodyFloorDepth) - :az+floorT*dz; + const double floorAlong=a.distance+alongLength*floorT; + const double floorRadius=std::lerp(a.radius,b.radius,floorT); + const double floorCenterZ=std::lerp(a.z,b.z,floorT); const double floorDepth=passageTubeDepthAt(floorRadius, bodyFloorDepth); const double floorZ=floorCenterZ-floorDepth; const VoxelForgeCore::Vec3d position{ - sampleX/coreMetres, - sampleY/coreMetres, + sampleX/coreMetres,sampleY/coreMetres, (floorZ-depth)/coreMetres}; const double sampleZ=floorZ-depth; - if(passageInteriorWinsAt(sampleX,sampleY,sampleZ, - static_cast(&passage- - ResolvedLayout->passages.data()),segment)) + if(passageInteriorWinsAt(sampleX,sampleY,sampleZ)) { ++audit.floorInteriorWins; continue; } ++audit.floorSamples; - const float nativeFloor=Native.densityAt(position); - if(nativeFloor>=0.0f) + if(Native.densityAt(position)>=0.0f) { ++audit.openAirSamples; audit.openAirDistancesMetres.push_back(along); const bool shellExpected=depth+0.011 - =0.0f) ||(!shellExpected&&density<0.0f)) @@ -4166,64 +4327,68 @@ namespace StandaloneStreamBenchmark ++audit.floorViolations; if(audit.firstViolationCount1.0e-6) - { - if(std::min(along,totalRouteLength-along)<5.0) - { - audit.wallMouthBlendSkips+=5; - continue; - } - const double nx=-dy/tangentLength,ny=dx/tangentLength; - const std::array,5> wallDirections{{ - {{0.0,0.0,1.0}}, - {{-0.7071067811865475*nx,-0.7071067811865475*ny, - 0.7071067811865475}}, - {{0.7071067811865475*nx,0.7071067811865475*ny, - 0.7071067811865475}}, - {{-nx,-ny,0.0}},{{nx,ny,0.0}}}}; - for(const auto& direction:wallDirections) - { - const double sampleX=centerX+direction[0]*(radius+0.375); - const double sampleY=centerY+direction[1]*(radius+0.375); - const double sampleZ=centerZ+direction[2]*(radius+0.375); - const VoxelForgeCore::Vec3d wallPosition{sampleX/coreMetres, - sampleY/coreMetres,sampleZ/coreMetres}; - const bool wallInteriorWins=passageInteriorWinsAt(sampleX, - sampleY,sampleZ,static_cast(&passage- - ResolvedLayout->passages.data()),segment); - if(wallInteriorWins)++audit.wallInteriorWins; - else if(Native.densityAt(wallPosition)>=0.0f) - { - ++audit.openAirSamples; - ++audit.wallSamples; - audit.openAirDistancesMetres.push_back(along); - const float density=Field.densityAt(wallPosition); - // The largest authored wall-relief excursion is 0.21 m; - // test only where the shell must remain solid for every - // relief phase. - const int shellExpected=expectedPassageShellAt( - sampleX,sampleY,sampleZ); - if(shellExpected>=0 - &&((shellExpected==1&&density>=0.0f) - ||(shellExpected==0&&density<0.0f))) - { - ++audit.wallViolations; - if(audit.firstWallViolationCount - wallProbes{{ + {0.0,0.0,true},{-1.0,0.25,false},{1.0,0.25,false}, + {-1.0,0.75,false},{1.0,0.75,false}}}; + for(const auto& probe:wallProbes) + { + double sampleX=centerX,sampleY=centerY,sampleZ=0.0; + if(probe.roof) + sampleZ=centerZ+radius+0.375; + else + { + const double vertical=probe.height-floorDepth; + const double wallRadiusSquared=radius*radius-vertical*vertical; + if(wallRadiusSquared<=0.0)continue; + const double wallRadius=std::sqrt(wallRadiusSquared); + sampleX+=nx*probe.side*(wallRadius+0.375); + sampleY+=ny*probe.side*(wallRadius+0.375); + sampleZ=floorZ+probe.height; + } + const VoxelForgeCore::Vec3d wallPosition{sampleX/coreMetres, + sampleY/coreMetres,sampleZ/coreMetres}; + if(passageInteriorWinsAt(sampleX,sampleY,sampleZ, + passageIndex,b.segmentIndex)) + { + ++audit.wallInteriorWins; + continue; + } + if(Native.densityAt(wallPosition)<0.0f)continue; + ++audit.openAirSamples; + ++audit.wallSamples; + audit.openAirDistancesMetres.push_back(along); + const float density=Field.densityAt(wallPosition); + const int shellExpected=expectedPassageShellAt( + sampleX,sampleY,sampleZ); + if(shellExpected>=0 + &&((shellExpected==1&&density>=0.0f) + ||(shellExpected==0&&density<0.0f))) + { + ++audit.wallViolations; + if(audit.firstWallViolationCount + passages.size();++passageIndex) { const auto& passage=ResolvedLayout->passages[passageIndex]; - std::vector> path;path.reserve(passage.controlPoints.size()); - const auto routeDistances=passageRouteDistances(passage); - const double routeLength=routeDistances.empty()?0.0:routeDistances[ - std::clamp(passage.lowerMouthControlIndex,1, - passage.controlPoints.size()-1)]; - const double bodyFloorDepth=passageBodyFloorDepth(passage); - const auto mouthFloorDepths=passageMouthFloorDepths(passage); - const std::size_t routeEnd=std::clamp( - passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1); - for(std::size_t segment=1;segment<=routeEnd;++segment) + const auto& smoothRoute=allPassageSmoothRoutes[passageIndex]; + const double bodyFloorDepth=allPassageBodyFloorDepths[passageIndex]; + std::vector> path;path.reserve(smoothRoute.size()); + for(const auto& point:smoothRoute) { - const auto& a=passage.controlPoints[segment-1]; - const auto& b=passage.controlPoints[segment]; - const double ax=qToMetres(a.positionSubvoxels[0]); - const double ay=qToMetres(a.positionSubvoxels[1]); - const double az=qToMetres(a.positionSubvoxels[2]); - const double bx=qToMetres(b.positionSubvoxels[0]); - const double by=qToMetres(b.positionSubvoxels[1]); - const double bz=qToMetres(b.positionSubvoxels[2]); - const double length=routeDistances[segment]-routeDistances[segment-1]; - const std::size_t intervals=std::max(1, - static_cast(std::ceil(length/0.5))); - for(std::size_t step=segment==1?0:1;step<=intervals;++step) - { - const double t=static_cast(step)/intervals; - const double along=routeDistances[segment-1]+length*t; - const double centerZ=std::lerp(az,bz,t); - const double radius=std::lerp(qToMetres(a.radiusSubvoxels), - qToMetres(b.radiusSubvoxels),t); - const double floorDepth=passageFloorDepthAt(along,routeLength, - radius,bodyFloorDepth,mouthFloorDepths[0], - mouthFloorDepths[1]); - const double playerZ=centerZ-floorDepth+1.13; - path.push_back({std::lerp(ax,bx,t)/coreMetres, - std::lerp(ay,by,t)/coreMetres,playerZ/coreMetres}); - } + const double playerZ=point.z + -passageTubeDepthAt(point.radius,bodyFloorDepth)+1.13; + path.push_back({point.x/coreMetres,point.y/coreMetres, + playerZ/coreMetres}); } // Probe below the capsule feet far enough to enter the new 0.75 m // floor shell; the old quarter-metre search only checked the air @@ -4298,7 +4436,8 @@ namespace StandaloneStreamBenchmark {return passageInteriorWinsAt(x,y,z-0.25);}; const FWalkAudit passageAudit=auditPlayerPath(Shape,path,0.375,0.125, 1.125,3,0.45,passageSupportExemption); - const FPassageNaturalnessAudit naturalAudit=auditPassageNaturalness(Shape,passage); + const FPassageNaturalnessAudit naturalAudit= + auditPassageNaturalness(Shape,passageIndex); const auto lowerProfileStratum=std::find_if(ResolvedLayout->strates.begin(), ResolvedLayout->strates.end(),[&](const auto& stratum) {return stratum.strateIndex==passage.lowerStrateIndex;}); @@ -4309,10 +4448,10 @@ namespace StandaloneStreamBenchmark {return anchor.kind=="heightfield_profile";}); const FPassageMouthProfileAudit mouthProfileAudit= auditPassageMouthProfiles(Shape,passage,profiledLowerSurface); - const FPassageRippleAudit rippleAudit=auditPassageRipples(Shape,passage, - passageIndex); + const FPassageRippleAudit rippleAudit= + auditPassageRipples(Shape,passageIndex); const FPassageShellAudit shellAudit=auditPassageShell(Shape,blockedWorld, - passage); + passage,passageIndex); std::vector openAirDistances=shellAudit.openAirDistancesMetres; std::sort(openAirDistances.begin(),openAirDistances.end()); openAirDistances.erase(std::unique(openAirDistances.begin(), @@ -4326,29 +4465,10 @@ namespace StandaloneStreamBenchmark const double startAlong=openAirDistances[first]; const double endAlong=openAirDistances[end-1]; const double midpoint=(startAlong+endAlong)*0.5; - const auto upper=std::lower_bound(routeDistances.begin()+1, - routeDistances.end(),midpoint); - const std::size_t segment=std::min( - static_cast(upper-routeDistances.begin())-1, - passage.controlPoints.size()-2); - const double segmentLength=routeDistances[segment+1]-routeDistances[segment]; - const double t=segmentLength>1.0e-9 - ?std::clamp((midpoint-routeDistances[segment])/segmentLength,0.0,1.0):0.0; - const auto& a=passage.controlPoints[segment]; - const auto& b=passage.controlPoints[segment+1]; - const double effectiveAz=passageEffectiveCenterZ(passage,segment, - routeDistances,routeLength,bodyFloorDepth,mouthFloorDepths); - const double effectiveBz=passageEffectiveCenterZ(passage,segment+1, - routeDistances,routeLength,bodyFloorDepth,mouthFloorDepths); + const auto center=interpolatePassageRoute(smoothRoute,midpoint); std::printf("[PassageOpenAirSection] id=%s section=%zu start_m=%.3f end_m=%.3f samples=%zu midpoint_xyz_m=(%.3f,%.3f,%.3f) midpoint_radius_m=%.3f capture_views_required=3\n", passage.stableId.c_str(),section+1,startAlong,endAlong,end-first, - std::lerp(qToMetres(a.positionSubvoxels[0]), - qToMetres(b.positionSubvoxels[0]),t), - std::lerp(qToMetres(a.positionSubvoxels[1]), - qToMetres(b.positionSubvoxels[1]),t), - std::lerp(effectiveAz,effectiveBz,t), - std::lerp(qToMetres(a.radiusSubvoxels), - qToMetres(b.radiusSubvoxels),t)); + center.x,center.y,center.z,center.radius); first=end; } shellAudits+=1; diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py index 6ccb6de..07b6d9c 100644 --- a/Standalone/Tools/verify/verify.py +++ b/Standalone/Tools/verify/verify.py @@ -122,11 +122,11 @@ RECIPE_RE = re.compile( def owner_hash_baseline(recipe: str, style: int) -> tuple[Path, str]: if recipe == "surface-world" and style == 1: - return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v217.baseline", - "L109-v217-flat-floor-hollow-shell") + return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v218.baseline", + "L111-v218-smooth-spline-half-pipe") if recipe == "floating-islands" and style == 2: - return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v217.baseline", - "L109-v217-flat-floor-hollow-shell") + return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v218.baseline", + "L111-v218-smooth-spline-half-pipe") raise VerificationFailure(f"no pinned owner hash baseline for {recipe} style {style}")