diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 4799b9e..e3ecf5e 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -512,7 +512,8 @@ namespace VoxelForgeCore // boundary-blended host rock from widening the authored passage wall. // v238 plans one resolved passage spline with explicit landing chambers. // v239 plans their floor planes as the passage's continuous floor profile. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 239; + // v240 gives landing chambers and their route approaches tapered floor support. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 240; enum class FieldChannel : std::uint32_t { diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 6b1a6af..f0e0da4 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -57,6 +57,7 @@ static constexpr double VF_PassageShellThicknessMetres=0.75; static constexpr double VF_PassageWallReliefMetres=0.21; static constexpr double VF_PassageCornerBlendMetres=0.50; static constexpr double VF_PassageReachMarginMetres=0.01; +static constexpr double VF_LandingSupportBoundsDepthMetres=2.20; // Radial support in metres: shell + maximum relief + the smooth-max support // (corner-blend width / 6) + a 1 cm guard for float and bound rounding. static constexpr double VF_PassageInfluenceReachMetres= @@ -1260,11 +1261,92 @@ public: constexpr double WallReliefMetres=VF_PassageWallReliefMetres; constexpr double OpenAirSideWallHeightMetres=1.0; constexpr double MouthShellBlendMetres=5.0; + // Landing floors carry a tapered footing below their walk plane. Surface + // landings use local native ground within a 2 m budget; cave and shaft + // landings reuse the cave planner's 0.75 m floor-support depth. + constexpr double LandingSupportMaximumDepthMetres=2.0; + constexpr double LandingSupportFallbackDepthMetres=0.75; + constexpr double LandingSupportTaperMetres=1.0; + constexpr double LandingSupportGroundEmbedMetres=0.20; + constexpr double LandingSupportTopBiasSubvoxels=2.0; const double qPerMetre=Q*1000000.0/ std::max(1,ResolvedLayout->coreVoxelSizeMicrometres); const double qx=static_cast(X)*Q,qy=static_cast(Y)*Q, qz=static_cast(Z)*Q; const double shellThickness=FloorShellThicknessMetres*qPerMetre; + const auto* queryStratum=FindBoundStratumForManagerZ(Z); + double landingSupportDistanceVoxels=std::numeric_limits::infinity(); + double landingSupportAirClipVoxels=-std::numeric_limits::infinity(); + double routeUnionDistance=std::numeric_limits::infinity(); + bool surfaceGroundQueried=false,surfaceGroundValid=false; + double surfaceGroundVoxels=0.0; + const bool surfaceLandingStratum=queryStratum&&queryStratum->Resolved + &&queryStratum->Resolved->generatorKind=="SurfaceWorld"; + const auto surfaceGroundAtQuery=[&]() + { + if(!surfaceGroundQueried) + { + surfaceGroundQueried=true; + const double layoutWidth=GetResolvedLayoutChunkWidthVoxels(); + if(layoutWidth>0.0) + { + const int32 chunkZ=FMath::FloorToInt( + static_cast(static_cast(Z)/layoutWidth)); + float ground=0.0f,ceiling=0.0f; + surfaceGroundValid=GetNativeSurfaceHeightsAt( + X,Y,chunkZ,ground,ceiling); + if(surfaceGroundValid)surfaceGroundVoxels=ground; + } + } + return surfaceGroundValid?surfaceGroundVoxels + :std::numeric_limits::quiet_NaN(); + }; + const auto includeLandingSupport=[&](double floorSubvoxels, + double centerDistanceSubvoxels,double footprintRadiusSubvoxels) + { + if(!(footprintRadiusSubvoxels>0.0))return; + const double floorVoxels=floorSubvoxels/Q; + const double centerDistanceVoxels=centerDistanceSubvoxels/Q; + const double radiusVoxels=footprintRadiusSubvoxels/Q; + const double supportDepthVoxels=(surfaceLandingStratum + ?LandingSupportMaximumDepthMetres:LandingSupportFallbackDepthMetres) + *qPerMetre/Q; + const double taperVoxels=LandingSupportTaperMetres*qPerMetre/Q; + const double embedVoxels=LandingSupportGroundEmbedMetres*qPerMetre/Q; + const double topBiasVoxels=LandingSupportTopBiasSubvoxels/Q; + if(centerDistanceVoxels>=radiusVoxels+taperVoxels + ||static_cast(Z)(Z)>floorVoxels+topBiasVoxels)return; + + double groundAnchorVoxels=floorVoxels-supportDepthVoxels; + if(surfaceLandingStratum) + { + const double ground=surfaceGroundAtQuery(); + if(std::isfinite(ground)) + groundAnchorVoxels=std::max(groundAnchorVoxels, + std::min(ground,floorVoxels)); + } + const double radialFade=std::clamp( + (radiusVoxels+taperVoxels-centerDistanceVoxels)/taperVoxels, + 0.0,1.0); + const double smoothFade=radialFade*radialFade*(3.0-2.0*radialFade); + const double supportTop=groundAnchorVoxels + +(floorVoxels+topBiasVoxels-groundAnchorVoxels)*smoothFade; + const double supportBottom=groundAnchorVoxels-embedVoxels*smoothFade; + const double supportDistance=std::max( + centerDistanceVoxels-(radiusVoxels+taperVoxels), + std::max(static_cast(Z)-supportTop, + supportBottom-static_cast(Z))); + landingSupportDistanceVoxels=std::min(landingSupportDistanceVoxels, + supportDistance); + }; + const auto applyLandingSupport=[&](float composed) + { + if(!std::isfinite(landingSupportDistanceVoxels))return composed; + const double clippedSupportDistance=std::max(landingSupportDistanceVoxels, + landingSupportAirClipVoxels); + return std::min(composed,static_cast(clippedSupportDistance)); + }; struct FNearestTubeSample { double signedDistance=std::numeric_limits::infinity(); @@ -1284,7 +1366,6 @@ public: std::numeric_limits::infinity(); double plannedTunnelPassageShellDistance=std::numeric_limits::infinity(); const std::uint32_t reliefSeed=static_cast(Owner?Owner->Seed:0); - const auto* queryStratum=FindBoundStratumForManagerZ(Z); const bool plannedTunnelStratum=queryStratum&&queryStratum->Resolved &&(queryStratum->Resolved->generatorKind=="TunnelNetwork" ||queryStratum->Resolved->generatorKind=="VerticalShafts") @@ -1346,6 +1427,9 @@ public: const double radialDistance=DeterministicSqrt( (qx-centerX)*(qx-centerX)+(qy-centerY)*(qy-centerY)) -landingChamber->radiusSubvoxels; + includeLandingSupport(floorCenterZ, + radialDistance+landingChamber->radiusSubvoxels, + landingChamber->radiusSubvoxels); const double floorDistance=floorCenterZ-qz; const double ceilingDistance=qz-(floorCenterZ +landingChamber->clearHeightSubvoxels); @@ -1578,6 +1662,42 @@ public: visitShellSegment); } if(!nearest.valid)continue; + if(queryStratum&&queryStratum->Resolved + &&(queryStratum->StrateIndex==passage.upperStrateIndex + ||queryStratum->StrateIndex==passage.lowerStrateIndex) + &&nearest.floorSegmentIndexStrateIndex + ==passage.upperStrateIndex + &&floorAlongMetres>=-LandingSupportTaperMetres + &&floorAlongMetres<=LandingApproachLengthMetres; + const bool lowerApproach=queryStratum->StrateIndex + ==passage.lowerStrateIndex + &&constants.totalPathLengthMetres-floorAlongMetres + <=LandingApproachLengthMetres + &&constants.totalPathLengthMetres-floorAlongMetres + >=-LandingSupportTaperMetres; + if(floorSegment.routeSegment&&(upperApproach||lowerApproach)) + { + const double routeX=floorSegment.a[0] + +nearest.floorT*floorSegment.delta[0]; + const double routeY=floorSegment.a[1] + +nearest.floorT*floorSegment.delta[1]; + const double routeDistance=DeterministicSqrt( + (qx-routeX)*(qx-routeX)+(qy-routeY)*(qy-routeY)); + const double floorRadius=floorSegment.radiusA + +std::clamp(nearest.floorT,0.0,1.0)*floorSegment.radiusDelta; + const double supportRadius=std::max(0.0, + floorRadius-0.23*qPerMetre); + includeLandingSupport(nearest.floorCenterZ-nearest.floorDepth, + routeDistance,supportRadius); + } + } const double wallElevationMetres=(qz-(nearest.floorCenterZ -nearest.floorDepth))/qPerMetre; double distanceToCenter=std::max(1.0e-9, @@ -1648,6 +1768,8 @@ public: static_cast(shapedDistance)-floorShellThickness); nearest.floorShellSignedDistance=std::min( nearest.floorShellSignedDistance,floorShellDistance); + routeUnionDistance=std::min(routeUnionDistance, + static_cast(shapedDistance)); unionDistance=std::min(unionDistance,static_cast(shapedDistance)); unionFloorShellDistance=std::min(unionFloorShellDistance, nearest.floorShellSignedDistance); @@ -1657,13 +1779,18 @@ public: std::min(localShellDistance,nearest.shellSignedDistance)); } - if(!std::isfinite(unionDistance))return density; + if(!std::isfinite(unionDistance))return applyLandingSupport(density); const float passageAirDistance=static_cast(-unionDistance/Q); + // Keep support out of the passage bore while allowing it under the + // chamber's own floor, where the chamber volume is naturally open. + if(std::isfinite(routeUnionDistance)) + landingSupportAirClipVoxels=-routeUnionDistance/Q; // Keep the landing floor shell where it is exposed to the chamber. At // the tunnel joint, the passage void must be able to open through that // floor shell so its own continuous floor and shell define the throat. if(unionLandingChamberFloorShellDistance<0.0&&unionDistance>0.0) - return static_cast(unionLandingChamberFloorShellDistance/Q); + return applyLandingSupport( + static_cast(unionLandingChamberFloorShellDistance/Q)); const auto plannedPortalWalkCorridorContains=[&]() { if(!queryStratum||!queryStratum->Resolved @@ -1855,7 +1982,7 @@ public: // otherwise the neighboring field can widen its wall by a whole // shell thickness. Native graph air still wins above via the // positive nativeTunnelFieldDensity path. - return passageAirDistance; + return applyLandingSupport(passageAirDistance); } // Air is the union of the native graph and passage voids. Keeping the // signed passage distance outside the void as well gives marching cubes @@ -1865,9 +1992,10 @@ public: &&plannedTunnelPassageShellDistance<0.0&&unionDistance>0.0) { if(nativeTunnelFieldDensity>=0.0f&&plannedPortalWalkCorridorContains()) - return std::max(nativeTunnelFieldDensity,passageAirDistance); - return std::min(density, - static_cast(plannedTunnelPassageShellDistance/Q)); + return applyLandingSupport( + std::max(nativeTunnelFieldDensity,passageAirDistance)); + return applyLandingSupport(std::min(density, + static_cast(plannedTunnelPassageShellDistance/Q))); } if(nativeTunnelFieldDensity>=0.0f) { @@ -1875,29 +2003,30 @@ public: // cave voids after the explicit mouth fade has ended. Keep this scoped // to the planner-crossing passage shell so nearby portal joints stay open. if(plannedTunnelPassageShellDistance<0.0&&unionDistance>0.0) - return std::min(nativeTunnelFieldDensity, - static_cast(plannedTunnelPassageShellDistance/Q)); + return applyLandingSupport(std::min(nativeTunnelFieldDensity, + static_cast(plannedTunnelPassageShellDistance/Q))); constexpr float NativeCaveAirMarginVoxels=0.75f; if(unionDistance>0.0 &&nativeTunnelFieldDensity(unionFloorShellDistance/Q)); - return std::max(nativeTunnelFieldDensity,passageAirDistance); + return applyLandingSupport(std::min(nativeTunnelFieldDensity, + static_cast(unionFloorShellDistance/Q))); + return applyLandingSupport( + std::max(nativeTunnelFieldDensity,passageAirDistance)); } if(nativeFloorSupport!=std::numeric_limits::lowest() &&nativeFloorSupport<0.0f) - return std::min(nativeFloorSupport,unionDistance<=0.0 - ?passageAirDistance:density); + return applyLandingSupport(std::min(nativeFloorSupport,unionDistance<=0.0 + ?passageAirDistance:density)); if(nativeTunnelFieldDensity!=std::numeric_limits::lowest()) - return std::max(density,passageAirDistance); + return applyLandingSupport(std::max(density,passageAirDistance)); if(unionDistance>0.0&&unionShellDistance<0.0&&density>=0.0f) { if(connectedSurfaceLandingShellContains()) - return std::max(density,passageAirDistance); - return static_cast(unionShellDistance/Q); + return applyLandingSupport(std::max(density,passageAirDistance)); + return applyLandingSupport(static_cast(unionShellDistance/Q)); } - return std::max(density,passageAirDistance); + return applyLandingSupport(std::max(density,passageAirDistance)); } float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const @@ -2044,8 +2173,11 @@ public: static_cast(chamber.floorGradientQ16[0])/65536.0, static_cast(chamber.floorGradientQ16[1])/65536.0); const double extent=reachCoreVoxels+0.01; + const double supportDepth=(VF_LandingSupportBoundsDepthMetres*1000000.0/ + std::max(1, + ResolvedLayout->coreVoxelSizeMicrometres)); const double low[3]={cx-radius-extent,cy-radius-extent, - cz-floorExtent-extent}; + cz-floorExtent-supportDepth-extent}; const double high[3]={cx+radius+extent,cy+radius+extent, cz+floorExtent+static_cast( chamber.clearHeightSubvoxels)/Q+extent}; @@ -5484,8 +5616,12 @@ private: const double height=static_cast( chamber.clearHeightSubvoxels)/Q; const double extent=requiredReachCoreVoxels+0.01; + const double supportDepth=(VF_LandingSupportBoundsDepthMetres*1000000.0/ + std::max(1, + ResolvedLayout->coreVoxelSizeMicrometres)); const std::array minimum{ - cx-radius-extent,cy-radius-extent,cz-floorExtent-extent}; + cx-radius-extent,cy-radius-extent, + cz-floorExtent-supportDepth-extent}; const std::array maximum{ cx+radius+extent,cy+radius+extent, cz+floorExtent+height+extent};