diff --git a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp index 5e66412..8d21685 100644 --- a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp +++ b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp @@ -892,6 +892,7 @@ bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager, bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager, const FStrateSlot& Slot, VoxelForgeCore::WorldAssemblyManifest& Manifest, + const VoxelForgeCore::ResolvedLayoutSnapshotV3& Layout, double CoreVoxelSizeCm, std::int64_t LayoutChunkWidthSubvoxels, FString& OutError) { using namespace VoxelForgeCore; @@ -903,7 +904,8 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager, IslandPlan.topChunkZ = Slot.TopChunkZ; IslandPlan.bottomChunkZ = Slot.BottomChunkZ; auto& Recipe = IslandPlan.manifest.recipe; - Recipe.geometryVersion = 8; + Recipe.geometryVersion = 10; + Recipe.primaryIslandCount = 9; Recipe.terrainFollowingRoute = static_cast(Passage.Style) == 0; Recipe.layoutChunkWidthSubvoxels = LayoutChunkWidthSubvoxels; Recipe.seed = static_cast(StrateManager->GetWorldSeed()); @@ -929,13 +931,24 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager, / Recipe.ownerToCoreScaleDenominator; Recipe.spacingSubvoxels = WorldAssemblyFixed(Params.IslandSpacing * OwnerToCoreScale); Recipe.minimumRadiusSubvoxels = WorldAssemblyFixed(Params.IslandMinRadius * OwnerToCoreScale); - Recipe.maximumRadiusSubvoxels = WorldAssemblyFixed(Params.IslandMaxRadius * OwnerToCoreScale); + Recipe.maximumRadiusSubvoxels = WorldAssemblyFixed(28.0 * OwnerToCoreScale); Recipe.thicknessRatioQ16 = WorldAssemblyQ16(Params.ThicknessRatio); Recipe.verticalJitterQ16 = WorldAssemblyQ16(Params.VerticalJitter); Recipe.topFlattenQ16 = WorldAssemblyQ16(Params.TopFlatten); - Recipe.roughnessSubvoxels = WorldAssemblyFixed(Params.SurfaceRoughness * OwnerToCoreScale); - Recipe.blendRadiusSubvoxels = WorldAssemblyFixed(Params.SDFBlendRadius * OwnerToCoreScale); + Recipe.roughnessSubvoxels = std::min(WorldAssemblyFixed( + Params.SurfaceRoughness * OwnerToCoreScale), WorldAssemblySubvoxelsPerVoxel / 2); + Recipe.blendRadiusSubvoxels = 2 * WorldAssemblySubvoxelsPerVoxel; + Recipe.contourWarpSubvoxels = WorldAssemblySubvoxelsPerVoxel; Recipe.boundarySealSubvoxels = WorldAssemblyFixed(Params.BoundarySealThickness * OwnerToCoreScale); + Recipe.routeConstraints.version = 1; + Recipe.routeConstraints.capsuleRadiusSubvoxels = 348; + Recipe.routeConstraints.capsuleHeightSubvoxels = 1802; + Recipe.routeConstraints.maximumGradeQ16 = 17560; + Recipe.routeConstraints.sweepSpacingSubvoxels = 128; + Recipe.routeConstraints.supportProbeRadiusSubvoxels = 230; + Recipe.routeConstraints.supportSearchDepthSubvoxels = 512; + Recipe.routeConstraints.minimumClearanceSubvoxels = 128; + Recipe.routeConstraints.minimumSupportProbes = 3; const auto OwnerVoxelsToCoreQ=[&](double Value) { return static_cast(std::llround(Value*Recipe.ownerVoxelSizeMicrons @@ -944,40 +957,21 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager, Recipe.anchorIslandOrdinal=Recipe.primaryIslandCount-1; Recipe.anchorPlacementRadiusSubvoxels=WorldAssemblyMicrometresToSubvoxels( Slot.PassageDistanceMaximumMicrometres, Recipe.coreVoxelSizeMicrons); - const std::int64_t spiralRadius=WorldAssemblyMicrometresToSubvoxels( - Slot.PassageSpiralRadiusMicrometres, Recipe.coreVoxelSizeMicrons); - // Reserve at least a 1.5 m mid-bore through the anchor root. - constexpr double AnchorPassageMinimumBoreOwnerVoxels=12.0; - const std::int64_t OwnerVoxelMicrometres = Recipe.ownerVoxelSizeMicrons; - const std::int64_t mouthBore=std::max( - WorldAssemblyMicrometresToSubvoxels(Slot.PassageMouthRadiusMicrometres, - Recipe.coreVoxelSizeMicrons), - WorldAssemblyMicrometresToSubvoxels(12 * OwnerVoxelMicrometres, - Recipe.coreVoxelSizeMicrons)); - const std::int64_t bore=std::max( - WorldAssemblyMicrometresToSubvoxels(Slot.PassageMidRadiusMicrometres, - Recipe.coreVoxelSizeMicrons), - WorldAssemblyMicrometresToSubvoxels( - static_cast(AnchorPassageMinimumBoreOwnerVoxels * OwnerVoxelMicrometres), - Recipe.coreVoxelSizeMicrons)); - const std::int64_t wallReserve=WorldAssemblyMicrometresToSubvoxels( - 12 * OwnerVoxelMicrometres, Recipe.coreVoxelSizeMicrons); - const std::int64_t FourOwnerVoxelsSubvoxels=WorldAssemblyMicrometresToSubvoxels( - 4 * OwnerVoxelMicrometres, Recipe.coreVoxelSizeMicrons); - Recipe.anchorMouthOffsetSubvoxels=spiralRadius+mouthBore+FourOwnerVoxelsSubvoxels; - const std::int64_t excursion=std::max( - WorldAssemblyMicrometresToSubvoxels(Slot.PassageWanderMicrometres, - Recipe.coreVoxelSizeMicrons), spiralRadius); - Recipe.anchorRootRadiusSubvoxels=excursion+bore+wallReserve; - Recipe.anchorIslandMinimumRadiusSubvoxels=std::max(Recipe.minimumRadiusSubvoxels, - Recipe.anchorMouthOffsetSubvoxels+mouthBore+FourOwnerVoxelsSubvoxels); + Recipe.anchorMouthOffsetSubvoxels = OwnerVoxelsToCoreQ(4.0); + Recipe.anchorRootRadiusSubvoxels = std::max(OwnerVoxelsToCoreQ(16.0), + Recipe.anchorMouthOffsetSubvoxels + Recipe.routeConstraints.capsuleRadiusSubvoxels + + WorldAssemblySubvoxelsPerVoxel); + Recipe.anchorIslandMinimumRadiusSubvoxels = std::max(Recipe.maximumRadiusSubvoxels, + Recipe.anchorMouthOffsetSubvoxels + Recipe.routeConstraints.capsuleRadiusSubvoxels + + WorldAssemblySubvoxelsPerVoxel); Recipe.anchorRootBottomZSubvoxels=Recipe.layoutMinimumZSubvoxels +Recipe.boundarySealSubvoxels; std::string PlanError; - const std::optional Planned = PlanFloatingIslandCluster(Recipe, PlanError); + const std::optional Planned = + PlanFloatingIslandClusterAroundPassages(Recipe, Layout, Slot.StrateIndex, PlanError); if (!Planned) { - OutError = FString::Printf(TEXT("FloatingIslands automatic plan for strate %d failed: %s"), + OutError = FString::Printf(TEXT("FloatingIslands passage-aware plan for strate %d failed: %s"), Slot.StrateIndex, UTF8_TO_TCHAR(PlanError.c_str())); return false; } @@ -1671,18 +1665,6 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings, Generated.plan.strates.push_back(std::move(Stratum)); ++BuiltSurfaceCount; } - // Native floating-island evaluation remains an explicit imported-plan opt-in. - // The default owner path uses the legacy generator for this slot; emitting an - // automatic FI plan here silently switches density dispatch and changes the - // shared passage mouth before the owner has opted into that field. - else if (Type == ECaveGeneratorType::FloatingIslands && bImportedOverride) - { - if (!BuildFloatingIslandStratumPlan(StrateManager, Slot, Generated, - CoreVoxelSizeCm, LayoutChunkWidthSubvoxels, OutError)) - { - return false; - } - } } if (BuiltSurfaceCount > 1) { @@ -2031,6 +2013,98 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings, { return false; } + bool bAddedFloatingIslandPlan = false; + for (const FStrateSlot& Slot : StrateManager->GetLayout()) + { + if (Slot.Definition == nullptr + || StrateManager->GetGeneratorTypeForChunk(FIntVector(0, 0, Slot.TopChunkZ)) + != ECaveGeneratorType::FloatingIslands) + { + continue; + } + const auto Existing = std::find_if(Manifest->floatingIslands.begin(), + Manifest->floatingIslands.end(), [&Slot](const auto& Plan) + { return Plan.strateIndex == Slot.StrateIndex; }); + if (Existing != Manifest->floatingIslands.end()) continue; + if (!BuildFloatingIslandStratumPlan(StrateManager, Slot, *Manifest, Layout, + CoreVoxelSizeCm, LayoutChunkWidthSubvoxels, OutError)) + { + return false; + } + bAddedFloatingIslandPlan = true; + } + if (bAddedFloatingIslandPlan) + { + if (!Manifest->finalize(CoreError)) + { + OutError = FString::Printf(TEXT("Core passage-aware FloatingIslands planning failed: %s"), + UTF8_TO_TCHAR(CoreError.c_str())); + return false; + } + ManifestHash = FString::Printf(TEXT("%016llx"), + static_cast(Manifest->contentHash)); + for (const auto& Plan : Manifest->floatingIslands) + { + auto Stratum = std::find_if(Layout.strates.begin(), Layout.strates.end(), + [&Plan](const auto& Item) { return Item.strateIndex == Plan.strateIndex; }); + if (Stratum == Layout.strates.end()) + { + OutError = FString::Printf(TEXT("FloatingIslands strate %d disappeared before anchor export."), + Plan.strateIndex); + return false; + } + Stratum->recipeVersion = Plan.manifest.recipe.geometryVersion; + std::erase_if(Stratum->anchors, [](const auto& Anchor) + { + return Anchor.kind == "island_anchor" || Anchor.kind == "passage_anchor" + || Anchor.kind == "route_point"; + }); + const auto& NativePlan = Plan.manifest.plan; + const auto& NativeRecipe = Plan.manifest.recipe; + for (std::size_t IslandIndex = 0; IslandIndex < NativePlan.islands.size(); ++IslandIndex) + { + const auto& Island = NativePlan.islands[IslandIndex]; + VoxelForgeCore::ResolvedLayoutAnchorV2 Anchor; + Anchor.kind = "island_anchor"; + Anchor.stableId = "island/" + std::to_string(IslandIndex); + Anchor.units = "1/256 Core voxel"; + Anchor.values = { Island.centerSubvoxels[0], Island.centerSubvoxels[1], + Island.centerSubvoxels[2], Island.radiusSubvoxels, Island.topHalfSubvoxels }; + Stratum->anchors.push_back(std::move(Anchor)); + if (IslandIndex == NativeRecipe.anchorIslandOrdinal) + { + VoxelForgeCore::ResolvedLayoutAnchorV2 PassageAnchor; + PassageAnchor.kind = "passage_anchor"; + PassageAnchor.stableId = "passage-anchor/island/" + + std::to_string(IslandIndex); + PassageAnchor.units = "1/256 Core voxel"; + PassageAnchor.values = { Island.centerSubvoxels[0], Island.centerSubvoxels[1], + NativeRecipe.anchorRootRadiusSubvoxels, + NativeRecipe.anchorMouthOffsetSubvoxels, + NativeRecipe.anchorPlacementRadiusSubvoxels, + NativeRecipe.anchorRootBottomZSubvoxels, Island.radiusSubvoxels }; + if (!NativePlan.route.empty()) + { + const auto& Mouth = NativePlan.route.back().positionSubvoxels; + PassageAnchor.values.insert(PassageAnchor.values.end(), + { Mouth[0], Mouth[1], Mouth[2] }); + } + Stratum->anchors.push_back(std::move(PassageAnchor)); + } + } + for (std::size_t RouteIndex = 0; RouteIndex < NativePlan.route.size(); ++RouteIndex) + { + const auto& Point = NativePlan.route[RouteIndex]; + VoxelForgeCore::ResolvedLayoutAnchorV2 Anchor; + Anchor.kind = "route_point"; + Anchor.stableId = "route/" + std::to_string(RouteIndex); + Anchor.units = "1/256 Core voxel"; + Anchor.values = { Point.positionSubvoxels[0], Point.positionSubvoxels[1], + Point.positionSubvoxels[2], Point.radiusSubvoxels }; + Stratum->anchors.push_back(std::move(Anchor)); + } + } + } std::string LayoutError; if (!Layout.finalize(LayoutError)) { diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 8c44461..ec7cfe3 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -511,7 +511,7 @@ 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; + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 239; enum class FieldChannel : std::uint32_t { @@ -1417,6 +1417,19 @@ namespace VoxelForgeCore bool passed = false; }; + struct FloatingIslandPassageClearanceAuditResult + { + std::uint64_t passageSamples = 0; + std::uint64_t blockedIslandSamples = 0; + std::uint64_t routeSamples = 0; + std::uint64_t blockedRouteSamples = 0; + std::uint32_t arrivalMouthSupportProbes = 0; + std::uint32_t departureMouthSupportProbes = 0; + std::int64_t minimumExcessClearanceSubvoxels = 0; + double portalExclusionMetres = 0.0; + bool passed = false; + }; + struct FloatingIslandInfluenceBounds { bool finite = false; @@ -1461,11 +1474,17 @@ namespace VoxelForgeCore // Planner output is ordinary reviewed data; clients can load it without re-running the planner. VFCORE_API std::optional PlanFloatingIslandCluster( const FloatingIslandRecipeV1& recipe, std::string& error); + VFCORE_API std::optional PlanFloatingIslandClusterAroundPassages( + const FloatingIslandRecipeV1& recipe, const ResolvedLayoutSnapshotV3& layout, + std::int32_t strateIndex, std::string& error); VFCORE_API FloatingIslandBoundsAuditResult AuditFloatingIslandBounds( const FloatingIslandFieldV1& field, std::uint32_t boxCount = 256, std::uint64_t seed = 0x49534c414e444231ull); VFCORE_API FloatingIslandRouteAuditResult AuditFloatingIslandRoute( const FloatingIslandFieldV1& field, std::int64_t routeOffsetSubvoxels = 0); + VFCORE_API FloatingIslandPassageClearanceAuditResult AuditFloatingIslandPassageClearance( + const FloatingIslandFieldV1& field, const ResolvedLayoutSnapshotV3& layout, + std::int32_t strateIndex, double portalExclusionMetres = 10.0); VFCORE_API FloatingIslandThicknessAuditResult AuditFloatingIslandThickness( const FloatingIslandPlanV1& plan, double minimumDepthToDiameterRatio = 0.70); diff --git a/Standalone/Core/src/FloatingIslandsInternal.h b/Standalone/Core/src/FloatingIslandsInternal.h index 33d97d6..58f0959 100644 --- a/Standalone/Core/src/FloatingIslandsInternal.h +++ b/Standalone/Core/src/FloatingIslandsInternal.h @@ -4,6 +4,7 @@ #include "VoxelForgeCore/CpuCycleCounter.h" #include "VoxelForgeCore/FloatingIslandInstrumentation.h" #include "VoxelForgeCore/SharedColumnCache.h" +#include "VoxelForgeCore/PassageSpline.h" #include "Json.h" #include "VoxelForgeCore/VoxelForgeCore.h" @@ -285,7 +286,7 @@ namespace floating_island_detail ? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0; const std::uint32_t ExpectedDenominator = ScaleGcd > 0 ? recipe.coreVoxelSizeMicrons / ScaleGcd : 0; - if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 8 + if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 10 || (recipe.geometryVersion == 1 && (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0)) || recipe.unitsPerVoxel != Q @@ -1988,6 +1989,229 @@ std::string FloatingIslandManifestV1::ToJson() const return out.str(); } +namespace floating_island_detail +{ + struct FIslandPassageSpan + { + PassageSpline::FSegment segment; + double alongMetres = 0.0; + double totalMetres = 0.0; + double radiusSubvoxels = 0.0; + bool portalAtStart = false; + }; + + inline double SegmentDistanceSquared3D(const PassageSpline::FPoint& p1, + const PassageSpline::FPoint& q1, const PassageSpline::FPoint& p2, + const PassageSpline::FPoint& q2) + { + const std::array d1={q1[0]-p1[0],q1[1]-p1[1],q1[2]-p1[2]}; + const std::array d2={q2[0]-p2[0],q2[1]-p2[1],q2[2]-p2[2]}; + const std::array r={p1[0]-p2[0],p1[1]-p2[1],p1[2]-p2[2]}; + const auto dot=[](const auto& a,const auto& b) + {return a[0]*b[0]+a[1]*b[1]+a[2]*b[2];}; + const double a=dot(d1,d1),e=dot(d2,d2),f=dot(d2,r); + double s=0.0,t=0.0; + if(a<=1.0e-18&&e<=1.0e-18)return dot(r,r); + if(a<=1.0e-18)t=std::clamp(f/e,0.0,1.0); + else + { + const double c=dot(d1,r); + if(e<=1.0e-18)s=std::clamp(-c/a,0.0,1.0); + else + { + const double b=dot(d1,d2); + const double denominator=a*e-b*b; + if(std::abs(denominator)>1.0e-18) + s=std::clamp((b*f-c*e)/denominator,0.0,1.0); + t=(b*s+f)/e; + if(t<0.0){t=0.0;s=std::clamp(-c/a,0.0,1.0);} + else if(t>1.0){t=1.0;s=std::clamp((b-c)/a,0.0,1.0);} + } + } + std::array delta{}; + for(std::size_t axis=0;axis<3;++axis) + delta[axis]=r[axis]+s*d1[axis]-t*d2[axis]; + return dot(delta,delta); + } + + inline std::vector BuildIslandPassageSpans( + const ResolvedLayoutSnapshotV3& layout,std::int32_t strateIndex, + const FloatingIslandRecipeV1& recipe,double portalExclusionMetres) + { + std::vector spans; + const double qPerMetre=static_cast(WorldAssemblySubvoxelsPerVoxel) + *1000000.0/std::max(1,recipe.coreVoxelSizeMicrons); + for(const auto& passage:layout.passages) + { + const bool upper=passage.upperStrateIndex==strateIndex; + const bool lower=passage.lowerStrateIndex==strateIndex; + if((!upper&&!lower)||passage.controlPoints.size()<2)continue; + std::vector controls; + controls.reserve(passage.controlPoints.size()); + for(const auto& point:passage.controlPoints) + controls.push_back({static_cast(point.positionSubvoxels[0]), + static_cast(point.positionSubvoxels[1]), + static_cast(point.positionSubvoxels[2]), + static_cast(point.radiusSubvoxels)}); + const auto built=PassageSpline::BuildSegments(controls,controls.size()-1, + 0.25,qPerMetre,{},0.04); + if(built.empty())continue; + const double total=built.back().alongMetres+built.back().lengthMetres; + const auto& first=controls.front(); + const auto& last=controls.back(); + const auto distanceSquaredTo=[&](const PassageSpline::FPoint& point, + const std::array& mouth) + { + double result=0.0; + for(std::size_t axis=0;axis<3;++axis) + {const double delta=point[axis]-mouth[axis];result+=delta*delta;} + return result; + }; + const bool upperAtStart=distanceSquaredTo(first,passage.upperMouthSubvoxels) + <=distanceSquaredTo(last,passage.upperMouthSubvoxels); + const bool portalAtStart=upper?upperAtStart:!upperAtStart; + for(const auto& segment:built) + { + const double along=segment.alongMetres+0.5*segment.lengthMetres; + const double portalDistance=portalAtStart?along:total-along; + if(portalDistance<=portalExclusionMetres)continue; + const PassageSpline::FPoint midpoint={ + 0.5*(segment.a[0]+segment.b[0]), + 0.5*(segment.a[1]+segment.b[1]), + 0.5*(segment.a[2]+segment.b[2]), + 0.5*(segment.a[3]+segment.b[3])}; + const double minZ=std::min(segment.a[2],segment.b[2]); + const double maxZ=std::max(segment.a[2],segment.b[2]); + if(maxZ(recipe.layoutMinimumZSubvoxels) + ||minZ>static_cast(recipe.layoutMaximumZSubvoxels))continue; + spans.push_back({segment,along,total,midpoint[3],portalAtStart}); + } + } + return spans; + } + + inline std::array EvaluateIslandRouteBezier( + const std::array& a,const std::array& b, + const std::array& waypoint,double t) + { + const double u=1.0-t; + const std::array result={ + u*u*u*a[0]+3.0*u*u*t*waypoint[0]+3.0*u*t*t*waypoint[0]+t*t*t*b[0], + u*u*u*a[1]+3.0*u*u*t*waypoint[1]+3.0*u*t*t*waypoint[1]+t*t*t*b[1], + std::lerp(a[2],b[2],t),t}; + return result; + } + + struct FIslandDetourCandidate + { + std::array,9> centers{}; + double horizontalLengthMetres=0.0; + double conservativeClearanceMetres=-std::numeric_limits::infinity(); + double waypointAngle=0.0; + double waypointRadiusMetres=0.0; + }; + + inline std::vector BuildIslandDetourCandidates( + const FloatingIslandRecipeV1& recipe,const ResolvedLayoutPassageV1& arrival, + const ResolvedLayoutPassageV1& departure, + const std::vector& spans) + { + std::vector candidates; + const double qPerMetre=static_cast(WorldAssemblySubvoxelsPerVoxel) + *1000000.0/std::max(1,recipe.coreVoxelSizeMicrons); + const std::array a={static_cast(arrival.lowerMouthSubvoxels[0]), + static_cast(arrival.lowerMouthSubvoxels[1]), + static_cast(arrival.lowerMouthSubvoxels[2])}; + const std::array b={static_cast(departure.upperMouthSubvoxels[0]), + static_cast(departure.upperMouthSubvoxels[1]), + static_cast(departure.upperMouthSubvoxels[2])}; + for(int angleIndex=0;angleIndex<16;++angleIndex) + for(const double radiusMetres:{60.0,72.0,84.0}) + { + FIslandDetourCandidate candidate; + candidate.waypointAngle=2.0*Pi*angleIndex/16.0; + candidate.waypointRadiusMetres=radiusMetres; + const std::array waypoint={ + static_cast(recipe.layoutOriginXSubvoxels) + +std::cos(candidate.waypointAngle)*radiusMetres*qPerMetre, + static_cast(recipe.layoutOriginYSubvoxels) + +std::sin(candidate.waypointAngle)*radiusMetres*qPerMetre, + 0.5*(a[2]+b[2])}; + constexpr std::size_t CurveSteps=512; + std::array arc{}; + std::array,CurveSteps+1> curve{}; + auto prior=EvaluateIslandRouteBezier(a,b,waypoint,0.0); + curve[0]={prior[0],prior[1],prior[2]}; + for(std::size_t n=1;n<=CurveSteps;++n) + { + const auto value=EvaluateIslandRouteBezier(a,b,waypoint, + static_cast(n)/CurveSteps); + curve[n]={value[0],value[1],value[2]}; + const double dx=(value[0]-prior[0])/qPerMetre; + const double dy=(value[1]-prior[1])/qPerMetre; + arc[n]=arc[n-1]+std::hypot(dx,dy); + prior=value; + } + candidate.horizontalLengthMetres=arc.back(); + if(!(candidate.horizontalLengthMetres>1.0))continue; + for(std::size_t island=0;island( + CurveSteps,static_cast(upper-arc.begin())); + const std::size_t lowerIndex=upperIndex?upperIndex-1:0; + const double span=arc[upperIndex]-arc[lowerIndex]; + const double t=span>1.0e-9?(target-arc[lowerIndex])/span:0.0; + auto point=std::array{ + std::lerp(curve[lowerIndex][0],curve[upperIndex][0],t), + std::lerp(curve[lowerIndex][1],curve[upperIndex][1],t), + std::lerp(a[2],b[2],target/arc.back())}; + if(island==0)point=a; + if(island+1==candidate.centers.size())point=b; + for(std::size_t axis=0;axis<3;++axis) + candidate.centers[island][axis]=RoundQ(point[axis]); + } + double minimumClearance=std::numeric_limits::infinity(); + for(std::size_t routeIndex=1;routeIndex(candidate.centers[routeIndex-1][axis]); + routeB[axis]=static_cast(candidate.centers[routeIndex][axis]); + } + for(const auto& span:spans) + { + const double distance=std::sqrt(SegmentDistanceSquared3D( + routeA,routeB,span.segment.a,span.segment.b))/qPerMetre; + const double passageRadius=span.radiusSubvoxels/qPerMetre; + const double capsuleRadius=0.5*recipe.routeConstraints.capsuleHeightSubvoxels/qPerMetre; + const double featureReach=recipe.maximumRadiusSubvoxels/qPerMetre + +(recipe.contourWarpSubvoxels+IslandBlendMargin(recipe) + +recipe.roughnessSubvoxels)/qPerMetre; + minimumClearance=std::min(minimumClearance,distance-passageRadius + -1.0533-capsuleRadius-featureReach-0.15); + } + } + candidate.conservativeClearanceMetres=std::isfinite(minimumClearance) + ?minimumClearance:1.0e6; + candidates.push_back(candidate); + } + std::stable_sort(candidates.begin(),candidates.end(), + [](const auto& left,const auto& right) + { + const bool leftSafe=left.conservativeClearanceMetres>=0.0; + const bool rightSafe=right.conservativeClearanceMetres>=0.0; + if(leftSafe!=rightSafe)return leftSafe; + if(left.horizontalLengthMetres!=right.horizontalLengthMetres) + return left.horizontalLengthMetresright.conservativeClearanceMetres; + }); + return candidates; + } +} + std::optional PlanFloatingIslandCluster( const FloatingIslandRecipeV1& inputRecipe, std::string& error) { @@ -2264,6 +2488,210 @@ std::optional PlanFloatingIslandCluster( return manifest; } +std::optional PlanFloatingIslandClusterAroundPassages( + const FloatingIslandRecipeV1& inputRecipe,const ResolvedLayoutSnapshotV3& layout, + std::int32_t strateIndex,std::string& error) +{ + using namespace floating_island_detail; + FloatingIslandRecipeV1 recipe=inputRecipe; + if(recipe.routeConstraints.version==0) + { + recipe.routeConstraints.version=1; + recipe.routeConstraints.capsuleRadiusSubvoxels=348; + recipe.routeConstraints.capsuleHeightSubvoxels=1802; + recipe.routeConstraints.maximumGradeQ16=17560; + recipe.routeConstraints.sweepSpacingSubvoxels=128; + recipe.routeConstraints.supportProbeRadiusSubvoxels=230; + recipe.routeConstraints.supportSearchDepthSubvoxels=512; + recipe.routeConstraints.minimumClearanceSubvoxels=128; + recipe.routeConstraints.minimumSupportProbes=3; + } + if(recipe.geometryVersion<10||recipe.primaryIslandCount!=9) + {error="corridor-aware FloatingIslands requires geometry version 10 and nine route islands";return std::nullopt;} + if(!ValidateRecipe(recipe,error))return std::nullopt; + + const ResolvedLayoutPassageV1* arrival=nullptr; + const ResolvedLayoutPassageV1* departure=nullptr; + for(const auto& passage:layout.passages) + { + if(passage.lowerStrateIndex==strateIndex + &&passage.upperStrateIndexstrateIndex)departure=&passage; + } + if(!arrival||!departure) + {error="corridor-aware FloatingIslands needs resolved arrival and departure mouths";return std::nullopt;} + + constexpr double PortalExclusionMetres=13.0; + const auto passageSpans=BuildIslandPassageSpans(layout,strateIndex,recipe, + PortalExclusionMetres); + if(passageSpans.empty()) + {error="corridor-aware FloatingIslands found no resolved passage spans inside its strate";return std::nullopt;} + auto candidates=BuildIslandDetourCandidates(recipe,*arrival,*departure,passageSpans); + const double qPerMetre=static_cast(WorldAssemblySubvoxelsPerVoxel) + *1000000.0/std::max(1,recipe.coreVoxelSizeMicrons); + const double capsuleHalf=0.5*recipe.routeConstraints.capsuleHeightSubvoxels; + const double routeLift=capsuleHalf+recipe.routeConstraints.minimumClearanceSubvoxels+Q/8.0; + std::uint32_t tried=0; + std::string lastFailure="no outer detour has conservative tube clearance"; + for(const auto& candidate:candidates) + { + if(candidate.conservativeClearanceMetres<0.0)break; + ++tried; + FloatingIslandManifestV1 manifest; + manifest.recipe=recipe; + auto& plan=manifest.plan; + plan.islands.reserve(recipe.primaryIslandCount); + bool candidateValid=true; + for(std::uint32_t ordinal=0;ordinal(ordinal)/(recipe.primaryIslandCount-1); + const std::int64_t topZ=RoundQ(std::lerp( + static_cast(arrival->lowerMouthSubvoxels[2])-routeLift, + static_cast(departure->upperMouthSubvoxels[2])-routeLift,t)); + auto island=MakeIsland(recipe,ordinal,center[0],center[1],topZ, + 0x100000000ull+ordinal); + if(ordinal==recipe.anchorIslandOrdinal) + island.radiusSubvoxels=std::max(island.radiusSubvoxels, + recipe.anchorIslandMinimumRadiusSubvoxels); + island.topHalfSubvoxels=std::max(1,island.radiusSubvoxels/5); + island.centerSubvoxels[2]=island.topZSubvoxels-island.topHalfSubvoxels; + island.bottomZSubvoxels=IslandBottom(recipe,island.topZSubvoxels, + island.radiusSubvoxels); + SetIslandBounds(recipe,island); + if(island.bottomZSubvoxelsrecipe.layoutMaximumZSubvoxels + -recipe.boundarySealSubvoxels) + {candidateValid=false;lastFailure="detour island escaped the sealed vertical layout";break;} + plan.islands.push_back(std::move(island)); + } + if(!candidateValid)continue; + for(std::uint32_t ordinal=0;ordinal+1(b[0]-a[0]); + const double dy=static_cast(b[1]-a[1]); + const std::uint32_t pieces=static_cast(std::max(1.0, + std::ceil(std::hypot(dx,dy)/routeStep))); + for(std::uint32_t piece=segment==0?0:1;piece<=pieces;++piece) + { + const double t=static_cast(piece)/pieces; + const std::int64_t x=RoundQ(static_cast(a[0])+dx*t); + const std::int64_t y=RoundQ(static_cast(a[1])+dy*t); + const auto surface=FindCapsuleFootingTopZ(recipe,plan,x,y); + if(!surface) + {candidateValid=false;lastFailure="detour route has no full capsule-width island support";break;} + WorldAssemblyRoutePointV1 point; + point.stableFeatureId=StableId(recipe.seed,0x500000000ull+routeOrdinal, + 0x524f555445ull); + point.positionSubvoxels={x,y,*surface+static_cast(std::llround(routeLift))}; + point.radiusSubvoxels=4*Q; + if(segment==0&&piece==0) + point.positionSubvoxels=arrival->lowerMouthSubvoxels; + if(segment+2==plan.islands.size()&&piece==pieces) + point.positionSubvoxels=departure->upperMouthSubvoxels; + if(!plan.route.empty()&&plan.route.back().positionSubvoxels==point.positionSubvoxels) + continue; + plan.route.push_back(point); + ++routeOrdinal; + } + if(!candidateValid)break; + } + if(!candidateValid||plan.route.size()<2)continue; + + const double maximumGrade=Q16ToDouble(recipe.routeConstraints.maximumGradeQ16)*0.95; + std::vector baseHeights; + baseHeights.reserve(plan.route.size()); + for(const auto& point:plan.route)baseHeights.push_back(point.positionSubvoxels[2]); + std::vector profile=baseHeights; + for(std::size_t i=0;i(baseHeights[i]); + for(std::size_t j=0;j(plan.route[i].positionSubvoxels[0] + -plan.route[j].positionSubvoxels[0]); + const double dy=static_cast(plan.route[i].positionSubvoxels[1] + -plan.route[j].positionSubvoxels[1]); + adjusted=std::max(adjusted,static_cast(baseHeights[j]) + -maximumGrade*std::hypot(dx,dy)); + } + profile[i]=RoundQ(adjusted); + } + const auto arrivalZ=arrival->lowerMouthSubvoxels[2]; + const auto departureZ=departure->upperMouthSubvoxels[2]; + if(profile.front()>arrivalZ+Q/2||profile.back()>departureZ+Q/2) + {lastFailure="island support profile cannot meet both fixed mouth elevations";continue;} + for(std::size_t i=1;i+1lowerMouthSubvoxels; + plan.route.back().positionSubvoxels=departure->upperMouthSubvoxels; + + if(!manifest.finalize(error)) + {lastFailure=error;continue;} + const FloatingIslandFieldV1 field(manifest.recipe,manifest.plan); + const auto routeAudit=AuditFloatingIslandRoute(field); + if(!routeAudit.passed) + { + lastFailure="detour route sweep failed: blocked=" + +std::to_string(routeAudit.blockedCapsuleSamples)+" unsupported=" + +std::to_string(routeAudit.unsupportedCenterSamples)+" grade=" + +std::to_string(routeAudit.gradeFailures)+" detour_angle_deg=" + +std::to_string(candidate.waypointAngle*180.0/Pi)+" detour_radius_m=" + +std::to_string(candidate.waypointRadiusMetres)+" first_unsupported_q=(" + +std::to_string(routeAudit.firstUnsupportedCenterSubvoxels[0])+","+ + std::to_string(routeAudit.firstUnsupportedCenterSubvoxels[1])+","+ + std::to_string(routeAudit.firstUnsupportedCenterSubvoxels[2])+") segment=" + +std::to_string(routeAudit.firstUnsupportedSegment); + continue; + } + const auto clearance=AuditFloatingIslandPassageClearance(field,layout, + strateIndex,PortalExclusionMetres); + if(!clearance.passed) + { + lastFailure="passage clearance failed: island=" + +std::to_string(clearance.blockedIslandSamples)+" route=" + +std::to_string(clearance.blockedRouteSamples)+" mouth_support=" + +std::to_string(clearance.arrivalMouthSupportProbes)+"/" + +std::to_string(clearance.departureMouthSupportProbes); + continue; + } + const std::string encoded=manifest.ToJson(); + const auto roundTrip=FloatingIslandManifestV1::FromJson(encoded,error); + if(!roundTrip||roundTrip->ToJson()!=encoded + ||roundTrip->contentHash!=manifest.contentHash) + {if(error.empty())error="corridor-aware floating-island plan did not canonical-round-trip";return std::nullopt;} + std::printf("[FloatingIslandCorridorPlan] strate=%d detours_tried=%u waypoint_angle_deg=%.1f waypoint_radius_m=%.1f route_m=%.1f conservative_clearance_m=%.2f portal_exclusion_m=%.1f islands=%zu arrival_binding=PASS departure_binding=PASS status=PASS\n", + strateIndex,tried,candidate.waypointAngle*180.0/Pi, + candidate.waypointRadiusMetres,candidate.horizontalLengthMetres, + candidate.conservativeClearanceMetres,PortalExclusionMetres, + manifest.plan.islands.size()); + error.clear(); + return manifest; + } + const auto bestCandidate=std::max_element(candidates.begin(),candidates.end(), + [](const auto& left,const auto& right) + {return left.conservativeClearanceMetresconservativeClearanceMetres)) + +" last="+lastFailure; + return std::nullopt; +} + FloatingIslandFieldV1::FloatingIslandFieldV1(const FloatingIslandRecipeV1& recipe, const FloatingIslandPlanV1& plan) noexcept : recipe_(&recipe), plan_(&plan) {} @@ -2848,6 +3276,107 @@ FloatingIslandRouteAuditResult AuditFloatingIslandRoute(const FloatingIslandFiel return result; } +FloatingIslandPassageClearanceAuditResult AuditFloatingIslandPassageClearance( + const FloatingIslandFieldV1& field,const ResolvedLayoutSnapshotV3& layout, + std::int32_t strateIndex,double portalExclusionMetres) +{ + using namespace floating_island_detail; + FloatingIslandPassageClearanceAuditResult result; + result.portalExclusionMetres=portalExclusionMetres; + if(!field.isValid()||!field.recipe()||!field.plan() + ||field.plan()->route.size()<2||!std::isfinite(portalExclusionMetres) + ||portalExclusionMetres<0.0)return result; + const auto& recipe=*field.recipe(); + const double qPerMetre=static_cast(WorldAssemblySubvoxelsPerVoxel) + *1000000.0/std::max(1,recipe.coreVoxelSizeMicrons); + const std::int64_t influenceReach=RoundQ(1.0533*qPerMetre); + const std::int64_t capsuleReach=recipe.routeConstraints.capsuleHeightSubvoxels/2; + const std::int64_t sampleGuard=RoundQ(0.15*qPerMetre); + const auto spans=BuildIslandPassageSpans(layout,strateIndex,recipe, + portalExclusionMetres); + bool haveClearance=false; + std::int64_t minimumExcess=std::numeric_limits::max(); + for(const auto& span:spans) + { + const PassageSpline::FPoint center={ + 0.5*(span.segment.a[0]+span.segment.b[0]), + 0.5*(span.segment.a[1]+span.segment.b[1]), + 0.5*(span.segment.a[2]+span.segment.b[2]), + 0.5*(span.segment.a[3]+span.segment.b[3])}; + const std::int64_t density=field.densitySubvoxels(RoundQ(center[0]), + RoundQ(center[1]),RoundQ(center[2])); + const std::int64_t required=RoundQ(center[3])+influenceReach + +capsuleReach+sampleGuard; + const std::int64_t excess=density-required; + minimumExcess=std::min(minimumExcess,excess); + haveClearance=true; + ++result.passageSamples; + if(excess<0)++result.blockedIslandSamples; + + for(std::size_t routeSegment=0;routeSegment+1route.size();++routeSegment) + { + const auto& routeA=field.plan()->route[routeSegment].positionSubvoxels; + const auto& routeB=field.plan()->route[routeSegment+1].positionSubvoxels; + PassageSpline::FPoint a{},b{}; + for(std::size_t axis=0;axis<3;++axis) + {a[axis]=static_cast(routeA[axis]);b[axis]=static_cast(routeB[axis]);} + const double distance=std::sqrt(SegmentDistanceSquared3D(a,b, + span.segment.a,span.segment.b)); + const double requiredDistance=center[3]+influenceReach + +capsuleReach+sampleGuard; + ++result.routeSamples; + if(distancestrateIndex) + departure=&passage; + } + const auto mouthSupport=[&](const std::array& mouth) + { + constexpr std::array,5> probes={{{0.0,0.0}, + {1.0,0.0},{-1.0,0.0},{0.0,1.0},{0.0,-1.0}}}; + const double radius=static_cast(recipe.routeConstraints.supportProbeRadiusSubvoxels); + const std::int64_t foot=mouth[2]-recipe.routeConstraints.capsuleHeightSubvoxels/2; + const std::int64_t sampleZ=foot-recipe.routeConstraints.minimumClearanceSubvoxels-Q/2; + std::uint32_t supported=0; + for(const auto& probe:probes) + { + const auto x=RoundQ(static_cast(mouth[0])+probe[0]*radius); + const auto y=RoundQ(static_cast(mouth[1])+probe[1]*radius); + if(field.densitySubvoxels(x,y,sampleZ)<0)++supported; + } + return supported; + }; + bool bound=false; + if(arrival&&departure) + { + const auto& route=field.plan()->route; + const auto close=[](const auto& a,const auto& b) + { + for(std::size_t axis=0;axis<3;++axis) + if(std::abs(a[axis]-b[axis])>2)return false; + return true; + }; + bound=close(route.front().positionSubvoxels,arrival->lowerMouthSubvoxels) + &&close(route.back().positionSubvoxels,departure->upperMouthSubvoxels); + result.arrivalMouthSupportProbes=mouthSupport(arrival->lowerMouthSubvoxels); + result.departureMouthSupportProbes=mouthSupport(departure->upperMouthSubvoxels); + } + result.passed=bound&&haveClearance&&result.blockedIslandSamples==0 + &&result.blockedRouteSamples==0 + &&result.arrivalMouthSupportProbes>=recipe.routeConstraints.minimumSupportProbes + &&result.departureMouthSupportProbes>=recipe.routeConstraints.minimumSupportProbes; + return result; +} + FloatingIslandThicknessAuditResult AuditFloatingIslandThickness( const FloatingIslandPlanV1& plan, double minimumDepthToDiameterRatio) { diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 1a57f85..98dabf5 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -1754,9 +1754,81 @@ public: ?passageAirDistance:density); if(nativeTunnelFieldDensity!=std::numeric_limits::lowest()) return std::max(density,passageAirDistance); + const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1* landingPlan=nullptr; + if(WorldAssembly&&queryStratum&&queryStratum->Resolved + &&queryStratum->Resolved->generatorKind=="FloatingIslands") + { + const auto island=std::find_if(WorldAssembly->floatingIslands.begin(), + WorldAssembly->floatingIslands.end(),[&](const auto& candidate) + {return candidate.strateIndex==queryStratum->StrateIndex;}); + if(island!=WorldAssembly->floatingIslands.end())landingPlan=&*island; + } + const auto preserveFloatingIslandLandingSupport=[&](float composedDensity) + { + if(!landingPlan||landingPlan->manifest.recipe.geometryVersion<10) + return composedDensity; + const auto& constraints=landingPlan->manifest.recipe.routeConstraints; + if(constraints.version!=1)return composedDensity; + const auto* routeField=std::get_if( + &queryStratum->Evaluator); + const auto& route=landingPlan->manifest.plan.route; + if(!routeField||route.size()<2)return composedDensity; + + constexpr std::size_t LandingRouteSegments=18; + const double supportRadiusMetres=std::min(0.45, + constraints.supportProbeRadiusSubvoxels/qPerMetre+0.10); + const double queryX=qx,queryY=qy,queryZ=qz; + for(std::size_t segment=0;segment+1=LandingRouteSegments + &&segment+LandingRouteSegments(b[0]-a[0]); + const double dy=static_cast(b[1]-a[1]); + const double dz=static_cast(b[2]-a[2]); + const double horizontalSquared=dx*dx+dy*dy; + if(horizontalSquared<=1.0e-12)continue; + const double t=std::clamp(((queryX-a[0])*dx+(queryY-a[1])*dy) + /horizontalSquared,0.0,1.0); + const double routeX=a[0]+t*dx,routeY=a[1]+t*dy; + if(std::hypot(queryX-routeX,queryY-routeY) + >supportRadiusMetres*qPerMetre) + continue; + const double routeCenterZ=a[2]+t*dz; + const double footZ=routeCenterZ + -0.5*constraints.capsuleHeightSubvoxels; + const double shallowDepth=constraints.minimumClearanceSubvoxels+Q/2.0; + const double deepDepth=constraints.minimumClearanceSubvoxels + +constraints.supportSearchDepthSubvoxels; + if(queryZ<=footZ-shallowDepth&&queryZ>=footZ-deepDepth) + { + const std::int64_t nativeDensity=routeField->densitySubvoxelsAt( + static_cast(std::llround(queryX)), + static_cast(std::llround(queryY)), + static_cast(std::llround(queryZ))); + if(nativeDensity<0) + return std::min(composedDensity, + static_cast(static_cast(nativeDensity)/Q)); + } + } + return composedDensity; + }; if(unionDistance>0.0&&unionShellDistance<0.0&&density>=0.0f) return static_cast(unionShellDistance/Q); - return std::max(density,passageAirDistance); + float composedDensity=std::max(density,passageAirDistance); + if(landingPlan&&landingPlan->manifest.recipe.geometryVersion>=10 + &&density>=0.0f&&unionDistance<0.0&&unionShellDistance>0.0) + { + // In open-air parts of the island stratum, the boolean air union saturates + // the passage's positive interior distance to the farther island-air + // distance. Keep the nearest positive bore/shell distance so the explicit + // passage shell remains measurable without changing any field signs. + composedDensity=std::min(composedDensity, + std::min(passageAirDistance, + static_cast(unionShellDistance/Q))); + } + return preserveFloatingIslandLandingSupport(composedDensity); } float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const @@ -7217,9 +7289,11 @@ private: Density=filteredDensity; if(routeNativeBlend>0.0&&NativeDensity<0) { + const double routePreservation=Plan->manifest.recipe.geometryVersion>=10 + ?routeNativeBlend:routeNativeBlend*0.60; const double routePreserved=std::lerp(static_cast(Density), static_cast(std::min(Density,NativeDensity)), - routeNativeBlend*0.60); + routePreservation); Density=static_cast(std::llround(routePreserved)); } // Marching Cubes can retain an isolated solid with no two-metre axis diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp index 1c0ed4c..80ad688 100644 --- a/Standalone/Source/StandaloneViewer.cpp +++ b/Standalone/Source/StandaloneViewer.cpp @@ -9208,11 +9208,15 @@ int main(int Argc,char** Argv) { const auto PlanStart=std::chrono::steady_clock::now(); VoxelForgeCore::WorldAssemblyManifest WorldAssembly; + VoxelForgeCore::ResolvedLayoutSnapshotV3 ResolvedLayout; const bool bPlanReady=StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(*App.World,App.OwnerJsonRoot, Error,WorldAssemblyManifestOutputPath,&WorldAssembly) &&(!bFloatingIslandsRecipe ||StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly( - *App.World,App.OwnerJsonRoot,WorldAssembly,Error)); + *App.World,App.OwnerJsonRoot,WorldAssembly,Error)) + &&(!bFloatingIslandsRecipe + ||StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot( + *App.World,WorldAssembly,"floating-islands",ResolvedLayout,Error)); const double PlanMilliseconds=std::chrono::duration( std::chrono::steady_clock::now()-PlanStart).count(); std::printf("[WorldAssemblerPlanTiming] total_ms=%.3f status=%s\n", diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index 1afcff5..f22afe9 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -1830,31 +1830,11 @@ int main(int argc,char** argv) else if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype( *ownerWorld,ownerJsonRoot,error,{},&worldAssembly)) {std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;} - if(((!useExportedCorePlan&&options.FloatingIslandsRecipe) - ||(options.NativeOwnerFloatingIslandsRecipe - &&worldAssembly.floatingIslands.empty())) + if(!useExportedCorePlan&&options.FloatingIslandsRecipe + &&!options.NativeOwnerFloatingIslandsRecipe &&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly( *ownerWorld,ownerJsonRoot,worldAssembly,error)) {std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;} - if(!options.WorldAssemblyManifestOutputPath.empty()) - { - const std::string manifestText=worldAssembly.ToJson(); - const auto& outputPath=options.WorldAssemblyManifestOutputPath; - std::error_code directoryError; - if(!outputPath.parent_path().empty()) - std::filesystem::create_directories(outputPath.parent_path(),directoryError); - if(directoryError) - {std::fprintf(stderr,"Could not create the complete world assembly manifest output directory.\n");return 2;} - std::ofstream output(outputPath,std::ios::binary); - if(!output) - {std::fprintf(stderr,"Could not open the complete world assembly manifest output path.\n");return 2;} - output.write(manifestText.data(),static_cast(manifestText.size())); - if(!output) - {std::fprintf(stderr,"Could not write the complete world assembly manifest payload.\n");return 2;} - std::printf("[WorldAssemblerFullManifest] bytes=%zu surface_strates=%zu floating_islands=%zu path=%s status=PASS\n", - manifestText.size(),worldAssembly.plan.strates.size(),worldAssembly.floatingIslands.size(), - outputPath.string().c_str()); - } if (options.AuditHeightfieldProfile && !RunHeightfieldProfileAudit(worldAssembly)) { std::fprintf(stderr,"Profiled SurfaceWorld interval, classification or route audit failed.\n"); @@ -1886,6 +1866,31 @@ int main(int argc,char** argv) &&!StandaloneWorldAssemblyPrototype::ValidateExportedCorePlanMatchesOwner( *ownerWorld,worldAssembly,resolvedLayout,error)) {std::fprintf(stderr,"Unreal-resolved Core plan/owner slot validation failed: %s\n",error.c_str());return 2;} + if(options.NativeOwnerFloatingIslandsRecipe + &&worldAssembly.floatingIslands.empty() + &&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly( + *ownerWorld,ownerJsonRoot,worldAssembly,resolvedLayout,error, + true)) + {std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;} + if(!options.WorldAssemblyManifestOutputPath.empty()) + { + const std::string manifestText=worldAssembly.ToJson(); + const auto& outputPath=options.WorldAssemblyManifestOutputPath; + std::error_code directoryError; + if(!outputPath.parent_path().empty()) + std::filesystem::create_directories(outputPath.parent_path(),directoryError); + if(directoryError) + {std::fprintf(stderr,"Could not create the complete world assembly manifest output directory.\n");return 2;} + std::ofstream output(outputPath,std::ios::binary); + if(!output) + {std::fprintf(stderr,"Could not open the complete world assembly manifest output path.\n");return 2;} + output.write(manifestText.data(),static_cast(manifestText.size())); + if(!output) + {std::fprintf(stderr,"Could not write the complete world assembly manifest payload.\n");return 2;} + std::printf("[WorldAssemblerFullManifest] bytes=%zu surface_strates=%zu floating_islands=%zu path=%s status=PASS\n", + manifestText.size(),worldAssembly.plan.strates.size(),worldAssembly.floatingIslands.size(), + outputPath.string().c_str()); + } if(!std::holds_alternative(ownerJsonRoot.Get("motif_family_override").Data)) { std::vector requests; diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h index 4e20068..70c4c61 100644 --- a/Standalone/Source/WorldAssemblyPrototype.h +++ b/Standalone/Source/WorldAssemblyPrototype.h @@ -1366,7 +1366,9 @@ namespace StandaloneWorldAssemblyPrototype } inline bool AttachFloatingIslandWorldAssembly(FStandaloneOwnerWorld& owner, - VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,std::string& error) + VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly, + VoxelForgeCore::ResolvedLayoutSnapshotV3& resolvedLayout, + std::string& error,bool planAroundPassages=false) { if(!owner.Manager) {error="floating-island assembly needs the resolved standalone strate manager";return false;} @@ -1386,7 +1388,8 @@ namespace StandaloneWorldAssemblyPrototype FIntVector(0,0,islandSlot->TopChunkZ)); const FStratePassageConfig& passage=islandSlot->Definition->PassageConfig; recipe.seed=static_cast(owner.Seed); - recipe.geometryVersion=8; + recipe.geometryVersion=planAroundPassages?10:8; + if(planAroundPassages)recipe.primaryIslandCount=9; recipe.terrainFollowingRoute=static_cast(passage.Style)==0; recipe.ownerVoxelSizeMicrons=owner.OwnerVoxelSizeMicrons; recipe.coreVoxelSizeMicrons=owner.CoreVoxelSizeMicrons; @@ -1406,43 +1409,79 @@ namespace StandaloneWorldAssemblyPrototype recipe.islandDensityQ16=WorldAssemblyQ16(std::clamp(params.IslandDensity,0.0,1.0)); recipe.spacingSubvoxels=WorldAssemblyFixed(params.IslandSpacing*ownerToCoreScale); recipe.minimumRadiusSubvoxels=WorldAssemblyFixed(params.IslandMinRadius*ownerToCoreScale); - recipe.maximumRadiusSubvoxels=WorldAssemblyFixed(params.IslandMaxRadius*ownerToCoreScale); + recipe.maximumRadiusSubvoxels=WorldAssemblyFixed((planAroundPassages + ?28.0:params.IslandMaxRadius)*ownerToCoreScale); recipe.thicknessRatioQ16=WorldAssemblyQ16(params.ThicknessRatio); recipe.verticalJitterQ16=WorldAssemblyQ16(params.VerticalJitter); recipe.topFlattenQ16=WorldAssemblyQ16(params.TopFlatten); - recipe.blendRadiusSubvoxels=WorldAssemblyFixed(params.SDFBlendRadius*ownerToCoreScale); + recipe.blendRadiusSubvoxels=planAroundPassages + ?2*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel + :WorldAssemblyFixed(params.SDFBlendRadius*ownerToCoreScale); + if(planAroundPassages) + recipe.contourWarpSubvoxels=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; recipe.roughnessSubvoxels=WorldAssemblyFixed(params.SurfaceRoughness*ownerToCoreScale); + if(planAroundPassages) + recipe.roughnessSubvoxels=std::min(recipe.roughnessSubvoxels, + VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel/2); recipe.boundarySealSubvoxels=WorldAssemblyFixed(params.BoundarySealThickness*ownerToCoreScale); + if(planAroundPassages) + { + recipe.routeConstraints.version=1; + recipe.routeConstraints.capsuleRadiusSubvoxels=348; + recipe.routeConstraints.capsuleHeightSubvoxels=1802; + recipe.routeConstraints.maximumGradeQ16=17560; + recipe.routeConstraints.sweepSpacingSubvoxels=128; + recipe.routeConstraints.supportProbeRadiusSubvoxels=230; + recipe.routeConstraints.supportSearchDepthSubvoxels=512; + recipe.routeConstraints.minimumClearanceSubvoxels=128; + recipe.routeConstraints.minimumSupportProbes=3; + } const auto ownerVoxelsToCoreQ=[&](double value) {return static_cast(std::llround(value*owner.OwnerVoxelSizeMicrons *VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel/owner.CoreVoxelSizeMicrons));}; recipe.anchorIslandOrdinal=recipe.primaryIslandCount-1; recipe.anchorPlacementRadiusSubvoxels=ownerVoxelsToCoreQ(passage.DistanceMax); - const std::int64_t spiralRadius=ownerVoxelsToCoreQ(passage.SpiralRadius); - const std::int64_t mouthBore=std::max(ownerVoxelsToCoreQ(passage.MouthRadius), - ownerVoxelsToCoreQ(36.0)); - const std::int64_t bore=std::max(ownerVoxelsToCoreQ(passage.MidRadius), - ownerVoxelsToCoreQ(24.0)); - const std::int64_t wallReserve=ownerVoxelsToCoreQ(20.0); - // Retain a 4.5-metre mouth for capsule clearance while keeping the long - // ramp at three metres to leave more rock around the first coil. - recipe.anchorMouthOffsetSubvoxels=ownerVoxelsToCoreQ(40.0); - const std::int64_t supportOverhang=ownerVoxelsToCoreQ(16.0); - const std::int64_t rootTaperReserve=ownerVoxelsToCoreQ(10.0); - const std::int64_t mouthEnvelope=recipe.anchorMouthOffsetSubvoxels+mouthBore; - const std::int64_t treadEnvelope=spiralRadius+bore+supportOverhang; - recipe.anchorRootRadiusSubvoxels=std::max(mouthEnvelope,treadEnvelope) - +wallReserve+rootTaperReserve; - recipe.anchorIslandMinimumRadiusSubvoxels=std::max(recipe.minimumRadiusSubvoxels, - std::max(recipe.anchorMouthOffsetSubvoxels+mouthBore+ownerVoxelsToCoreQ(4.0), - recipe.anchorRootRadiusSubvoxels+wallReserve)); + if(planAroundPassages) + { + recipe.anchorMouthOffsetSubvoxels=ownerVoxelsToCoreQ(4.0); + recipe.anchorRootRadiusSubvoxels=std::max(ownerVoxelsToCoreQ(16.0), + recipe.anchorMouthOffsetSubvoxels+recipe.routeConstraints.capsuleRadiusSubvoxels + +VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + recipe.anchorIslandMinimumRadiusSubvoxels=std::max(recipe.maximumRadiusSubvoxels, + recipe.anchorMouthOffsetSubvoxels+recipe.routeConstraints.capsuleRadiusSubvoxels + +VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + } + else + { + const std::int64_t spiralRadius=ownerVoxelsToCoreQ(passage.SpiralRadius); + const std::int64_t mouthBore=std::max(ownerVoxelsToCoreQ(passage.MouthRadius), + ownerVoxelsToCoreQ(36.0)); + const std::int64_t bore=std::max(ownerVoxelsToCoreQ(passage.MidRadius), + ownerVoxelsToCoreQ(24.0)); + const std::int64_t wallReserve=ownerVoxelsToCoreQ(20.0); + recipe.anchorMouthOffsetSubvoxels=ownerVoxelsToCoreQ(40.0); + const std::int64_t supportOverhang=ownerVoxelsToCoreQ(16.0); + const std::int64_t rootTaperReserve=ownerVoxelsToCoreQ(10.0); + const std::int64_t mouthEnvelope=recipe.anchorMouthOffsetSubvoxels+mouthBore; + const std::int64_t treadEnvelope=spiralRadius+bore+supportOverhang; + recipe.anchorRootRadiusSubvoxels=std::max(mouthEnvelope,treadEnvelope) + +wallReserve+rootTaperReserve; + recipe.anchorIslandMinimumRadiusSubvoxels=std::max(recipe.minimumRadiusSubvoxels, + std::max(recipe.anchorMouthOffsetSubvoxels+mouthBore+ownerVoxelsToCoreQ(4.0), + recipe.anchorRootRadiusSubvoxels+wallReserve)); + } recipe.anchorRootBottomZSubvoxels=recipe.layoutMinimumZSubvoxels +recipe.boundarySealSubvoxels; - auto planned=VoxelForgeCore::PlanFloatingIslandCluster(recipe,error); + auto planned=planAroundPassages + ?VoxelForgeCore::PlanFloatingIslandClusterAroundPassages( + recipe,resolvedLayout,islandSlot->StrateIndex,error) + :VoxelForgeCore::PlanFloatingIslandCluster(recipe,error); if(!planned)return false; const VoxelForgeCore::FloatingIslandFieldV1 routeField( planned->recipe,planned->plan); + const double metresPerSubvoxel=static_cast(owner.CoreVoxelSizeMicrons) + /(1000000.0*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const auto routeAudit=VoxelForgeCore::AuditFloatingIslandRoute(routeField); std::printf("[FloatingIslandRouteAudit] points=%zu centers=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu status=%s\n", planned->plan.route.size(), @@ -1453,8 +1492,27 @@ namespace StandaloneWorldAssemblyPrototype static_cast(routeAudit.unsupportedCenterSamples), static_cast(routeAudit.gradeFailures), routeAudit.passed?"PASS":"FAIL"); - const double metresPerSubvoxel=static_cast(owner.CoreVoxelSizeMicrons) - /(1000000.0*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + VoxelForgeCore::FloatingIslandPassageClearanceAuditResult passageAudit; + if(planAroundPassages) + { + constexpr double PortalExclusionMetres=13.0; + passageAudit=VoxelForgeCore::AuditFloatingIslandPassageClearance( + routeField,resolvedLayout,islandSlot->StrateIndex,PortalExclusionMetres); + std::printf("[FloatingIslandPassageClearance] strate=%d tube_samples=%llu island_blocked=%llu route_checks=%llu route_blocked=%llu arrival_support=%u/5 departure_support=%u/5 clearance_excess_m=%.3f portal_exclusion_m=%.1f influence_reach_m=1.053 capsule_bound_m=%.3f status=%s\n", + islandSlot->StrateIndex, + static_cast(passageAudit.passageSamples), + static_cast(passageAudit.blockedIslandSamples), + static_cast(passageAudit.routeSamples), + static_cast(passageAudit.blockedRouteSamples), + passageAudit.arrivalMouthSupportProbes, + passageAudit.departureMouthSupportProbes, + passageAudit.minimumExcessClearanceSubvoxels*metresPerSubvoxel, + passageAudit.portalExclusionMetres, + planned->recipe.routeConstraints.capsuleHeightSubvoxels*metresPerSubvoxel*0.5, + passageAudit.passed?"PASS":"FAIL"); + if(!routeAudit.passed||!passageAudit.passed) + {error="corridor-aware floating-island route or passage audit failed";return false;} + } const double bedrockTopMetres=(minimumZ+recipe.boundarySealSubvoxels)*metresPerSubvoxel; const double ceilingBottomMetres=(maximumZ-recipe.boundarySealSubvoxels)*metresPerSubvoxel; for(const auto& island:planned->plan.islands) @@ -1473,18 +1531,82 @@ namespace StandaloneWorldAssemblyPrototype assembly.floatingIslands.push_back(std::move(islands)); if(!assembly.finalize(error))return false; + if(planAroundPassages) + { + auto resolvedStratum=std::find_if(resolvedLayout.strates.begin(), + resolvedLayout.strates.end(),[&](const auto& item) + {return item.strateIndex==islandSlot->StrateIndex;}); + if(resolvedStratum==resolvedLayout.strates.end()) + {error="resolved FloatingIslands stratum disappeared before native anchor attachment";return false;} + resolvedStratum->recipeVersion=assembly.floatingIslands.back().manifest.recipe.geometryVersion; + resolvedStratum->anchors.erase(std::remove_if(resolvedStratum->anchors.begin(), + resolvedStratum->anchors.end(),[](const auto& anchor) + {return anchor.kind=="island_anchor"||anchor.kind=="passage_anchor" + ||anchor.kind=="route_point";}),resolvedStratum->anchors.end()); + const auto& nativePlan=assembly.floatingIslands.back().manifest.plan; + const auto& nativeRecipe=assembly.floatingIslands.back().manifest.recipe; + for(std::size_t n=0;nanchors.push_back(std::move(anchor)); + if(n==nativeRecipe.anchorIslandOrdinal) + { + VoxelForgeCore::ResolvedLayoutAnchorV2 passageAnchor; + passageAnchor.kind="passage_anchor"; + passageAnchor.stableId="passage-anchor/island/"+std::to_string(n); + passageAnchor.units="1/256 Core voxel"; + passageAnchor.values={island.centerSubvoxels[0],island.centerSubvoxels[1], + nativeRecipe.anchorRootRadiusSubvoxels, + nativeRecipe.anchorMouthOffsetSubvoxels, + nativeRecipe.anchorPlacementRadiusSubvoxels, + nativeRecipe.anchorRootBottomZSubvoxels,island.radiusSubvoxels}; + if(!nativePlan.route.empty()) + { + const auto& mouth=nativePlan.route.back().positionSubvoxels; + passageAnchor.values.insert(passageAnchor.values.end(), + {mouth[0],mouth[1],mouth[2]}); + } + resolvedStratum->anchors.push_back(std::move(passageAnchor)); + } + } + for(std::size_t n=0;nanchors.push_back(std::move(anchor)); + } + if(!resolvedLayout.finalize(error))return false; + } + VFJson::Value assemblyJson; if(!VFJson::Parser(assembly.ToJson()).Parse(assemblyJson,error))return false; auto* rootObject=std::get_if(&root.Data); if(!rootObject){error="owner snapshot root is not a JSON object";return false;} (*rootObject)["world_assembly"]=std::move(assemblyJson); - std::printf("[FloatingIslandWorldAssembly] seed=%d strate_index=%d chunks=%d..%d island_count=%zu recipe_hash=%016llx status=PASS\n", + std::printf("[FloatingIslandWorldAssembly] seed=%d strate_index=%d chunks=%d..%d island_count=%zu dispatch=FloatingIslands-native recipe_hash=%016llx status=PASS\n", owner.Seed,islandSlot->StrateIndex,islandSlot->TopChunkZ,islandSlot->BottomChunkZ, assembly.floatingIslands.back().manifest.plan.islands.size(), static_cast(assembly.floatingIslands.back().manifest.recipe.contentHash)); return true; } + inline bool AttachFloatingIslandWorldAssembly(FStandaloneOwnerWorld& owner, + VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly, + std::string& error) + { + VoxelForgeCore::ResolvedLayoutSnapshotV3 unusedLayout; + return AttachFloatingIslandWorldAssembly(owner,root,assembly,unusedLayout,error,false); + } + inline const char* GeneratorKind(ECaveGeneratorType type) { switch(type) @@ -1964,6 +2086,7 @@ namespace StandaloneWorldAssemblyPrototype :it->manifest.plan.route.size()-1; std::size_t resolvedRoutePointCount=it->manifest.plan.route.size(); if(it->manifest.recipe.geometryVersion>=5 + &&it->manifest.recipe.geometryVersion<10 &&!it->manifest.plan.route.empty() &&it->manifest.recipe.anchorIslandOrdinalmanifest.plan.islands.size()) {