diff --git a/Standalone/Core/include/VoxelForgeCore/PassageSpline.h b/Standalone/Core/include/VoxelForgeCore/PassageSpline.h index 24c68ab..c8610eb 100644 --- a/Standalone/Core/include/VoxelForgeCore/PassageSpline.h +++ b/Standalone/Core/include/VoxelForgeCore/PassageSpline.h @@ -13,6 +13,7 @@ namespace VoxelForgeCore::PassageSpline using FPoint = std::array; inline constexpr std::size_t ArcTableSteps = 32; inline constexpr double UpperMouthFloorBlendRadiusMetres = 3.5; + inline constexpr double FlatFloorCornerBlendMetres = 0.5; struct FSegment { @@ -60,6 +61,36 @@ namespace VoxelForgeCore::PassageSpline return result; } + inline FPoint EvaluateRawDerivative(std::span controls, + std::size_t span,double t,double unitsPerMetre) + { + 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)]; + const double dt0=KnotSpan(p0,p1,unitsPerMetre); + const double dt1=KnotSpan(p1,p2,unitsPerMetre); + const double dt2=KnotSpan(p2,p3,unitsPerMetre); + const double t2=t*t; + const double h00=6.0*t2-6.0*t; + const double h01=-h00; + const double h10=3.0*t2-4.0*t+1.0; + const double h11=3.0*t2-2.0*t; + FPoint result{}; + for(const std::size_t axis:{0u,1u,3u}) + { + 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]=h00*p1[axis]+h01*p2[axis]+h10*m1+h11*m2; + } + result[2]=p2[2]-p1[2]; + return result; + } + inline bool ClosestRouteHeightAtXY(std::span> route, double x,double y,double& height,double& distanceSquared) { diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 8c44461..e712747 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -511,7 +511,8 @@ namespace VoxelForgeCore // the planned graph route, preserves its walk corridor through shell overlaps, and keeps // boundary-blended host rock from widening the authored passage wall. // v237 adds the shared Core cave-graph shaft primitive and basal terminal route. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 237; + // v238 joins cave edges to passage mouth tubes with matched geometry and clearance. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 238; enum class FieldChannel : std::uint32_t { diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index 7b7520a..095418b 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -2367,8 +2367,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, constexpr std::int64_t MinimumBoreRadius=1750000; constexpr std::int64_t MinimumBoreHeight=2200000; constexpr std::int64_t PlannedBoreHeight=4500000; - constexpr std::int64_t MouthOverlap=2000000; - constexpr std::int64_t PortalLeadLength=6000000; + constexpr std::int64_t PortalLeadLength=1000000; constexpr std::int64_t MaximumBoreRadius=10000000; constexpr std::int64_t MaximumWalkGradeDenominator=5; // 1:5 = 11.31 degrees. const auto integerSqrt=[](std::uint64_t value) @@ -2394,171 +2393,77 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, const auto qToMicrometres=[&](std::int64_t q) {return q*static_cast(layout.coreVoxelSizeMicrometres) /WorldAssemblySubvoxelsPerVoxel;}; - const std::int64_t mouthBoreRadius=std::clamp( - std::max(MinimumBoreRadius,settings.minimumTunnelRadiusMicrometres), - MinimumBoreRadius,MaximumBoreRadius); - const auto chooseMouthRouteTangent=[&](const ResolvedLayoutPassageV1& passage, - std::size_t mouthIndex,bool lowerSide, - const std::array& authoredRouteTangentQ16, - std::int64_t authoredFloorSlopeQ16,const FPoint& mouthFloorPoint) + std::int64_t mouthBoreRadius=std::max(MinimumBoreRadius, + settings.minimumTunnelRadiusMicrometres); + const auto includePassageMouthRadius=[&]( + const ResolvedLayoutPassageV1* passage,bool lowerSide) { - constexpr std::array,16> DirectionsQ16={{ - {{65536,0}},{{60547,25080}},{{46341,46341}},{{25080,60547}}, - {{0,65536}},{{-25080,60547}},{{-46341,46341}},{{-60547,25080}}, - {{-65536,0}},{{-60547,-25080}},{{-46341,-46341}},{{-25080,-60547}}, - {{0,-65536}},{{25080,-60547}},{{46341,-46341}},{{60547,-25080}}}}; - const auto scoreDirection=[&](const std::array& outwardQ16) - { - const std::size_t segmentCount=passage.controlPoints.size()>1 - ?passage.controlPoints.size()-1:0; - std::int64_t worst=std::numeric_limits::max(); - bool sampled=false; - const std::int64_t projectedSlopeQ16=(authoredFloorSlopeQ16 - *(outwardQ16[0]*authoredRouteTangentQ16[0] - +outwardQ16[1]*authoredRouteTangentQ16[1]) - /(65536ll*65536ll)); - constexpr std::int64_t MouthClearanceProbeExtent=48000000; - for(std::int64_t distance=2000000; - distance<=MouthClearanceProbeExtent;distance+=2000000) - { - const std::int64_t x=mouthFloorPoint[0] - +outwardQ16[0]*distance/65536; - const std::int64_t y=mouthFloorPoint[1] - +outwardQ16[1]*distance/65536; - const std::int64_t caveFloor=mouthFloorPoint[2] - +projectedSlopeQ16*distance/65536; - const std::int64_t caveCeiling=caveFloor+PlannedBoreHeight; - for(std::size_t segmentIndex=0;segmentIndex=mouthIndex)continue; - if(!lowerSide&&segmentIndex<2)continue; - const auto& a=passage.controlPoints[segmentIndex]; - const auto& b=passage.controlPoints[segmentIndex+1]; - const std::int64_t ax=qToMicrometres(a.positionSubvoxels[0]); - const std::int64_t ay=qToMicrometres(a.positionSubvoxels[1]); - const std::int64_t az=qToMicrometres(a.positionSubvoxels[2]); - const std::int64_t bx=qToMicrometres(b.positionSubvoxels[0]); - const std::int64_t by=qToMicrometres(b.positionSubvoxels[1]); - const std::int64_t bz=qToMicrometres(b.positionSubvoxels[2]); - const std::int64_t dx=bx-ax,dy=by-ay; - const double lengthSquared=static_cast(dx)*dx - +static_cast(dy)*dy; - const double projection=lengthSquared>0.0 - ?std::clamp((static_cast(x-ax)*dx - +static_cast(y-ay)*dy)/lengthSquared,0.0,1.0):0.0; - const std::int64_t tQ16=static_cast(std::llround( - projection*65536.0)); - const std::int64_t closestX=ax+dx*tQ16/65536; - const std::int64_t closestY=ay+dy*tQ16/65536; - const std::int64_t horizontalDistance=static_cast( - integerSqrt(static_cast((x-closestX)*(x-closestX) - +(y-closestY)*(y-closestY)))); - const std::int64_t passageCenterZ=qToMicrometres( - a.positionSubvoxels[2]) - +(qToMicrometres(b.positionSubvoxels[2]) - -qToMicrometres(a.positionSubvoxels[2]))*tQ16/65536; - const std::int64_t passageRadius=qToMicrometres(a.radiusSubvoxels) - +(qToMicrometres(b.radiusSubvoxels) - -qToMicrometres(a.radiusSubvoxels))*tQ16/65536; - const std::int64_t horizontalGap=horizontalDistance - -passageRadius-mouthBoreRadius; - const std::int64_t passageFloor=passageCenterZ-passageRadius; - const std::int64_t passageCeiling=passageCenterZ+passageRadius; - const std::int64_t verticalGap=caveFloor>passageCeiling - ?caveFloor-passageCeiling - :(passageFloor>caveCeiling?passageFloor-caveCeiling - :-std::min(caveCeiling-passageFloor, - passageCeiling-caveFloor)); - const std::int64_t separation=horizontalGap>=0&&verticalGap>=0 - ?static_cast(integerSqrt(static_cast( - horizontalGap*horizontalGap+verticalGap*verticalGap))) - :std::max(horizontalGap,verticalGap); - worst=std::min(worst,separation);sampled=true; - } - } - return sampled?worst:std::numeric_limits::max(); - }; - - // Keep the authored tangent whenever it clears the other passage spans. - // If the passage folds back over the mouth, choose the outward direction - // with the greatest 3D clearance while preserving deterministic tie order. - const std::array authoredOutwardQ16={ - lowerSide?authoredRouteTangentQ16[0]:-authoredRouteTangentQ16[0], - lowerSide?authoredRouteTangentQ16[1]:-authoredRouteTangentQ16[1]}; - const std::int64_t authoredScore=scoreDirection(authoredOutwardQ16); - if(authoredScore>=0)return authoredRouteTangentQ16; - std::array bestOutwardQ16=authoredOutwardQ16; - std::int64_t bestScore=authoredScore; - std::int64_t bestAlignment=authoredOutwardQ16[0]*authoredOutwardQ16[0] - +authoredOutwardQ16[1]*authoredOutwardQ16[1]; - for(const auto& candidate:DirectionsQ16) - { - const std::int64_t score=scoreDirection(candidate); - const std::int64_t alignment=candidate[0]*authoredOutwardQ16[0] - +candidate[1]*authoredOutwardQ16[1]; - if(score>bestScore||(score==bestScore&&alignment>bestAlignment)) - {bestScore=score;bestAlignment=alignment;bestOutwardQ16=candidate;} - } - std::printf("[CavernMouthClearancePlan] passage=%s side=%s authored_clearance_m=%.3f selected_clearance_m=%.3f outward_q16=(%lld,%lld)\n", - passage.stableId.c_str(),lowerSide?"lower":"upper", - authoredScore/1.0e6,bestScore/1.0e6, - static_cast(bestOutwardQ16[0]), - static_cast(bestOutwardQ16[1])); - return std::array{ - lowerSide?bestOutwardQ16[0]:-bestOutwardQ16[0], - lowerSide?bestOutwardQ16[1]:-bestOutwardQ16[1]}; + if(!passage||passage->controlPoints.empty())return; + const std::size_t index=lowerSide + ?std::min(passage->lowerMouthControlIndex,passage->controlPoints.size()-1):0; + mouthBoreRadius=std::max(mouthBoreRadius, + qToMicrometres(passage->controlPoints[index].radiusSubvoxels)); }; + includePassageMouthRadius(arrivalPassage,true); + includePassageMouthRadius(departurePassage,false); + mouthBoreRadius=std::clamp(mouthBoreRadius,MinimumBoreRadius,MaximumBoreRadius); const auto mouthInnerPoint=[&](const ResolvedLayoutPassageV1& passage, bool lowerSide,FPoint& point,std::array& routeTangentQ16, std::int64_t& floorSlopeQ16) { - for(std::size_t axis=0;axis<3;++axis) - point[axis]=qToMicrometres(lowerSide?passage.lowerMouthSubvoxels[axis] - :passage.upperMouthSubvoxels[axis]); if(passage.controlPoints.size()<2)return false; const std::size_t mouthIndex=lowerSide ?std::min(passage.lowerMouthControlIndex,passage.controlPoints.size()-1):0; - const std::size_t tangentIndex=lowerSide - ?(mouthIndex>0?mouthIndex-1:1):1; - // The lower arrival continues from the previous passage point through its - // mouth. The upper departure approaches its mouth from the cave and then - // continues toward control point 1, so the two route tangents have opposite - // signs even though both portal offsets lie on the cave side. + if(lowerSide&&mouthIndex==0)return false; + const std::size_t span=lowerSide?mouthIndex-1:0; + const double endpointT=lowerSide?1.0:0.0; const std::int64_t direction=lowerSide?1:-1; - const std::int64_t tx=direction*(passage.controlPoints[mouthIndex].positionSubvoxels[0] - -passage.controlPoints[tangentIndex].positionSubvoxels[0]); - const std::int64_t ty=direction*(passage.controlPoints[mouthIndex].positionSubvoxels[1] - -passage.controlPoints[tangentIndex].positionSubvoxels[1]); - const auto tangentLength=static_cast(integerSqrt( - static_cast(tx*tx+ty*ty))); - if(tangentLength<=0)return false; - routeTangentQ16={tx*65536/tangentLength,ty*65536/tangentLength}; + std::vector splineControls; + std::vector floorProfile; + splineControls.reserve(passage.controlPoints.size()); + floorProfile.reserve(passage.controlPoints.size()); + for(const auto& control:passage.controlPoints) + { + const std::int64_t x=qToMicrometres(control.positionSubvoxels[0]); + const std::int64_t y=qToMicrometres(control.positionSubvoxels[1]); + const std::int64_t z=qToMicrometres(control.positionSubvoxels[2]); + const std::int64_t radius=qToMicrometres(control.radiusSubvoxels); + splineControls.push_back({static_cast(x),static_cast(y), + static_cast(z),static_cast(radius)}); + floorProfile.push_back(static_cast(z-radius)); + } + auto derivative=VoxelForgeCore::PassageSpline::EvaluateRawDerivative( + splineControls,span,endpointT,MicrometresPerMetre); + if(!lowerSide) + { + derivative[0]=-derivative[0]; + derivative[1]=-derivative[1]; + } + const double tangentLength=std::hypot(derivative[0],derivative[1]); + if(!(tangentLength>1.0e-9)||!std::isfinite(tangentLength))return false; + routeTangentQ16={ + static_cast(std::llround(derivative[0]*65536.0/tangentLength)), + static_cast(std::llround(derivative[1]*65536.0/tangentLength))}; + const auto passageSegments=VoxelForgeCore::PassageSpline::BuildSegments( + splineControls,splineControls.size()-1,2.0,MicrometresPerMetre, + floorProfile); + double spanHorizontalLengthMicrometres=0.0; + for(const auto& segment:passageSegments) + if(segment.controlSpan==span) + spanHorizontalLengthMicrometres+=std::hypot( + segment.b[0]-segment.a[0],segment.b[1]-segment.a[1]); + if(!(spanHorizontalLengthMicrometres>1.0e-9))return false; const auto& mouthControl=passage.controlPoints[mouthIndex]; - const auto& tangentControl=passage.controlPoints[tangentIndex]; const std::int64_t mouthFloor=mouthControl.positionSubvoxels[2] -mouthControl.radiusSubvoxels; - const std::int64_t tangentFloor=tangentControl.positionSubvoxels[2] - -tangentControl.radiusSubvoxels; - // Resolved passage mouths already store the player center at floor + 1.13 m. - // Cave edge control points store the floor itself, so start from the passage - // control's floor rather than reusing the player-center mouth height. + const std::size_t tangentIndex=lowerSide?mouthIndex-1:1; + const double passageFloorDelta=direction*(floorProfile[mouthIndex] + -floorProfile[tangentIndex]); + const double slope=passageFloorDelta/spanHorizontalLengthMicrometres; + floorSlopeQ16=static_cast(std::llround(slope*65536.0)); + point[0]=qToMicrometres(mouthControl.positionSubvoxels[0]); + point[1]=qToMicrometres(mouthControl.positionSubvoxels[1]); point[2]=qToMicrometres(mouthFloor); - const std::int64_t passageFloorDelta=direction*(mouthFloor-tangentFloor); - floorSlopeQ16=passageFloorDelta*65536/tangentLength; - const auto authoredRouteTangentQ16=routeTangentQ16; - routeTangentQ16=chooseMouthRouteTangent(passage,mouthIndex,lowerSide, - authoredRouteTangentQ16,floorSlopeQ16,point); - const std::int64_t tangentAlignment=routeTangentQ16[0] - *authoredRouteTangentQ16[0]+routeTangentQ16[1] - *authoredRouteTangentQ16[1]; - floorSlopeQ16=floorSlopeQ16*tangentAlignment/(65536ll*65536ll); - const std::int64_t offsetDirection=lowerSide?1:-1; - point[0]+=offsetDirection*routeTangentQ16[0]*MouthOverlap/65536; - point[1]+=offsetDirection*routeTangentQ16[1]*MouthOverlap/65536; - point[2]+=offsetDirection*floorSlopeQ16*MouthOverlap/65536; return true; }; FPoint routeStart{},routeEnd{}; @@ -2738,7 +2643,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, controls.size()-1,2.0,1000000.0); const double totalLength=segments.empty()?0.0: segments.back().alongMetres+segments.back().lengthMetres; - constexpr double PortalExclusionMetres=10.0; + constexpr double PortalExclusionMetres=2.0; for(const auto& segment:segments) { const double midpoint=segment.alongMetres+segment.lengthMetres*0.5; @@ -2815,7 +2720,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, if(routePassageClearanceSpans.empty()||points.size()<2) return std::numeric_limits::infinity(); const double totalLength=routeLength(points)/1.0e6; - constexpr double PortalExclusionMetres=10.0; + constexpr double PortalExclusionMetres=2.0; double minimumClearance=std::numeric_limits::infinity(); double along=0.0; for(std::size_t index=1;index& tangentQ16,std::int64_t floorSlopeQ16, + std::int64_t profileStartDistance,std::int64_t profileEndDistance) + { + const std::size_t mouthIndex=lowerSide + ?std::min(passage.lowerMouthControlIndex,passage.controlPoints.size()-1):0; + const auto& mouth=passage.controlPoints[mouthIndex]; + const std::int64_t mouthRadius=qToMicrometres(mouth.radiusSubvoxels); + auto edge=makeEdge(std::move(id),std::move(startAnchor),start,endNode, + mouthRadius,profileStartDistance,profileEndDistance); + edge.boreRadiusMicrometres=mouthRadius; + edge.boreHeightMicrometres=2*mouthRadius; + edge.smoothWidthMicrometres=static_cast(std::llround( + VoxelForgeCore::PassageSpline::FlatFloorCornerBlendMetres + *MicrometresPerMetre)); + edge.unionWidthMicrometres=smooth; + const FPoint end={endNode.point[0],endNode.point[1],endNode.point[2]}; + const std::int64_t horizontalRun=distance2d(start,end); + const std::int64_t controlHandle=std::max(1, + std::min(PortalLeadLength,std::max(1,horizontalRun))/3); + edge.controlPointsMicrometres[1][0]=start[0] + +tangentQ16[0]*controlHandle/65536; + edge.controlPointsMicrometres[1][1]=start[1] + +tangentQ16[1]*controlHandle/65536; + edge.controlPointsMicrometres[1][2]=start[2] + +floorSlopeQ16*controlHandle/65536; + return edge; + }; struct FPlannedShaftLanding { FRoomNode node; @@ -3396,9 +3331,13 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, { const auto& node=nodes[nodeIndex]; const std::string edgeId=routePrefix+"route-edge-"+std::to_string(nodeIndex); - auto edge=makeEdge(edgeId,previousAnchor,previousPoint,node, - selectedBoreRadius(edgeId), - previousProfileDistance,node.floorProfileDistanceMicrometres); + auto edge=nodeIndex==0 + ?makePassageMouthEdge(edgeId,previousAnchor,previousPoint,node, + *arrivalPassage,true,arrivalTangentQ16,arrivalFloorSlopeQ16, + previousProfileDistance,node.floorProfileDistanceMicrometres) + :makeEdge(edgeId,previousAnchor,previousPoint,node, + selectedBoreRadius(edgeId),previousProfileDistance, + node.floorProfileDistanceMicrometres); edges.push_back(std::move(edge)); previousPoint={node.point[0],node.point[1],node.point[2]}; previousProfileDistance=node.floorProfileDistanceMicrometres; @@ -3407,8 +3346,9 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, if(!nodes.empty()&&departurePassage) { const std::string departureEdgeId=routePrefix+"departure-edge"; - auto edge=makeEdge(departureEdgeId,departurePassage->stableId+"/upper-mouth", - routeEnd,nodes.back(),selectedBoreRadius(departureEdgeId), + auto edge=makePassageMouthEdge(departureEdgeId, + departurePassage->stableId+"/upper-mouth",routeEnd,nodes.back(), + *departurePassage,false,departureTangentQ16,departureFloorSlopeQ16, totalRouteLength,nodes.back().floorProfileDistanceMicrometres); edges.push_back(std::move(edge)); } diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 1a57f85..d17aa02 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -4443,11 +4443,12 @@ private: buildSegmentNode(buildSegmentNode,0,runtime.segmentOrder.size()); const double radius=edge.boreRadiusMicrometres/1000000.0; const double height=edge.boreHeightMicrometres/1000000.0; + const bool passageMouthTube=IsPlannerPassageMouthContinuationEdge(edge); if(radius>0.0&&height>0.0) for(auto& node:runtime.segmentNodes) for(std::size_t axis=0;axis<3;++axis) { - const double scale=axis==2?height:radius; + const double scale=axis==2&&!passageMouthTube?height:radius; node.minimumNormalized[axis]=node.minimum[axis]/scale; node.maximumNormalized[axis]=node.maximum[axis]/scale; } @@ -5728,6 +5729,8 @@ public: } else if(feature.edge&&!feature.segments.empty()) { + if(IsPlannerPassageMouthContinuationEdge(*feature.edge)) + return {0.0,0.0,false}; const double radius=feature.edge->boreRadiusMicrometres/1000000.0; const double height=feature.edge->boreHeightMicrometres/1000000.0; if(!(radius>0.0&&height>0.0))return {0.0,0.0,false}; @@ -7832,6 +7835,14 @@ private: return composed; } + static bool IsPlannerPassageMouthContinuationEdge( + const VoxelForgeCore::ResolvedCavernEdgeV1& edge) + { + const bool sharedPlannerEdge=edge.stableId.starts_with("tunnel-network/") + ||edge.stableId.starts_with("vertical-shafts/"); + return sharedPlannerEdge&&edge.startAnchorStableId.ends_with("-mouth"); + } + static double SmoothMinMetres(double a,double b,double width) { if(!(width>0.0))return std::min(a,b); @@ -7997,9 +8008,11 @@ private: double nearestFloor=0.0,nearestGrade=0.0,nearestRadial=0.0; const double radius=feature.edge->boreRadiusMicrometres/1000000.0; const double height=feature.edge->boreHeightMicrometres/1000000.0; + const bool passageMouthTube= + IsPlannerPassageMouthContinuationEdge(*feature.edge); constexpr double SegmentDistanceTieToleranceMetres=1.0e-12; if(!(radius>0.0&&height>0.0))continue; - const double minScale=std::min(radius,height); + const double minScale=passageMouthTube?radius:std::min(radius,height); std::uint32_t nearestSegment=std::numeric_limits::max(); const auto evaluateSegment=[&](std::uint32_t segmentIndex) { @@ -8010,7 +8023,9 @@ private: { const double delta=segment.end[axis]-segment.start[axis]; lengthSquared+=delta*delta; - dot+=(point[axis]-segment.start[axis])*delta; + const double query=axis==2&&passageMouthTube + ?point[axis]-radius:point[axis]; + dot+=(query-segment.start[axis])*delta; } const double t=lengthSquared>0.0 ?std::clamp(dot/lengthSquared,0.0,1.0):0.0; @@ -8020,11 +8035,12 @@ private: const double delta=point[axis]-(segment.start[axis] +t*(segment.end[axis]-segment.start[axis])); if(axis<2)radialSquared+=delta*delta; - else vertical=delta; + else vertical=delta-(passageMouthTube?radius:0.0); } + const double verticalScale=passageMouthTube?radius:height; const double normalized=std::sqrt(radialSquared/(radius*radius) - +vertical*vertical/(height*height)); - double distance=(normalized-1.0)*std::min(radius,height); + +vertical*vertical/(verticalScale*verticalScale)); + double distance=(normalized-1.0)*minScale; if(feature.irregularityAmplitudeVoxels>0.0f) { const float floorVoxels=static_cast(nearestFloor/metresPerVoxel); @@ -8049,8 +8065,10 @@ private: ?(segment.end[2]-segment.start[2])/horizontal:0.0; } }; + const double verticalScale=passageMouthTube?radius:height; const std::array normalizedPoint={ - point[0]/radius,point[1]/radius,point[2]/height}; + point[0]/radius,point[1]/radius, + (point[2]-(passageMouthTube?radius:0.0))/verticalScale}; const auto nodeLowerBound=[&](const FCavernRuntimeFeature::FSegmentNode& node) { double gapSquared=0.0; diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h index 4e20068..0c3cbb7 100644 --- a/Standalone/Source/WorldAssemblyPrototype.h +++ b/Standalone/Source/WorldAssemblyPrototype.h @@ -1732,6 +1732,44 @@ namespace StandaloneWorldAssemblyPrototype if(std::abs(a[axis]-b[axis])>2)return false; return true; }; + const auto pointAlongEdge=[&](const ResolvedCavernEdgeV1& edge, + double distanceFromStart) + { + const auto evaluate=[&](double t) + { + const double one=1.0-t; + const std::array weights={one*one*one, + 3.0*one*one*t,3.0*one*t*t,t*t*t}; + std::array point{}; + for(std::size_t control=0;control<4;++control) + for(std::size_t axis=0;axis<3;++axis) + point[axis]+=weights[control] + *edge.controlPointsMicrometres[control][axis]; + return point; + }; + auto previous=evaluate(0.0); + double travelled=0.0; + constexpr int Samples=256; + for(int sample=1;sample<=Samples;++sample) + { + const auto next=evaluate(static_cast(sample)/Samples); + const double segmentLength=std::hypot(next[0]-previous[0], + next[1]-previous[1])/1000000.0; + if(travelled+segmentLength>=distanceFromStart + &&segmentLength>1.0e-9) + { + const double t=std::clamp((distanceFromStart-travelled) + /segmentLength,0.0,1.0); + std::array point{}; + for(std::size_t axis=0;axis<3;++axis) + point[axis]=std::lerp(previous[axis],next[axis],t); + return point; + } + travelled+=segmentLength; + previous=next; + } + return evaluate(1.0); + }; auto start=arrival->lowerMouthSubvoxels; appendRoutePoint("000-arrival",start); for(std::size_t edgeIndex=0;edgeIndex<256;++edgeIndex) @@ -1755,15 +1793,14 @@ namespace StandaloneWorldAssemblyPrototype } if(edgeIndex==0) { - auto inner=edge->controlPointsMicrometres.front(); - // Keep the exact authored mouth anchor and use the planner - // edge's floor height for the first cave-side walking point. - // Passage composition uses these same two points to blend the - // floor profile into the native cave route. - inner[2]+=PlayerCenterAboveFloorMicrometres; + const auto edgeStart=pointAlongEdge(*edge,2.0); const std::array point={ - toCoreSubvoxels(inner[0]),toCoreSubvoxels(inner[1]), - toCoreSubvoxels(inner[2])}; + toCoreSubvoxels(static_cast(std::llround(edgeStart[0]))), + toCoreSubvoxels(static_cast(std::llround(edgeStart[1]))), + toCoreSubvoxels(static_cast(std::llround(edgeStart[2] + +PlayerCenterAboveFloorMicrometres)))}; + // Keep a walking point on the actual edge curve so audits and + // passage floor blending follow the same mouth continuation. if(!sameRoutePoint(point,start)) appendRoutePoint("000-edge-start",point); } @@ -1783,11 +1820,12 @@ namespace StandaloneWorldAssemblyPrototype &&value.stableId==departureEdgeId;}); if(departureEdge==layout.cavernEdges.end()) {error="native cave route lost its planned departure edge";return false;} - auto inner=departureEdge->controlPointsMicrometres.front(); - inner[2]+=PlayerCenterAboveFloorMicrometres; + const auto edgeInner=pointAlongEdge(*departureEdge,2.0); const std::array innerPoint={ - toCoreSubvoxels(inner[0]),toCoreSubvoxels(inner[1]), - toCoreSubvoxels(inner[2])}; + toCoreSubvoxels(static_cast(std::llround(edgeInner[0]))), + toCoreSubvoxels(static_cast(std::llround(edgeInner[1]))), + toCoreSubvoxels(static_cast(std::llround(edgeInner[2] + +PlayerCenterAboveFloorMicrometres)))}; auto end=departure->upperMouthSubvoxels; if(!sameRoutePoint(innerPoint,end)) appendRoutePoint("998-departure-inner",innerPoint); diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v238.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v238.baseline new file mode 100644 index 0000000..98d4cc8 --- /dev/null +++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v238.baseline @@ -0,0 +1,12 @@ +[PipelineFullDetailHash] tiles=960 density=0x91931a57a067fd92 mesh=0xa818654be89790ac payload=0x370ff80451105a39 topology=0x06e507557aa70173 +[PipelineWholeViewHash] tiles=2370 density=0x7111b4b743e03c95 mesh=0x8c7240939db21b88 payload=0x3981ff5ca7076219 topology=0x07947d2e691b2e55 +[PipelineLODHash] lod=0 tiles=512 density=0x2fd4c78685a43cfe mesh=0x50478cff1c1f8ca0 payload=0x54e48e57e0af26de topology=0x860c011cfdb9833a +[PipelineLODHash] lod=1 tiles=448 density=0x4b8d9a6dcd2942ad mesh=0x17e89b7087bfe711 payload=0x2389c109eb780db6 topology=0x7eccd4cf560d0630 +[PipelineLODHash] lod=2 tiles=320 density=0x48d4c21a852e6e21 mesh=0x5516a17618d142d5 payload=0x59cda38ab9ca6b7a topology=0x15ae353bd835e6a1 +[PipelineLODHash] lod=3 tiles=208 density=0x09b4811098a48c8d mesh=0xbac3fb6d54bca8e9 payload=0xca779555d55be411 topology=0xc63e8ee46890a03a +[PipelineLODHash] lod=4 tiles=240 density=0xfb11591772a3d18e mesh=0xebf2baee8764591b payload=0xe491d7ac9c0061ad topology=0xd618ac33b95ce494 +[PipelineLODHash] lod=5 tiles=144 density=0x2f8b7b7fb1e67258 mesh=0x4cd508cbe97344fd payload=0x2b6e96a6050458c9 topology=0x4ff6d0eda8988573 +[PipelineLODHash] lod=6 tiles=96 density=0x0114a34c8b8d6113 mesh=0xb1fec4d01ae1fa44 payload=0x3e50cfd429b8e950 topology=0x08090c96d15c1618 +[PipelineLODHash] lod=7 tiles=96 density=0x3836ed8cfce73418 mesh=0x40bc6589c5fc1aac payload=0x677349bcae16790f topology=0x36e3aa53192e6a6b +[PipelineLODHash] lod=8 tiles=306 density=0x144f3c1fd722708b mesh=0xef77b28bf7fd9453 payload=0xe3f47b37fcd61489 topology=0x5a3c73e295494e04 +[PipelineExactQueryHash] probes=9 density=0x7b7c50ac24ccadcc unclamped=0x7b7c50ac24ccadcc detail=0xefcba20c0c70c415 collision=0xe604823a249029bf path=direct-Core-queries diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v238.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v238.baseline new file mode 100644 index 0000000..e43aaf0 --- /dev/null +++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v238.baseline @@ -0,0 +1,12 @@ +[PipelineFullDetailHash] tiles=960 density=0xace56c8f205238ad mesh=0xa9c603a1248de6b8 payload=0xea55f1f4fcc6a894 topology=0x5ea7bc78a1b678ae +[PipelineWholeViewHash] tiles=2370 density=0x826704417f556bb8 mesh=0x3dc31324d910ad2b payload=0x32cd1847a8e05d0a topology=0xb800610aa85336d7 +[PipelineLODHash] lod=0 tiles=512 density=0xcd6ab28cfa504aca mesh=0xf03dc80a669fca31 payload=0x397aded1c3d2d297 topology=0xb4a1dc84fabc1fe3 +[PipelineLODHash] lod=1 tiles=448 density=0xce6e713828b2a20a mesh=0x1cf408c25d67d18c payload=0x8a2b5d20571c997e topology=0x51cd985fe8e7d100 +[PipelineLODHash] lod=2 tiles=320 density=0xd7f003d75d73e5b1 mesh=0x4e9c56918da506ae payload=0x3867c5915b409d6d topology=0x02a2a7c0ee9ac809 +[PipelineLODHash] lod=3 tiles=208 density=0xce02d09814028b15 mesh=0xe4c2addc4c54c439 payload=0x7e25d9eee6469c5a topology=0xe603e61c9b9bb733 +[PipelineLODHash] lod=4 tiles=240 density=0x0be8798407724eaf mesh=0x131c303cbbe51434 payload=0x2a90ecf0a2411ca0 topology=0xe3e054a371152e08 +[PipelineLODHash] lod=5 tiles=144 density=0xf20897ad272103a9 mesh=0x13d00b6253ae1f72 payload=0x0db3880693a962fa topology=0x462bd87a80f15e89 +[PipelineLODHash] lod=6 tiles=96 density=0xc040fcac457d6313 mesh=0x1ed132eeb718fb62 payload=0xcd0214e35577c6e3 topology=0x2c56c69e6fdcfd64 +[PipelineLODHash] lod=7 tiles=96 density=0xc2d419446355fdb8 mesh=0x4ea44452574fc84b payload=0xfd9779bedfc9ae0f topology=0x8948546e770c9b72 +[PipelineLODHash] lod=8 tiles=306 density=0x62db31696f2da831 mesh=0xd373f9e9481532fe payload=0x540a60526e0e3e46 topology=0xc725cb901e6dc335 +[PipelineExactQueryHash] probes=9 density=0xc949c4df2ce6f9d9 unclamped=0xc949c4df2ce6f9d9 detail=0x114c501e2d830d94 collision=0xe6011d3a248d4849 path=direct-Core-queries diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py index e97c250..12f4405 100644 --- a/Standalone/Tools/verify/verify.py +++ b/Standalone/Tools/verify/verify.py @@ -125,11 +125,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-v237.baseline", - "L139-v237-vertical-shafts-core-planner") + return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v238.baseline", + "L149-v238-passage-mouth-tube-joins") if recipe == "floating-islands" and style == 2: - return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v237.baseline", - "L139-v237-vertical-shafts-core-planner") + return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v238.baseline", + "L149-v238-passage-mouth-tube-joins") raise VerificationFailure(f"no pinned owner hash baseline for {recipe} style {style}")