diff --git a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp index 3fedaaa..4b0a558 100644 --- a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp +++ b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp @@ -33,26 +33,14 @@ #include "VoxelStrateMeasure.h" #include "VoxelTypes.h" // Pour VOXEL_NOISE_SCALE, SmoothStep01 #include "VoxelStrateTypes.h" -namespace -{ - using FVFPlayerDensitySampler = FTunnelSupportFloorDensitySampler; - - bool VF_ValidatePlayerFitPose( - const FVoxelStrateMeasureSettings& Settings, - const FVector& CandidateFeetPoint, - float StrateTopZ, - float StrateBottomZ, - float BoundarySealThickness, - FVFPlayerDensitySampler SampleDensity, - FVector& OutPoint); - -} #include "VoxelNoise.h" // Pure FBM used by the slab landing query #include "VoxelPassageGeometry.h" #include "VoxelTerrainOpDefinition.h" namespace { + using FVFPlayerDensitySampler = TFunctionRef; + // Density coordinates and cached cull radii are float-valued. FVector is double-valued in // UE5, so using FVector::DistSquared here needlessly widens every candidate reject. Keep the // cull in the same build-fixed float domain as the density query; the exact field output is @@ -83,171 +71,394 @@ namespace return DX * DX + DY * DY + DZ * DZ; } - // A graph tunnel's rounded capsule is a candidate natural floor only in the part of the tube - // where the player's complete support footprint remains inside a sufficiently wide, - // sufficiently flat cross-section. This analytic prefilter only saves density probes; it is - // never the decision by itself. The exact no-floor field is passed through the same player-fit - // validator used by the capability measurement, and failure retains the old support slab. - bool VF_IsNaturalTunnelFloorWalkable( - const FCachedTunnel& Tunnel, - float WorldX, - float WorldY, - float FloorZ, - float SupportRadius, - int32 SegmentIndex, - const FTunnelSupportFloorDensitySampler* NaturalSampler, - float StrateTopZ, - float StrateBottomZ, - float BoundarySealThickness) + struct FVFTunnelShapeEvaluation { - if (SegmentIndex < 0 - || SegmentIndex + 1 >= Tunnel.WorldControlPoints.Num() - || Tunnel.WorldControlRadii.Num() != Tunnel.WorldControlPoints.Num()) + float SDF = FLT_MAX; + bool bSupportFloor = false; + }; + + // FMath::PerlinNoise2D is the same two-octave field used by room floors. The derivative bound + // below is deliberately loose: each Grad2 component is in [-1, 1], the interpolation + // difference is bounded by four, and SmoothCurve' <= 1.875. Keeping this bound conservative + // is what lets the corridor use the authored relief without turning the walkability promise + // into a sampling accident. + constexpr float VF_Perlin2DPartialAbsBound = 8.5f; + constexpr float VF_MaxTunnelFloorReliefScale = 0.20f; + + // Keep corridor relief on the exact two-octave field already used by room floors. The + // endpoint fade is not a second noise system: it is the landing apron that lets the swept + // corridor meet the room's own floor without a seed-dependent vertical lip. + FORCEINLINE float VF_TunnelFloorRelief( + float X, float Y, const FCachedTunnel& Tunnel, + float Envelope, float ReliefScale) + { + if (!(Tunnel.FloorReliefStrength > 0.0f) + || !FMath::IsFinite(Tunnel.FloorReliefStrength) + || !FMath::IsFinite(Tunnel.FloorReliefFrequency) + || !(Envelope > 0.0f) + || !(ReliefScale > 0.0f)) { - return false; + return 0.0f; } - const FVector& Start = Tunnel.WorldControlPoints[SegmentIndex]; - const FVector& End = Tunnel.WorldControlPoints[SegmentIndex + 1]; - const float StartRadius = FMath::Abs(Tunnel.WorldControlRadii[SegmentIndex]); - const float EndRadius = FMath::Abs(Tunnel.WorldControlRadii[SegmentIndex + 1]); - const FVector2D Delta(End.X - Start.X, End.Y - Start.Y); + const float RF = Tunnel.FloorReliefFrequency; + const float SF = static_cast(Tunnel.FloorSeed) * 0.00001f; + float Noise = FMath::PerlinNoise2D( + FVector2D(X * RF + SF, Y * RF + SF * 1.7f)) * 0.65f; + Noise += FMath::PerlinNoise2D( + FVector2D(X * RF * 2.3f + SF * 3.1f, + Y * RF * 2.3f + SF * 5.3f)) * 0.35f; + return Noise * VOXEL_NOISE_SCALE * Tunnel.FloorReliefStrength + * Envelope * ReliefScale; + } + + FORCEINLINE bool VF_TunnelFloorNeedsTerrace( + const FVector& A, float RadiusA, + const FVector& B, float RadiusB) + { + const float HorizontalRun = FVector2D( + static_cast(B.X - A.X), static_cast(B.Y - A.Y)).Size(); + const float FloorDelta = FMath::Abs( + VoxelPassageGeometry::TunnelFloorZ(B, FMath::Abs(RadiusB)) + - VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA))); + return HorizontalRun > KINDA_SMALL_NUMBER + && FloorDelta > HorizontalRun + * VoxelPassageGeometry::PlayerWalkableFloorMaxGradient; + } + + FORCEINLINE float VF_TunnelFloorProfileT( + const FVector& A, float RadiusA, + const FVector& B, float RadiusB, + float T) + { + const float FloorA = VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA)); + const float FloorB = VoxelPassageGeometry::TunnelFloorZ(B, FMath::Abs(RadiusB)); + const float HorizontalRun = FVector2D( + static_cast(B.X - A.X), static_cast(B.Y - A.Y)).Size(); + const float FloorDelta = FloorB - FloorA; + if (VF_TunnelFloorNeedsTerrace(A, RadiusA, B, RadiusB)) + { + // A steep room-to-room change is a staircase, not a steep walking ramp. Every + // vertical riser is at most the authored 45 cm player step; climbing systems may + // still choose to treat those risers as explicit climb surfaces later. + // Leave a small deterministic margin for floating-point evaluation at the riser. The + // terrace has no relief (see VF_TunnelFloorReliefScale), so this is the complete + // adjacent-column step budget rather than a visual tuning constant. + constexpr float TerraceStepSafety = 0.95f; + const float SafeStepHeight = + VoxelPassageGeometry::MaxStepHeightVoxels * TerraceStepSafety; + const int32 NumSteps = FMath::Clamp( + FMath::CeilToInt(FMath::Abs(FloorDelta) + / SafeStepHeight), + 1, 4096); + const float ClampedT = FMath::Clamp(T, 0.0f, 1.0f); + const float StairT = ClampedT >= 1.0f + ? 1.0f + : FMath::FloorToFloat(ClampedT * static_cast(NumSteps)) + / static_cast(NumSteps); + return StairT; + } + return FMath::Clamp(T, 0.0f, 1.0f); + } + + FORCEINLINE float VF_TunnelFloorReliefScale( + const FVector& A, float RadiusA, + const FVector& B, float RadiusB, + int32 SegmentIndex, int32 NumSegments, + const FCachedTunnel& Tunnel) + { + if (!(Tunnel.FloorReliefStrength > 0.0f) + || !FMath::IsFinite(Tunnel.FloorReliefStrength) + || !FMath::IsFinite(Tunnel.FloorReliefFrequency)) + { + return 0.0f; + } + + // A terrace already consumes the available vertical step at its risers. Keeping the + // terrace segments level and relief-free makes the adjacent-column step bound exact; + // gentle segments retain the bounded authored relief below. + if (VF_TunnelFloorNeedsTerrace(A, RadiusA, B, RadiusB)) + { + return 0.0f; + } + + const float HorizontalRun = FVector2D( + static_cast(B.X - A.X), static_cast(B.Y - A.Y)).Size(); + if (!(HorizontalRun > KINDA_SMALL_NUMBER)) + { + return 0.0f; + } + + const float FloorDelta = FMath::Abs( + VoxelPassageGeometry::TunnelFloorZ(B, FMath::Abs(RadiusB)) + - VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA))); + const float BaseGradient = VF_TunnelFloorNeedsTerrace( + A, RadiusA, B, RadiusB) + ? 0.0f : FloorDelta / HorizontalRun; + const float AvailableGradient = + VoxelPassageGeometry::PlayerWalkableFloorMaxGradient - BaseGradient; + if (!(AvailableGradient > 0.0f)) + { + return 0.0f; + } + + const float Frequency = FMath::Abs(Tunnel.FloorReliefFrequency); + const float Amplitude = FMath::Abs(Tunnel.FloorReliefStrength) + * VOXEL_NOISE_SCALE; + const float OctaveFrequencyWeight = 0.65f + 0.35f * 2.3f; + const float NoiseGradientBound = + 1.4142135623730951f * VF_Perlin2DPartialAbsBound + * OctaveFrequencyWeight * Frequency * Amplitude; + const int32 LandingEnvelopeCount = (SegmentIndex == 0 ? 1 : 0) + + (SegmentIndex + 1 == NumSegments ? 1 : 0); + const float EnvelopeDerivativeBound = LandingEnvelopeCount > 0 + ? (1.5f * static_cast(LandingEnvelopeCount) + / VoxelPassageGeometry::WalkableTunnelLandingApronVoxels) + * Amplitude + : 0.0f; + const float ReliefGradientBound = NoiseGradientBound + + EnvelopeDerivativeBound; + if (!(ReliefGradientBound > KINDA_SMALL_NUMBER) + || !FMath::IsFinite(ReliefGradientBound)) + { + return 0.0f; + } + + return FMath::Clamp( + FMath::Min( + VF_MaxTunnelFloorReliefScale, + AvailableGradient / ReliefGradientBound), + 0.0f, VF_MaxTunnelFloorReliefScale); + } + + FORCEINLINE float VF_TunnelFloorAtSegment( + const FVector& A, float RadiusA, + const FVector& B, float RadiusB, + float T, int32 SegmentIndex, int32 NumSegments, + const FVector& Position, + const FCachedTunnel& Tunnel) + { + const float ProfileT = VF_TunnelFloorProfileT( + A, RadiusA, B, RadiusB, T); + float FloorZ = FMath::Lerp( + VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA)), + VoxelPassageGeometry::TunnelFloorZ(B, FMath::Abs(RadiusB)), + ProfileT); + + const float HorizontalRun = FVector2D( + static_cast(B.X - A.X), static_cast(B.Y - A.Y)).Size(); + float ReliefEnvelope = 1.0f; + if (SegmentIndex == 0) + { + ReliefEnvelope *= SmoothStep01(FMath::Clamp( + (T * HorizontalRun) + / VoxelPassageGeometry::WalkableTunnelLandingApronVoxels, + 0.0f, 1.0f)); + } + if (SegmentIndex + 1 == NumSegments) + { + ReliefEnvelope *= SmoothStep01(FMath::Clamp( + ((1.0f - T) * HorizontalRun) + / VoxelPassageGeometry::WalkableTunnelLandingApronVoxels, + 0.0f, 1.0f)); + } + const float ReliefScale = VF_TunnelFloorReliefScale( + A, RadiusA, B, RadiusB, SegmentIndex, NumSegments, Tunnel); + FloorZ += VF_TunnelFloorRelief( + static_cast(Position.X), static_cast(Position.Y), + Tunnel, ReliefEnvelope, ReliefScale); + return FloorZ; + } + + FORCEINLINE bool VF_ProjectTunnelSegmentXY( + const FVector& Position, const FVector& A, const FVector& B, + float& OutT, float& OutDistanceSquared) + { + const FVector2D AXY(static_cast(A.X), static_cast(A.Y)); + const FVector2D Delta( + static_cast(B.X - A.X), static_cast(B.Y - A.Y)); const float LengthSquared = Delta.SizeSquared(); - if (!(LengthSquared > KINDA_SMALL_NUMBER) - || !FMath::IsFinite(StartRadius) - || !FMath::IsFinite(EndRadius) - || !FMath::IsFinite(FloorZ) - || !FMath::IsFinite(SupportRadius)) + if (LengthSquared <= KINDA_SMALL_NUMBER) { + OutT = 0.0f; + OutDistanceSquared = FLT_MAX; return false; } - const FVector2D Query(WorldX, WorldY); - const float T = FMath::Clamp( - FVector2D::DotProduct(Query - FVector2D(Start.X, Start.Y), Delta) - / LengthSquared, + const FVector2D QueryXY( + static_cast(Position.X), static_cast(Position.Y)); + OutT = FMath::Clamp( + FVector2D::DotProduct(QueryXY - AXY, Delta) / LengthSquared, 0.0f, 1.0f); - const FVector2D Closest = FVector2D(Start.X, Start.Y) + Delta * T; - const float CentrelineDistance = (Query - Closest).Size(); - const float TubeRadius = FMath::Lerp(StartRadius, EndRadius, T); - constexpr float CapsuleSafetyMargin = 0.25f; - // The first candidate was still too permissive: the analytic tube was walkable, but - // roughness outside the structural core could remove the last supporting voxels. Only - // the broad, nearly-centreline part of the widest tubes is a certificate strong enough to - // replace the authored floor; all marginal geometry keeps the slab. - constexpr float NaturalMinimumTubeRadius = 7.5f; - constexpr float NaturalCentrelineTolerance = 0.5f; - constexpr float NaturalWallMargin = 2.0f; - constexpr float NaturalMaximumAxialGradient = 0.12f; - const float PlayerRadius = VoxelPassageGeometry::PlayerRadiusVoxels; - const float FootprintRadius = PlayerRadius + CapsuleSafetyMargin; - if (TubeRadius < NaturalMinimumTubeRadius - || CentrelineDistance > NaturalCentrelineTolerance - || !(TubeRadius > 0.0f) - || CentrelineDistance + FootprintRadius - > TubeRadius - NaturalWallMargin - || SupportRadius + KINDA_SMALL_NUMBER < PlayerRadius) + const FVector2D ClosestXY = AXY + Delta * OutT; + OutDistanceSquared = (QueryXY - ClosestXY).SizeSquared(); + return true; + } + + static FVFTunnelShapeEvaluation VF_EvaluateSweptTunnelChain( + const FVector& Position, + const TArray& ControlPoints, + const TArray& ControlRadii, + const FCachedTunnel& Tunnel, + float SDFBlendRadius, + bool bApplyFloorCut) + { + FVFTunnelShapeEvaluation Result; + if (ControlPoints.Num() < 2 + || ControlRadii.Num() != ControlPoints.Num()) { - return false; + return Result; } - const float FarthestSupportRadius = CentrelineDistance + FootprintRadius; - const float CrossSectionHalfWidth = FMath::Sqrt(FMath::Max( - TubeRadius * TubeRadius - - FarthestSupportRadius * FarthestSupportRadius, - 0.0f)); - if (2.0f * CrossSectionHalfWidth - < VoxelPassageGeometry::PlayerHeightVoxels - + 2.0f * CapsuleSafetyMargin) + const int32 NumSegments = ControlPoints.Num() - 1; + const float FloorBlend = FMath::Max(SDFBlendRadius, 0.0f) * 0.35f; + for (int32 SegmentIndex = 0; + SegmentIndex < NumSegments; + ++SegmentIndex) { - return false; - } + const FVector& A = ControlPoints[SegmentIndex]; + const FVector& B = ControlPoints[SegmentIndex + 1]; + const float RadiusA = ControlRadii[SegmentIndex]; + const float RadiusB = ControlRadii[SegmentIndex + 1]; + const float SegmentSDF = VoxelSDF::TaperedCapsule( + Position, A, B, RadiusA, RadiusB); - // The round floor's steepest support-patch slope is attained at its outer edge. Keep it - // no steeper than the authored 15-degree tunnel contract, rather than using a generous - // engine walkability ceiling merely to make the optimization fire more often. - const float RadialDenominator = FMath::Sqrt(FMath::Max( - TubeRadius * TubeRadius - - FarthestSupportRadius * FarthestSupportRadius, - KINDA_SMALL_NUMBER)); - const float RadialGradient = FarthestSupportRadius / RadialDenominator; - const float AxialGradient = VoxelPassageGeometry::TunnelFloorGradient( - Start, StartRadius, End, EndRadius); - if (!FMath::IsFinite(RadialGradient) - || !FMath::IsFinite(AxialGradient) - || RadialGradient > VoxelPassageGeometry::WalkableTunnelMaxGradient - || AxialGradient > NaturalMaximumAxialGradient) - { - return false; - } - - // Reject bends and segment endpoints where the projected support plane changes abruptly. - // The flat-floor path remains responsible for those transition columns. - constexpr float ProbeDistance = 1.0f; - const FVector ProbePositions[] = { - FVector(WorldX - ProbeDistance, WorldY, 0.0f), - FVector(WorldX + ProbeDistance, WorldY, 0.0f), - FVector(WorldX, WorldY - ProbeDistance, 0.0f), - FVector(WorldX, WorldY + ProbeDistance, 0.0f) - }; - for (const FVector& Probe : ProbePositions) - { - float ProbeFloorZ = 0.0f; - float ProbeSupportRadius = 0.0f; - int32 ProbeSegmentIndex = INDEX_NONE; - if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor( - Tunnel.WorldControlPoints, - Tunnel.WorldControlRadii, - Probe, - ProbeFloorZ, - ProbeSupportRadius, - &ProbeSegmentIndex) - || ProbeSegmentIndex != SegmentIndex - || FMath::Abs(ProbeFloorZ - FloorZ) - > VoxelPassageGeometry::WalkableTunnelMaxGradient - * ProbeDistance + CapsuleSafetyMargin) + float T = 0.0f; + float HorizontalDistanceSquared = FLT_MAX; + const bool bHasHorizontalProjection = VF_ProjectTunnelSegmentXY( + Position, A, B, T, HorizontalDistanceSquared); + if (!bHasHorizontalProjection) { - return false; + // A vertical/degenerate XY segment has no finite floor projection. Retain the + // capsule for air, but never invent a support surface there. + Result.SDF = FMath::Min(Result.SDF, SegmentSDF); + continue; + } + + const float FloorZ = VF_TunnelFloorAtSegment( + A, RadiusA, B, RadiusB, + T, SegmentIndex, NumSegments, Position, Tunnel); + const float SegmentResultSDF = bApplyFloorCut + ? VoxelSDF::SmoothMax( + SegmentSDF, FloorZ - static_cast(Position.Z), FloorBlend) + : SegmentSDF; + Result.SDF = FMath::Min(Result.SDF, SegmentResultSDF); + + const float SupportRadius = FMath::Max( + FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f, + VoxelPassageGeometry::PlayerRadiusVoxels); + if (SegmentSDF <= 0.0f + && HorizontalDistanceSquared <= FMath::Square(SupportRadius) + && Position.Z >= FloorZ + - VoxelPassageGeometry::LandingFloorThicknessVoxels + && Position.Z <= FloorZ + + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels) + { + Result.bSupportFloor = true; } } + return Result; + } - if (NaturalSampler == nullptr) + static FVFTunnelShapeEvaluation VF_EvaluateSweptTunnel( + const FVector& Position, + const FCachedTunnel& Tunnel, + bool bWorldChain, + float SDFBlendRadius, + bool bApplyFloorCut) + { + const TArray& ShapeControlPoints = bWorldChain + ? Tunnel.WorldControlPoints : Tunnel.ControlPoints; + const TArray& ShapeControlRadii = bWorldChain + ? Tunnel.WorldControlRadii : Tunnel.ControlRadii; + if (ShapeControlPoints.Num() >= 2 + && ShapeControlRadii.Num() == ShapeControlPoints.Num()) + { + return VF_EvaluateSweptTunnelChain( + Position, ShapeControlPoints, ShapeControlRadii, + Tunnel, SDFBlendRadius, bApplyFloorCut); + } + + if (Tunnel.bHasMidpoint) + { + TArray Points; + TArray Radii; + Points.Add(Tunnel.EndpointA); + Points.Add(Tunnel.Midpoint); + Points.Add(Tunnel.EndpointB); + Radii.Add(Tunnel.RadiusA); + Radii.Add(Tunnel.RadiusMid); + Radii.Add(Tunnel.RadiusB); + // The fallback descriptor is only retained for cooked/old cache compatibility. The + // temporary arrays stay inline and do not allocate on the normal chain path. + return VF_EvaluateSweptTunnelChain( + Position, Points, Radii, + Tunnel, SDFBlendRadius, bApplyFloorCut); + } + + TArray Points; + TArray Radii; + Points.Add(Tunnel.EndpointA); + Points.Add(Tunnel.EndpointB); + Radii.Add(Tunnel.RadiusA); + Radii.Add(Tunnel.RadiusB); + return VF_EvaluateSweptTunnelChain( + Position, Points, Radii, + Tunnel, SDFBlendRadius, bApplyFloorCut); + } + + static bool VF_ProjectSweptTunnelFloor( + const FVector& Position, + const FCachedTunnel& Tunnel, + bool bWorldChain, + float& OutFloorZ, + float& OutSupportRadius) + { + const TArray& ControlPoints = bWorldChain + ? Tunnel.WorldControlPoints : Tunnel.ControlPoints; + const TArray& ControlRadii = bWorldChain + ? Tunnel.WorldControlRadii : Tunnel.ControlRadii; + if (ControlPoints.Num() < 2 + || ControlRadii.Num() != ControlPoints.Num()) { return false; } - FVoxelStrateMeasureSettings FitSettings; - const float MaxStepHeightVoxels = - FVoxelPlayerCapsuleConstants::MaxStepHeightMeters - / FVoxelPlayerCapsuleConstants::VoxelSizeMeters; - // The projected graph floor is the centreline's authored support level, but natural rock - // may leave a legal footing a little above or below it. Search a small fixed offset set so - // that the conditional path does not mistake a vertically displaced natural ledge for a - // failed walkability test. The validator still enforces the actual max-step and capsule - // clearance rules for every trial; no geometry is accepted by this search alone. - constexpr int32 FeetOffsetCount = 3; - const float FeetOffsets[FeetOffsetCount] = { - 0.0f, -1.0f, 1.0f - }; - for (int32 OffsetIndex = 0; OffsetIndex < FeetOffsetCount; ++OffsetIndex) + const int32 NumSegments = ControlPoints.Num() - 1; + float BestDistanceSquared = FLT_MAX; + bool bFound = false; + for (int32 SegmentIndex = 0; + SegmentIndex < NumSegments; + ++SegmentIndex) { - const FVector CandidateFeet( - WorldX, WorldY, - FloorZ + MaxStepHeightVoxels + 0.5f + FeetOffsets[OffsetIndex]); - FVector FitPoint = FVector::ZeroVector; - if (VF_ValidatePlayerFitPose( - FitSettings, - CandidateFeet, - StrateTopZ, - StrateBottomZ, - BoundarySealThickness, - *NaturalSampler, - FitPoint)) + float T = 0.0f; + float DistanceSquared = FLT_MAX; + if (!VF_ProjectTunnelSegmentXY( + Position, + ControlPoints[SegmentIndex], ControlPoints[SegmentIndex + 1], + T, DistanceSquared) + || DistanceSquared >= BestDistanceSquared) { - return true; + continue; } + + const float RadiusA = ControlRadii[SegmentIndex]; + const float RadiusB = ControlRadii[SegmentIndex + 1]; + OutFloorZ = VF_TunnelFloorAtSegment( + ControlPoints[SegmentIndex], RadiusA, + ControlPoints[SegmentIndex + 1], RadiusB, + T, SegmentIndex, NumSegments, Position, Tunnel); + OutSupportRadius = FMath::Max( + FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f, + VoxelPassageGeometry::PlayerRadiusVoxels); + BestDistanceSquared = DistanceSquared; + bFound = true; } - return false; + + return bFound + && FMath::IsFinite(OutFloorZ) + && FMath::IsFinite(OutSupportRadius) + && BestDistanceSquared <= FMath::Square(OutSupportRadius); } } @@ -268,6 +479,7 @@ void FChunkSDFCache::Reset() SupportColumnCellsY = 0; SupportColumnEntries.Reset(); SupportColumns.Reset(); + SDFBlendRadius = 0.0f; } void FChunkSDFCache::Release() @@ -3679,6 +3891,7 @@ void VoxelCaveMorphology::BuildChunkCache( { // Clear previous data (arrays keep their allocation for reuse) OutCache.Reset(); + OutCache.SDFBlendRadius = Params.SDFBlendRadius; // Combine world seed with strate index so each strate gets unique caves const uint32 StrateSeed = VoxelCaveMorphology::MakeStrateSeed(Seed, StrateIndex); @@ -4235,6 +4448,9 @@ void VoxelCaveMorphology::BuildChunkCache( CT.EndpointB = EndB; CT.RadiusA = RadA; CT.RadiusB = RadB; + CT.FloorReliefStrength = Params.FloorReliefStrength; + CT.FloorReliefFrequency = Params.FloorReliefFrequency; + CT.FloorSeed = VoxelHash::Mix(TunnelHash ^ 0xF100F1u); // A room edge is not a straight capsule between cell centres. Build a deterministic // chain of hash-jittered control points instead. The chain is keyed only by the pair @@ -4973,44 +5189,11 @@ float VoxelCaveMorphology::EvaluateSDFCached( VoxelDensityProfile::ECounter::CaveTunnelEvaluated); } - float TunnelSDF; - - if (Tunnel.ControlPoints.Num() >= 2 - && Tunnel.ControlRadii.Num() == Tunnel.ControlPoints.Num()) - { - // A wandering chain is evaluated segment-by-segment. Min keeps the shared control - // point watertight and preserves the old capsule union semantics; the outer - // SmoothMin is still the room/tunnel blend. - TunnelSDF = FLT_MAX; - for (int32 SegmentIndex = 0; - SegmentIndex + 1 < Tunnel.ControlPoints.Num(); - ++SegmentIndex) - { - TunnelSDF = FMath::Min( - TunnelSDF, - VoxelSDF::TaperedCapsule( - Pos, - Tunnel.ControlPoints[SegmentIndex], - Tunnel.ControlPoints[SegmentIndex + 1], - Tunnel.ControlRadii[SegmentIndex], - Tunnel.ControlRadii[SegmentIndex + 1])); - } - } - else if (Tunnel.bHasMidpoint) - { - // Backward-compatible descriptor path for older cooked caches. - float SegA = VoxelSDF::TaperedCapsule(Pos, Tunnel.EndpointA, Tunnel.Midpoint, - Tunnel.RadiusA, Tunnel.RadiusMid); - float SegB = VoxelSDF::TaperedCapsule(Pos, Tunnel.Midpoint, Tunnel.EndpointB, - Tunnel.RadiusMid, Tunnel.RadiusB); - TunnelSDF = FMath::Min(SegA, SegB); - } - else - { - // Backward-compatible straight descriptor path. - TunnelSDF = VoxelSDF::TaperedCapsule(Pos, Tunnel.EndpointA, Tunnel.EndpointB, - Tunnel.RadiusA, Tunnel.RadiusB); - } + // The corridor is a swept floor cut: each tapered capsule segment is intersected with + // its interpolated floor half-space before the tunnel union is blended into rooms. + const float TunnelSDF = VF_EvaluateSweptTunnel( + Pos, Tunnel, /*bWorldChain=*/false, BlendK, + /*bApplyFloorCut=*/true).SDF; MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK); }; @@ -5053,44 +5236,11 @@ float VoxelCaveMorphology::EvaluateTunnelCoreSDF( return; } - if (Tunnel.ControlPoints.Num() >= 2 - && Tunnel.ControlRadii.Num() == Tunnel.ControlPoints.Num()) - { - for (int32 SegmentIndex = 0; - SegmentIndex + 1 < Tunnel.ControlPoints.Num(); - ++SegmentIndex) - { - MinSDF = FMath::Min( - MinSDF, - VoxelSDF::TaperedCapsule( - Pos, - Tunnel.ControlPoints[SegmentIndex], - Tunnel.ControlPoints[SegmentIndex + 1], - Tunnel.ControlRadii[SegmentIndex], - Tunnel.ControlRadii[SegmentIndex + 1])); - } - } - else if (Tunnel.bHasMidpoint) - { - MinSDF = FMath::Min( - MinSDF, - VoxelSDF::TaperedCapsule( - Pos, Tunnel.EndpointA, Tunnel.Midpoint, - Tunnel.RadiusA, Tunnel.RadiusMid)); - MinSDF = FMath::Min( - MinSDF, - VoxelSDF::TaperedCapsule( - Pos, Tunnel.Midpoint, Tunnel.EndpointB, - Tunnel.RadiusMid, Tunnel.RadiusB)); - } - else - { - MinSDF = FMath::Min( - MinSDF, - VoxelSDF::TaperedCapsule( - Pos, Tunnel.EndpointA, Tunnel.EndpointB, - Tunnel.RadiusA, Tunnel.RadiusB)); - } + MinSDF = FMath::Min( + MinSDF, + VF_EvaluateSweptTunnel( + Pos, Tunnel, /*bWorldChain=*/false, Cache.SDFBlendRadius, + /*bApplyFloorCut=*/false).SDF); }; for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx) @@ -5154,10 +5304,6 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld( VoxelDensityProfile::ECounter::TunnelCoreEvaluated); } - const TArray& ControlPoints = bHasWorldChain - ? Tunnel.WorldControlPoints : Tunnel.ControlPoints; - const TArray& ControlRadii = bHasWorldChain - ? Tunnel.WorldControlRadii : Tunnel.ControlRadii; if (bHasWorldChain) { if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled()) @@ -5172,12 +5318,8 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld( const bool bProjectedFloor = SupportColumn != nullptr ? VoxelCaveMorphology::GetTunnelSupportFloorColumnBand( TunnelIdx, *SupportColumn, FloorZ, SupportMinZ, SupportMaxZ) - : VoxelPassageGeometry::ProjectWalkableTunnelFloor( - ControlPoints, ControlRadii, Pos, FloorZ, SupportRadius); - // The first cull above already used WorldBoundCenter/WorldBoundRadiusSq for a - // world-chain tunnel. Repeating the same squared-distance test here only doubled - // arithmetic on every accepted tunnel; keep the projection and Z checks, which are - // the distinct support-floor predicates. + : VF_ProjectSweptTunnelFloor( + Pos, Tunnel, /*bWorldChain=*/true, FloorZ, SupportRadius); if (bProjectedFloor && WorldZ <= FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels @@ -5191,50 +5333,18 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld( if (bProjectedFloor && WorldZ <= FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels) { - // The finite floor slab owns the bottom of this tunnel. Another intersecting - // tunnel may still win below its own floor; the loop continues so that air - // ownership remains the union of all valid tunnel cores. + // Preserve the established ownership contract while the SDF-space evaluator is + // still raw: the floor owns the lower half of the corridor, and the capsule owns + // only the air above it. This diagnostic is what distinguishes a coordinate-space + // mismatch from a genuine swept-floor capability loss. return; } } - if (ControlPoints.Num() >= 2 - && ControlRadii.Num() == ControlPoints.Num()) - { - for (int32 SegmentIndex = 0; - SegmentIndex + 1 < ControlPoints.Num(); - ++SegmentIndex) - { - Result.SDF = FMath::Min( - Result.SDF, - VoxelSDF::TaperedCapsule( - Pos, - ControlPoints[SegmentIndex], - ControlPoints[SegmentIndex + 1], - ControlRadii[SegmentIndex], - ControlRadii[SegmentIndex + 1])); - } - } - else if (Tunnel.bHasMidpoint) - { - Result.SDF = FMath::Min( - Result.SDF, - VoxelSDF::TaperedCapsule( - Pos, Tunnel.EndpointA, Tunnel.Midpoint, - Tunnel.RadiusA, Tunnel.RadiusMid)); - Result.SDF = FMath::Min( - Result.SDF, - VoxelSDF::TaperedCapsule( - Pos, Tunnel.Midpoint, Tunnel.EndpointB, - Tunnel.RadiusMid, Tunnel.RadiusB)); - } - else - { - Result.SDF = FMath::Min( - Result.SDF, - VoxelSDF::TaperedCapsule( - Pos, Tunnel.EndpointA, Tunnel.EndpointB, - Tunnel.RadiusA, Tunnel.RadiusB)); - } + + const FVFTunnelShapeEvaluation TunnelShape = VF_EvaluateSweptTunnel( + Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius, + /*bApplyFloorCut=*/true); + Result.SDF = FMath::Min(Result.SDF, TunnelShape.SDF); }; for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx) @@ -5268,43 +5378,35 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( auto TestTunnel = [&](int32 TunnelIdx) -> bool { const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx]; - if (Tunnel.WorldControlPoints.Num() < 2 - || Tunnel.WorldControlRadii.Num() != Tunnel.WorldControlPoints.Num()) - { - return false; - } - - if (Tunnel.WorldInfluenceRadius > 0.0f + const bool bHasWorldChain = Tunnel.WorldControlPoints.Num() >= 2 + && Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num(); + const FVector& CenterlineMin = bHasWorldChain + ? Tunnel.WorldCenterlineMin : Tunnel.SDFCenterlineMin; + const FVector& CenterlineMax = bHasWorldChain + ? Tunnel.WorldCenterlineMax : Tunnel.SDFCenterlineMax; + const float InfluenceRadius = bHasWorldChain + ? Tunnel.WorldInfluenceRadius : Tunnel.SDFInfluenceRadius; + if (InfluenceRadius > 0.0f && VF_DistanceSquaredToAabb( - WorldX, WorldY, WorldZ, - Tunnel.WorldCenterlineMin, Tunnel.WorldCenterlineMax) - > FMath::Square(Tunnel.WorldInfluenceRadius)) + WorldX, WorldY, WorldZ, CenterlineMin, CenterlineMax) + > FMath::Square(InfluenceRadius)) { return false; } - float FloorZ = 0.0f; - float SupportRadius = 0.0f; - if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor( - Tunnel.WorldControlPoints, - Tunnel.WorldControlRadii, - Pos, - FloorZ, - SupportRadius, - nullptr) - || WorldZ > FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels - || WorldZ < FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels) + const FVector& BoundCenter = bHasWorldChain + ? Tunnel.WorldBoundCenter : Tunnel.BoundCenter; + const float BoundRadiusSq = bHasWorldChain + ? Tunnel.WorldBoundRadiusSq : Tunnel.BoundRadiusSq; + if (BoundRadiusSq > 0.0f + && VF_FloatDistSquared(WorldX, WorldY, WorldZ, BoundCenter) > BoundRadiusSq) { return false; } - if (Tunnel.WorldBoundRadiusSq <= 0.0f - || VF_FloatDistSquared(WorldX, WorldY, WorldZ, Tunnel.WorldBoundCenter) - <= Tunnel.WorldBoundRadiusSq) - { - return true; - } - return false; + return VF_EvaluateSweptTunnel( + Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius, + /*bApplyFloorCut=*/true).bSupportFloor; }; bool bSupport = false; @@ -5327,11 +5429,7 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( void VoxelCaveMorphology::BuildTunnelSupportFloorColumn( float WorldX, float WorldY, const FChunkSDFCache& Cache, - FTunnelSupportFloorColumn& OutColumn, - const FTunnelSupportFloorDensitySampler* NaturalSampler, - float NaturalStrateTopZ, - float NaturalStrateBottomZ, - float NaturalBoundarySealThickness) + FTunnelSupportFloorColumn& OutColumn) { OutColumn.Reset(); @@ -5355,45 +5453,16 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn( continue; } const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx]; - if (Tunnel.WorldControlPoints.Num() < 2 - || Tunnel.WorldControlRadii.Num() != Tunnel.WorldControlPoints.Num()) - { - continue; - } - const FVector ColumnPosition(WorldX, WorldY, 0.0f); float FloorZ = 0.0f; float SupportRadius = 0.0f; - int32 SegmentIndex = INDEX_NONE; - if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor( - Tunnel.WorldControlPoints, - Tunnel.WorldControlRadii, - ColumnPosition, - FloorZ, - SupportRadius, - &SegmentIndex)) + if (!VF_ProjectSweptTunnelFloor( + ColumnPosition, Tunnel, /*bWorldChain=*/true, + FloorZ, SupportRadius)) { continue; } - if (NaturalSampler != nullptr && VoxelDensityProfile::AreCountersEnabled()) - { - VoxelDensityProfile::AddCounter( - VoxelDensityProfile::ECounter::TunnelSupportFloorNaturalTests); - } - if (VF_IsNaturalTunnelFloorWalkable( - Tunnel, WorldX, WorldY, FloorZ, SupportRadius, SegmentIndex, - NaturalSampler, NaturalStrateTopZ, NaturalStrateBottomZ, - NaturalBoundarySealThickness)) - { - if (VoxelDensityProfile::AreCountersEnabled()) - { - VoxelDensityProfile::AddCounter( - VoxelDensityProfile::ECounter::TunnelSupportFloorNaturalSkips); - } - continue; - } - float MinZ = FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels; float MaxZ = FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels; diff --git a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index 624e974..b175c4e 100644 --- a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -3591,10 +3591,8 @@ namespace { public: FRoomGraphSource(const FStrateGenerationParams& InP, int32 InSeed, - const UVoxelStrateManager* InManager, - const UVoxelGenerator* InGenerator = nullptr) + const UVoxelStrateManager* InManager) : P(InP), Seed(InSeed), SeedU((uint32)InSeed), Manager(InManager) - , Generator(InGenerator) , ManagerLifetimeId(InManager ? InManager->GetCacheLifetimeId() : 0) , ParamsFingerprint(FCrc::MemCrc32(&InP, sizeof(InP))) {} @@ -3788,40 +3786,13 @@ namespace * Lu par les arches, les dômes, le pincement et le biais de sol. */ int32 GetNearestRoomIdx() const { return State().NearestRoom; } - bool HasTunnelAirGeometry() const - { - return P.RoomDensity > 0.0f && P.RoomSpacing > 0.0f; - } - void BuildSupportColumn( float WorldX, float WorldY, const FChunkSDFCache& Cache, FTunnelSupportFloorColumn& OutColumn) const { - if (Generator == nullptr) - { - VoxelCaveMorphology::BuildTunnelSupportFloorColumn( - WorldX, WorldY, Cache, OutColumn); - return; - } - - // This is a synchronous, non-owning view of the exact field with only the graph - // support slab disabled. BuildTunnelSupportFloorColumn consumes it immediately and - // retains only the proven interval decision in the worker-local column cache. - const auto SampleWithoutTunnelSupportFloor = - [this](float X, float Y, float Z) -> float - { - return Generator->GetDensityWithParams( - X, Y, Z, P, ParamsFingerprint, LayoutVersion, - /*bApplyLegacyStructuralPosts=*/true, - /*bApplyTunnelSupportFloor=*/false); - }; - FTunnelSupportFloorDensitySampler NaturalSampler = - SampleWithoutTunnelSupportFloor; VoxelCaveMorphology::BuildTunnelSupportFloorColumn( - WorldX, WorldY, Cache, OutColumn, &NaturalSampler, - P.StrateTopWorldZ, P.StrateBottomWorldZ, - P.BoundarySealThickness); + WorldX, WorldY, Cache, OutColumn); } const FTunnelSupportFloorColumn* GetSupportColumn(const FVector& Position) const @@ -3857,18 +3828,6 @@ namespace return &S.FractionalSupportColumn; } - float GetTunnelCoreSDF() const - { - FState& S = State(); - const FVector& Position = S.LastWorldPosition; - const FTunnelSupportFloorColumn* SupportColumn = GetSupportColumn(Position); - S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld( - Position.X, Position.Y, Position.Z, GetCache(), SupportColumn); - S.LastTunnelCoreEvaluationPosition = Position; - S.bLastTunnelCoreWorldEvaluationValid = true; - return S.LastTunnelCoreWorldEvaluation.SDF; - } - bool TryGetLastTunnelCoreWorldEvaluation( FTunnelCoreWorldEvaluation& OutEvaluation) const override { @@ -4200,7 +4159,8 @@ namespace } float CaveSDF = VoxelCaveMorphology::EvaluateSDFCached( - WarpedX, WarpedY, WarpedZ, GetCache(), P.SDFBlendRadius, &S.NearestRoom); + WarpedX, WarpedY, WarpedZ, GetCache(), P.SDFBlendRadius, + &S.NearestRoom); //--------------------------------------------------------------- // PITS & CHEMINÉES — coordonnées RÉELLES, SmoothMin dans le même canal SDF @@ -6640,131 +6600,16 @@ namespace const TCHAR* DebugName() const override { return TEXT("RoomGraphSource"); } - bool IsTunnelSupportFloor() const - { - FState& S = State(); - const FVector& Position = S.LastWorldPosition; - if (VoxelDensityProfile::AreCountersEnabled()) - { - VoxelDensityProfile::AddCounter( - VoxelDensityProfile::ECounter::TunnelSupportFloorQueries); - } - - const FTunnelSupportFloorColumn* Column = GetSupportColumn(Position); - const bool bSupport = Column != nullptr - && VoxelCaveMorphology::IsTunnelSupportFloorColumnZ( - static_cast(Position.Z), *Column); - S.LastTunnelCoreWorldEvaluation = FTunnelCoreWorldEvaluation(); - S.LastTunnelCoreWorldEvaluation.bSupportFloor = bSupport; - S.LastTunnelCoreEvaluationPosition = Position; - S.bLastTunnelCoreWorldEvaluationValid = true; - return bSupport; - } - private: FStrateGenerationParams P; int32 Seed; uint32 SeedU; TWeakObjectPtr Manager; - const UVoxelGenerator* Generator = nullptr; uint64 ManagerLifetimeId = 0; uint32 ParamsFingerprint; uint32 LayoutVersion = 0; }; - // The graph tunnel floor closes the rounded bottom of the core into a finite, walkable support - // slab. It is separate from the inter-strate passage floor: this owns only the wandering - // intra-strate graph edges and does not create any new corridor. - class FCaveTunnelFloorOp final : public IVoxelDensityOp - { - public: - FCaveTunnelFloorOp(const FRoomGraphSource* InRooms, float InBase) - : Rooms(InRooms), Base(InBase) - {} - - EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; } - EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; } - EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } - EVoxelOpResourceMask RequiredResources() const override - { - return VoxelOpResources::RoomGeometry; - } - bool IsAdditive() const override { return false; } - void PrepareChunk(const FVoxelOpContext&) override {} - - void Eval(float, float, float, FVoxelOpSample& InOut) const override - { - if (Rooms != nullptr && Rooms->IsTunnelSupportFloor()) - { - InOut.Density = FMath::Max( - InOut.Density, - FMath::Max(Base * 2.0f, 1.0f)); - } - } - - EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override - { - return Rooms != nullptr && Rooms->HasTunnelAirGeometry() - ? EVoxelOpEffect::Both - : EVoxelOpEffect::Identity; - } - - const TCHAR* DebugName() const override { return TEXT("CaveTunnelFloorOp"); } - - private: - const FRoomGraphSource* Rooms = nullptr; - float Base = 8.0f; - }; - - // A graph tunnel's raw SDF is the structural air contract for the wandering chain. It runs - // after the passage post because a different passage can project a solid support floor onto - // the same voxel; the core keeps its own half-voxel support band and restores the route above - // it. FRoomGraphSource evaluates this post against the cached world-space chain, so the cave - // warp cannot turn a walkable floor ramp into a vertical break. - class FCaveTunnelAirOp final : public IVoxelDensityOp - { - public: - FCaveTunnelAirOp(const FRoomGraphSource* InRooms, float InBase, float InInset) - : Rooms(InRooms), Base(InBase), Inset(InInset) - {} - - EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; } - EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; } - EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } - EVoxelOpResourceMask RequiredResources() const override - { - return VoxelOpResources::RoomGeometry; - } - bool IsAdditive() const override { return false; } - void PrepareChunk(const FVoxelOpContext&) override {} - - void Eval(float, float, float, FVoxelOpSample& InOut) const override - { - if (Rooms != nullptr - && !Rooms->IsTunnelSupportFloor() - && Rooms->GetTunnelCoreSDF() < -Inset) - { - InOut.Density = FMath::Min( - InOut.Density, - -FMath::Max(Base * 2.0f, 1.0f)); - } - } - - EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override - { - return Rooms != nullptr && Rooms->HasTunnelAirGeometry() - ? EVoxelOpEffect::CarveOnly - : EVoxelOpEffect::Identity; - } - - const TCHAR* DebugName() const override { return TEXT("CaveTunnelAirOp"); } - - private: - const FRoomGraphSource* Rooms = nullptr; - float Base = 8.0f; - float Inset = 2.0f; - }; - //========================================================================= // LE GATE DE BOÎTE DES MODIFICATEURS DE CAVE //========================================================================= @@ -9442,8 +9287,7 @@ namespace VoxelDensityOps void BuildTunnelNetworkStack(FVoxelOpStack& OutStack, const FStrateGenerationParams& P, int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, - bool bAppendStructuralPosts, - const UVoxelGenerator* Generator) + bool bAppendStructuralPosts) { // ⚠️ ÉTAPES A + B + C1 — LA PILE EST COMPLÈTE POUR CET ARCHÉTYPE. // Portés : échelle verticale, roc de base, warp, graphe de salles (+ pits + cheminées), @@ -9488,7 +9332,7 @@ namespace VoxelDensityOps // La source de salles est retenue par pointeur non possédant : les terrasses re-interrogent // son cache SDF en Z±1. Même motif que `FShaftFieldSource` → `FShaftLedgeMod`. TUniquePtr RoomSource = MakeUnique( - P, Seed, StrateManager, Generator); + P, Seed, StrateManager); const FRoomGraphSource* RoomPtr = RoomSource.Get(); OutStack.Add(MakeConstantRockSource(P.BaseDensity)); diff --git a/Source/VoxelForge/Private/VoxelDensityProfile.cpp b/Source/VoxelForge/Private/VoxelDensityProfile.cpp index 89985c0..b221e2f 100644 --- a/Source/VoxelForge/Private/VoxelDensityProfile.cpp +++ b/Source/VoxelForge/Private/VoxelDensityProfile.cpp @@ -414,8 +414,6 @@ namespace VoxelDensityProfile case ECounter::TunnelSupportColumnCandidates:return TEXT("TunnelSupportColumnCandidates"); case ECounter::TunnelSupportFloorQueries:return TEXT("TunnelSupportFloorQueries"); case ECounter::TunnelSupportFloorChecks:return TEXT("TunnelSupportFloorChecks"); - case ECounter::TunnelSupportFloorNaturalTests:return TEXT("TunnelSupportFloorNaturalTests"); - case ECounter::TunnelSupportFloorNaturalSkips:return TEXT("TunnelSupportFloorNaturalSkips"); case ECounter::PassageCandidates: return TEXT("PassageCandidates"); case ECounter::PassageEvaluated: return TEXT("PassageEvaluated"); case ECounter::MesherBlockTests: return TEXT("MesherBlockTests"); @@ -467,8 +465,6 @@ namespace VoxelDensityProfile { TEXT("OriginSpineOp"), EBucket::OriginSpineOp }, { TEXT("BoundarySealOp"), EBucket::BoundarySealOp }, { TEXT("PassageCarveOp"), EBucket::PassageCarveOp }, - { TEXT("CaveTunnelFloorOp"), EBucket::CaveTunnelFloorOp }, - { TEXT("CaveTunnelAirOp"), EBucket::CaveTunnelAirOp }, { TEXT("XYEdgeSealOp"), EBucket::XYEdgeSealOp }, { TEXT("DensityPrologue"), EBucket::DensityPrologue }, { TEXT("DensityCore"), EBucket::DensityCore }, @@ -529,8 +525,6 @@ namespace VoxelDensityProfile case EBucket::OriginSpineOp: return TEXT("OriginSpineOp"); case EBucket::BoundarySealOp: return TEXT("BoundarySealOp"); case EBucket::PassageCarveOp: return TEXT("PassageCarveOp"); - case EBucket::CaveTunnelFloorOp:return TEXT("CaveTunnelFloorOp"); - case EBucket::CaveTunnelAirOp: return TEXT("CaveTunnelAirOp"); case EBucket::XYEdgeSealOp: return TEXT("XYEdgeSealOp"); case EBucket::OtherOp: return TEXT("OtherOp"); case EBucket::DensityPrologue: return TEXT("DensityPrologue"); diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index 5cc115b..7b4d080 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -507,11 +507,7 @@ namespace static void BuildTunnelSupportColumnsForChunk( FChunkSDFCache& Cache, - const FIntVector& ChunkCoord, - const UVoxelGenerator* Generator, - const FStrateGenerationParams& Params, - uint32 ParamsFingerprint, - uint32 LayoutVersion); + const FIntVector& ChunkCoord); static const FTunnelSupportFloorColumn* FindTunnelSupportColumn( const FChunkSDFCache& Cache, @@ -607,7 +603,6 @@ namespace } void PrepareTunnelCoreCache( - const UVoxelGenerator& Generator, const UVoxelStrateManager& Manager, const FIntVector& ChunkCoord, const FStrateGenerationParams& Params, @@ -644,8 +639,7 @@ namespace Params, static_cast(Manager.GetWorldSeed()), StrateIndex, nullptr); BuildTunnelSupportColumnsForChunk( - OutState.Cache, ChunkCoord, &Generator, Params, - ParamsFingerprint, LayoutVersion); + OutState.Cache, ChunkCoord); OutState.OwnerId = OwnerId; OutState.ManagerLifetimeId = ManagerLifetimeId; OutState.Chunk = ChunkCoord; @@ -656,11 +650,7 @@ namespace static void BuildTunnelSupportColumnsForChunk( FChunkSDFCache& Cache, - const FIntVector& ChunkCoord, - const UVoxelGenerator* Generator, - const FStrateGenerationParams& Params, - uint32 ParamsFingerprint, - uint32 LayoutVersion) + const FIntVector& ChunkCoord) { const int32 MinX = ChunkCoord.X * CHUNK_SIZE - 1; const int32 MinY = ChunkCoord.Y * CHUNK_SIZE - 1; @@ -672,25 +662,6 @@ namespace Cache.SupportColumnEntries.Init(INDEX_NONE, Cells * Cells); Cache.SupportColumns.Reset(); - // The conditional floor is tested against the exact density pipeline with only the - // graph support slab disabled. The sampler is synchronous and lives only for this cache - // build; the immutable cache stores the resulting interval decision, never the callback. - const auto SampleWithoutTunnelSupportFloor = - [Generator, &Params, ParamsFingerprint, LayoutVersion]( - float X, float Y, float Z) -> float - { - return Generator != nullptr - ? Generator->GetDensityWithParams( - X, Y, Z, Params, ParamsFingerprint, LayoutVersion, - /*bApplyLegacyStructuralPosts=*/true, - /*bApplyTunnelSupportFloor=*/false) - : 0.0f; - }; - FTunnelSupportFloorDensitySampler NaturalSamplerRef = - SampleWithoutTunnelSupportFloor; - const FTunnelSupportFloorDensitySampler* NaturalSampler = - Generator != nullptr ? &NaturalSamplerRef : nullptr; - // Empty columns are represented only by INDEX_NONE in the slot map, so // each stored column retains the inline bands for actual graph tubes. for (int32 Y = 0; Y < Cells; ++Y) @@ -702,11 +673,7 @@ namespace static_cast(MinX + X), static_cast(MinY + Y), Cache, - Column, - NaturalSampler, - Params.StrateTopWorldZ, - Params.StrateBottomWorldZ, - Params.BoundarySealThickness); + Column); if (Column.Intervals.Num() == 0) { continue; @@ -770,8 +737,7 @@ namespace */ bool VF_BuildOpStackForChunk(ECaveGeneratorType Type, FVoxelStackParamRefs& Refs, int32 Seed, float SpineRadius, const UVoxelStrateManager* SM, - FVoxelOpStack& OutStack, FVoxelOpContext& OutCtx, - const UVoxelGenerator* Generator = nullptr) + FVoxelOpStack& OutStack, FVoxelOpContext& OutCtx) { switch (Type) { @@ -830,7 +796,7 @@ namespace OutCtx.StrateBottomWorldZ = Refs.Tunnel->StrateBottomWorldZ; VoxelDensityOps::BuildTunnelNetworkStack( OutStack, *Refs.Tunnel, Seed, SpineRadius, SM, - /*bAppendStructuralPosts=*/true, Generator); + /*bAppendStructuralPosts=*/true); return true; default: @@ -1296,7 +1262,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co CP_UseOpStack = VF_BuildOpStackForChunk( CP_GenType, Refs, Seed, OriginSpineRadius, - StrateManager, CP_OpStack, OpCtx, this); + StrateManager, CP_OpStack, OpCtx); // Le test appelle PrepareChunk, pas la production : c'est exactement la divergence // qui rend un opérateur vert en test et faux en jeu. Les sept `PrepareChunk` @@ -1330,7 +1296,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co if (bCanPrepareNativeTunnelCore) { PrepareTunnelCoreCache( - *this, *StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP, + *StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP, LayoutVersion, ManagerLifetimeId, DensityCacheOwnerId, GTunnelCoreCache); } else @@ -1574,6 +1540,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co if (bHaveTunnelCore) { const bool bTunnelSupportFloor = TunnelCore.bSupportFloor; + // The swept capsule owns the relief profile, while this finite support band is + // the conservative post-disturbance backstop that keeps terrain modifiers from + // removing the only player support at a mouth or a narrow cross-section. if (bTunnelSupportFloor) { Result = FMath::Min( @@ -1684,8 +1653,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float WorldZ, const FStrateGenerationParams& Params, uint32 ParamsFingerprint, uint32 LayoutVersion, - bool bApplyLegacyStructuralPosts, - bool bApplyTunnelSupportFloor) const + bool bApplyLegacyStructuralPosts) const { //========================================================================= // STRATE DENSITY FUNCTION (Morphology Pipeline) @@ -2222,8 +2190,10 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo // // We use the cached SDF so this costs two extra SDF evaluations per voxel, // only when near a surface — the common case is cheap (DistFromSurface > TerraceRange). - float SDF_Zp1 = VoxelCaveMorphology::EvaluateSDFCached(WorldX, WorldY, WorldZ + 1.0f, SDFCache, Params.SDFBlendRadius); - float SDF_Zm1 = VoxelCaveMorphology::EvaluateSDFCached(WorldX, WorldY, WorldZ - 1.0f, SDFCache, Params.SDFBlendRadius); + float SDF_Zp1 = VoxelCaveMorphology::EvaluateSDFCached( + WorldX, WorldY, WorldZ + 1.0f, SDFCache, Params.SDFBlendRadius); + float SDF_Zm1 = VoxelCaveMorphology::EvaluateSDFCached( + WorldX, WorldY, WorldZ - 1.0f, SDFCache, Params.SDFBlendRadius); float GZ = (SDF_Zp1 - SDF_Zm1) * 0.5f; // Normalized vertical component: 1 = perfectly horizontal surface (floor/ceiling) // 0 = perfectly vertical surface (wall) @@ -2748,12 +2718,13 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo if (bApplyLegacyStructuralPosts) { // Terrain operations and passage support floors are allowed to write solid density, but - // the graph's own tunnel floor/core are structural route geometry. Direct callers of + // the graph's own tunnel floor/core are structural route geometry. Direct callers of // GetDensityWithParams retain this historical tail; GetDensityAt skips it and applies the // same predicates once in its common MC-space post section below both generation paths. - const bool bTunnelSupportFloor = bApplyTunnelSupportFloor - && VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( - WorldX, WorldY, WorldZ, SDFCache); + const bool bTunnelSupportFloor = VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( + WorldX, WorldY, WorldZ, SDFCache); + // Keep the direct legacy entry point capability-safe for callers outside the canonical + // operator-stack path; its support band follows the same relieved swept floor. if (bTunnelSupportFloor) { Density = FMath::Max(Density, FMath::Max(Params.BaseDensity * 2.0f, 1.0f)); @@ -4791,7 +4762,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in FVoxelOpStack TileStack; if (!VF_BuildOpStackForChunk(CaveType, Refs, Seed, OriginSpineRadius, - StrateManager, TileStack, OpCtx, this)) + StrateManager, TileStack, OpCtx)) { // Strate dégénérée ou archétype non porté : `GetDensityAt` retomberait sur le `switch`, // donc la pile ne décrit pas ce que le mesher verra. Aucun verdict. diff --git a/Source/VoxelForge/Public/VoxelCaveMorphology.h b/Source/VoxelForge/Public/VoxelCaveMorphology.h index 5363623..e097ffd 100644 --- a/Source/VoxelForge/Public/VoxelCaveMorphology.h +++ b/Source/VoxelForge/Public/VoxelCaveMorphology.h @@ -640,6 +640,11 @@ struct FCachedTunnel // turning a gentle authored floor ramp into a vertical break when the cave warp is nonlinear. TArray WorldControlPoints; TArray WorldControlRadii; + // The corridor floor is a swept SmoothMax cut, not a later slab. These are copied from the + // same strate fields used by room floors; the seed is derived once from the tunnel pair hash. + float FloorReliefStrength = 0.0f; + float FloorReliefFrequency = 0.015f; + uint32 FloorSeed = 0; FVector WorldBoundCenter = FVector::ZeroVector; float WorldBoundRadiusSq = 0.0f; // Centerline AABBs and scalar influence radii used by the immutable broad phase. The @@ -698,12 +703,6 @@ struct FTunnelSupportFloorColumn } }; -// A non-owning probe used only while a support-column is being prepared. The caller supplies the -// exact MC-facing density field with the graph support slab disabled; the morphology layer then -// runs the same player-fit predicate used by the capability measurement. A missing sampler is a -// deliberate safe fallback: retain the authored support band rather than making an unproven skip. -using FTunnelSupportFloorDensitySampler = TFunctionRef; - // A pre-baked pit shaft — position and dimensions resolved during BuildChunkCache. // // Pits are included in the main SDF evaluation (SmoothMin alongside rooms and @@ -771,6 +770,10 @@ struct FChunkSDFCache TArray SupportColumnEntries; TArray SupportColumns; + // Kept with the cache because the world-space structural evaluator has no params argument. + // A zero value is a valid hard-intersection fallback for legacy/empty caches. + float SDFBlendRadius = 0.0f; + // Allocator-backed bytes owned by this cache, including the nested tunnel chains. The returned // value excludes sizeof(FChunkSDFCache) itself so callers can add the enclosing entry's inline // storage exactly once. @@ -909,18 +912,13 @@ namespace VoxelCaveMorphology const FChunkSDFCache& Cache ); - // Build/query the same support-floor predicate at column granularity. When NaturalSampler is - // supplied, a projected band is omitted only if the exact field with that graph slab disabled - // accepts a player-fit pose at this column. Without that proof the old interval is retained. - // The sampler is synchronous and non-owning; it is never retained by the cache. + // Build/query the same support-floor predicate at column granularity. The column stores its + // finite support interval, so every Z sample in a chunk reuses the same deterministic + // projection and never re-runs a natural-floor validator. VOXELFORGE_API void BuildTunnelSupportFloorColumn( float WorldX, float WorldY, const FChunkSDFCache& Cache, - FTunnelSupportFloorColumn& OutColumn, - const FTunnelSupportFloorDensitySampler* NaturalSampler = nullptr, - float NaturalStrateTopZ = 0.0f, - float NaturalStrateBottomZ = 0.0f, - float NaturalBoundarySealThickness = 0.0f + FTunnelSupportFloorColumn& OutColumn ); VOXELFORGE_API bool IsTunnelSupportFloorColumnZ( diff --git a/Source/VoxelForge/Public/VoxelDensityOpStack.h b/Source/VoxelForge/Public/VoxelDensityOpStack.h index 7e4f77f..aec682c 100644 --- a/Source/VoxelForge/Public/VoxelDensityOpStack.h +++ b/Source/VoxelForge/Public/VoxelDensityOpStack.h @@ -474,8 +474,7 @@ namespace VoxelDensityOps const FStrateGenerationParams& P, int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, - bool bAppendStructuralPosts = true, - const UVoxelGenerator* Generator = nullptr); + bool bAppendStructuralPosts = true); /** * DIAGNOSTIC — la ventilation par CLASSE DE PRIMITIVE de la dernière propagation d'intervalle diff --git a/Source/VoxelForge/Public/VoxelDensityProfile.h b/Source/VoxelForge/Public/VoxelDensityProfile.h index 6dc6eb2..259fb21 100644 --- a/Source/VoxelForge/Public/VoxelDensityProfile.h +++ b/Source/VoxelForge/Public/VoxelDensityProfile.h @@ -54,8 +54,6 @@ namespace VoxelDensityProfile OriginSpineOp, BoundarySealOp, PassageCarveOp, - CaveTunnelFloorOp, - CaveTunnelAirOp, XYEdgeSealOp, OtherOp, // Non-overlapping top-level phases. The named operation buckets above are intentionally @@ -98,8 +96,6 @@ namespace VoxelDensityProfile TunnelSupportColumnCandidates, TunnelSupportFloorQueries, TunnelSupportFloorChecks, - TunnelSupportFloorNaturalTests, - TunnelSupportFloorNaturalSkips, PassageCandidates, PassageEvaluated, MesherBlockTests, diff --git a/Source/VoxelForge/Public/VoxelGenerator.h b/Source/VoxelForge/Public/VoxelGenerator.h index 73b938d..e27be55 100644 --- a/Source/VoxelForge/Public/VoxelGenerator.h +++ b/Source/VoxelForge/Public/VoxelGenerator.h @@ -167,8 +167,7 @@ public: float GetDensityWithParams(float WorldX, float WorldY, float WorldZ, const FStrateGenerationParams& Params, uint32 ParamsFingerprint, uint32 LayoutVersion, - bool bApplyLegacyStructuralPosts = true, - bool bApplyTunnelSupportFloor = true) const; + bool bApplyLegacyStructuralPosts = true) const; /** * Densité pour une strate Slab (FlatPlain / CrystalChamber). diff --git a/Source/VoxelForge/Public/VoxelPassageGeometry.h b/Source/VoxelForge/Public/VoxelPassageGeometry.h index 206a036..9ae7a0b 100644 --- a/Source/VoxelForge/Public/VoxelPassageGeometry.h +++ b/Source/VoxelForge/Public/VoxelPassageGeometry.h @@ -21,6 +21,7 @@ namespace VoxelPassageGeometry MinimumTurnFloorWidthVoxels * 0.5f; constexpr float MinimumRoomHeightVoxels = 12.0f; // 3 m constexpr float LandingFloorThicknessVoxels = 3.0f; + constexpr float MaxStepHeightVoxels = 1.8f; // 45 cm at 25 cm/voxel constexpr float SealSafetyMarginVoxels = 4.0f; constexpr float RoomRoundingVoxels = 1.5f; constexpr float LandingCarveBlendVoxels = 4.0f; @@ -47,6 +48,10 @@ namespace VoxelPassageGeometry // latter is a scramble limit, not a sensible default for a tunnel the player walks through. constexpr float WalkableTunnelMaxGradientDegrees = 15.0f; constexpr float WalkableTunnelMaxGradient = 0.2679491924311227f; // tan(15 degrees) + // Player-fit measurements use the engine's 44 degree walkability contract. The 15 degree + // value above remains the preferred authored tunnel ramp; this wider bound is only the + // fallback threshold at which a room-to-room floor must become a terrace. + constexpr float PlayerWalkableFloorMaxGradient = 0.9656887748070738f; // tan(44 degrees) // The walkability contract is about the floor carried by the tube, not only its centreline. // A tapered tube changes that floor by the radius delta, so the construction and the audit