#pragma once #include "OwnerWorldLoader.h" #include "StandaloneResearchTunnels.h" #include "VoxelCaveMorphology.h" #include "VoxelNoise.h" #include "VoxelStrateManager.h" #include "VoxelDensityPrimitives.h" #include "VoxelForgeCore/VoxelForgeCore.h" #include "VoxelForgeFieldShared.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Standalone-only world compiler. It compiles the runtime room/tunnel/passages into // axis-line intervals, composes rock/cave signs into packed Z-column words, then displaces // only the conservative surface band. The copied Unreal generator remains the A/B reference. class FStandaloneShapeWorld { public: const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout=nullptr; struct FResolvedStratumBounds { double BottomZ=0.0; double TopZ=0.0; double SealThickness=0.0; }; const std::vector& GetPassageVoidCrossings() const {return PassageVoidCrossings;} class FScopedPassageTileQueries { public: FScopedPassageTileQueries(const FStandaloneShapeWorld& shape, const std::array& minimumSubvoxels, const std::array& maximumSubvoxels) :shape_(&shape),minimumSubvoxels_(minimumSubvoxels), maximumSubvoxels_(maximumSubvoxels),previous_(ActivePassageTileQueries) { mayContainResolvedBoundaryBand_=shape.ResolvedBoundaryBandsIntersectSubvoxelRange( minimumSubvoxels_[2],maximumSubvoxels_[2]); mayContainTransitionPassageFade_=shape.ResolvedTransitionPassageFadeIntersectsTileXY( minimumSubvoxels_,maximumSubvoxels_); featureIndices_=shape.BuildPassageFeatureTileList( minimumSubvoxels_,maximumSubvoxels_); passageRoutes_.assign(shape.ResolvedLayout?shape.ResolvedLayout->passages.size():0,0); passageColumns_=passageRoutes_; passageMouthSupports_=passageRoutes_; for(const auto& crossing:shape.PassageVoidCrossings) if(crossing.needsColumn &&crossing.bottomZSubvoxels<=maximumSubvoxels_[2] &&crossing.topZSubvoxels>=minimumSubvoxels_[2]) columnZOverlap_=true; for(const std::uint32_t featureIndex:featureIndices_) { if(featureIndex>=shape.PassageTileFeatures.size())continue; const auto& feature=shape.PassageTileFeatures[featureIndex]; if(feature.passageIndex>=passageRoutes_.size())continue; if(feature.bColumn)passageColumns_[feature.passageIndex]=1; else if(feature.bMouthSupport)passageMouthSupports_[feature.passageIndex]=1; else { passageRoutes_[feature.passageIndex]=1; routeFeatureOverlap_=true; } } ActivePassageTileQueries=this; } ~FScopedPassageTileQueries(){ActivePassageTileQueries=previous_;} FScopedPassageTileQueries(const FScopedPassageTileQueries&)=delete; FScopedPassageTileQueries& operator=(const FScopedPassageTileQueries&)=delete; bool matches(const FStandaloneShapeWorld& shape) const{return shape_==&shape;} bool hasFeature(std::size_t passageIndex,bool columnFeature) const { const auto& candidates=columnFeature?passageColumns_:passageRoutes_; return passageIndexResolvedBoundaryBandMayAffectAtZMicrometres(zMicrometres); } std::size_t featureCount() const{return featureIndices_.size();} private: const FStandaloneShapeWorld* shape_; std::array minimumSubvoxels_,maximumSubvoxels_; std::vector featureIndices_; std::vector passageRoutes_,passageColumns_,passageMouthSupports_; bool routeFeatureOverlap_=false,columnZOverlap_=false; bool mayContainResolvedBoundaryBand_=true; bool mayContainTransitionPassageFade_=true; const FScopedPassageTileQueries* previous_; }; bool IsResolvedPassageColumnSample(std::size_t PassageIndex,std::size_t SegmentIndex, const VoxelForgeCore::Vec3d& Position) const { (void)SegmentIndex; for(const auto& range:PassageVoidCrossings) { if(!range.needsColumn||range.passageIndex!=PassageIndex)continue; if(PassageColumnSignedDistance(range,static_cast(Position.x), static_cast(Position.y),static_cast(Position.z))<=0.0)return true; } return false; } bool IsResolvedPassageShortSpanSample(std::size_t PassageIndex,std::size_t SegmentIndex, const VoxelForgeCore::Vec3d& Position) const { return IsPassageShortSpanSegment(PassageIndex,SegmentIndex) &&PassageBridgeSignedDistance(PassageIndex,SegmentIndex, static_cast(Position.x),static_cast(Position.y), static_cast(Position.z))<=0.0; } double GetCoreVoxelSizeCm() const { return Owner == nullptr || Owner->CoreVoxelSizeMicrons == 0 ? static_cast(VOXEL_SIZE) : static_cast(Owner->CoreVoxelSizeMicrons) / 10000.0; } double GetResolvedLayoutChunkWidthVoxels() const { return ResolvedLayout&&ResolvedLayout->strateLayoutChunkWidthSubvoxels>0 ?static_cast(ResolvedLayout->strateLayoutChunkWidthSubvoxels) /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel :Owner?static_cast(Owner->StrateLayoutChunkWidthSubvoxels) /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel:0.0; } const VoxelForgeCore::ResolvedLayoutStratumV2* FindResolvedLayoutStratum(int32 StrateIndex) const { if(!ResolvedLayout)return nullptr; if(StrateIndex>=0&&static_cast(StrateIndex)strates.size()) { const auto& direct=ResolvedLayout->strates[static_cast(StrateIndex)]; if(direct.strateIndex==StrateIndex)return &direct; } const auto it=std::find_if(ResolvedLayout->strates.begin(),ResolvedLayout->strates.end(), [StrateIndex](const auto& item){return item.strateIndex==StrateIndex;}); return it==ResolvedLayout->strates.end()?nullptr:&*it; } const VoxelForgeCore::ResolvedLayoutStratumV2* FindResolvedLayoutStratumByOrder(int32 Order) const { if(!ResolvedLayout||Order<0)return nullptr; if(static_cast(Order)strates.size()) { const auto& direct=ResolvedLayout->strates[static_cast(Order)]; if(direct.order==Order)return &direct; } const auto it=std::find_if(ResolvedLayout->strates.begin(),ResolvedLayout->strates.end(), [Order](const auto& item){return item.order==Order;}); return it==ResolvedLayout->strates.end()?nullptr:&*it; } bool IsResolvedLayoutTransitionActive(const VoxelForgeCore::ResolvedLayoutStratumV2& Upper) const { return Upper.transitionMode!="Hard"&&!Upper.transitionFallsBackToHard &&Upper.transitionWidthMicrometres>0 &&Upper.transitionHonouredInCore; } bool ResolvedBoundaryBandsIntersectSubvoxelRange(std::int64_t minimumZSubvoxels, std::int64_t maximumZSubvoxels) const { if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0 ||minimumZSubvoxels>maximumZSubvoxels)return false; constexpr double Subvoxels=static_cast( VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double queryMinimumMicrometres=static_cast(minimumZSubvoxels) *Owner->CoreVoxelSizeMicrons/Subvoxels; const double queryMaximumMicrometres=static_cast(maximumZSubvoxels) *Owner->CoreVoxelSizeMicrons/Subvoxels; const double sampleMarginMicrometres=static_cast(Owner->CoreVoxelSizeMicrons); for(const auto& interval:ResolvedBoundaryZIntervalsMicrometres) { if(queryMaximumMicrometres+sampleMarginMicrometres>=interval[0]-1.0 &&queryMinimumMicrometres-sampleMarginMicrometresat(1); } void BuildResolvedBoundaryZIntervals() { ResolvedBoundaryZIntervalsMicrometres.clear(); if(!ResolvedLayout)return; for(const auto& stratum:ResolvedLayout->strates) { if(stratum.ceiling.presentInCurrentField &&stratum.ceiling.bottomMicrometres(stratum.ceiling.bottomMicrometres), static_cast(stratum.ceiling.topMicrometres)}); if(stratum.bedrock.presentInCurrentField &&stratum.bedrock.bottomMicrometres(stratum.bedrock.bottomMicrometres), static_cast(stratum.bedrock.topMicrometres)}); } for(std::int32_t order=0;order+1(ResolvedLayout->strates.size()); ++order) { const auto* upper=FindResolvedLayoutStratumByOrder(order); const auto* lower=FindResolvedLayoutStratumByOrder(order+1); if(!upper||!lower||!IsResolvedLayoutTransitionActive(*upper))continue; const auto displacement=ResolvedTransitionMaximumFaceDisplacementMicrometres( *upper,*lower); ResolvedBoundaryZIntervalsMicrometres.push_back({ static_cast(std::min(upper->bedrock.bottomMicrometres, lower->ceiling.bottomMicrometres)-displacement), static_cast(std::max(upper->bedrock.topMicrometres, lower->ceiling.topMicrometres)+displacement)}); } std::sort(ResolvedBoundaryZIntervalsMicrometres.begin(), ResolvedBoundaryZIntervalsMicrometres.end(), [](const auto& a,const auto& b){return a[0]0&&interval[0]<=ResolvedBoundaryZIntervalsMicrometres[write-1][1]) ResolvedBoundaryZIntervalsMicrometres[write-1][1]=std::max( ResolvedBoundaryZIntervalsMicrometres[write-1][1],interval[1]); else ResolvedBoundaryZIntervalsMicrometres[write++]=interval; } ResolvedBoundaryZIntervalsMicrometres.resize(write); } std::int64_t ResolvedTransitionMaximumFaceDisplacementMicrometres( const VoxelForgeCore::ResolvedLayoutStratumV2& Upper, const VoxelForgeCore::ResolvedLayoutStratumV2& Lower) const { const std::int64_t width=std::max(0,Upper.transitionWidthMicrometres); if(Upper.transitionMode=="Gradient") return width/8; if(Upper.transitionMode!="Interleaved")return 0; const std::int64_t upperThickness=std::max(0, Upper.bedrock.topMicrometres-Upper.bedrock.bottomMicrometres); const std::int64_t lowerThickness=std::max(0, Lower.ceiling.topMicrometres-Lower.ceiling.bottomMicrometres); const std::int64_t maximumCrossing=std::min(width,upperThickness+lowerThickness); return (maximumCrossing+1)/2; } bool ResolvedTransitionPassageFadeIntersectsTileXY( const std::array& minimumSubvoxels, const std::array& maximumSubvoxels) const { if(!Owner||!ResolvedLayout||Owner->CoreVoxelSizeMicrons==0)return true; for(std::size_t axis=0;axis<2;++axis) if(minimumSubvoxels[axis]>maximumSubvoxels[axis])return true; constexpr std::int64_t Subvoxels=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; constexpr std::int64_t SupportMarginMicrometres=1000000; constexpr std::int64_t FadeWidthMicrometres=2000000; const double voxelMicrometres=static_cast(Owner->CoreVoxelSizeMicrons); const double minimumX=static_cast(minimumSubvoxels[0])*voxelMicrometres /Subvoxels-voxelMicrometres; const double maximumX=static_cast(maximumSubvoxels[0])*voxelMicrometres /Subvoxels+voxelMicrometres; const double minimumY=static_cast(minimumSubvoxels[1])*voxelMicrometres /Subvoxels-voxelMicrometres; const double maximumY=static_cast(maximumSubvoxels[1])*voxelMicrometres /Subvoxels+voxelMicrometres; for(std::int32_t order=0;order+1(ResolvedLayout->strates.size()); ++order) { const auto* upper=FindResolvedLayoutStratumByOrder(order); const auto* lower=FindResolvedLayoutStratumByOrder(order+1); if(!upper||!lower||!IsResolvedLayoutTransitionActive(*upper))continue; const std::int64_t seam=upper->bedrock.bottomMicrometres; for(const auto& passage:ResolvedLayout->passages) { if(passage.upperStrateIndex!=upper->strateIndex ||passage.lowerStrateIndex!=lower->strateIndex)continue; const std::int64_t mouthRadius=std::max(0, passage.mouthRadiusSubvoxels*Owner->CoreVoxelSizeMicrons/Subvoxels); const std::int64_t protectedRadius=mouthRadius+SupportMarginMicrometres; const double fadeEnd=static_cast(protectedRadius+FadeWidthMicrometres); for(const auto* mouth:{&passage.upperMouthSubvoxels,&passage.lowerMouthSubvoxels}) { const std::int64_t mouthZ=mouth->at(2)*Owner->CoreVoxelSizeMicrons/Subvoxels; if(std::abs(mouthZ-seam) >upper->transitionWidthMicrometres/2+mouthRadius)continue; const double mouthX=static_cast(mouth->at(0) *Owner->CoreVoxelSizeMicrons/Subvoxels); const double mouthY=static_cast(mouth->at(1) *Owner->CoreVoxelSizeMicrons/Subvoxels); const double dx=mouthXmaximumX?mouthX-maximumX:0.0; const double dy=mouthYmaximumY?mouthY-maximumY:0.0; if(dx*dx+dy*dyCoreVoxelSizeMicrons==0)return Q16; const std::int64_t seam=Upper.bedrock.bottomMicrometres; const std::int64_t coreMicrometres=Owner->CoreVoxelSizeMicrons; const std::int64_t subvoxelsPerVoxel=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; std::int64_t weight=Q16; for(const auto& passage:ResolvedLayout->passages) { if(passage.upperStrateIndex!=Upper.strateIndex ||passage.lowerStrateIndex!=Lower.strateIndex)continue; const std::int64_t mouthRadiusMicrometres=std::max(0, passage.mouthRadiusSubvoxels*coreMicrometres/subvoxelsPerVoxel); const std::int64_t protectedRadius=mouthRadiusMicrometres +SupportMarginMicrometres; for(const auto* mouth:{&passage.upperMouthSubvoxels,&passage.lowerMouthSubvoxels}) { #if defined(VF_L30_ENABLE_COST_COUNTERS) ++ThreadClassificationStats().TransitionPassageMouthTests; #endif const std::int64_t mouthZMicrometres=mouth->at(2)*coreMicrometres /subvoxelsPerVoxel; if(std::abs(mouthZMicrometres-seam) >Upper.transitionWidthMicrometres/2+mouthRadiusMicrometres)continue; const double dx=static_cast(XMicrometres-mouth->at(0)*coreMicrometres /subvoxelsPerVoxel); const double dy=static_cast(YMicrometres-mouth->at(1)*coreMicrometres /subvoxelsPerVoxel); const std::int64_t fadeEnd=protectedRadius+FadeWidthMicrometres; const double distanceSquared=dx*dx+dy*dy; if(distanceSquared>=static_cast(fadeEnd)*fadeEnd)continue; if(distanceSquared<=static_cast(protectedRadius)*protectedRadius)return 0; const double distance=std::sqrt(distanceSquared); const double t=static_cast(static_cast(distance) -protectedRadius)/FadeWidthMicrometres; const double smooth=t*t*(3.0-2.0*t); weight=std::min(weight,static_cast(std::llround(smooth*Q16))); } } return weight; } VoxelForgeCore::ResolvedLayoutTransitionBandsV1 ResolvedTransitionBandsAtXY( const VoxelForgeCore::ResolvedLayoutStratumV2& Upper, const VoxelForgeCore::ResolvedLayoutStratumV2& Lower, std::int64_t XMicrometres,std::int64_t YMicrometres,bool SkipPassageFade=false) const { auto bands=VoxelForgeCore::EvaluateResolvedLayoutTransitionBandsV1(Upper,Lower, Owner?Owner->Seed:0,ResolvedLayout?ResolvedLayout->strateLayoutChunkSizeMicrometres:0, XMicrometres,YMicrometres); constexpr std::int64_t TransitionQ16=1ll<<16; const std::int64_t weight=SkipPassageFade?TransitionQ16: ResolvedTransitionPassageWeightQ16(Upper,Lower,XMicrometres,YMicrometres); if(weight>=TransitionQ16)return bands; const std::int64_t seam=Upper.bedrock.bottomMicrometres; const std::int64_t upperThickness=Upper.bedrock.topMicrometres-seam; const std::int64_t lowerThickness=Lower.ceiling.topMicrometres -Lower.ceiling.bottomMicrometres; const auto scaled=[&](std::int64_t value){return (value*weight)/TransitionQ16;}; bands.upperBedrockBottomMicrometres=seam+scaled( bands.upperBedrockBottomMicrometres-seam); bands.upperBedrockTopMicrometres=bands.upperBedrockBottomMicrometres+upperThickness; bands.lowerCeilingTopMicrometres=seam+scaled( bands.lowerCeilingTopMicrometres-seam); bands.lowerCeilingBottomMicrometres=bands.lowerCeilingTopMicrometres-lowerThickness; bands.crossingDepthMicrometres=std::max(0, bands.lowerCeilingTopMicrometres-bands.upperBedrockBottomMicrometres); return bands; } bool ResolvedBedrockTopAt(const VoxelForgeCore::ResolvedLayoutStratumV2& Upper, double X,double Y,double& TopZ) const { const auto* lower=FindResolvedLayoutStratumByOrder(Upper.order+1); if(!lower||!Owner||Owner->CoreVoxelSizeMicrons==0 ||!IsResolvedLayoutTransitionActive(Upper))return false; const auto toMicrometres=[&](double value) {return static_cast(std::llround(value*Owner->CoreVoxelSizeMicrons));}; const auto* tileQueries=ActivePassageTileQueries; const bool skipPassageFade=tileQueries&&tileQueries->matches(*this) &&!tileQueries->mayContainTransitionPassageFade(); const auto bands=ResolvedTransitionBandsAtXY(Upper,*lower,toMicrometres(X), toMicrometres(Y),skipPassageFade); TopZ=static_cast(bands.upperBedrockTopMicrometres) /Owner->CoreVoxelSizeMicrons; return std::isfinite(TopZ); } bool ResolvedCeilingBottomAt(const VoxelForgeCore::ResolvedLayoutStratumV2& Lower, double X,double Y,double& BottomZ) const { const auto* upper=FindResolvedLayoutStratumByOrder(Lower.order-1); if(!upper||!Owner||Owner->CoreVoxelSizeMicrons==0 ||!IsResolvedLayoutTransitionActive(*upper))return false; const auto toMicrometres=[&](double value) {return static_cast(std::llround(value*Owner->CoreVoxelSizeMicrons));}; const auto* tileQueries=ActivePassageTileQueries; const bool skipPassageFade=tileQueries&&tileQueries->matches(*this) &&!tileQueries->mayContainTransitionPassageFade(); const auto bands=ResolvedTransitionBandsAtXY(*upper,Lower,toMicrometres(X), toMicrometres(Y),skipPassageFade); BottomZ=static_cast(bands.lowerCeilingBottomMicrometres) /Owner->CoreVoxelSizeMicrons; return std::isfinite(BottomZ); } bool IsInsideResolvedStrateBoundaryBand(float Z) const { #if defined(VF_L30_ENABLE_COST_COUNTERS) ++ThreadClassificationStats().BoundaryBandCalls; #endif const auto* tileQueries=ActivePassageTileQueries; if(tileQueries&&tileQueries->matches(*this)) { if(!tileQueries->mayContainResolvedBoundaryBand()) { #if defined(VF_L30_ENABLE_COST_COUNTERS) ++ThreadClassificationStats().BoundaryTileCullRejects; #endif return false; } if(Owner&&!tileQueries->mayContainResolvedBoundaryBandAtZ( static_cast(Z)*Owner->CoreVoxelSizeMicrons)) { #if defined(VF_L30_ENABLE_COST_COUNTERS) ++ThreadClassificationStats().BoundaryPointCullRejects; #endif return false; } // This predicate only selects the scalar path for boundary-capable Z // samples. The merged intervals are conservative, so a positive is safe. return true; } if(!ResolvedLayout||!Owner||!Owner->Manager||!std::isfinite(Z))return false; const double worldZCentimetres=static_cast(Z)*GetCoreVoxelSizeCm(); const int32 strateIndex=Owner->Manager->GetStrateIndex(static_cast(worldZCentimetres)); const auto* stratum=FindResolvedLayoutStratum(strateIndex); if(!stratum)return false; const double zMicrometres=static_cast(Z)*Owner->CoreVoxelSizeMicrons; const auto solidAt=[&](const VoxelForgeCore::ResolvedLayoutBandV2& band) {return band.presentInCurrentField&&zMicrometres>=band.bottomMicrometres &&zMicrometresbedrock.bottomMicrometres, Lower->ceiling.bottomMicrometres)-maxDisplacement; const double maximum=std::max(Upper->bedrock.topMicrometres, Lower->ceiling.topMicrometres)+maxDisplacement; const bool touches=zMicrometres>=minimum&&zMicrometresorder+1); const auto* upper=stratum->order>0?FindResolvedLayoutStratumByOrder(stratum->order-1):nullptr; if(mayTouchTransition(stratum,lower)||mayTouchTransition(upper,stratum))return true; return solidAt(stratum->ceiling)||solidAt(stratum->bedrock); } float ApplyResolvedStrateBoundaryBand(float density,float X,float Y,float Z) const { #if defined(VF_L30_ENABLE_COST_COUNTERS) ++ThreadClassificationStats().BoundaryBandCalls; #endif (void)X;(void)Y; const auto* tileQueries=ActivePassageTileQueries; if(tileQueries&&tileQueries->matches(*this)) { if(!tileQueries->mayContainResolvedBoundaryBand()) { #if defined(VF_L30_ENABLE_COST_COUNTERS) ++ThreadClassificationStats().BoundaryTileCullRejects; #endif return density; } if(Owner&&!tileQueries->mayContainResolvedBoundaryBandAtZ( static_cast(Z)*Owner->CoreVoxelSizeMicrons)) { #if defined(VF_L30_ENABLE_COST_COUNTERS) ++ThreadClassificationStats().BoundaryPointCullRejects; #endif return density; } } if(!ResolvedLayout||!Owner||!Owner->Manager||Owner->CoreVoxelSizeMicrons==0 ||!std::isfinite(Z))return density; const double zMicrometres=static_cast(Z)*Owner->CoreVoxelSizeMicrons; if(ResolvedLayout->strates.size()==2) { const auto* transitionUpper=FindResolvedLayoutStratumByOrder(0); const auto* transitionLower=FindResolvedLayoutStratumByOrder(1); if(transitionUpper&&transitionLower &&IsResolvedLayoutTransitionActive(*transitionUpper)) { #if defined(VF_L30_ENABLE_COST_COUNTERS) ++ThreadClassificationStats().TransitionWindowChecks; #endif const auto maximumDisplacement=ResolvedTransitionMaximumFaceDisplacementMicrometres( *transitionUpper,*transitionLower); const double minimum=std::min(transitionUpper->bedrock.bottomMicrometres, transitionLower->ceiling.bottomMicrometres)-maximumDisplacement; const double maximum=std::max(transitionUpper->bedrock.topMicrometres, transitionLower->ceiling.topMicrometres)+maximumDisplacement; if(zMicrometres>=minimum&&zMicrometresmatches(*this) &&!tileQueries->mayContainTransitionPassageFade(); #if defined(VF_L30_ENABLE_COST_COUNTERS) if(skipPassageFade) ++ThreadClassificationStats().TransitionPassageTileSkips; #endif const auto xMicrometres=static_cast(std::llround( static_cast(X)*Owner->CoreVoxelSizeMicrons)); const auto yMicrometres=static_cast(std::llround( static_cast(Y)*Owner->CoreVoxelSizeMicrons)); const auto bands=ResolvedTransitionBandsAtXY(*transitionUpper, *transitionLower,xMicrometres,yMicrometres,skipPassageFade); const std::int64_t solidBottom=std::min( bands.upperBedrockBottomMicrometres,bands.lowerCeilingBottomMicrometres); const std::int64_t solidTop=std::max( bands.upperBedrockTopMicrometres,bands.lowerCeilingTopMicrometres); const double signedDistanceMicrometres=zMicrometres(solidBottom-zMicrometres) :zMicrometres>solidTop ?static_cast(zMicrometres-solidTop) :-static_cast(std::min(zMicrometres-solidBottom, solidTop-zMicrometres)); const std::int64_t seam=transitionUpper->bedrock.bottomMicrometres; const bool floatingIslandAirGap= transitionUpper->generatorKind=="FloatingIslands" &&transitionUpper->transitionMode=="Gradient" &&bands.upperBedrockTopMicrometres=bands.upperBedrockTopMicrometres &&zMicrometres0.0&&density<0.0f &&!floatingIslandAirGap)) return static_cast(signedDistanceMicrometres /Owner->CoreVoxelSizeMicrons); } } } const double worldZCentimetres=static_cast(Z)*GetCoreVoxelSizeCm(); const int32 strateIndex=Owner->Manager->GetStrateIndex(static_cast(worldZCentimetres)); const auto* stratum=FindResolvedLayoutStratum(strateIndex); if(!stratum)return density; const auto solidAt=[&](const VoxelForgeCore::ResolvedLayoutBandV2& band) {return band.presentInCurrentField&&zMicrometres>=band.bottomMicrometres &&zMicrometresbedrock.bottomMicrometres, lower->ceiling.bottomMicrometres)-maxDisplacement; const double maximum=std::max(upper->bedrock.topMicrometres, lower->ceiling.topMicrometres)+maxDisplacement; if(zMicrometres=maximum)return false; #if defined(VF_L30_ENABLE_COST_COUNTERS) ++ThreadClassificationStats().TransitionWindowHits; ++ThreadClassificationStats().TransitionEvaluatorCalls; #endif if(upper==stratum)bBedrockHandledByTransition=true; if(lower==stratum)bCeilingHandledByTransition=true; const auto* tileQueries=ActivePassageTileQueries; const std::int64_t xMicrometres=static_cast(std::llround( static_cast(X)*Owner->CoreVoxelSizeMicrons)); const std::int64_t yMicrometres=static_cast(std::llround( static_cast(Y)*Owner->CoreVoxelSizeMicrons)); const bool skipPassageFade=tileQueries&&tileQueries->matches(*this) &&!tileQueries->mayContainTransitionPassageFade(); #if defined(VF_L30_ENABLE_COST_COUNTERS) if(skipPassageFade)++ThreadClassificationStats().TransitionPassageTileSkips; #endif const auto resolvedBands=ResolvedTransitionBandsAtXY(*upper,*lower, xMicrometres,yMicrometres,skipPassageFade); // Treat the two authored bands as one continuous solid interval. Its // exterior faces remain the lower ceiling bottom and upper bedrock top; // measuring signed distance from those faces also clears native generator // density on both air sides of the displaced interface. const std::int64_t solidBottom=std::min( resolvedBands.upperBedrockBottomMicrometres, resolvedBands.lowerCeilingBottomMicrometres); const std::int64_t solidTop=std::max( resolvedBands.upperBedrockTopMicrometres, resolvedBands.lowerCeilingTopMicrometres); const double signedDistanceMicrometres=zMicrometres(solidBottom-zMicrometres) :zMicrometres>solidTop ?static_cast(zMicrometres-solidTop) :-static_cast(std::min(zMicrometres-solidBottom, solidTop-zMicrometres)); const std::int64_t seam=upper->bedrock.bottomMicrometres; const bool floatingIslandAirGap=upper->generatorKind=="FloatingIslands" &&upper->transitionMode=="Gradient" &&resolvedBands.upperBedrockTopMicrometres=resolvedBands.upperBedrockTopMicrometres &&zMicrometres0.0&&density<0.0f&&!floatingIslandAirGap) return false; density=static_cast(signedDistanceMicrometres /std::max(1,Owner->CoreVoxelSizeMicrons)); return true; }; const auto* lower=FindResolvedLayoutStratumByOrder(stratum->order+1); const auto* upper=stratum->order>0?FindResolvedLayoutStratumByOrder(stratum->order-1):nullptr; if(applyInterface(stratum,lower)||applyInterface(upper,stratum))return density; const auto* band=(!bCeilingHandledByTransition&&solidAt(stratum->ceiling)) ?&stratum->ceiling:(!bBedrockHandledByTransition&&solidAt(stratum->bedrock)) ?&stratum->bedrock:nullptr; if(!band)return density; return -std::max(0.05f,static_cast(band->solidDensityQ16)/65536.0f); } float ApplyResolvedStratePassageCarves(float density,float X,float Y,float Z) const { if(!ResolvedLayout||ResolvedLayout->passages.empty()||!std::isfinite(X) ||!std::isfinite(Y)||!std::isfinite(Z))return density; const auto* passageTileQueries=ActivePassageTileQueries; const bool passageTileScopeMatches=passageTileQueries &&passageTileQueries->matches(*this); if(passageTileScopeMatches&&density>=0.0f &&!passageTileQueries->hasRouteFeatureOverlap() &&!passageTileQueries->hasColumnZOverlap())return density; constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double qx=static_cast(X)*Q,qy=static_cast(Y)*Q,qz=static_cast(Z)*Q; const double qPerMetre=Q*1000000.0/std::max(1,ResolvedLayout->coreVoxelSizeMicrometres); const double floorThicknessVoxels=qPerMetre/Q; const double playerCenterAboveFloorQ=1.13*qPerMetre; float composed=density; const auto composeRouteMouthSupport=[&](std::int32_t strateIndex, const std::array& mouth,bool departure) { // Support is allowed to reinforce native solid terrain only. Without this // guard the route-point tread stamps a floating shelf into an open stratum. if(!(density<0.0f))return; const double mouthDx=qx-static_cast(mouth[0]); const double mouthDy=qy-static_cast(mouth[1]); const double routeSupportReach=13.0*qPerMetre; if(mouthDx*mouthDx+mouthDy*mouthDy>routeSupportReach*routeSupportReach)return; const auto stratum=std::find_if(ResolvedLayout->strates.begin(), ResolvedLayout->strates.end(),[strateIndex](const auto& value) {return value.strateIndex==strateIndex;}); if(stratum==ResolvedLayout->strates.end())return; std::array route{}; std::size_t routeCount=0; for(const auto& anchor:stratum->anchors) if(anchor.kind=="route_point"&&anchor.values.size()>=3) { if(departure) { if(routeCount(1, ResolvedLayout->coreVoxelSizeMicrometres); for(std::size_t index=0;index(b.values[0]-a.values[0]); const double dy=static_cast(b.values[1]-a.values[1]); const double lengthSq=dx*dx+dy*dy; const double t=lengthSq>0.0?std::clamp(((qx-a.values[0])*dx +(qy-a.values[1])*dy)/lengthSq,0.0,1.0):0.0; const double centerX=a.values[0]+t*dx,centerY=a.values[1]+t*dy; const double centerZ=a.values[2]+t*(static_cast(b.values[2])-a.values[2]); const double routeRadius=a.values.size()>=4 ?a.values[3]+t*(static_cast(b.values[3])-a.values[3]):0.0; const double halfWidth=std::max(0.75*qPerMetre,routeRadius); const double horizontalX=qx-centerX,horizontalY=qy-centerY; if(horizontalX*horizontalX+horizontalY*horizontalY>halfWidth*halfWidth)continue; const double floorVoxels=(centerZ-1.20*qPerMetre)/Q; if(ZfloorVoxels+1.0/Q)continue; composed=std::min(composed,static_cast( std::clamp(Z-floorVoxels,-floorThickness,0.0))); } }; // Keep the native surface route connected to the entrance. Passage carving // then removes only the parts that overlap the tunnel, whose own tread owns // the interior ramp. for(std::size_t passageIndex=0;passageIndexpassages.size();++passageIndex) { const auto& passage=ResolvedLayout->passages[passageIndex]; const bool tileHasSupport=passageTileScopeMatches; if(!tileHasSupport||ActivePassageTileQueries->hasMouthSupport(passageIndex)) composeRouteMouthSupport(passage.upperStrateIndex, passage.upperMouthSubvoxels,true); } for(std::size_t passageIndex=0;passageIndexpassages.size();++passageIndex) { const auto& passage=ResolvedLayout->passages[passageIndex]; if(passage.controlPoints.size()<2)continue; const double firstFloorQ=passage.controlPoints.front().positionSubvoxels[2] -passage.controlPoints.front().radiusSubvoxels+playerCenterAboveFloorQ; const double lastFloorQ=passage.controlPoints.back().positionSubvoxels[2] -passage.controlPoints.back().radiusSubvoxels+playerCenterAboveFloorQ; for(const auto& crossing:PassageVoidCrossings) { if(!crossing.needsColumn||crossing.passageIndex!=passageIndex ||qz>crossing.topZSubvoxels||qzhasFeature(passageIndex,true); if(tileProvesColumnExterior) { #if defined(VF_L30_ENABLE_COST_COUNTERS) ++ThreadClassificationStats().ColumnGuardCalls; #endif if(composed<0.0f) { #if defined(VF_L30_ENABLE_COST_COUNTERS) ++ThreadClassificationStats().ColumnGuardNonpositiveSkips; #endif continue; } const double dx=qx-crossing.centerXSubvoxels; const double dy=qy-crossing.centerYSubvoxels; const double horizontalSquared=dx*dx+dy*dy; const double maximumRadius=static_cast(crossing.maximumRadiusSubvoxels); const double exteriorLimit=maximumRadius +static_cast(composed)*Q+1.0e-4; #if defined(VF_L30_ENABLE_COST_COUNTERS) if(horizontalSquared>exteriorLimit*exteriorLimit) { ++ThreadClassificationStats().ColumnGuardRadialRejects; continue; } ++ThreadClassificationStats().ColumnSignedDistanceCalls; #else if(horizontalSquared>exteriorLimit*exteriorLimit)continue; #endif columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z); } else { #if defined(VF_L30_ENABLE_COST_COUNTERS) ++ThreadClassificationStats().ColumnSignedDistanceCalls; #endif columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z); } composed=std::min(composed,static_cast(columnDistance/Q)); } const bool tilePassageFree=passageTileScopeMatches &&!ActivePassageTileQueries->hasFeature(passageIndex,false); if(composed>=0.0f&&tilePassageFree) { continue; } if(qz>firstFloorQ+2.0*passage.mouthRadiusSubvoxels ||qz(passage.controlPoints.size())-1; const std::ptrdiff_t lowerMouthIndex=std::clamp( static_cast(passage.lowerMouthControlIndex),1,lastIndex); const auto mainEnd=passage.controlPoints.begin()+lowerMouthIndex+1; // The tube radius is measured from its centerline, while floorProfile is // one player-center offset away. Search the full vertical radius of the SDF; // a narrow point window misses earlier turns of a low-pitch helix. const double carveSearchMargin=std::max(passage.mouthRadiusSubvoxels, passage.midRadiusSubvoxels)+0.25*qPerMetre; const auto carveHigh=std::lower_bound(passage.controlPoints.begin(),mainEnd, qz+carveSearchMargin, [&](const auto& point,double value){return floorProfile(point)>value;}); const auto carveLow=std::lower_bound(passage.controlPoints.begin(),mainEnd, qz-carveSearchMargin, [&](const auto& point,double value){return floorProfile(point)>value;}); const std::ptrdiff_t carveBegin=std::max(0, (carveHigh-passage.controlPoints.begin())-1); const std::ptrdiff_t carveEnd=std::min(lowerMouthIndex-1, (carveLow-passage.controlPoints.begin())+1); const std::ptrdiff_t mouthSearchBegin=std::max(0,lowerMouthIndex-8); const std::ptrdiff_t mouthSearchEnd=std::min(lastIndex, lowerMouthIndex+8); if(density<0.0f&&tilePassageFree &&!PassageMayAffectDensity(passageIndex,carveBegin,carveEnd, lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,qz, -static_cast(composed)*Q+0.24*qPerMetre)) { continue; } // For a positive source sample, the tube can only change it within relief reach; // the supported column tread is within 2.25 m of its centerline. A short-span // bridge's positive exterior SDF is not bounded this way, so keep its full query. // Pour un échantillon source positif, le tube ne change le champ qu'à portée du relief ; // le plancher du pilier reste à 2,25 m de son axe. Le SDF extérieur d'une courte passerelle // n'a pas cette borne, donc sa requête complète est conservée. const bool canCullPositiveRoute=passageTileScopeMatches&&density>=0.0f &&!PassageHasShortSpanSegments(passageIndex); if(canCullPositiveRoute&&!PassageMayAffectDensity(passageIndex,carveBegin,carveEnd, lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,qz, std::max(2.25*qPerMetre,-static_cast(composed)*Q +0.24*qPerMetre)))continue; double bestSignedDistance=std::numeric_limits::infinity(); double bestPlayerCapsuleSignedDistance=std::numeric_limits::infinity(); double bestT=0.0; std::ptrdiff_t bestIndex=0; int bestScanPhase=std::numeric_limits::max(); std::ptrdiff_t bestScanIndex=std::numeric_limits::max(); std::ptrdiff_t bestSupportSegmentIndex=0; bool hasSupportFloor=false; double bestSupportDensity=0.0; const auto considerCarveSegment=[&](std::size_t segmentIndex,int scanPhase) { const std::ptrdiff_t index=static_cast(segmentIndex); const auto& a=passage.controlPoints[static_cast(index)]; const auto& b=passage.controlPoints[static_cast(index+1)]; const double ax=static_cast(a.positionSubvoxels[0]); const double ay=static_cast(a.positionSubvoxels[1]); const double az=static_cast(a.positionSubvoxels[2]); const double dx=static_cast(b.positionSubvoxels[0])-ax; const double dy=static_cast(b.positionSubvoxels[1])-ay; const double dz=static_cast(b.positionSubvoxels[2])-az; const double lengthSq=dx*dx+dy*dy+dz*dz; if(lengthSq<=0.0)return; const double t=std::clamp(((qx-ax)*dx+(qy-ay)*dy+(qz-az)*dz)/lengthSq,0.0,1.0); if(!passageTileScopeMatches) { const double horizontalLengthSq=dx*dx+dy*dy; const double playerT=horizontalLengthSq>0.0 ?std::clamp(((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):t; const double playerDx=qx-(ax+playerT*dx); const double playerDy=qy-(ay+playerT*dy); const double playerRadius=static_cast(a.radiusSubvoxels) +playerT*(b.radiusSubvoxels-a.radiusSubvoxels); const double playerCenterZ=az+playerT*dz-playerRadius+playerCenterAboveFloorQ; const double playerVertical=std::max(0.0, std::abs(qz-playerCenterZ)-0.54*qPerMetre); bestPlayerCapsuleSignedDistance=std::min(bestPlayerCapsuleSignedDistance, DeterministicSqrt(playerDx*playerDx+playerDy*playerDy +playerVertical*playerVertical)-0.34*qPerMetre); } const double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz; const double ex=qx-cx,ey=qy-cy,ez=qz-cz; const double radius=static_cast(a.radiusSubvoxels) +t*(b.radiusSubvoxels-a.radiusSubvoxels); const double signedDistance=DeterministicSqrt(ex*ex+ey*ey+ez*ez)-radius; if(signedDistance(8,lowerMouthIndex)-1,qx,qy,qz, 1,bestSignedDistance,considerCarveSegment); VisitNearestPassageSegmentRange(passageIndex,mouthSearchBegin,mouthSearchEnd-1, qx,qy,qz,2,bestSignedDistance,considerCarveSegment); VisitNearestPassageSegmentRange(passageIndex,lowerMouthIndex,lastIndex-1, qx,qy,qz,3,bestSignedDistance,considerCarveSegment); } else { for(std::ptrdiff_t index=carveBegin;index<=carveEnd;++index) considerCarveSegment(static_cast(index),0); for(std::ptrdiff_t index=0;index(8,lowerMouthIndex);++index) considerCarveSegment(static_cast(index),1); for(std::ptrdiff_t index=mouthSearchBegin;index(index),2); for(std::ptrdiff_t index=lowerMouthIndex;index(index),3); } if(!std::isfinite(bestSignedDistance))continue; const auto& nearestA=passage.controlPoints[static_cast(bestIndex)]; const auto& nearestB=passage.controlPoints[static_cast(bestIndex+1)]; const double nearestRadius=nearestA.radiusSubvoxels+bestT*( static_cast(nearestB.radiusSubvoxels)-nearestA.radiusSubvoxels); const double distanceToCenter=std::max(1.0e-9,bestSignedDistance+nearestRadius); const double verticalWallAlignment=distanceToCenter>1.0e-9 ?std::clamp(-(qz-(nearestA.positionSubvoxels[2]+bestT*( static_cast(nearestB.positionSubvoxels[2])-nearestA.positionSubvoxels[2]))) /distanceToCenter,0.0,1.0):0.0; const float relief=VF_FieldSurfaceRelief(static_cast(X),static_cast(Y), static_cast(Z),static_cast(Owner?Owner->Seed:0)); const double wallReliefWeight=1.0-0.80*verticalWallAlignment; const double roughSignedDistance=bestSignedDistance +static_cast(std::clamp(relief,-1.0f,1.0f))*0.24*qPerMetre*wallReliefWeight; if(IsPassageShortSpanSegment(passageIndex,static_cast(bestIndex))) { const double bridgeDistance=PassageBridgeSignedDistance(passageIndex, static_cast(bestIndex),X,Y,Z); composed=std::min(composed,static_cast(bridgeDistance/Q)); } // Positive density is air; rough rock is cut with full side/ceiling detail and // gentler relief on the gravity-facing wall so the supported tread stays open. composed=std::max(composed,static_cast(-roughSignedDistance/Q)); double bestSupportDistanceSquared=std::numeric_limits::infinity(); const auto considerSupportSegment=[&](std::ptrdiff_t index) { const auto& segmentA=passage.controlPoints[static_cast(index)]; const auto& segmentB=passage.controlPoints[static_cast(index+1)]; const double dx=static_cast(segmentB.positionSubvoxels[0])-segmentA.positionSubvoxels[0]; const double dy=static_cast(segmentB.positionSubvoxels[1])-segmentA.positionSubvoxels[1]; const double radiusA=segmentA.radiusSubvoxels; const double radiusB=segmentB.radiusSubvoxels; // Keep the tread within the probe band and below the capsule's // lowest sampled point. const double floorOffset=IsPassageShortSpanSegment(passageIndex, static_cast(index)) ||IsPassageColumnSegment(passageIndex,static_cast(index)) ?PassageBridgeDeckOffsetMetres(passageIndex,static_cast(index)) :0.20; const double horizontalLengthSq=dx*dx+dy*dy; const double supportT=horizontalLengthSq>0.0 ?std::clamp(((qx-segmentA.positionSubvoxels[0])*dx +(qy-segmentA.positionSubvoxels[1])*dy)/horizontalLengthSq,0.0,1.0) :bestT; const double centerX=segmentA.positionSubvoxels[0]+supportT*dx; const double centerY=segmentA.positionSubvoxels[1]+supportT*dy; const double centerZ=segmentA.positionSubvoxels[2]+supportT*( static_cast(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]); const double radius=segmentA.radiusSubvoxels+supportT*( static_cast(segmentB.radiusSubvoxels)-segmentA.radiusSubvoxels); const double horizontalX=qx-centerX,horizontalY=qy-centerY; const double horizontalDistance=DeterministicSqrt(horizontalX*horizontalX +horizontalY*horizontalY); const double supportPlayerCenterZ=centerZ-radius+playerCenterAboveFloorQ; const double supportQueryDepth=supportPlayerCenterZ-qz; // Support belongs just below the avatar's foot plane. Queries within // the capsule body, or a neighbouring coil's overlapping bore, must // not turn that tread into a ceiling or mid-air shelf. if(supportQueryDepth<0.93*qPerMetre||supportQueryDepth>1.30*qPerMetre)return; const bool fallbackSupport=qz>=supportPlayerCenterZ-1.50*qPerMetre; const double supportThicknessVoxels=fallbackSupport ?qPerMetre/Q:0.5*qPerMetre/Q; const double floorQ=centerZ-radius+floorOffset*qPerMetre; const double floorVoxels=floorQ/Q; // Keep the supported ramp to a comfortable walking width. Extending a // full bore-radius shelf across overlapping helix coils can block the // capsule on the neighbouring turn. const bool lowerMouthBodyOverlap=index>=lowerMouthIndex-2 &&supportQueryDepth<0.98*qPerMetre; const double supportHalfWidth=std::min(radius, (lowerMouthBodyOverlap?0.90:1.25)*qPerMetre); if(ZfloorVoxels+1.0/Q)return; if(horizontalDistance>supportHalfWidth)return; const double supportDistanceSq=horizontalDistance*horizontalDistance +(Z-floorVoxels)*(Z-floorVoxels); if(supportDistanceSq>=bestSupportDistanceSquared)return; bestSupportDistanceSquared=supportDistanceSq; hasSupportFloor=true; bestSupportSegmentIndex=index; bestSupportDensity=std::clamp(Z-floorVoxels, -supportThicknessVoxels,0.0); }; std::ptrdiff_t supportCenterIndex=bestIndex; if(bestIndexvalue;}); const std::ptrdiff_t profileIndex=std::clamp( supportProfile-passage.controlPoints.begin(),0,lowerMouthIndex); // At floor height the nearest 3D centerline can belong to the next // coil below. The descending floor profile identifies the coil that // owns this tread, even when it is more than eight control points away. supportCenterIndex=profileIndex; } const std::ptrdiff_t supportBegin=std::max(0,supportCenterIndex-1); const std::ptrdiff_t supportEnd=std::min(lastIndex-1, supportCenterIndex+1); for(std::ptrdiff_t index=supportBegin;index<=supportEnd;++index) considerSupportSegment(index); // Keep route treads inside the native rock envelope. Carving may reopen the // floor above this point, but a support cannot create a new surface in air. const bool passageColumnFloorSupport=hasSupportFloor &&IsPassageColumnSegment(passageIndex, static_cast(bestSupportSegmentIndex)); const bool shortSpanFloorSupport=hasSupportFloor &&IsPassageShortSpanSegment(passageIndex, static_cast(bestSupportSegmentIndex)); const bool capsuleAllowsSupport=passageTileScopeMatches ?!PassagePlayerCapsuleBlocksSupport(passageIndex,carveBegin,carveEnd, lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,qz,qPerMetre) :bestPlayerCapsuleSignedDistance>=0.05*qPerMetre; if(hasSupportFloor&&(density<0.0f||passageColumnFloorSupport||shortSpanFloorSupport) &&capsuleAllowsSupport) composed=std::min(composed,static_cast(bestSupportDensity)); } return composed; } float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const {return ApplyResolvedStratePassageCarves(ApplyResolvedStrateBoundaryBand(density,X,Y,Z),X,Y,Z);} bool ResolvedPassageIntersectsBox(const int32 minimum[3],const int32 maximum[3]) const { if(!ResolvedLayout||ResolvedLayout->passages.empty())return false; if(PassageProofNodes.empty()) { // Native-only readiness tiles have not built the passage BVH. Keep their // exact conservative fallback instead of indexing an empty tree. constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double voxelsPerMetre=Owner&&Owner->CoreVoxelSizeMicrons>0 ?1000000.0/Owner->CoreVoxelSizeMicrons:4.0; const double bridgeHalfWidthVoxels=3.5*voxelsPerMetre; const double bridgeFootDepthVoxels=(5.0-0.13)*voxelsPerMetre; const auto overlaps=[&](const double low[3],const double high[3]) { for(int axis=0;axis<3;++axis) if(high[axis]maximum[axis])return false; return true; }; for(const auto& passage:ResolvedLayout->passages) { for(std::size_t i=1;i(std::max(a.radiusSubvoxels,b.radiusSubvoxels))/Q; const double floorThicknessVoxels=voxelsPerMetre; const double horizontalExtent=std::max(radius,bridgeHalfWidthVoxels); const double footDepth=std::max(floorThicknessVoxels,bridgeFootDepthVoxels); const double low[3]={ std::min(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q-horizontalExtent, std::min(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q-horizontalExtent, std::min(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q-radius-footDepth}; const double high[3]={ std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+horizontalExtent, std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+horizontalExtent, std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius}; if(overlaps(low,high))return true; } const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), ResolvedLayout->strates.end(),[&passage](const auto& value) {return value.strateIndex==passage.upperStrateIndex;}); if(upperStratum!=ResolvedLayout->strates.end()) { std::array route{}; std::size_t routeCount=0; for(const auto& anchor:upperStratum->anchors) if(anchor.kind=="route_point"&&anchor.values.size()>=3) { if(routeCount=2) { const double mouthReach=13.0*voxelsPerMetre*Q; double minimumZ=std::numeric_limits::infinity(); double maximumZ=-std::numeric_limits::infinity(); for(std::size_t index=0;index(route[index]->values[2])); maximumZ=std::max(maximumZ,static_cast(route[index]->values[2])); } const double mouthX=passage.upperMouthSubvoxels[0]; const double mouthY=passage.upperMouthSubvoxels[1]; const double low[3]={(mouthX-mouthReach)/Q,(mouthY-mouthReach)/Q, (minimumZ-3.0*voxelsPerMetre*Q)/Q}; const double high[3]={(mouthX+mouthReach)/Q,(mouthY+mouthReach)/Q, (maximumZ+3.0*voxelsPerMetre*Q)/Q}; if(overlaps(low,high))return true; } } } for(const auto& crossing:PassageVoidCrossings) { if(!crossing.needsColumn)continue; const double radius=static_cast(crossing.maximumRadiusSubvoxels)/Q; const double low[3]={crossing.centerXSubvoxels/Q-radius, crossing.centerYSubvoxels/Q-radius,crossing.bottomZSubvoxels/Q}; const double high[3]={crossing.centerXSubvoxels/Q+radius, crossing.centerYSubvoxels/Q+radius,crossing.topZSubvoxels/Q}; if(overlaps(low,high))return true; } return false; } const double low[3]={static_cast(minimum[0]),static_cast(minimum[1]), static_cast(minimum[2])}; const double high[3]={static_cast(maximum[0]),static_cast(maximum[1]), static_cast(maximum[2])}; return PassageProofBoundsOverlapNode(0,low,high); } static bool MakeResolvedStratumBounds(double BottomZ,double TopZ,double SealThickness, FResolvedStratumBounds& Out) { if(!std::isfinite(BottomZ)||!std::isfinite(TopZ)||!std::isfinite(SealThickness) ||TopZ<=BottomZ||SealThickness<0.0)return false; Out={BottomZ,TopZ,SealThickness}; return true; } bool GetResolvedStratumBounds(const FStrateSlot& Slot,FResolvedStratumBounds& Out) const { if(!Owner||!Owner->Manager)return false; if(const auto* resolved=FindResolvedLayoutStratum(Slot.StrateIndex)) { const double coreMicrons=std::max(1,Owner->CoreVoxelSizeMicrons); const double bottom=static_cast(resolved->interiorBottomMicrometres)/coreMicrons; const double top=static_cast(resolved->interiorTopMicrometres)/coreMicrons; const double ceiling=static_cast(resolved->ceiling.topMicrometres -resolved->ceiling.bottomMicrometres)/coreMicrons; const double bedrock=static_cast(resolved->bedrock.topMicrometres -resolved->bedrock.bottomMicrometres)/coreMicrons; return MakeResolvedStratumBounds(bottom,top,std::max(ceiling,bedrock),Out); } const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2; if(bUseNativeManifestFields&&WorldAssembly &&Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,MidChunkZ)) ==ECaveGeneratorType::FloatingIslands) { const auto* Plan=FindWorldAssemblyFloatingIslandStratum(Slot.StrateIndex,MidChunkZ); if(Plan&&Plan->manifest.recipe.geometryVersion>=3) { constexpr double Scale=static_cast( VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); return MakeResolvedStratumBounds( static_cast(Plan->manifest.recipe.layoutMinimumZSubvoxels)/Scale, static_cast(Plan->manifest.recipe.layoutMaximumZSubvoxels)/Scale, static_cast(Plan->manifest.recipe.boundarySealSubvoxels)/Scale,Out); } } const FStrateGenerationParams Params=Owner->Manager->GetGenerationParams( FIntVector(0,0,MidChunkZ)); const double chunkWidth=GetResolvedLayoutChunkWidthVoxels(); return MakeResolvedStratumBounds(static_cast(Slot.BottomChunkZ)*chunkWidth, static_cast(Slot.TopChunkZ+1)*chunkWidth, static_cast(Params.BoundarySealThickness),Out); } enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel }; struct FBoxClassification { EVoxelTileClass Verdict=EVoxelTileClass::Mixed; bool bNativeSurface=false; bool bUnknown=false; }; struct FClassificationThreadStats { uint64 SolidBoxes=0,AirBoxes=0,MixedBoxes=0,UnknownBoxes=0; uint64 NativeFloatFieldCalls=0,NativeLatticeFieldCalls=0,NativeUnclampedFieldCalls=0; double Milliseconds=0.0; #if defined(VF_L30_ENABLE_COST_COUNTERS) uint64 BoundaryBandCalls=0,TransitionWindowChecks=0,TransitionWindowHits=0; uint64 TransitionEvaluatorCalls=0,TransitionPassageFadeCalls=0,TransitionPassageMouthTests=0; uint64 BoundaryTileCullRejects=0,BoundaryPointCullRejects=0; uint64 ColumnZOverlapCalls=0,ColumnGuardCalls=0,ColumnGuardNonpositiveSkips=0; uint64 ColumnGuardRadialRejects=0,ColumnSignedDistanceCalls=0; uint64 TransitionPassageTileSkips=0; #endif }; static void ResetThreadClassificationStats() { ThreadClassificationStats()={}; } static FClassificationThreadStats GetThreadClassificationStats() { return ThreadClassificationStats(); } struct FFarConnectorSegment { FVector Start=FVector::ZeroVector; FVector End=FVector::ZeroVector; float Radius=0.0f; }; struct FEditBrush { enum class EShape : uint8 { Sphere, Capsule, RoundedBox }; enum class EOperation : uint8 { Carve, AddRock, DensityOffset }; EShape Shape=EShape::Sphere; EOperation Operation=EOperation::Carve; std::array A{}; std::array B{}; std::array HalfExtent{}; double Radius=0.0; double Rounding=0.0; float Strength=0.0f; float Falloff=0.0f; }; struct FParallelTiming { double WallSeconds = 0.0; double WorkerSeconds = 0.0; }; struct FDetailWorkerProfile { uint64 Lines=0,Candidates=0,DetailEvaluations=0,Flips=0; uint64 LocalBandSkips=0; uint64 GpuVisualCandidates=0,CpuVisualCandidates=0,GpuOnlyCandidates=0; uint64 GpuVisualDetailEvaluations=0,GpuOnlyDetailEvaluations=0; uint64 DistanceSamples=0,DistanceSampleNanoseconds=0; uint64 DetailSamples=0,DetailSampleNanoseconds=0; uint64 ReliefSampleNanoseconds=0,LayerLookupSampleNanoseconds=0; uint64 OwnerRoughnessSampleNanoseconds=0,OwnerRoughnessFieldCalls=0; uint64 OwnerRoughnessActiveCalls=0,OwnerNoiseCacheHits=0,OwnerNoiseCacheMisses=0; }; struct FBuildStats { double ShapeSeconds = 0.0; double IntervalSeconds = 0.0; double SurfaceSeconds = 0.0; double FarShapeSeconds = 0.0; uint64 PrimitiveCount = 0; uint64 RoomCount = 0; uint64 RoomFloorJoinCount = 0; uint64 TunnelCount = 0; uint64 PassageCount = 0; uint64 SignNodes = 0; uint64 SurfaceCandidates = 0; uint64 SurfaceProxyNodes = 0; uint64 DetailFlips = 0; uint64 WalkSamples = 0; uint64 WalkFailures = 0; double MaxCapsuleJoinRadiusDelta = 0.0; double MaxCapsuleJoinRadiusSlopeDelta = 0.0; double MaxCapsuleJoinFloorDelta = 0.0; double MaxCapsuleJoinFloorSlopeDelta = 0.0; double MaxRawUnmarkedFloorSlopeDelta = 0.0; double MaxUnmarkedFloorSlopeDelta = 0.0; double MaxUnmarkedFloorDirectionSlopeDelta = 0.0; double MaxMarkedFloorSlopeDelta = 0.0; double MaxFloorCurveDeviation = 0.0; double MaxFloorProfileEndpointValueError = 0.0; double MaxFloorProfileEndpointSlopeError = 0.0; bool FloorProfileEndpointStepControlDetected = false; bool FloorProfileEndpointSlopeControlDetected = false; double LocalPrimitiveSeconds = 0.0; double LocalLineBinSeconds = 0.0; double CandidateMergeSeconds = 0.0; double DetailApplySeconds = 0.0; double SignEdgeDistanceSeconds = 0.0; double PassageValidationSeconds = 0.0; double SharedFieldSeconds = 0.0; double FarPrimitiveSeconds = 0.0; double FarBinSeconds = 0.0; FParallelTiming RockIntervalLines; FParallelTiming CandidateAxisLines; FParallelTiming CandidateMergeLines; FParallelTiming DetailBandLines; uint64 CurvedFloorSegments = 0; uint64 MarkedFloorSlopeJoins = 0; double LegacySegmentRadiusStep = 0.0; double SmoothedProfileRadiusStep = 0.0; double MaxTunnelBendDegrees = 0.0; FVector MaxTunnelBendVoxels = FVector::ZeroVector; FVector MaxTunnelBendDirection = FVector::ZeroVector; uint64 CapsuleJoinCount = 0; }; struct FPrimitive { int32 CenterSub[3]{}; int32 CapsuleStartSub[3]{}; int32 CapsuleEndSub[3]{}; double CapsuleStartWorld[3]{}; double CapsuleAxisDelta[3]{}; double CapsuleAxisLengthSquared = 0.0; double CapsuleAxisLength = 0.0; int64 Q[3][3]{}; int32 Min[3]{}; int32 Max[3]{}; float MinRadius = 1.0f; float CapsuleStartRadius = 1.0f; float CapsuleEndRadius = 1.0f; float CapsuleProfileStartRadius = 1.0f; float CapsuleProfileEndRadius = 1.0f; double CapsuleProfileStartT = 0.0; double CapsuleProfileEndT = 1.0; double CapsuleRadiusSlopeStart = 0.0; double CapsuleRadiusSlopeEnd = 0.0; double BlendRadius = 0.0; double FloorBlendRadius = 0.0; bool bCarve = true; bool bCritical = false; bool bPassageSurface = false; bool bCapsule = false; bool bSmoothCapsuleRadius = false; bool bRoundedBox = false; bool bHasFloorClip = false; bool bCurvedFloorClip = false; double RoundedHalfExtent[3]{}; double Rounding = 0.0; double FloorCenterZ = 0.0; double FloorSlopeX = 0.0; double FloorSlopeY = 0.0; double FloorCurveOriginX = 0.0; double FloorCurveOriginY = 0.0; double FloorCurveDeltaX = 0.0; double FloorCurveDeltaY = 0.0; double FloorCurveLength = 0.0; double FloorCurveStartZ = 0.0; double FloorCurveEndZ = 0.0; double FloorCurveTangentStart = 0.0; double FloorCurveTangentEnd = 0.0; double FloorCurveStartBlend = 0.0; double FloorCurveEndBlend = 0.0; double FloorCurveMaxDeviation = 0.0; double FloorCurveEndpointValueError = 0.0; double FloorCurveEndpointSlopeError = 0.0; }; struct FInterval { int32 Lo = 0, Hi = -1; }; struct FBinKey { int32 X=0,Y=0,Z=0; bool operator==(const FBinKey&)const=default; }; struct FBinHash { size_t operator()(const FBinKey& K) const noexcept { uint64 H=static_cast(K.X)*0x9E3779B185EBCA87ull; H^=static_cast(static_cast(K.Y))*0xC2B2AE3D27D4EB4Full; H^=static_cast(static_cast(K.Z))*0x165667B19E3779F9ull; H^=H>>29;H*=0xBF58476D1CE4E5B9ull;H^=H>>31; return static_cast(H); } }; struct FBinItems { static constexpr size_t InlineCapacity=4; struct FHeapStorage {uint32* Data=nullptr;uint32 Capacity=0;uint32 Reserved=0;}; union FStorage { std::array Inline; FHeapStorage Heap; FStorage():Inline{}{} ~FStorage(){} } Storage; uint32 Count=0; uint32 bHeap=0; FBinItems()=default; FBinItems(const FBinItems&)=delete; FBinItems& operator=(const FBinItems&)=delete; FBinItems(FBinItems&& Other) noexcept {MoveFrom(Other);} FBinItems& operator=(FBinItems&& Other) noexcept { if(this!=&Other){Release();MoveFrom(Other);} return *this; } ~FBinItems(){Release();} void push_back(uint32 Id) { if(!bHeap&&Count Previous=Storage.Inline; Storage.Heap={new uint32[8],8,0}; std::copy(Previous.begin(),Previous.end(),Storage.Heap.Data); bHeap=1; } else if(Count==Storage.Heap.Capacity) { const uint32 NewCapacity=Storage.Heap.Capacity*2; uint32* NewData=new uint32[NewCapacity]; std::copy_n(Storage.Heap.Data,Count,NewData); delete[] Storage.Heap.Data; Storage.Heap={NewData,NewCapacity,0}; } Storage.Heap.Data[Count++]=Id; } size_t size() const {return Count;} size_t capacity() const {return bHeap?Storage.Heap.Capacity:InlineCapacity;} const uint32* begin() const {return bHeap?Storage.Heap.Data:Storage.Inline.data();} const uint32* end() const {return begin()+Count;} private: void Release() { if(bHeap)delete[] Storage.Heap.Data; bHeap=0;Count=0; } void MoveFrom(FBinItems& Other) { Count=Other.Count;bHeap=Other.bHeap; if(bHeap) { Storage.Heap=Other.Storage.Heap; Other.bHeap=0;Other.Count=0; } else { Storage.Inline=Other.Storage.Inline; Other.Count=0; } } }; static constexpr int32 QScale = 1 << 28; static constexpr int32 SubPerVoxel = 16; static constexpr int32 DetailBand = 6; static constexpr float DetailSupremum = 4.0f; static constexpr double JoinBlendRadius = 1.5; static constexpr double RoomUnionBlendRadius = 12.0; static constexpr double FloorClipBlendRadius = 16.0; static constexpr float StrataCaveBlendRadius = 12.0f; static constexpr int32 LineBinSize = 32; static constexpr int32 SpatialBinSize = 256; bool Build(FStandaloneOwnerWorld& World, const FIntVector& MinVoxels, const FIntVector& MaxVoxels, const FIntVector& ClipMinVoxels, const FIntVector& ClipMaxVoxels, int32 Workers, EDetailVariant Variant, bool bIncludeFarWorld, std::string& Error, const VoxelForgeCore::JobScheduler* InScheduler=nullptr, bool bInRoundTunnels=false,bool bInProfileDetailBand=false, bool bValidatePassageRoutes=true, const std::vector& InGpuVisualDetailTiles={}, const std::vector& InCpuVisualDetailTiles={}, const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly=nullptr, const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout=nullptr) { using Clock=std::chrono::steady_clock; const auto TotalStart=Clock::now(); Owner=&World;WorldAssembly=InWorldAssembly;ResolvedLayout=InResolvedLayout; WorkerCount=std::clamp(Workers,1,16);DetailVariant=Variant; Scheduler=InScheduler;RoundTunnelsEnabled=bInRoundTunnels; bUseNativeManifestFields=true; bAnalyticOnly=false; bProfileDetailBand=bInProfileDetailBand; for(int32 A=0;A<3;++A) { Min[A]=(&MinVoxels.X)[A]-2; Max[A]=(&MaxVoxels.X)[A]+2; ClipMin[A]=(&ClipMinVoxels.X)[A]; ClipMax[A]=(&ClipMaxVoxels.X)[A]; Size[A]=Max[A]-Min[A]+1; if(Size[A]<=0||Size[A]>4096||ClipMax[A](Size[0])*Size[1]*Size[2]; if(NodeCount>160000000ull) {Error="shape interval region exceeds the 160-million-node safety bound";return false;} Stats={};Stats.SignNodes=NodeCount; AuditFloorProfileBlendNegativeControls(); auto Stage=Clock::now(); BuildLayerBounds(); auto Substage=Clock::now(); BuildOwnerPrimitives(); Stats.LocalPrimitiveSeconds=std::chrono::duration(Clock::now()-Substage).count(); Substage=Clock::now(); if(RoundTunnelsEnabled) BuildRoundTunnelPrimitives(); BuildSpatialBins(LineBinSize); Stats.LocalLineBinSeconds=std::chrono::duration(Clock::now()-Substage).count(); const auto SharedStage=Clock::now(); RebuildSharedFieldPrimitives(); Stats.SharedFieldSeconds=std::chrono::duration(Clock::now()-SharedStage).count(); Stats.ShapeSeconds=std::chrono::duration(Clock::now()-Stage).count(); Stats.PrimitiveCount=Primitives.size(); if(Primitives.empty()) {Error="owner shape extraction produced no rooms, tunnels, or passages";return false;} Stage=Clock::now(); const size_t NZ=static_cast(Size[2]); WordsPerZ=(NZ+63u)/64u; RockWords.assign(static_cast(Size[0])*Size[1]*WordsPerZ,0); SurfaceDistanceQ=std::make_unique_for_overwrite(static_cast(NodeCount)); BuildPackedRockIntervals(Stats.RockIntervalLines); // Native density queries for non-heightfield strates use the exact interval // evaluator. Build those native intervals before scanning passage support; // the scan itself still completes before any resolved column is composed. BuildPassageVoidCrossingPlan(); BuildCandidateBands(Stats.CandidateAxisLines,Stats.CandidateMergeLines, Stats.CandidateMergeSeconds); if(!BuildGpuVisualDetailNodeSets(InGpuVisualDetailTiles,InCpuVisualDetailTiles,Error)) return false; Stats.IntervalSeconds=std::chrono::duration(Clock::now()-Stage).count(); Stage=Clock::now(); Substage=Clock::now(); ApplyDetailBand(Stats.DetailBandLines); Stats.DetailApplySeconds=std::chrono::duration(Clock::now()-Substage).count(); Substage=Clock::now(); Stats.SurfaceProxyNodes+=CompleteSignEdgeDistances(); Stats.SignEdgeDistanceSeconds=std::chrono::duration(Clock::now()-Substage).count(); Stats.SurfaceSeconds=std::chrono::duration(Clock::now()-Stage).count(); // Route validation is world-wide. Detail regions reuse the same authored route data and // do not need to repeat the 5,642-point world audit for every entering tile group. Substage=Clock::now(); if(bValidatePassageRoutes&&!ValidatePassageRoutes()) { Error="compiled shape world contains a solid passage centreline sample"; return false; } Stats.PassageValidationSeconds=std::chrono::duration(Clock::now()-Substage).count(); Built=true; if(bIncludeFarWorld)BuildFarWorld(); const uint64 WordBytes=static_cast(RockWords.size()+CandidateWords.size())*sizeof(uint64); uint64 CandidateAxisBytes=0; for(const auto& AxisWords:CandidateAxisWords) CandidateAxisBytes+=static_cast(AxisWords.capacity())*sizeof(uint64); uint64 BinReferences=0,BinCapacity=0,InlineBins=0,OverflowBins=0; for(const auto& Bin:SpatialBins) { BinReferences+=Bin.second.size();BinCapacity+=Bin.second.capacity(); if(Bin.second.size()<=FBinItems::InlineCapacity)++InlineBins; else ++OverflowBins; } std::printf("[ShapeProfile] local_primitives=%.3fms line_bins=%.3fms rock_lines_wall=%.3fms rock_lines_worker=%.3fms candidate_axes_wall=%.3fms candidate_axes_worker=%.3fms candidate_merge_wall=%.3fms candidate_merge_worker=%.3fms candidate_merge_serial=%.3fms detail_lines_wall=%.3fms detail_lines_worker=%.3fms\n", Stats.LocalPrimitiveSeconds*1000.0,Stats.LocalLineBinSeconds*1000.0, Stats.RockIntervalLines.WallSeconds*1000.0,Stats.RockIntervalLines.WorkerSeconds*1000.0, Stats.CandidateAxisLines.WallSeconds*1000.0,Stats.CandidateAxisLines.WorkerSeconds*1000.0, Stats.CandidateMergeLines.WallSeconds*1000.0,Stats.CandidateMergeLines.WorkerSeconds*1000.0, Stats.CandidateMergeSeconds*1000.0,Stats.DetailBandLines.WallSeconds*1000.0, Stats.DetailBandLines.WorkerSeconds*1000.0); std::printf("[ShapeSubstage] detail_apply=%.3fms sign_edge_distances=%.3fms passage_validation=%.3fms shared_field_primitives=%.3fms far_parents=%.3fms\n", Stats.DetailApplySeconds*1000.0,Stats.SignEdgeDistanceSeconds*1000.0, Stats.PassageValidationSeconds*1000.0,Stats.SharedFieldSeconds*1000.0, Stats.FarShapeSeconds*1000.0); std::printf("[ShapeAllocation] words=%.2fMiB candidate_axis_words=%.2fMiB proxy=%.2fMiB primitives=%.2fMiB spatial_bins=%llu spatial_bin_refs=%llu spatial_bin_capacity=%llu inline_bins=%llu overflow_bins=%llu far_primitive_build=%.3fms far_bin_rebuild=%.3fms\n", static_cast(WordBytes)/1048576.0,static_cast(CandidateAxisBytes)/1048576.0, static_cast(Stats.SignNodes*sizeof(int16))/1048576.0, static_cast(Primitives.capacity()*sizeof(FPrimitive))/1048576.0, static_cast(SpatialBins.size()), static_cast(BinReferences), static_cast(BinCapacity), static_cast(InlineBins), static_cast(OverflowBins),Stats.FarPrimitiveSeconds*1000.0, Stats.FarBinSeconds*1000.0); std::printf("[ShapeCompile] primitives=%llu rooms=%llu tunnels=%llu passages=%llu nodes=%llu packed=%.2f MiB surface_proxy_nodes=%llu proxy_address_capacity=%.2f MiB workers=%d\n", static_cast(Stats.PrimitiveCount), static_cast(Stats.RoomCount), static_cast(Stats.TunnelCount), static_cast(Stats.PassageCount), static_cast(Stats.SignNodes), static_cast(WordBytes)/1048576.0, static_cast(Stats.SurfaceProxyNodes), static_cast(Stats.SignNodes*sizeof(int16))/1048576.0,WorkerCount); std::printf("[ShapeStage] local-shapes=%.3f ms intervals+signs=%.3f ms surface-band=%.3f ms far-parents=%.3f ms total=%.3f ms candidates=%llu flips=%llu walk=%llu/%llu\n", Stats.ShapeSeconds*1000.0,Stats.IntervalSeconds*1000.0,Stats.SurfaceSeconds*1000.0,Stats.FarShapeSeconds*1000.0, std::chrono::duration(Clock::now()-TotalStart).count()*1000.0, static_cast(Stats.SurfaceCandidates), static_cast(Stats.DetailFlips), static_cast(Stats.WalkSamples-Stats.WalkFailures), static_cast(Stats.WalkSamples)); std::printf("[RoomFloorJoinAudit] automatic_room_overlaps=%llu thin_positive_support_overlays=0 status=PASS\n", static_cast(Stats.RoomFloorJoinCount)); const double SmoothUnionGrowthBound=JoinBlendRadius/6.0; const bool bFloorSlopePass=Stats.MaxUnmarkedFloorSlopeDelta<=0.75; const bool bFloorBlendPass=Stats.MaxFloorProfileEndpointValueError<=0.01 &&Stats.MaxFloorProfileEndpointSlopeError<=0.001 &&Stats.FloorProfileEndpointStepControlDetected &&Stats.FloorProfileEndpointSlopeControlDetected; std::printf("[FloorSlopeJoinAudit] raw_unmarked_max_delta=%.5f vox generated_tangent_delta=%.5f vox bend_direction_slope_change=%.5f vox marked_max_delta=%.5f vox marked_joins=%llu threshold=0.750 status=%s\n", Stats.MaxRawUnmarkedFloorSlopeDelta,Stats.MaxUnmarkedFloorSlopeDelta, Stats.MaxUnmarkedFloorDirectionSlopeDelta,Stats.MaxMarkedFloorSlopeDelta, static_cast(Stats.MarkedFloorSlopeJoins),bFloorSlopePass?"PASS":"FAIL"); std::printf("[FloorProfileBlend] c1_segments=%llu max_chord_deviation=%.3f vox informational endpoint_value_residual=%.8f vox endpoint_slope_residual=%.8f vox_per_m thresholds=(0.010,0.001) negative_controls=%s/%s status=%s\n", static_cast(Stats.CurvedFloorSegments),Stats.MaxFloorCurveDeviation, Stats.MaxFloorProfileEndpointValueError,Stats.MaxFloorProfileEndpointSlopeError, Stats.FloorProfileEndpointStepControlDetected?"step_detected":"step_missed", Stats.FloorProfileEndpointSlopeControlDetected?"slope_detected":"slope_missed", bFloorBlendPass?"PASS":"FAIL"); std::printf("[TunnelRadiusAudit] shared_joins=%llu legacy_fixed_segment_max_step=%.4f vox smoothed_profile_max_step=%.4f vox tapered_endpoint_max_delta=%.6f vox radius_slope_join_delta=%.8f floor_endpoint_max_delta=%.6f vox floor_slope_join_delta=%.5f unmarked_floor_slope_delta=%.5f marked_floor_slope_delta=%.5f marked_slope_joins=%llu smooth_union_growth_bound=%.4f vox thresholds=(0.005,0.000001,0.005,0.25) status=%s\n", static_cast(Stats.CapsuleJoinCount),Stats.LegacySegmentRadiusStep, Stats.SmoothedProfileRadiusStep,Stats.MaxCapsuleJoinRadiusDelta, Stats.MaxCapsuleJoinRadiusSlopeDelta, Stats.MaxCapsuleJoinFloorDelta,Stats.MaxCapsuleJoinFloorSlopeDelta, Stats.MaxUnmarkedFloorSlopeDelta,Stats.MaxMarkedFloorSlopeDelta, static_cast(Stats.MarkedFloorSlopeJoins),SmoothUnionGrowthBound, Stats.MaxCapsuleJoinRadiusDelta<=0.005&&Stats.MaxCapsuleJoinRadiusSlopeDelta<=1.0e-6 &&Stats.MaxCapsuleJoinFloorDelta<=0.005 &&SmoothUnionGrowthBound<=0.25?"PASS":"FAIL"); const double BendHorizontal=std::sqrt(Stats.MaxTunnelBendDirection.X*Stats.MaxTunnelBendDirection.X +Stats.MaxTunnelBendDirection.Y*Stats.MaxTunnelBendDirection.Y); const double BendYaw=std::atan2(Stats.MaxTunnelBendDirection.X,Stats.MaxTunnelBendDirection.Y)*57.29577951308232; const double BendPitch=std::atan2(Stats.MaxTunnelBendDirection.Z,BendHorizontal)*57.29577951308232; std::printf("[TunnelBendAudit] sharpest_bend=%.2f deg centerline=(%.2f,%.2f,%.2f) vox tangent=(%.4f,%.4f,%.4f) camera_yaw=%.2f camera_pitch=%.2f\n", Stats.MaxTunnelBendDegrees,Stats.MaxTunnelBendVoxels.X, Stats.MaxTunnelBendVoxels.Y,Stats.MaxTunnelBendVoxels.Z, Stats.MaxTunnelBendDirection.X,Stats.MaxTunnelBendDirection.Y, Stats.MaxTunnelBendDirection.Z,BendYaw,BendPitch); std::fflush(stdout); return true; } bool BuildAnalyticRegion(FStandaloneOwnerWorld& World,const FIntVector& MinVoxels, const FIntVector& MaxVoxels,const FIntVector& ClipMinVoxels, const FIntVector& ClipMaxVoxels,EDetailVariant Variant, bool bInRoundTunnels, const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly, const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout, std::string& Error) { using Clock=std::chrono::steady_clock; Owner=&World;WorldAssembly=InWorldAssembly;ResolvedLayout=InResolvedLayout; WorkerCount=1;DetailVariant=Variant;Scheduler=nullptr; // Analytic pages retain their bounded owner bins for route bookkeeping. Once published, // the Core world binds each page to its base shape as the single density evaluator. bUseNativeManifestFields=true; RoundTunnelsEnabled=bInRoundTunnels;bAnalyticOnly=true;bProfileDetailBand=false; for(int32 A=0;A<3;++A) { Min[A]=(&MinVoxels.X)[A]-2;Max[A]=(&MaxVoxels.X)[A]+2; ClipMin[A]=(&ClipMinVoxels.X)[A];ClipMax[A]=(&ClipMaxVoxels.X)[A]; Size[A]=Max[A]-Min[A]+1; if(Size[A]<=0||Size[A]>4096||ClipMax[A](Clock::now()-Stage).count(); Stats.PrimitiveCount=Primitives.size(); if(Primitives.empty()) {Error="owner shape extraction produced no rooms, tunnels, or passages";return false;} RebuildSharedFieldPrimitives(); Built=true; return true; } void InitializeNativeSurfaceOnly(FStandaloneOwnerWorld& World, const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly, const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout, const FIntVector& InMinimum,const FIntVector& InMaximum) { Owner=&World;WorldAssembly=InWorldAssembly;ResolvedLayout=InResolvedLayout; NativeOnlyMinimum=InMinimum;NativeOnlyMaximum=InMaximum; bUseNativeManifestFields=true;bNativeOnly=true;bAnalyticOnly=true;Built=true; NativeOnlyViolations.store(0,std::memory_order_relaxed); } bool BuildShapeDependencyProof(FStandaloneOwnerWorld& World, const FIntVector& MinVoxels,const FIntVector& MaxVoxels,bool bInRoundTunnels, const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout=nullptr) { Owner=&World;ResolvedLayout=InResolvedLayout;WorkerCount=1;RoundTunnelsEnabled=bInRoundTunnels; bAnalyticOnly=true;bNativeOnly=false;Stats={}; for(int32 Axis=0;Axis<3;++Axis) { Min[Axis]=(&MinVoxels.X)[Axis]-2;Max[Axis]=(&MaxVoxels.X)[Axis]+2; ClipMin[Axis]=(&MinVoxels.X)[Axis];ClipMax[Axis]=(&MaxVoxels.X)[Axis]; Size[Axis]=Max[Axis]-Min[Axis]+1; if(Size[Axis]<=0||Size[Axis]>4096||ClipMax[Axis]=BMin[Axis] &&Primitive.Min[Axis]-DetailBand<=BMax[Axis]; if(bOverlaps)return true; } for(const FInterval& Range:LayerRanges) if(Range.Lo<=BMax[2]&&Range.Hi>=BMin[2] &&(Range.Lo>=BMin[2]||Range.Hi<=BMax[2]))return true; return false; } bool IsNativeOnly() const { return bNativeOnly; } uint32 GetNativeOnlyViolationCount() const { return NativeOnlyViolations.load(std::memory_order_relaxed); } void RecordNativeOnlyViolation() const { NativeOnlyViolations.fetch_add(1,std::memory_order_relaxed); } const FBuildStats& GetStats() const { return Stats; } bool IsBuilt() const { return Built; } bool HasFarWorld() const { return FarBuilt; } const std::vector& GetFarConnectorSegments() const { return FarConnectorSegments; } void FindFarConnectorCandidates(const FIntVector& QueryMin,const FIntVector& QueryMax, std::vector& OutIndices) const { constexpr int32 BinSize=2048; OutIndices.clear(); const int32 MinX=FloorDiv(QueryMin.X,BinSize),MaxX=FloorDiv(QueryMax.X,BinSize); const int32 MinY=FloorDiv(QueryMin.Y,BinSize),MaxY=FloorDiv(QueryMax.Y,BinSize); const int32 MinZ=FloorDiv(QueryMin.Z,BinSize),MaxZ=FloorDiv(QueryMax.Z,BinSize); for(int32 Z=MinZ;Z<=MaxZ;++Z)for(int32 Y=MinY;Y<=MaxY;++Y) for(int32 X=MinX;X<=MaxX;++X) { const auto It=FarConnectorSpatialBins.find({X,Y,Z}); if(It!=FarConnectorSpatialBins.end()) OutIndices.insert(OutIndices.end(),It->second.begin(),It->second.end()); } std::sort(OutIndices.begin(),OutIndices.end()); OutIndices.erase(std::unique(OutIndices.begin(),OutIndices.end()),OutIndices.end()); } static uint32 FarFieldBinHash(int32 X,int32 Y,int32 Z) { uint32 Hash=2166136261u; Hash=(Hash^static_cast(X))*16777619u; Hash=(Hash^static_cast(Y))*16777619u; Hash=(Hash^static_cast(Z))*16777619u; Hash^=Hash>>16;Hash*=0x7FEB352Du;Hash^=Hash>>15; Hash*=0x846CA68Bu;Hash^=Hash>>16; return Hash; } static VoxelForgeCore::FarFieldPrimitiveGpu ToFarFieldGpuPrimitive(const VF_FieldPrimitive& S) { VoxelForgeCore::FarFieldPrimitiveGpu G{}; G.flags=S.flags; for(int32 A=0;A<3;++A) { G.minVoxel[A]=S.minVoxel[A];G.maxVoxel[A]=S.maxVoxel[A]; G.center[A]=static_cast(S.center[A]); G.capsuleStart[A]=static_cast(S.capsuleStart[A]); G.capsuleDelta[A]=static_cast(S.capsuleDelta[A]); G.roundedHalfExtent[A]=static_cast(S.roundedHalfExtent[A]); } for(int32 I=0;I<9;++I)G.q[I]=static_cast(S.q[I]); G.minRadius=static_cast(S.minRadius); G.capsuleAxisLengthSquared=static_cast(S.capsuleAxisLengthSquared); G.capsuleAxisLength=static_cast(S.capsuleAxisLength); G.capsuleStartRadius=static_cast(S.capsuleStartRadius); G.capsuleEndRadius=static_cast(S.capsuleEndRadius); G.profileStartT=static_cast(S.profileStartT);G.profileEndT=static_cast(S.profileEndT); G.profileStartRadius=static_cast(S.profileStartRadius);G.profileEndRadius=static_cast(S.profileEndRadius); G.radiusSlopeStart=static_cast(S.radiusSlopeStart);G.radiusSlopeEnd=static_cast(S.radiusSlopeEnd); G.rounding=static_cast(S.rounding); G.blendRadius=static_cast(S.blendRadius);G.floorBlendRadius=static_cast(S.floorBlendRadius); G.floorCenterZ=static_cast(S.floorCenterZ);G.floorSlopeX=static_cast(S.floorSlopeX); G.floorSlopeY=static_cast(S.floorSlopeY); G.floorCurveOriginX=static_cast(S.floorCurveOriginX);G.floorCurveOriginY=static_cast(S.floorCurveOriginY); G.floorCurveDeltaX=static_cast(S.floorCurveDeltaX);G.floorCurveDeltaY=static_cast(S.floorCurveDeltaY); G.floorCurveLength=static_cast(S.floorCurveLength); G.floorCurveStartZ=static_cast(S.floorCurveStartZ);G.floorCurveEndZ=static_cast(S.floorCurveEndZ); G.floorCurveTangentStart=static_cast(S.floorCurveTangentStart); G.floorCurveTangentEnd=static_cast(S.floorCurveTangentEnd); G.floorCurveStartBlend=static_cast(S.floorCurveStartBlend); G.floorCurveEndBlend=static_cast(S.floorCurveEndBlend); return G; } std::shared_ptr GetFarFieldWorldInput( std::string& Error) const { std::lock_guard Lock(FarFieldGpuInputMutex); if(CachedFarFieldGpuInput)return CachedFarFieldGpuInput; if(!FarBuilt||SpatialBins.empty()||SharedFieldPrimitives.empty()) {Error="far world has no shared primitives or spatial bins";return {};} if(SpatialBins.size()>std::numeric_limits::max()/2u) {Error="far world spatial-bin table exceeds the GPU 32-bit slot limit";return {};} const size_t TableCapacity=std::bit_ceil(std::max(2,SpatialBins.size()*2)); if(TableCapacity>std::numeric_limits::max()) {Error="far world spatial-bin table exceeds the D3D11 element limit";return {};} auto Data=std::make_shared(); Data->spatialBinSize=static_cast(ActiveSpatialBinSize); Data->spatialBinMask=static_cast(TableCapacity-1); Data->fieldSeed=static_cast(Owner?Owner->Seed:0); for(int32 Axis=0;Axis<3;++Axis) {Data->detailMinVoxel[Axis]=Min[Axis];Data->detailMaxVoxel[Axis]=Max[Axis];} Data->shapes.reserve(SharedFieldPrimitives.size()); for(const VF_FieldPrimitive& Shape:SharedFieldPrimitives) Data->shapes.push_back(ToFarFieldGpuPrimitive(Shape)); Data->detailLayers.reserve(LayerFields.size()); for(const FLayerField& Field:LayerFields) { const FStrateGenerationParams& Params=Field.Params; Data->detailLayers.push_back({Field.Lo,Field.Hi,Params.SurfaceRoughness, Params.RoughnessFrequency,Params.VerticalScale,Params.DomainWarpStrength, Params.DomainWarpFrequency,static_cast(Params.RoughnessNoiseType)}); } Data->spatialBins.resize(TableCapacity); size_t ReferenceCount=0; for(const auto& Bin:SpatialBins)ReferenceCount+=Bin.second.size(); Data->spatialBinShapeIds.reserve(ReferenceCount); for(const auto& [Key,Items]:SpatialBins) { if(Items.size()>std::numeric_limits::max() ||Data->spatialBinShapeIds.size()+Items.size()>std::numeric_limits::max()) {Error="far world spatial-bin shape IDs exceed the GPU 32-bit offset limit";return {};} uint32 Slot=FarFieldBinHash(Key.X,Key.Y,Key.Z)&Data->spatialBinMask; while(Data->spatialBins[Slot].count!=0)Slot=(Slot+1)&Data->spatialBinMask; VoxelForgeCore::FarFieldSpatialBin& GpuBin=Data->spatialBins[Slot]; GpuBin.x=Key.X;GpuBin.y=Key.Y;GpuBin.z=Key.Z; GpuBin.offset=static_cast(Data->spatialBinShapeIds.size()); GpuBin.count=static_cast(Items.size()); for(const uint32 Id:Items)Data->spatialBinShapeIds.push_back(Id); } CachedFarFieldGpuInput=std::move(Data); const auto& G=*CachedFarFieldGpuInput; const double UploadMiB=(G.shapes.size()*sizeof(VoxelForgeCore::FarFieldPrimitiveGpu) +G.spatialBins.size()*sizeof(VoxelForgeCore::FarFieldSpatialBin) +G.spatialBinShapeIds.size()*sizeof(uint32))/1048576.0; std::printf("[FarGpuWorldData] shapes=%zu spatial_bins=%zu hash_slots=%zu bin_shape_ids=%zu bin_size=%d detail_min=(%d,%d,%d) detail_max=(%d,%d,%d) bytes=%.3fMiB upload_scope=once_per_world\n", G.shapes.size(),SpatialBins.size(),G.spatialBins.size(),G.spatialBinShapeIds.size(), ActiveSpatialBinSize,G.detailMinVoxel[0],G.detailMinVoxel[1],G.detailMinVoxel[2], G.detailMaxVoxel[0],G.detailMaxVoxel[1],G.detailMaxVoxel[2],UploadMiB); return CachedFarFieldGpuInput; } bool ExportFarFieldTileInput(const FIntVector& Origin,int32 Cells,int32 Step, bool bNearDetail,VoxelForgeCore::FarFieldTileInput& Out,std::string& Error) const { if(!FarBuilt||Cells<=0||Cells>32||Step<=0) {Error="far field export requires a built far world and a bounded coarse tile";return false;} const int32 Dim=Cells+3; const size_t SampleCount=static_cast(Dim)*Dim*Dim; Out={};Error.clear();Out.origin={Origin.X,Origin.Y,Origin.Z};Out.cells=Cells;Out.step=Step;Out.sampleDim=Dim; Out.nearDetailEnabled=bNearDetail; Out.worldInput=GetFarFieldWorldInput(Error); if(!Out.worldInput)return false; if(LayerRanges.empty()) {Out.strataLo=-100000.0f;Out.strataHi=100000.0f;} else {Out.strataLo=static_cast(LayerRanges[0].Lo);Out.strataHi=static_cast(LayerRanges[0].Hi);} Out.strataBlend=StrataCaveBlendRadius; if(Owner&&Owner->Manager) { const double TileZ0=Origin.Z,TileZ1=Origin.Z+static_cast(Cells)*Step; for(const FStrateSlot& Slot:Owner->Manager->GetLayout()) { FResolvedStratumBounds Bounds; if(!GetResolvedStratumBounds(Slot,Bounds))continue; const float HalfWidth=static_cast(std::max(8.0, Bounds.SealThickness+8.0)); const float Boundaries[2]={static_cast(Bounds.BottomZ), static_cast(Bounds.TopZ)}; for(const float Z:Boundaries) if(Z+HalfWidth>=TileZ0&&Z-HalfWidth<=TileZ1) Out.stratumBoundaries.push_back({Z,HalfWidth}); } std::sort(Out.stratumBoundaries.begin(),Out.stratumBoundaries.end(), [](const auto& A,const auto& B){return A.z::max()); const int32 LatticeMin[3]={Origin.X-Step,Origin.Y-Step,Origin.Z-Step}; const int32 LatticeMax[3]={Origin.X+(Cells+1)*Step,Origin.Y+(Cells+1)*Step, Origin.Z+(Cells+1)*Step}; std::vector CriticalCandidates,OverlayCandidates; for(uint32 Id=0;Id=LatticeMin[Axis]-4 &&Shape.Min[Axis]<=LatticeMax[Axis]+4; OverlayOverlap&=Shape.Max[Axis]>=LatticeMin[Axis] &&Shape.Min[Axis]<=LatticeMax[Axis]; } if(CriticalOverlap)CriticalCandidates.push_back(Id); if(OverlayOverlap)OverlayCandidates.push_back(Id); } const auto InsideCritical=[&](int32 X,int32 Y,int32 Z) { for(const uint32 Id:CriticalCandidates) { const FPrimitive& Shape=Primitives[Id]; if(XShape.Max[0]||YShape.Max[1] ||ZShape.Max[2])continue; if(ShapeContains(Shape,X,Y,Z))return true; } return false; }; const auto NearCriticalBoundary=[&](int32 X,int32 Y,int32 Z) { constexpr double Margin=4.0; for(const uint32 Id:CriticalCandidates) { const FPrimitive& Shape=Primitives[Id]; if(XShape.Max[0]+Margin ||YShape.Max[1]+Margin ||ZShape.Max[2]+Margin)continue; float Distance=ShapeDistance(Shape,X,Y,Z); if(Shape.bHasFloorClip) { const float FloorDistance=static_cast(FloorAt(Shape,X,Y)-Z); Distance=SmoothMaxDistance(Distance,FloorDistance, static_cast(Shape.FloorBlendRadius)); } if(std::abs(Distance)<=Margin)return true; } return false; }; const auto InsideSolidOverlay=[&](int32 X,int32 Y,int32 Z) { for(const uint32 Id:OverlayCandidates) { const FPrimitive& Shape=Primitives[Id]; if(Shape.bCarve||XShape.Max[0] ||YShape.Max[1]||ZShape.Max[2])continue; if(ShapeDistance(Shape,X,Y,Z)<=0.0f)return true; } return false; }; const auto& Connectors=GetFarConnectorSegments(); // CPU repair panels are only used by the survival path at LOD6 and above. // Keep this sparse repair out of LOD2..5, where the base GPU surface has // sufficient resolution and the CPU path emits no panel geometry. if(Cells*Step>=(CHUNK_SIZE<<6)) { std::vector ConnectorCandidates; FindFarConnectorCandidates( FIntVector(Origin.X-64,Origin.Y-64,Origin.Z-64), FIntVector(Origin.X+Cells*Step+64,Origin.Y+Cells*Step+64, Origin.Z+Cells*Step+64),ConnectorCandidates); const double TileMin[3]={static_cast(Origin.X),static_cast(Origin.Y), static_cast(Origin.Z)}; const double TileMax[3]={TileMin[0]+Cells*Step,TileMin[1]+Cells*Step, TileMin[2]+Cells*Step}; for(const uint32 ConnectorIndex:ConnectorCandidates) { if(ConnectorIndex>=Connectors.size())continue; const FFarConnectorSegment& Connector=Connectors[ConnectorIndex]; const double Start[3]={Connector.Start.X,Connector.Start.Y,Connector.Start.Z}; const double Delta[3]={Connector.End.X-Connector.Start.X, Connector.End.Y-Connector.Start.Y,Connector.End.Z-Connector.Start.Z}; double TMin=0.0,TMax=1.0;bool Intersects=true; for(int32 Axis=0;Axis<3;++Axis) { if(std::abs(Delta[Axis])<1.0e-9) {if(Start[Axis]TileMax[Axis])Intersects=false;continue;} double A=(TileMin[Axis]-Start[Axis])/Delta[Axis]; double B=(TileMax[Axis]-Start[Axis])/Delta[Axis]; if(A>B)std::swap(A,B); TMin=std::max(TMin,A);TMax=std::min(TMax,B); if(TMin>TMax){Intersects=false;break;} } const double Length=std::sqrt(Delta[0]*Delta[0]+Delta[1]*Delta[1]+Delta[2]*Delta[2]); if(!Intersects||Length<=1.0e-6||TMax-TMin<=1.0e-7)continue; const uint32 AxialSampleCount=std::max(1,static_cast(std::ceil(Length/128.0))); const double FirstValue=std::ceil(TMin*AxialSampleCount-0.5-1.0e-8); const double LastValue=std::floor(TMax*AxialSampleCount-0.5+1.0e-8); const uint32 FirstSample=static_cast(std::clamp(FirstValue,0.0, static_cast(AxialSampleCount))); const uint32 SampleCount=LastValue>=FirstValue?static_cast(std::max(0.0, std::min(LastValue,static_cast(AxialSampleCount-1))-FirstValue+1.0)):0; const uint32 RingCount=SampleCount+2; const uint64 VertexCount=static_cast(RingCount-1)*24u; if(Out.connectorVertexCount+VertexCount>std::numeric_limits::max()) {Error="far connector panel output exceeds the 32-bit tile limit";return false;} VoxelForgeCore::FarFieldConnectorPanel Panel{}; for(int32 Axis=0;Axis<3;++Axis) {Panel.start[Axis]=static_cast(Start[Axis]);Panel.end[Axis]=static_cast(Start[Axis]+Delta[Axis]);} Panel.radius=Connector.Radius;Panel.tMin=static_cast(TMin);Panel.tMax=static_cast(TMax); Panel.ringCount=RingCount;Panel.ringOffset=Out.connectorRingCount; Panel.vertexOffset=Out.connectorVertexCount; Panel.axialSampleStart=FirstSample;Panel.axialSampleCount=AxialSampleCount; Out.connectorRingCount+=Panel.ringCount;Out.connectorVertexCount+=static_cast(VertexCount); Out.connectorPanels.push_back(Panel); } } size_t SampleIndex=0; for(int32 Z=-1;Z<=Cells+1;++Z) for(int32 Y=-1;Y<=Cells+1;++Y) for(int32 X=-1;X<=Cells+1;++X,++SampleIndex) { const int32 WX=Origin.X+X*Step,WY=Origin.Y+Y*Step,WZ=Origin.Z+Z*Step; if(bNearDetail&&!bAnalyticOnly&&Contains(WX,WY,WZ)) { const size_t Column=static_cast(WY-Min[1])*Size[0] +static_cast(WX-Min[0]); const size_t Zi=static_cast(WZ-Min[2]); const size_t Word=Column*WordsPerZ+(Zi>>6); const uint64 Mask=uint64{1}<<(Zi&63u); if((CandidateWords[Word]&Mask)==0) Out.nearPackedProxy[SampleIndex]=0u; else { ++Out.nearPackedCandidateSampleCount; const size_t Node=Zi*static_cast(Size[1])*Size[0]+Column; const int32 Q=std::abs(static_cast(SurfaceDistanceQ[Node])); const uint32 Magnitude=static_cast(std::clamp(Q,1,32767)); if(Magnitude==1u) { // At the minimum quantization step a one-ULP GPU sign flip // can change an entire marching-cubes cell. Keep this sparse // ambiguous boundary decision aligned with the packed cache. const bool bRock=(RockWords[Word]&Mask)!=0; Out.nearPackedProxy[SampleIndex]=0x40000000u |(bRock?0x80000000u:0u)|Magnitude; } else Out.nearPackedProxy[SampleIndex]=Magnitude; } } if(!bNearDetail&&WX>=Min[0]&&WX<=Max[0]&&WY>=Min[1]&&WY<=Max[1] &&WZ>=Min[2]&&WZ<=Max[2]) {Error="tile lattice intersects the near analytic/detail region; this export is far-field only";return false;} if(InsideCritical(WX,WY,WZ)) { Out.protectedAir[SampleIndex>>5]|=1u<<(SampleIndex&31u); ++Out.protectedSampleCount; } else if(NearCriticalBoundary(WX,WY,WZ)) { // Preserve exact signs within roughly one far-field lattice step of // route air. This catches thin walls/floors at the route boundary // without evaluating the ordinary far field on the CPU. const float CriticalDensity=AnalyticDensityAt(static_cast(WX), static_cast(WY),static_cast(WZ)); Out.protectedThin[SampleIndex>>5]|=1u<<(SampleIndex&31u); if(CriticalDensity>=0.0f)Out.protectedAir[SampleIndex>>5]|=1u<<(SampleIndex&31u); else { Out.protectedSolid[SampleIndex>>5]|=1u<<(SampleIndex&31u); ++Out.protectedSolidSampleCount; } ++Out.protectedThinSampleCount; } else { bool bSealSample=false; for(const VoxelForgeCore::FarFieldStratumBoundary& Boundary:Out.stratumBoundaries) { if(std::abs(static_cast(WZ)-Boundary.z)>0.5)continue; // Only the exact lattice samples that lie on a seal height need a // CPU sign override. The surrounding far field remains GPU-decided. const float SealDensity=AnalyticDensityAt(static_cast(WX), static_cast(WY),static_cast(WZ)); if(SealDensity>=0.0f) Out.protectedAir[SampleIndex>>5]|=1u<<(SampleIndex&31u); else Out.protectedSolid[SampleIndex>>5]|=1u<<(SampleIndex&31u); ++Out.protectedSealSampleCount; if(SealDensity<0.0f)++Out.protectedSolidSampleCount; bSealSample=true; break; } if(!bSealSample&&InsideSolidOverlay(WX,WY,WZ)) { Out.protectedSolid[SampleIndex>>5]|=1u<<(SampleIndex&31u); ++Out.protectedSolidSampleCount; ++Out.protectedSolidOverlaySampleCount; } } } return true; } bool IsFarFieldTileOutsideNearRegion(const FIntVector& Origin,int32 Cells,int32 Step) const { if(!FarBuilt||Cells<=0||Cells>32||Step<=0)return false; const int32 Origins[3]={Origin.X,Origin.Y,Origin.Z}; for(int32 Axis=0;Axis<3;++Axis) { const double First=std::ceil((static_cast(Min[Axis])-Origins[Axis])/Step); const double Last=std::floor((static_cast(Max[Axis])-Origins[Axis])/Step); if(First>Cells+1||Last< -1)return true; } return false; } bool DebugCriticalAirAt(double X,double Y,double Z) const { return IsInsideCriticalAir(X,Y,Z); } bool DebugSolidOverlayAt(double X,double Y,double Z) const { return IsInsideSolidOverlay(X,Y,Z); } void SetEditTransaction(std::vector Brushes) { EditBrushes=std::move(Brushes); EditBoundsMin={MAX_int32,MAX_int32,MAX_int32}; EditBoundsMax={MIN_int32,MIN_int32,MIN_int32}; for(const FEditBrush& Brush:EditBrushes) { double Extent[3]{}; if(Brush.Shape==FEditBrush::EShape::Sphere) for(int32 Axis=0;Axis<3;++Axis)Extent[Axis]=Brush.Radius; else if(Brush.Shape==FEditBrush::EShape::Capsule) for(int32 Axis=0;Axis<3;++Axis)Extent[Axis]=Brush.Radius +(Brush.Operation==FEditBrush::EOperation::DensityOffset ?std::max(Brush.Falloff,0.01f):0.0f); else for(int32 Axis=0;Axis<3;++Axis)Extent[Axis]=Brush.HalfExtent[Axis]+Brush.Rounding +(Brush.Operation==FEditBrush::EOperation::DensityOffset ?std::max(Brush.Falloff,0.01f):0.0f); for(int32 Axis=0;Axis<3;++Axis) { const double Lo=Brush.Shape==FEditBrush::EShape::Capsule ?std::min(Brush.A[Axis],Brush.B[Axis])-Extent[Axis] :Brush.A[Axis]-Extent[Axis]; const double Hi=Brush.Shape==FEditBrush::EShape::Capsule ?std::max(Brush.A[Axis],Brush.B[Axis])+Extent[Axis] :Brush.A[Axis]+Extent[Axis]; (&EditBoundsMin.X)[Axis]=std::min((&EditBoundsMin.X)[Axis],static_cast(std::floor(Lo))); (&EditBoundsMax.X)[Axis]=std::max((&EditBoundsMax.X)[Axis],static_cast(std::ceil(Hi))); } } } void SetSharedDensitySource(const FStandaloneShapeWorld* Source) { SharedDensitySource=Source==this?nullptr:Source; } bool HasEditTransaction() const { return !EditBrushes.empty(); } FIntVector GetEditBoundsMin() const { return EditBoundsMin; } FIntVector GetEditBoundsMax() const { return EditBoundsMax; } void BuildFarWorld() { if(FarBuilt)return; auto Stage=std::chrono::steady_clock::now(); BuildFarParentShapes(); RebuildSharedFieldPrimitives(); Stats.FarPrimitiveSeconds=std::chrono::duration(std::chrono::steady_clock::now()-Stage).count(); Stage=std::chrono::steady_clock::now(); BuildSpatialBins(SpatialBinSize,false); Stats.FarBinSeconds=std::chrono::duration(std::chrono::steady_clock::now()-Stage).count(); Stats.FarShapeSeconds=Stats.FarPrimitiveSeconds+Stats.FarBinSeconds; Stats.PrimitiveCount=Primitives.size();FarBuilt=true; std::printf("[FarShapeStage] parent_shapes=%.3f ms spatial_bins=%.3f ms total=%.3f ms primitives=%llu\n", Stats.FarPrimitiveSeconds*1000.0,Stats.FarBinSeconds*1000.0,Stats.FarShapeSeconds*1000.0, static_cast(Stats.PrimitiveCount)); std::fflush(stdout); } // Lattice signs come from packed intervals; a bounded analytic distance proxy supplies // edge interpolation and normals to the existing mesher. float DensityAt(float X,float Y,float Z) const { if(SharedDensitySource)return SharedDensitySource->DensityAt(X,Y,Z); if(bNativeOnly) { if(!NativeOnlyContains(X,Y,Z)) {RecordNativeOnlyViolation();return 0.0f;} float NativeDensity=0.0f; if(!NativeSurfaceDensityAt(X,Y,Z,NativeDensity)) {RecordNativeOnlyViolation();return 0.0f;} return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z); } float IslandDensity=0.0f; if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity)) { ++ThreadClassificationStats().NativeFloatFieldCalls; return ApplyEditBrushes(ApplyResolvedStrateComposition(IslandDensity,X,Y,Z),X,Y,Z); } float NativeDensity=0.0f; if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity)) { ++ThreadClassificationStats().NativeFloatFieldCalls; return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z); } if(UsesLegacySurfaceField(X,Y,Z)) return LegacySurfaceDensityAt(X,Y,Z); int32 IX=0,IY=0,IZ=0; const bool bInteger=NearInteger(X,IX)&&NearInteger(Y,IY)&&NearInteger(Z,IZ); const bool bPacked=!bAnalyticOnly&&bInteger&&Contains(IX,IY,IZ); if(bPacked) return PackedDensityAt(IX,IY,IZ,IsRock(IX,IY,IZ),X,Y,Z); return AnalyticDensityAt(X,Y,Z); } std::string DensityDispatchPathAt(float X,float Y,float Z) const { if(SharedDensitySource)return SharedDensitySource->DensityDispatchPathAt(X,Y,Z); float Density=0.0f; if(NativeFloatingIslandDensityAt(X,Y,Z,Density))return "FloatingIslands-native"; if(NativeSurfaceDensityAt(X,Y,Z,Density))return "SurfaceWorld-native"; if(UsesLegacySurfaceField(X,Y,Z))return "SurfaceWorld-legacy"; if(Owner&&Owner->Manager) { const int32 ChunkZ=FMath::FloorToInt(Z/static_cast(GetResolvedLayoutChunkWidthVoxels())); const ECaveGeneratorType Type=Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ)); switch(Type) { case ECaveGeneratorType::TunnelNetwork:return "compiled-analytic/TunnelNetwork"; case ECaveGeneratorType::FlatPlain:return "compiled-analytic/FlatPlain"; case ECaveGeneratorType::CrystalChamber:return "compiled-analytic/CrystalChamber"; case ECaveGeneratorType::Maze:return "compiled-analytic/Maze"; case ECaveGeneratorType::SurfaceWorld:return "compiled-analytic/SurfaceWorld"; case ECaveGeneratorType::VerticalShafts:return "compiled-analytic/VerticalShafts"; case ECaveGeneratorType::FloatingIslands:return "compiled-analytic/FloatingIslands"; case ECaveGeneratorType::Underwater:return "compiled-analytic/Underwater"; } } return "compiled-analytic/fallback"; } float DensityAtLattice(int32 X,int32 Y,int32 Z) const { if(SharedDensitySource)return SharedDensitySource->DensityAtLattice(X,Y,Z); if(bNativeOnly) { if(!NativeOnlyContains(static_cast(X),static_cast(Y), static_cast(Z))) {RecordNativeOnlyViolation();return 0.0f;} float NativeDensity=0.0f; if(!NativeSurfaceDensityAt(static_cast(X),static_cast(Y), static_cast(Z),NativeDensity)) {RecordNativeOnlyViolation();return 0.0f;} return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z); } float IslandDensity=0.0f; if(NativeFloatingIslandDensityAt(static_cast(X),static_cast(Y), static_cast(Z),IslandDensity)) { ++ThreadClassificationStats().NativeLatticeFieldCalls; return ApplyEditBrushes(ApplyResolvedStrateComposition(IslandDensity,X,Y,Z),X,Y,Z); } float NativeDensity=0.0f; if(NativeSurfaceDensityAt(static_cast(X),static_cast(Y), static_cast(Z),NativeDensity)) { ++ThreadClassificationStats().NativeLatticeFieldCalls; return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z); } if(UsesLegacySurfaceField(static_cast(X),static_cast(Y), static_cast(Z))) return LegacySurfaceDensityAt(static_cast(X),static_cast(Y), static_cast(Z)); const bool bPacked=!bAnalyticOnly&&Contains(X,Y,Z); if(bPacked) return PackedDensityAt(X,Y,Z,IsRock(X,Y,Z),static_cast(X), static_cast(Y),static_cast(Z)); return AnalyticDensityAt(static_cast(X),static_cast(Y), static_cast(Z)); } float DensityAtUnclamped(float X,float Y,float Z) const { if(SharedDensitySource)return SharedDensitySource->DensityAtUnclamped(X,Y,Z); if(bNativeOnly) { if(!NativeOnlyContains(X,Y,Z)) {RecordNativeOnlyViolation();return 0.0f;} float NativeDensity=0.0f; if(!NativeSurfaceDensityAt(X,Y,Z,NativeDensity)) {RecordNativeOnlyViolation();return 0.0f;} return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z); } float IslandDensity=0.0f; if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity)) { ++ThreadClassificationStats().NativeUnclampedFieldCalls; return ApplyEditBrushes(ApplyResolvedStrateComposition(IslandDensity,X,Y,Z),X,Y,Z); } float NativeDensity=0.0f; if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity)) { ++ThreadClassificationStats().NativeUnclampedFieldCalls; return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z); } if(UsesLegacySurfaceField(X,Y,Z)) return LegacySurfaceDensityAt(X,Y,Z); return AnalyticDensityAt(X,Y,Z,false); } VoxelForgeCore::DetailFieldSample DetailSampleAt(float X,float Y,float Z) const { if(SharedDensitySource)return SharedDensitySource->DetailSampleAt(X,Y,Z); VoxelForgeCore::DetailFieldSample Sample; if(bNativeOnly) {RecordNativeOnlyViolation();return Sample;} AnalyticDensityAt(X,Y,Z,false,&Sample.displacement, &Sample.caveInfluence,&Sample.floorInfluence); return Sample; } bool IsProxyBandFallback(float Density) const { return std::abs(std::abs(Density)-(DetailSupremum+0.125f))<1.0e-4f; } bool IsAnalyticOnly() const { return bAnalyticOnly; } void DensityAtLatticeX8(int32 XStart,int32 XStep,int32 Y,int32 Z, int32 Count,float OutDensity[8]) const { if(SharedDensitySource) { SharedDensitySource->DensityAtLatticeX8(XStart,XStep,Y,Z,Count,OutDensity); return; } Count=std::clamp(Count,0,8); if(Count==0)return; if(bNativeOnly) { for(int32 Lane=0;Lane(Z))) { for(int32 Lane=0;Lane(XStart+Lane*XStep); const float SampleY=static_cast(Y); const float SampleZ=static_cast(Z); bHasLegacySurfaceSample|=UsesLegacySurfaceField(SampleX,SampleY,SampleZ); float IslandDensity=0.0f; bHasNativeIslandSample|=NativeFloatingIslandDensityAt( SampleX,SampleY,SampleZ,IslandDensity); } if(bHasLegacySurfaceSample||bHasNativeIslandSample) { for(int32 Lane=0;Lane=Min[1]&&Y<=Max[1] &&Z>=Min[2]&&Z<=Max[2] &&XStart>=Min[0] &&XStart+static_cast(Count-1)*XStep<=Max[0]; if(!bFast) { for(int32 Lane=0;Lane(Y-Min[1])*Size[0] +static_cast(XStart-Min[0]); const size_t Zi=static_cast(Z-Min[2]); const size_t WordOffset=Zi>>6; const uint64 Mask=uint64{1}<<(Zi&63u); const size_t NodeBase=Zi*static_cast(Size[1])*Size[0]+ColumnStart; constexpr float ProxyBandEdge=DetailSupremum+0.125f; for(int32 Lane=0;Lane(Lane*XStep); const size_t Word=Column*WordsPerZ+WordOffset; const bool bRock=(RockWords[Word]&Mask)!=0; float Density; if(CandidateWords[Word]&Mask) { const int16 Proxy=SurfaceDistanceQ[NodeBase +static_cast(Lane*XStep)]; Density=static_cast(Proxy)*(1.0f/256.0f); if((Density<0.0f)!=bRock) Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),1.0f/256.0f); } else Density=bRock?-ProxyBandEdge:ProxyBandEdge; OutDensity[Lane]=Density; } } private: struct FPassageTileFeature { std::size_t passageIndex=0,segmentIndex=0; bool bColumn=false,bMouthSupport=false; }; struct FPassageNearestNode { std::array minimum{},maximum{}; double maximumRadius=0.0; std::vector segments; int left=-1,right=-1; }; struct FPassageProofBound { std::array minimum{},maximum{}; }; struct FPassageProofNode { std::array minimum{},maximum{}; std::size_t first=0,end=0; int left=-1,right=-1; }; struct FPassageFeatureNode { std::array minimum{},maximum{}; std::size_t first=0,end=0; int left=-1,right=-1; }; int BuildPassageNearestNode(std::size_t passageIndex,std::vector segments) { const auto& points=ResolvedLayout->passages[passageIndex].controlPoints; FPassageNearestNode node; node.minimum.fill(std::numeric_limits::infinity()); node.maximum.fill(-std::numeric_limits::infinity()); for(const std::size_t segment:segments) { const auto& a=points[segment];const auto& b=points[segment+1]; for(std::size_t axis=0;axis<3;++axis) { node.minimum[axis]=std::min({node.minimum[axis], static_cast(a.positionSubvoxels[axis]), static_cast(b.positionSubvoxels[axis])}); node.maximum[axis]=std::max({node.maximum[axis], static_cast(a.positionSubvoxels[axis]), static_cast(b.positionSubvoxels[axis])}); } node.maximumRadius=std::max({node.maximumRadius, static_cast(a.radiusSubvoxels),static_cast(b.radiusSubvoxels)}); } node.segments=segments; std::sort(node.segments.begin(),node.segments.end()); const int nodeIndex=static_cast(PassageNearestNodes[passageIndex].size()); PassageNearestNodes[passageIndex].push_back(std::move(node)); if(segments.size()>4) { std::size_t splitAxis=0; for(std::size_t axis=1;axis<3;++axis) if(PassageNearestNodes[passageIndex][nodeIndex].maximum[axis] -PassageNearestNodes[passageIndex][nodeIndex].minimum[axis] >PassageNearestNodes[passageIndex][nodeIndex].maximum[splitAxis] -PassageNearestNodes[passageIndex][nodeIndex].minimum[splitAxis])splitAxis=axis; const std::size_t middle=segments.size()/2; const auto center=[&](std::size_t segment) {return static_cast(points[segment].positionSubvoxels[splitAxis]) +static_cast(points[segment+1].positionSubvoxels[splitAxis]);}; std::nth_element(segments.begin(),segments.begin()+middle,segments.end(), [&](std::size_t a,std::size_t b) {const double ca=center(a),cb=center(b);return ca==cb?a leftSegments(segments.begin(),segments.begin()+middle); std::vector rightSegments(segments.begin()+middle,segments.end()); const int left=BuildPassageNearestNode(passageIndex,std::move(leftSegments)); const int right=BuildPassageNearestNode(passageIndex,std::move(rightSegments)); PassageNearestNodes[passageIndex][nodeIndex].left=left; PassageNearestNodes[passageIndex][nodeIndex].right=right; } return nodeIndex; } int BuildPassageProofNode(std::size_t first,std::size_t end) { FPassageProofNode node;node.first=first;node.end=end; node.minimum.fill(std::numeric_limits::infinity()); node.maximum.fill(-std::numeric_limits::infinity()); for(std::size_t index=first;index(PassageProofNodes.size()); PassageProofNodes.push_back(node); if(end-first>8) { std::size_t splitAxis=0; for(std::size_t axis=1;axis<3;++axis) if(node.maximum[axis]-node.minimum[axis] >node.maximum[splitAxis]-node.minimum[splitAxis])splitAxis=axis; const std::size_t middle=first+(end-first)/2; std::nth_element(PassageProofBounds.begin()+first,PassageProofBounds.begin()+middle, PassageProofBounds.begin()+end,[splitAxis](const auto& a,const auto& b) {return a.minimum[splitAxis]+a.maximum[splitAxis] ::max()); node.maximum.fill(std::numeric_limits::min()); for(std::size_t offset=first;offset(PassageFeatureNodes.size()); PassageFeatureNodes.push_back(node); if(end-first>8) { std::size_t splitAxis=0; for(std::size_t axis=1;axis<3;++axis) if(static_cast(node.maximum[axis])-node.minimum[axis] >static_cast(node.maximum[splitAxis])-node.minimum[splitAxis]) splitAxis=axis; const std::size_t middle=first+(end-first)/2; std::nth_element(PassageFeatureOrder.begin()+first,PassageFeatureOrder.begin()+middle, PassageFeatureOrder.begin()+end,[this,splitAxis](std::uint32_t a,std::uint32_t b) { const auto& left=PassageFeatureBounds[a]; const auto& right=PassageFeatureBounds[b]; const double leftCenter=static_cast(left.minimumSubvoxels[splitAxis])*0.5 +static_cast(left.maximumSubvoxels[splitAxis])*0.5; const double rightCenter=static_cast(right.minimumSubvoxels[splitAxis])*0.5 +static_cast(right.maximumSubvoxels[splitAxis])*0.5; return leftCenter==rightCenter?a& minimum, const std::array& maximum, std::vector& result) const { if(nodeIndex<0)return; const auto& node=PassageFeatureNodes[static_cast(nodeIndex)]; for(std::size_t axis=0;axis<3;++axis) if(node.maximum[axis]maximum[axis])return; if(node.left<0) { for(std::size_t offset=node.first;offset=minimum[axis]; if(overlaps)result.push_back(featureIndex); } return; } QueryPassageFeatureNode(node.left,minimum,maximum,result); QueryPassageFeatureNode(node.right,minimum,maximum,result); } std::vector BuildPassageFeatureTileList( const std::array& minimum, const std::array& maximum) const { std::vector result; if(PassageFeatureBounds.empty())return result; bool failOpen=PassageFeatureBoundsFailOpen; for(std::size_t axis=0;axis<3;++axis)failOpen=failOpen||minimum[axis]>maximum[axis]; if(failOpen) { result.resize(PassageFeatureBounds.size()); for(std::size_t i=0;i(i); return result; } if(!PassageFeatureNodes.empty())QueryPassageFeatureNode(0,minimum,maximum,result); return result; } void BuildPassageSpatialIndices() { PassageTileFeatures.clear();PassageFeatureBounds.clear(); PassageFeatureOrder.clear();PassageFeatureNodes.clear(); PassageFeatureBoundsFailOpen=false; PassageNearestNodes.clear(); PassageProofBounds.clear();PassageProofNodes.clear(); PassageNearestNodes.resize(ResolvedLayout?ResolvedLayout->passages.size():0); PassageColumnSegmentFlags.clear();PassageShortSpanSegmentFlags.clear(); PassageHasShortSpanFlags.clear(); if(!ResolvedLayout||ResolvedLayout->passages.empty())return; constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double voxelsPerMetre=Owner&&Owner->CoreVoxelSizeMicrons>0 ?1000000.0/Owner->CoreVoxelSizeMicrons:4.0; const double bridgeHalfWidthVoxels=3.5*voxelsPerMetre; const double bridgeFootDepthVoxels=(5.0-0.13)*voxelsPerMetre; PassageColumnSegmentFlags.resize(ResolvedLayout->passages.size()); PassageShortSpanSegmentFlags.resize(ResolvedLayout->passages.size()); for(std::size_t passageIndex=0;passageIndexpassages.size();++passageIndex) { const auto& passage=ResolvedLayout->passages[passageIndex]; const std::size_t segmentCount=passage.controlPoints.size()>1 ?passage.controlPoints.size()-1:0; PassageColumnSegmentFlags[passageIndex].assign(segmentCount,0); PassageShortSpanSegmentFlags[passageIndex].assign(segmentCount,0); if(segmentCount>0) { std::vector segmentIds(segmentCount); for(std::size_t i=0;i(std::max(a.radiusSubvoxels,b.radiusSubvoxels))/Q; const double horizontalExtent=std::max(radius,bridgeHalfWidthVoxels); const double footDepth=std::max(voxelsPerMetre,bridgeFootDepthVoxels); FPassageProofBound proof; proof.minimum={std::min(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q-horizontalExtent, std::min(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q-horizontalExtent, std::min(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q-radius-footDepth}; proof.maximum={std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+horizontalExtent, std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+horizontalExtent, std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius}; PassageProofBounds.push_back(proof); const std::array featureMin={ static_cast(std::floor(proof.minimum[0]*Q)), static_cast(std::floor(proof.minimum[1]*Q)), static_cast(std::floor(proof.minimum[2]*Q))}; const std::array featureMax={ static_cast(std::ceil(proof.maximum[0]*Q)), static_cast(std::ceil(proof.maximum[1]*Q)), static_cast(std::ceil(proof.maximum[2]*Q))}; PassageFeatureBounds.push_back({static_cast(PassageTileFeatures.size()), featureMin,featureMax}); PassageTileFeatures.push_back({passageIndex,segment,false}); } const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), ResolvedLayout->strates.end(),[&passage](const auto& value) {return value.strateIndex==passage.upperStrateIndex;}); if(upperStratum!=ResolvedLayout->strates.end()) { std::array route{}; std::size_t routeCount=0; for(const auto& anchor:upperStratum->anchors) if(anchor.kind=="route_point"&&anchor.values.size()>=3) { if(routeCount=2) { const double mouthReach=13.0*voxelsPerMetre*Q; double minimumZ=std::numeric_limits::infinity(); double maximumZ=-std::numeric_limits::infinity(); for(std::size_t index=0;index(route[index]->values[2])); maximumZ=std::max(maximumZ,static_cast(route[index]->values[2])); } const double mouthX=passage.upperMouthSubvoxels[0]; const double mouthY=passage.upperMouthSubvoxels[1]; FPassageProofBound supportProof; supportProof.minimum={(mouthX-mouthReach)/Q,(mouthY-mouthReach)/Q, (minimumZ-3.0*voxelsPerMetre*Q)/Q}; supportProof.maximum={(mouthX+mouthReach)/Q,(mouthY+mouthReach)/Q, (maximumZ+3.0*voxelsPerMetre*Q)/Q}; PassageProofBounds.push_back(supportProof); const std::array featureMin={ static_cast(std::floor(supportProof.minimum[0]*Q)), static_cast(std::floor(supportProof.minimum[1]*Q)), static_cast(std::floor(supportProof.minimum[2]*Q))}; const std::array featureMax={ static_cast(std::ceil(supportProof.maximum[0]*Q)), static_cast(std::ceil(supportProof.maximum[1]*Q)), static_cast(std::ceil(supportProof.maximum[2]*Q))}; PassageFeatureBounds.push_back({static_cast(PassageTileFeatures.size()), featureMin,featureMax}); PassageTileFeatures.push_back({passageIndex,0,false,true}); } } } for(const auto& crossing:PassageVoidCrossings) { if(crossing.passageIndexpassages.size()) { const std::int32_t strate=ResolvedLayout->passages[ crossing.passageIndex].upperStrateIndex; const auto upper=std::find_if(ResolvedLayout->strates.begin(), ResolvedLayout->strates.end(),[strate](const auto& value) {return value.strateIndex==strate;}); surfaceWorldUpper=upper!=ResolvedLayout->strates.end() &&upper->generatorKind=="SurfaceWorld"; } const std::size_t shortSpanPadding=!crossing.needsColumn &&surfaceWorldUpper&&!flags.empty()?2:0; const std::size_t first=std::min(crossing.firstSegment>shortSpanPadding ?crossing.firstSegment-shortSpanPadding:0,flags.size()); const std::size_t last=std::min(crossing.lastSegment+shortSpanPadding, flags.empty()?0:flags.size()-1); if(!flags.empty()&&first<=last) std::fill(flags.begin()+first,flags.begin()+last+1,1); } if(!crossing.needsColumn)continue; FPassageProofBound proof; const double radius=static_cast(crossing.maximumRadiusSubvoxels)/Q; proof.minimum={crossing.centerXSubvoxels/Q-radius,crossing.centerYSubvoxels/Q-radius, crossing.bottomZSubvoxels/Q}; proof.maximum={crossing.centerXSubvoxels/Q+radius,crossing.centerYSubvoxels/Q+radius, crossing.topZSubvoxels/Q}; PassageProofBounds.push_back(proof); const std::array featureMin={ static_cast(std::floor(proof.minimum[0]*Q)), static_cast(std::floor(proof.minimum[1]*Q)), static_cast(std::floor(proof.minimum[2]*Q))}; const std::array featureMax={ static_cast(std::ceil(proof.maximum[0]*Q)), static_cast(std::ceil(proof.maximum[1]*Q)), static_cast(std::ceil(proof.maximum[2]*Q))}; PassageFeatureBounds.push_back({static_cast(PassageTileFeatures.size()), featureMin,featureMax}); PassageTileFeatures.push_back({crossing.passageIndex,0,true}); } PassageHasShortSpanFlags.assign(PassageShortSpanSegmentFlags.size(),0); for(std::size_t passageIndex=0;passageIndex( std::any_of(PassageShortSpanSegmentFlags[passageIndex].begin(), PassageShortSpanSegmentFlags[passageIndex].end(), [](std::uint8_t flag){return flag!=0;})); for(std::size_t i=0;i(i)); for(const auto& feature:PassageFeatureBounds) for(std::size_t axis=0;axis<3;++axis) PassageFeatureBoundsFailOpen=PassageFeatureBoundsFailOpen ||feature.minimumSubvoxels[axis]>feature.maximumSubvoxels[axis]; if(!PassageFeatureOrder.empty())BuildPassageFeatureNode(0,PassageFeatureOrder.size()); if(!PassageProofBounds.empty())BuildPassageProofNode(0,PassageProofBounds.size()); } double PassageNearestNodeLowerBound(const FPassageNearestNode& node, double qx,double qy,double qz) const { const double dx=qxnode.maximum[0]?qx-node.maximum[0]:0.0; const double dy=qynode.maximum[1]?qy-node.maximum[1]:0.0; const double dz=qznode.maximum[2]?qz-node.maximum[2]:0.0; return DeterministicSqrt(dx*dx+dy*dy+dz*dz)-node.maximumRadius-1.0e-6; } template void VisitNearestPassageSpatialNode(std::size_t passageIndex,int nodeIndex, std::size_t first,std::size_t end,double qx,double qy,double qz,int priority, double& bestDistance,FVisit& visit) const { if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return; const auto& nodes=PassageNearestNodes[passageIndex]; const auto& node=nodes[static_cast(nodeIndex)]; const auto candidate=std::lower_bound(node.segments.begin(),node.segments.end(),first); if(candidate==node.segments.end()||*candidate>=end)return; const double lowerBound=PassageNearestNodeLowerBound(node,qx,qy,qz); if(lowerBound>bestDistance)return; if(node.left<0) { for(auto it=candidate;it!=node.segments.end()&&*it(node.left)],qx,qy,qz); const double rightBound=PassageNearestNodeLowerBound( nodes[static_cast(node.right)],qx,qy,qz); const int nearChild=leftBound<=rightBound?node.left:node.right; const int farChild=leftBound<=rightBound?node.right:node.left; VisitNearestPassageSpatialNode(passageIndex,nearChild,first,end,qx,qy,qz, priority,bestDistance,visit); VisitNearestPassageSpatialNode(passageIndex,farChild,first,end,qx,qy,qz, priority,bestDistance,visit); } template void VisitNearestPassageSegmentRange(std::size_t passageIndex,std::ptrdiff_t first, std::ptrdiff_t last,double qx,double qy,double qz,int priority, double& bestDistance,FVisit&& visit) const { if(first>last||passageIndex>=PassageNearestNodes.size() ||PassageNearestNodes[passageIndex].empty())return; auto callback=std::forward(visit); VisitNearestPassageSpatialNode(passageIndex,0,static_cast(first), static_cast(last+1),qx,qy,qz,priority,bestDistance,callback); } bool PassagePotentialWithinNode(std::size_t passageIndex,int nodeIndex, std::size_t first,std::size_t end,double qx,double qy,double qz, double signedDistanceLimit) const { if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return false; const auto& nodes=PassageNearestNodes[passageIndex]; const auto& node=nodes[static_cast(nodeIndex)]; const auto candidate=std::lower_bound(node.segments.begin(),node.segments.end(),first); if(candidate==node.segments.end()||*candidate>=end)return false; if(PassageNearestNodeLowerBound(node,qx,qy,qz)>signedDistanceLimit)return false; if(node.left<0)return true; const double leftBound=PassageNearestNodeLowerBound( nodes[static_cast(node.left)],qx,qy,qz); const double rightBound=PassageNearestNodeLowerBound( nodes[static_cast(node.right)],qx,qy,qz); const int nearChild=leftBound<=rightBound?node.left:node.right; const int farChild=leftBound<=rightBound?node.right:node.left; return PassagePotentialWithinNode(passageIndex,nearChild,first,end,qx,qy,qz, signedDistanceLimit) ||PassagePotentialWithinNode(passageIndex,farChild,first,end,qx,qy,qz, signedDistanceLimit); } bool PassageMayAffectDensity(std::size_t passageIndex,std::ptrdiff_t carveBegin, std::ptrdiff_t carveEnd,std::ptrdiff_t lowerMouthIndex, std::ptrdiff_t mouthSearchBegin,std::ptrdiff_t mouthSearchEnd,std::ptrdiff_t lastIndex, double qx,double qy,double qz,double signedDistanceLimit) const { const auto possible=[&](std::ptrdiff_t first,std::ptrdiff_t last) { return first<=last&&passageIndex(first), static_cast(last+1),qx,qy,qz,signedDistanceLimit); }; return possible(carveBegin,carveEnd) ||possible(0,std::min(8,lowerMouthIndex)-1) ||possible(mouthSearchBegin,mouthSearchEnd-1) ||possible(lowerMouthIndex,lastIndex-1); } bool PassagePlayerCapsuleBlocksSupportNode(std::size_t passageIndex,int nodeIndex, std::size_t first,std::size_t end,double qx,double qy,double qz,double qPerMetre) const { if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return false; const auto& nodes=PassageNearestNodes[passageIndex]; const auto& node=nodes[static_cast(nodeIndex)]; auto candidate=std::lower_bound(node.segments.begin(),node.segments.end(),first); if(candidate==node.segments.end()||*candidate>=end)return false; const double xyReach=0.39*qPerMetre; if(qx+xyReachnode.maximum[0] ||qy+xyReachnode.maximum[1])return false; const double minimumCenterZ=node.minimum[2]-node.maximumRadius+0.20*qPerMetre; const double maximumCenterZ=node.maximum[2]+2.06*qPerMetre; if(qzmaximumCenterZ)return false; const auto& passage=ResolvedLayout->passages[passageIndex]; if(node.left<0) { for(auto it=candidate;it!=node.segments.end()&&*it(b.positionSubvoxels[0])-ax; const double dy=static_cast(b.positionSubvoxels[1])-ay; const double dz=static_cast(b.positionSubvoxels[2])-az; const double lengthSq=dx*dx+dy*dy+dz*dz; if(lengthSq<=0.0)continue; const double horizontalLengthSq=dx*dx+dy*dy; const double t=horizontalLengthSq>0.0 ?std::clamp(((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):0.0; const double centerX=ax+t*dx,centerY=ay+t*dy; const double centerZ=az+t*dz-(static_cast(a.radiusSubvoxels) +t*(static_cast(b.radiusSubvoxels)-a.radiusSubvoxels))+1.13*qPerMetre; const double horizontalX=qx-centerX,horizontalY=qy-centerY; const double vertical=std::max(0.0,std::abs(qz-centerZ)-0.54*qPerMetre); const double distance=DeterministicSqrt(horizontalX*horizontalX+horizontalY*horizontalY +vertical*vertical)-0.34*qPerMetre; if(distance<0.05*qPerMetre)return true; } return false; } return PassagePlayerCapsuleBlocksSupportNode(passageIndex,node.left,first,end, qx,qy,qz,qPerMetre) ||PassagePlayerCapsuleBlocksSupportNode(passageIndex,node.right,first,end, qx,qy,qz,qPerMetre); } bool PassagePlayerCapsuleBlocksSupport(std::size_t passageIndex, std::ptrdiff_t carveBegin,std::ptrdiff_t carveEnd,std::ptrdiff_t lowerMouthIndex, std::ptrdiff_t mouthSearchBegin,std::ptrdiff_t mouthSearchEnd,std::ptrdiff_t lastIndex, double qx,double qy,double qz,double qPerMetre) const { const auto blocked=[&](std::ptrdiff_t first,std::ptrdiff_t last) { return first<=last&&PassagePlayerCapsuleBlocksSupportNode(passageIndex,0, static_cast(first),static_cast(last+1), qx,qy,qz,qPerMetre); }; return blocked(carveBegin,carveEnd) ||blocked(0,std::min(8,lowerMouthIndex)-1) ||blocked(mouthSearchBegin,mouthSearchEnd-1) ||blocked(lowerMouthIndex,lastIndex-1); } bool PassageProofBoundsOverlapNode(int nodeIndex,const double minimum[3], const double maximum[3]) const { if(nodeIndex<0)return false; const auto& node=PassageProofNodes[static_cast(nodeIndex)]; for(std::size_t axis=0;axis<3;++axis) if(node.maximum[axis]maximum[axis])return false; if(node.left<0) { for(std::size_t index=node.first;index=minimum[axis] &&PassageProofBounds[index].minimum[axis]<=maximum[axis]; if(overlap)return true; } return false; } return PassageProofBoundsOverlapNode(node.left,minimum,maximum) ||PassageProofBoundsOverlapNode(node.right,minimum,maximum); } bool IsPassageColumnSegment(std::size_t PassageIndex,std::size_t SegmentIndex) const { return PassageIndex=ResolvedLayout->passages.size()) return std::numeric_limits::infinity(); const auto& passage=ResolvedLayout->passages[PassageIndex]; if(SegmentIndex+1>=passage.controlPoints.size()) return std::numeric_limits::infinity(); constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double qPerMetre=Q*1000000.0/std::max(1, ResolvedLayout->coreVoxelSizeMicrometres); const auto& a=passage.controlPoints[SegmentIndex]; const auto& b=passage.controlPoints[SegmentIndex+1]; const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), ResolvedLayout->strates.end(),[&passage](const auto& value) {return value.strateIndex==passage.upperStrateIndex;}); const bool surfaceWorldUpper=upperStratum!=ResolvedLayout->strates.end() &&upperStratum->generatorKind=="SurfaceWorld"; const double ax=static_cast(a.positionSubvoxels[0]); const double ay=static_cast(a.positionSubvoxels[1]); const double dx=static_cast(b.positionSubvoxels[0])-ax; const double dy=static_cast(b.positionSubvoxels[1])-ay; const double horizontalLengthSq=dx*dx+dy*dy; const double qx=static_cast(X)*Q,qy=static_cast(Y)*Q; const double qz=static_cast(Z)*Q; const double t=horizontalLengthSq>0.0 ?std::clamp(((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):0.0; const double centerX=ax+t*dx,centerY=ay+t*dy; const double centerZ=a.positionSubvoxels[2]+t*( static_cast(b.positionSubvoxels[2])-a.positionSubvoxels[2]); const double radius=a.radiusSubvoxels+t*( static_cast(b.radiusSubvoxels)-a.radiusSubvoxels); // Embed the deck 13 cm above the bore bottom: within the 25 cm support // probe, while leaving 16 cm between it and the capsule's lowest sample. const double floorQ=centerZ-radius +PassageBridgeDeckOffsetMetres(PassageIndex,SegmentIndex)*qPerMetre; const double relief=std::clamp(static_cast(VF_FieldSurfaceRelief(X,Y,Z, static_cast(Owner?Owner->Seed:0))),-1.0,1.0); const double halfWidth=(surfaceWorldUpper?3.65+0.10*relief:3.30+0.20*relief) *qPerMetre; const double thickness=(4.0+1.0*relief)*qPerMetre; const double horizontalDistance=DeterministicSqrt((qx-centerX)*(qx-centerX) +(qy-centerY)*(qy-centerY)); const double sideDistance=horizontalDistance-halfWidth; const double topDistance=qz-floorQ; const double bottomDistance=floorQ-thickness-qz; const double verticalDistance=std::max(topDistance,bottomDistance); const double outside=DeterministicSqrt( std::max(sideDistance,0.0)*std::max(sideDistance,0.0) +std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0)); const double inside=std::min(std::max(sideDistance,verticalDistance),0.0); return outside+inside; } double PassageColumnSignedDistance(const FStandalonePassageVoidCrossing& crossing, float X,float Y,float Z) const { if(crossing.maximumRadiusSubvoxels<=0||crossing.topZSubvoxels <=crossing.bottomZSubvoxels)return std::numeric_limits::infinity(); constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double qPerMetre=Q*1000000.0/std::max(1, ResolvedLayout?ResolvedLayout->coreVoxelSizeMicrometres:250000u); const double qx=static_cast(X)*Q,qy=static_cast(Y)*Q, qz=static_cast(Z)*Q; const double dx=qx-crossing.centerXSubvoxels; const double dy=qy-crossing.centerYSubvoxels; const double maximumRadius=static_cast(crossing.maximumRadiusSubvoxels); const double horizontalSquared=dx*dx+dy*dy; if(horizontalSquared>maximumRadius*maximumRadius) return DeterministicSqrt(horizontalSquared)-maximumRadius; if(qz>crossing.topZSubvoxels) return qz-crossing.topZSubvoxels; if(qzpassages.size() &&qzpassages[crossing.passageIndex]; const auto lower=std::find_if(ResolvedLayout->strates.begin(), ResolvedLayout->strates.end(),[&](const auto& value) {return value.strateIndex==passage.lowerStrateIndex;}); if(lower!=ResolvedLayout->strates.end()) { const double capsuleRadius=0.37*qPerMetre; const double capsuleHalfHeight=0.86*qPerMetre; const double capsuleRadiusSquared=capsuleRadius*capsuleRadius; const VoxelForgeCore::ResolvedLayoutAnchorV2* previous=nullptr; for(const auto& anchor:lower->anchors) { if(anchor.kind!="route_point"||anchor.values.size()<3)continue; if(previous) { const double ax=previous->values[0],ay=previous->values[1]; const double dx=static_cast(anchor.values[0])-ax; const double dy=static_cast(anchor.values[1])-ay; const double lengthSquared=dx*dx+dy*dy; const double t=lengthSquared>0.0?std::clamp( ((qx-ax)*dx+(qy-ay)*dy)/lengthSquared,0.0,1.0):0.0; const double routeX=ax+t*dx,routeY=ay+t*dy; const double routeZ=previous->values[2]+t*( static_cast(anchor.values[2])-previous->values[2]); const double routeDx=qx-routeX,routeDy=qy-routeY; if(routeDx*routeDx+routeDy*routeDy<=capsuleRadiusSquared &&std::abs(qz-routeZ)<=capsuleHalfHeight) return maximumRadius+qPerMetre; } previous=&anchor; } } } const double height=static_cast(crossing.topZSubvoxels -crossing.bottomZSubvoxels); const double t=std::clamp((crossing.topZSubvoxels-qz)/height,0.0,1.0); const auto smoothStep=[](double value) { value=std::clamp(value,0.0,1.0); return value*value*(3.0-2.0*value); }; const float z=static_cast(Z); const float scaledZ=z*0.12f; const float driftX=VF_FieldSurfaceRelief(0.0f,0.0f,scaledZ, static_cast(Owner?Owner->Seed:0)^0x434f4c58u); const float driftY=VF_FieldSurfaceRelief(0.0f,0.0f,scaledZ, static_cast(Owner?Owner->Seed:0)^0x434f4c59u); const double axisX=crossing.centerXSubvoxels +std::clamp(static_cast(driftX),-1.0,1.0)*0.35*qPerMetre; const double axisY=crossing.centerYSubvoxels +std::clamp(static_cast(driftY),-1.0,1.0)*0.35*qPerMetre; const double centerDx=qx-axisX,centerDy=qy-axisY; const double radialDistance=DeterministicSqrt(centerDx*centerDx+centerDy*centerDy); const float broadRelief=VF_FieldSurfaceRelief(X*0.12f,Y*0.12f,Z*0.12f, static_cast(Owner?Owner->Seed:0)^0x434f4c52u); const float surfaceRelief=VF_FieldSurfaceRelief(X,Y,Z, static_cast(Owner?Owner->Seed:0)^0x434f4c53u); const double baseFlare=6.0*qPerMetre*smoothStep((t-0.82)/0.18); const double ceilingFlare=2.0*qPerMetre*(1.0-smoothStep(t/0.075)); const double irregularity=1.25*qPerMetre +std::clamp(static_cast(broadRelief),-1.0,1.0)*1.10*qPerMetre +std::clamp(static_cast(surfaceRelief),-1.0,1.0)*0.35*qPerMetre; const double radius=static_cast(crossing.minimumRadiusSubvoxels) +std::max(0.15*qPerMetre,irregularity)+baseFlare+ceilingFlare; const double radialDistanceField=radialDistance-radius; const double topDistance=qz-crossing.topZSubvoxels; const double bottomDistance=crossing.bottomZSubvoxels-qz; const double verticalDistance=std::max(topDistance,bottomDistance); const double outside=DeterministicSqrt( std::max(radialDistanceField,0.0)*std::max(radialDistanceField,0.0) +std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0)); const double inside=std::min(std::max(radialDistanceField,verticalDistance),0.0); return outside+inside; } float NativePassageDensityAt(float X,float Y,float Z) const { float density=0.0f; if(!NativeFloatingIslandDensityAt(X,Y,Z,density) &&!NativeSurfaceDensityAt(X,Y,Z,density)) { if(UsesLegacySurfaceField(X,Y,Z)&&Owner&&Owner->Generator) density=Owner->Generator->GetDensityAt(X,Y,Z); else density=AnalyticDensityAt(X,Y,Z,true,nullptr,nullptr,nullptr, false,false); } return ApplyResolvedStrateBoundaryBand(density,X,Y,Z); } void BuildPassageVoidCrossingPlan() { BuildResolvedBoundaryZIntervals(); PassageVoidCrossings.clear(); if(!Owner||!ResolvedLayout||ResolvedLayout->passages.empty())return; if(!Owner->PassageVoidCrossingCache) {BuildPassageSpatialIndices();return;} auto& cache=*Owner->PassageVoidCrossingCache; std::lock_guard lock(cache.mutex); const std::string cacheKey=ResolvedLayout->stableIdentity+"/passage-void-columns-v1"; const auto cached=cache.byLayout.find(cacheKey); if(cached!=cache.byLayout.end()) {PassageVoidCrossings=cached->second;BuildPassageSpatialIndices();return;} constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double qPerMetre=Q*1000000.0/std::max(1, ResolvedLayout->coreVoxelSizeMicrometres); constexpr std::array,5> supportOffsets{{ {{0.0,0.0}},{{0.45,0.0}},{{-0.45,0.0}},{{0.0,0.45}},{{0.0,-0.45}}}}; constexpr std::array supportDepthsMetres{{0.125,0.25}}; constexpr double shortSpanMaximumMetres=12.5; // 2.5 x the 5 m legacy support thickness. struct FPathSample { std::size_t segment=0; std::array point{}; bool nativeSupported=false; }; const auto nativeSupportedAt=[&](const std::array& point) { const double floorQ=point[2]-point[3]+0.13*qPerMetre; std::uint32_t supported=0; for(const auto& offset:supportOffsets) { bool hasSolid=false; for(const double depth:supportDepthsMetres) { const double sampleZ=floorQ-depth*qPerMetre; const float density=NativePassageDensityAt( static_cast((point[0]+offset[0]*qPerMetre)/Q), static_cast((point[1]+offset[1]*qPerMetre)/Q), static_cast(sampleZ/Q)); if(density<0.0f){hasSolid=true;break;} } supported+=hasSolid; } return supported>=3; }; for(std::size_t passageIndex=0;passageIndexpassages.size(); ++passageIndex) { const auto& passage=ResolvedLayout->passages[passageIndex]; const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), ResolvedLayout->strates.end(),[&passage](const auto& value) {return value.strateIndex==passage.upperStrateIndex;}); const bool surfaceWorldUpper=upperStratum!=ResolvedLayout->strates.end() &&upperStratum->generatorKind=="SurfaceWorld"; if(passage.controlPoints.size()<2)continue; std::vector samples; samples.reserve(passage.controlPoints.size()*2); const auto pointAt=[&](std::size_t segment,double t) { const auto& a=passage.controlPoints[segment]; const auto& b=passage.controlPoints[segment+1]; FPathSample sample;sample.segment=segment; for(std::size_t axis=0;axis<3;++axis) sample.point[axis]=a.positionSubvoxels[axis]+t*( static_cast(b.positionSubvoxels[axis]) -a.positionSubvoxels[axis]); sample.point[3]=a.radiusSubvoxels+t*( static_cast(b.radiusSubvoxels)-a.radiusSubvoxels); sample.nativeSupported=nativeSupportedAt(sample.point); return sample; }; samples.push_back(pointAt(0,0.0)); for(std::size_t segment=0;segment+1(b.positionSubvoxels[0])-a.positionSubvoxels[0]; const double dy=static_cast(b.positionSubvoxels[1])-a.positionSubvoxels[1]; const double dz=static_cast(b.positionSubvoxels[2])-a.positionSubvoxels[2]; const double lengthMetres=DeterministicSqrt(dx*dx+dy*dy+dz*dz)/qPerMetre; const std::size_t intervals=std::max(1, static_cast(std::ceil(lengthMetres/0.5))); for(std::size_t step=1;step<=intervals;++step) samples.push_back(pointAt(segment,static_cast(step)/intervals)); } std::size_t cursor=0; while(cursor0?gapBegin-1:gapBegin; const std::size_t endSample=cursor0&&samples[startSample].nativeSupported; crossing.connectedAtEnd=cursorshortSpanMaximumMetres; if(crossing.needsColumn&&passage.controlPoints.size()>1) crossing.lastSegment=std::min(crossing.lastSegment+1, passage.controlPoints.size()-2); if(crossing.needsColumn) { std::int64_t sumX=0,sumY=0; std::uint64_t centerSamples=0; for(std::size_t index=gapBegin;index<=gapEnd;++index) { sumX+=static_cast(std::llround(samples[index].point[0])); sumY+=static_cast(std::llround(samples[index].point[1])); ++centerSamples; } crossing.centerXSubvoxels=centerSamples>0 ?static_cast(std::llround(static_cast(sumX)/centerSamples)):0; crossing.centerYSubvoxels=centerSamples>0 ?static_cast(std::llround(static_cast(sumY)/centerSamples)):0; for(const auto& stratum:ResolvedLayout->strates) if(stratum.strateIndex==passage.upperStrateIndex &&stratum.generatorKind=="FloatingIslands") for(const auto& anchor:stratum.anchors) if(anchor.kind=="passage_anchor"&&anchor.values.size()>=2) { crossing.centerXSubvoxels=anchor.values[0]; crossing.centerYSubvoxels=anchor.values[1]; break; } double maximumOffsetSubvoxels=0.0; double maximumBoreSubvoxels=0.0; double minimumCrossingZ=std::min(crossing.startPoint[2],crossing.endPoint[2]); double maximumCrossingZ=std::max(crossing.startPoint[2],crossing.endPoint[2]); for(std::size_t sampleIndex=0;sampleIndexcrossing.lastSegment)continue; } else if(sampleIndexendSample)continue; const auto& point=sample.point; const double dx=point[0]-crossing.centerXSubvoxels; const double dy=point[1]-crossing.centerYSubvoxels; maximumOffsetSubvoxels=std::max(maximumOffsetSubvoxels, DeterministicSqrt(dx*dx+dy*dy)); maximumBoreSubvoxels=std::max(maximumBoreSubvoxels,point[3]); if(surfaceWorldUpper) { minimumCrossingZ=std::min(minimumCrossingZ,point[2]); maximumCrossingZ=std::max(maximumCrossingZ,point[2]); } } const double maximumOffsetMetres=maximumOffsetSubvoxels/qPerMetre; const double maximumBoreMetres=maximumBoreSubvoxels/qPerMetre; const double minimumRadiusMetres=maximumOffsetMetres+maximumBoreMetres+2.0; const double nominalRadiusMetres=minimumRadiusMetres+1.25; crossing.maximumRouteOffsetMetres=maximumOffsetMetres; crossing.maximumBoreRadiusMetres=maximumBoreMetres; crossing.minimumRadiusSubvoxels=static_cast(std::ceil( minimumRadiusMetres*qPerMetre)); crossing.nominalRadiusSubvoxels=static_cast(std::llround( nominalRadiusMetres*qPerMetre)); crossing.maximumRadiusSubvoxels=crossing.minimumRadiusSubvoxels +static_cast(std::ceil(11.25*qPerMetre)); crossing.topZSubvoxels=static_cast(std::llround( maximumCrossingZ+5.0*qPerMetre)); crossing.bottomZSubvoxels=static_cast(std::llround( minimumCrossingZ-6.0*qPerMetre)); } PassageVoidCrossings.push_back(crossing); if(crossing.needsColumn) { const double coreRadius=crossing.nominalRadiusSubvoxels/qPerMetre; const double minimumWall=coreRadius-crossing.maximumRouteOffsetMetres -crossing.maximumBoreRadiusMetres; std::printf("[PassageVoidColumnPlan] id=%s segments=%zu..%zu unsupported_samples=%llu span_m=%.3f threshold_m=%.3f connected=%d/%d axis_m=(%.3f,%.3f) z_m=%.3f..%.3f route_radius_m=%.3f bore_radius_m=%.3f min_radius_m=%.3f nominal_radius_m=%.3f max_radius_m=%.3f nominal_wall_m=%.3f flare_m=6.000 status=%s\n", passage.stableId.c_str(),crossing.firstSegment,crossing.lastSegment, static_cast(crossing.unsupportedSamples),gapLength, shortSpanMaximumMetres,static_cast(crossing.connectedAtStart), static_cast(crossing.connectedAtEnd), crossing.centerXSubvoxels/qPerMetre,crossing.centerYSubvoxels/qPerMetre, crossing.topZSubvoxels/qPerMetre,crossing.bottomZSubvoxels/qPerMetre, crossing.maximumRouteOffsetMetres,crossing.maximumBoreRadiusMetres, crossing.minimumRadiusSubvoxels/qPerMetre,coreRadius, crossing.maximumRadiusSubvoxels/qPerMetre,minimumWall, crossing.connectedAtStart&&crossing.connectedAtEnd?"PLANNED":"UNATTACHED"); } else std::printf("[PassageShortGroundedSpanPlan] id=%s segments=%zu..%zu span_m=%.3f threshold_m=%.3f connected=%d/%d width_m=7.300 thickness_m=3.000..5.000 status=%s\n", passage.stableId.c_str(),crossing.firstSegment,crossing.lastSegment, gapLength,shortSpanMaximumMetres,static_cast(crossing.connectedAtStart), static_cast(crossing.connectedAtEnd), crossing.connectedAtStart&&crossing.connectedAtEnd?"PLANNED":"UNATTACHED"); } if(std::none_of(PassageVoidCrossings.begin(),PassageVoidCrossings.end(), [passageIndex](const auto& range){return range.passageIndex==passageIndex;})) std::printf("[PassageVoidColumnPlan] id=%s crossings=0 status=NOT_REQUIRED\n", passage.stableId.c_str()); } cache.byLayout.emplace(cacheKey,PassageVoidCrossings); BuildPassageSpatialIndices(); } static uint64 PackLatticeKey(int32 X,int32 Y,int32 Z) { constexpr uint64 Mask=0x1FFFFFull; return (static_cast(static_cast(X)&Mask)<<42) |(static_cast(static_cast(Y)&Mask)<<21) |(static_cast(static_cast(Z)&Mask)); } float PackedDensityAt(int32 X,int32 Y,int32 Z,bool bRock,float FX,float FY,float FZ) const { const size_t Column=static_cast(Y-Min[1])*Size[0]+static_cast(X-Min[0]); const size_t Zi=static_cast(Z-Min[2]); const size_t Word=Column*WordsPerZ+(Zi>>6); const uint64 Mask=uint64{1}<<(Zi&63u); if(CandidateWords[Word]&Mask) { const size_t Node=Zi*static_cast(Size[1])*Size[0]+Column; const int16 Proxy=SurfaceDistanceQ[Node]; float Density=static_cast(Proxy)*(1.0f/256.0f); if((Density<0.0f)!=bRock) Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),1.0f/256.0f); return ApplyEditBrushes(ApplyResolvedStrateComposition(Density,FX,FY,FZ),FX,FY,FZ); } // Match the fixed fallback to the edge of the stored proxy band. The // displacement is tapered to zero there, so this avoids a magnitude // jump along the candidate bit-mask boundary. constexpr float ProxyBandEdge=DetailSupremum+0.125f; return ApplyEditBrushes(ApplyResolvedStrateComposition( bRock?-ProxyBandEdge:ProxyBandEdge,FX,FY,FZ),FX,FY,FZ); } float AnalyticDensityAt(float X,float Y,float Z,bool bClampNearZero=true, float* OutDetailDisplacement=nullptr, float* OutCaveInfluence=nullptr, float* OutFloorInfluence=nullptr, bool bApplyEdits=true,bool bApplyComposition=true) const { if(SharedDensitySource)return SharedDensitySource->AnalyticDensityAt(X,Y,Z, bClampNearZero,OutDetailDisplacement,OutCaveInfluence,OutFloorInfluence, bApplyEdits,bApplyComposition); if(OutDetailDisplacement)*OutDetailDisplacement=0.0f; if(OutCaveInfluence)*OutCaveInfluence=0.0f; if(OutFloorInfluence)*OutFloorInfluence=0.0f; float IslandDensity=0.0f; if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity)) { const float composed=bApplyComposition ?ApplyResolvedStrateComposition(IslandDensity,X,Y,Z):IslandDensity; return bApplyEdits?ApplyEditBrushes(composed,X,Y,Z):composed; } if(UsesLegacySurfaceField(X,Y,Z)) { float NativeDensity=0.0f; if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity)) { if(bApplyComposition) NativeDensity=ApplyResolvedStrateComposition(NativeDensity,X,Y,Z); return bApplyEdits?ApplyEditBrushes(NativeDensity,X,Y,Z):NativeDensity; } const float Density=Owner->Generator->GetDensityAt(X,Y,Z); const float composed=bApplyComposition ?ApplyResolvedStrateComposition(Density,X,Y,Z):Density; return bApplyEdits?ApplyEditBrushes(composed,X,Y,Z):composed; } const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1] &&Z>=Min[2]&&Z<=Max[2]; if(!bInsideFine) { bool bCriticalAir=false; float RockDistance=CoarseRockDistance(X,Y,Z,&bCriticalAir); if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f); const bool bRock=RockDistance>=0.0f; float Density=-RockDistance; if((Density<0.0f)!=bRock||(bClampNearZero&&std::abs(Density)<0.05f)) Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density), bClampNearZero?0.05f:1.0e-7f); const float composed=bApplyComposition ?ApplyResolvedStrateComposition(Density,X,Y,Z):Density; return bApplyEdits?ApplyEditBrushes(composed,X,Y,Z):composed; } ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f; float PassageInfluence=0.0f; float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF, &bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir,nullptr,false, &PassageInfluence); if(OutCaveInfluence)*OutCaveInfluence=CaveInfluence; if(OutFloorInfluence)*OutFloorInfluence=FloorInfluence; if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth &&std::abs(RockDistance)<=DetailSupremum+0.125f) { const float Fade=DetailBandFadeAt(X,Y,Z,RockDistance,SurfaceSDF, CaveInfluence,FloorInfluence); if(Fade>0.0f) { const float Detail=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence, FloorInfluence,PassageInfluence)*Fade; RockDistance+=Detail; if(OutDetailDisplacement)*OutDetailDisplacement=Detail; } } if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f); const bool bRock=RockDistance>=0.0f; float Density=-RockDistance; if((Density<0.0f)!=bRock||(bClampNearZero&&std::abs(Density)<0.05f)) Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density), bClampNearZero?0.05f:1.0e-7f); if(bApplyComposition)Density=ApplyResolvedStrateComposition(Density,X,Y,Z); return bApplyEdits?ApplyEditBrushes(Density,X,Y,Z):Density; } public: bool GetNativeSurfaceHeightsAt(float X,float Y,int32 ChunkZ, float& OutGroundZ,float& OutCeilingZ) const { if(SharedDensitySource) return SharedDensitySource->GetNativeSurfaceHeightsAt(X,Y,ChunkZ,OutGroundZ,OutCeilingZ); if(!WorldAssembly||!ResolvedLayout||!Owner||!Owner->Manager||!Owner->Generator ||!std::isfinite(X)||!std::isfinite(Y))return false; const double ChunkWidth=GetResolvedLayoutChunkWidthVoxels(); const std::int64_t chunkMinimum=static_cast(std::llround(ChunkZ*ChunkWidth)); const std::int64_t chunkMaximum=static_cast(std::llround((ChunkZ+1)*ChunkWidth)); if(bNativeOnly&&(!NativeOnlyContainsColumn(X,Y) ||chunkMaximumNativeOnlyMaximum.Z ||Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ)) !=ECaveGeneratorType::SurfaceWorld)) {RecordNativeOnlyViolation();return false;} if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ)) !=ECaveGeneratorType::SurfaceWorld)return false; const int32 StrateIndex=Owner->Manager->GetStrateIndex( static_cast((static_cast(ChunkZ)+0.5) *ResolvedLayout->strateLayoutChunkSizeMicrometres/10000.0)); const auto* Plan=FindWorldAssemblyStratum(StrateIndex,ChunkZ); if(!Plan)return false; constexpr double Scale=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double MaxVoxel=static_cast(std::numeric_limits::max())/Scale; if(std::abs(static_cast(X))>MaxVoxel||std::abs(static_cast(Y))>MaxVoxel) return false; const auto ToFixed=[](float Value) {return static_cast(std::llround(static_cast(Value) *VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));}; const VoxelForgeCore::Heightfield2D Block(WorldAssembly->recipe,*Plan); if(!Block.isValid())return false; const auto Column=Block.point(ToFixed(X),ToFixed(Y), static_cast(ChunkZ)*ResolvedLayout->strateLayoutChunkWidthSubvoxels); OutGroundZ=static_cast(Column.groundHeightSubvoxels/Scale); OutCeilingZ=static_cast(Column.capHeightSubvoxels/Scale); return std::isfinite(OutGroundZ)&&std::isfinite(OutCeilingZ); } EVoxelTileClass ClassifyBox(const FIntVector& Origin,int32 Step,int32 Cells) const { return ClassifyBoxDetailed(Origin,Step,Cells).Verdict; } bool IsNativeSurfaceBox(const FIntVector& Origin,int32 Step,int32 Cells) const { if(SharedDensitySource)return SharedDensitySource->IsNativeSurfaceBox(Origin,Step,Cells); if(!WorldAssembly||!Owner||!Owner->Manager||Step<=0||Cells<=0)return false; const int32 Pad=std::max(Step,1); const int32 BMin=Origin.Z-Pad; const int32 BMax=Origin.Z+(Cells+1)*Step; const float LayoutWidth=static_cast(GetResolvedLayoutChunkWidthVoxels()); const int32 FirstChunk=FMath::FloorToInt(static_cast(BMin)/LayoutWidth); const int32 LastChunk=FMath::FloorToInt(static_cast(BMax)/LayoutWidth); const VoxelForgeCore::WorldAssemblyStratumPlanV1* Plan=nullptr; for(int32 ChunkZ=FirstChunk;ChunkZ<=LastChunk;++ChunkZ) { const auto It=std::find_if(WorldAssembly->plan.strates.begin(), WorldAssembly->plan.strates.end(),[ChunkZ](const auto& Entry) {return ChunkZ<=Entry.topChunkZ&&ChunkZ>=Entry.bottomChunkZ;}); if(It==WorldAssembly->plan.strates.end())return false; const auto* Candidate=&*It; if(Plan&&Plan!=Candidate)return false; if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ)) !=ECaveGeneratorType::SurfaceWorld)return false; Plan=Candidate; } if(bNativeOnly) { const int32 BMinX=Origin.X-Pad,BMinY=Origin.Y-Pad; const int32 BMaxX=Origin.X+(Cells+1)*Step; const int32 BMaxY=Origin.Y+(Cells+1)*Step; if(BMinXNativeOnlyMaximum.X ||BMaxY>NativeOnlyMaximum.Y||BMax>NativeOnlyMaximum.Z) {RecordNativeOnlyViolation();return false;} } if(bNativeOnly&&!Plan)RecordNativeOnlyViolation(); return Plan!=nullptr; } FBoxClassification ClassifyBoxDetailed(const FIntVector& Origin,int32 Step,int32 Cells, bool bUseNativeLatticeProof=false) const { if(SharedDensitySource) return SharedDensitySource->ClassifyBoxDetailed(Origin,Step,Cells,bUseNativeLatticeProof); using Clock=std::chrono::steady_clock; const auto Start=Clock::now(); const int32 Pad=std::max(Step,1); int32 BMin[3]={Origin.X-Pad,Origin.Y-Pad,Origin.Z-Pad}; int32 BMax[3]={Origin.X+(Cells+1)*Step,Origin.Y+(Cells+1)*Step,Origin.Z+(Cells+1)*Step}; const VoxelForgeCore::WorldAssemblyStratumPlanV1* SurfacePlan=nullptr; const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1* IslandPlan=nullptr; bool bSurfaceOverlap=false,bIslandOverlap=false,bOtherStrateOverlap=false; bool bIslandOtherStrateOverlap=false,bBadPlan=false; if(Owner&&Owner->Manager&&ResolvedLayout) { const float LayoutWidth=static_cast(GetResolvedLayoutChunkWidthVoxels()); const int32 FirstChunk=FMath::FloorToInt(static_cast(BMin[2])/LayoutWidth); const int32 LastChunk=FMath::FloorToInt(static_cast(BMax[2])/LayoutWidth); for(int32 ChunkZ=FirstChunk;ChunkZ<=LastChunk;++ChunkZ) { const ECaveGeneratorType Type=Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ)); const int32 StrateIndex=Owner->Manager->GetStrateIndex(static_cast( (static_cast(ChunkZ)+0.5)*ResolvedLayout->strateLayoutChunkSizeMicrometres/10000.0)); if(Type==ECaveGeneratorType::SurfaceWorld) { bSurfaceOverlap=true;bIslandOtherStrateOverlap=true; const auto* Candidate=FindWorldAssemblyStratum(StrateIndex,ChunkZ); if(!Candidate||(SurfacePlan&&SurfacePlan!=Candidate))bBadPlan=true; SurfacePlan=Candidate; } else if(Type==ECaveGeneratorType::FloatingIslands) { bIslandOverlap=true;bOtherStrateOverlap=true; const auto* Candidate=FindWorldAssemblyFloatingIslandStratum(StrateIndex,ChunkZ); if(!Candidate||(IslandPlan&&IslandPlan!=Candidate))bBadPlan=true; IslandPlan=Candidate; } else {bOtherStrateOverlap=true;bIslandOtherStrateOverlap=true;} } } const auto Finish=[&](EVoxelTileClass Verdict,bool bUnknown=false) { const bool bNative=(bSurfaceOverlap&&!bOtherStrateOverlap) ||(bIslandOverlap&&!bIslandOtherStrateOverlap); FBoxClassification Result{Verdict,bNative,bUnknown}; auto& Counts=ThreadClassificationStats(); Counts.Milliseconds+=std::chrono::duration(Clock::now()-Start).count(); if(!Result.bNativeSurface||bUnknown)++Counts.UnknownBoxes; else if(Verdict==EVoxelTileClass::AllSolid)++Counts.SolidBoxes; else if(Verdict==EVoxelTileClass::AllAir)++Counts.AirBoxes; else ++Counts.MixedBoxes; return Result; }; if(ResolvedPassageIntersectsBox(BMin,BMax)) return Finish(EVoxelTileClass::Mixed,true); if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0] &&EditBoundsMin.Y<=BMax[1]&&EditBoundsMax.Y>=BMin[1] &&EditBoundsMin.Z<=BMax[2]&&EditBoundsMax.Z>=BMin[2]) return Finish(EVoxelTileClass::Mixed,true); if(ResolvedLayout&&Owner) { const double ZMinMicrometres=static_cast(BMin[2])*Owner->CoreVoxelSizeMicrons; const double ZMaxMicrometres=static_cast(BMax[2])*Owner->CoreVoxelSizeMicrons; for(const auto& Stratum:ResolvedLayout->strates) for(const auto* Band:{&Stratum.ceiling,&Stratum.bedrock}) if(Band->presentInCurrentField&&ZMaxMicrometres>=Band->bottomMicrometres &&ZMinMicrometrestopMicrometres) { if(ZMinMicrometres>=Band->bottomMicrometres &&ZMaxMicrometrestopMicrometres) return Finish(EVoxelTileClass::AllSolid); return Finish(EVoxelTileClass::Mixed); } } if(bNativeOnly) { const bool bInsideProofBox=BMin[0]>=NativeOnlyMinimum.X &&BMin[1]>=NativeOnlyMinimum.Y&&BMin[2]>=NativeOnlyMinimum.Z &&BMax[0]<=NativeOnlyMaximum.X&&BMax[1]<=NativeOnlyMaximum.Y &&BMax[2]<=NativeOnlyMaximum.Z; if(!bInsideProofBox||!bSurfaceOverlap||bIslandOverlap||bOtherStrateOverlap ||bBadPlan||!EditBrushes.empty()) { RecordNativeOnlyViolation(); return Finish(EVoxelTileClass::Mixed,true); } } // A box crossing recipes cannot use one field interval; unknown composition stays Mixed. // Une boite qui traverse plusieurs recettes reste Mixed, car l'intervalle d'un seul champ ne suffit pas. if(bBadPlan||(bSurfaceOverlap&&(bIslandOverlap||bOtherStrateOverlap)) ||(bIslandOverlap&&(bIslandOtherStrateOverlap ||(!bSurfaceOverlap&&bOtherStrateOverlap)))) return Finish(EVoxelTileClass::Mixed,true); // The native interval path below is the exact field selected first by DensityAtLattice // for SurfaceWorld and FloatingIslands. Legacy analytic primitives/layer bounds cannot // modify that field; only the explicit edit transaction above can. Keep their guards on // the legacy classifier path, where those overlays do participate in density. if(!bNativeOnly&&(!bUseNativeLatticeProof||(!bSurfaceOverlap&&!bIslandOverlap))) { for(int32 BZ=FloorDiv(BMin[2],ActiveSpatialBinSize);BZ<=FloorDiv(BMax[2],ActiveSpatialBinSize);++BZ) for(int32 BY=FloorDiv(BMin[1],ActiveSpatialBinSize);BY<=FloorDiv(BMax[1],ActiveSpatialBinSize);++BY) for(int32 BX=FloorDiv(BMin[0],ActiveSpatialBinSize);BX<=FloorDiv(BMax[0],ActiveSpatialBinSize);++BX) { const auto It=SpatialBins.find({BX,BY,BZ});if(It==SpatialBins.end())continue; for(uint32 Id:It->second) { const FPrimitive& P=Primitives[Id];bool Overlap=true; for(int32 A=0;A<3;++A) Overlap &= P.Max[A]+DetailBand>=BMin[A]&&P.Min[A]-DetailBand<=BMax[A]; if(Overlap)return Finish(EVoxelTileClass::Mixed); } } for(const FInterval& R:LayerRanges) if(R.Lo<=BMax[2]&&R.Hi>=BMin[2] &&(R.Lo>=BMin[2]||R.Hi<=BMax[2])) return Finish(EVoxelTileClass::Mixed); } if(bSurfaceOverlap) { if(!WorldAssembly||!SurfacePlan)return Finish(EVoxelTileClass::Mixed,true); const VoxelForgeCore::Heightfield2D Block(WorldAssembly->recipe,*SurfacePlan); // classifyLatticeBox takes the exact extrema of ground/cap columns at every // MC lattice XY in this box. Since density is min(z-ground, cap-z), those extrema // bound every lattice sample in Z without a noise amplitude guess. const auto Verdict=Block.classifyLatticeBox( static_cast(BMin[0])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, static_cast(BMin[1])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, static_cast(BMin[2])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, static_cast(BMax[0])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, static_cast(BMax[1])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, static_cast(BMax[2])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, std::max(Step,1)); switch(Verdict) { case VoxelForgeCore::HeightfieldBoxClass::allAir: return Finish(EVoxelTileClass::AllAir); case VoxelForgeCore::HeightfieldBoxClass::allSolid: return Finish(EVoxelTileClass::AllSolid); case VoxelForgeCore::HeightfieldBoxClass::mixed: return Finish(EVoxelTileClass::Mixed); default:return Finish(EVoxelTileClass::Mixed,true); } } if(bIslandOverlap) { if(!IslandPlan)return Finish(EVoxelTileClass::Mixed,true); constexpr std::int64_t Scale=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; const std::int64_t Bottom=IslandPlan->manifest.recipe.layoutMinimumZSubvoxels; const std::int64_t Top=IslandPlan->manifest.recipe.layoutMaximumZSubvoxels; const std::int64_t Seal=IslandPlan->manifest.recipe.boundarySealSubvoxels; const std::int64_t BoxBottom=static_cast(BMin[2])*Scale; const std::int64_t BoxTop=static_cast(BMax[2])*Scale; const bool bSharedTopSealEnabled=IslandPlan->manifest.recipe.geometryVersion<3 ||!VoxelDensityAblation::IsBoundarySealOff(); const bool bTopSealOverlap=bSharedTopSealEnabled &&BoxBottom<=Top&&BoxTop>Top-Seal; const bool bBottomSealOverlap=IslandPlan->manifest.recipe.geometryVersion<3 &&BoxBottom=Bottom; if(bBottomSealOverlap) return Finish(BoxBottom>=Bottom&&BoxTop<=Bottom+Seal ?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed); if(bTopSealOverlap) return Finish((IslandPlan->manifest.recipe.geometryVersion<3 ?BoxBottom>=Top-Seal:BoxBottom>Top-Seal)&&BoxTop<=Top ?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed); const VoxelForgeCore::FloatingIslandFieldV1 Block( IslandPlan->manifest.recipe,IslandPlan->manifest.plan); if(!Block.isValid())return Finish(EVoxelTileClass::Mixed,true); const auto Interval=Block.interval(static_cast(BMin[0])*Scale, static_cast(BMin[1])*Scale,static_cast(BMin[2])*Scale, static_cast(BMax[0])*Scale,static_cast(BMax[1])*Scale, static_cast(BMax[2])*Scale); if(!Interval.known)return Finish(EVoxelTileClass::Mixed,true); if(Interval.minimumDensitySubvoxels>=0)return Finish(EVoxelTileClass::AllAir); if(Interval.maximumDensitySubvoxels<0)return Finish(EVoxelTileClass::AllSolid); return Finish(EVoxelTileClass::Mixed); } const bool bRock=EvaluateFast((BMin[0]+BMax[0])*0.5, (BMin[1]+BMax[1])*0.5,(BMin[2]+BMax[2])*0.5); return Finish(bRock?EVoxelTileClass::AllSolid:EVoxelTileClass::AllAir); } bool IsRock(int32 X,int32 Y,int32 Z) const { if(SharedDensitySource)return SharedDensitySource->IsRock(X,Y,Z); if(!Contains(X,Y,Z))return EvaluateFast(X,Y,Z); const size_t Line=static_cast(Y-Min[1])*Size[0] +static_cast(X-Min[0]); const size_t Zi=static_cast(Z-Min[2]); return ((RockWords[Line*WordsPerZ+(Zi>>6)]>>(Zi&63u))&1u)!=0; } bool IsDetailAuditPassing() const { const double Correlation=DetailAutocorrelation(DetailVariant); const bool bFlips=Stats.DetailFlips>=1000; const bool bCoherent=Correlation>0.45; const bool bFamilies=AuditSurfaceBandFamilies(); std::printf("[DetailAudit] mode=%s supremum=%.2f vox band=%d changed_signs=%llu adjacent_correlation=%.4f surface_detail_check=%s spatial_scale_check=%s\n", DetailVariant==EDetailVariant::Coherent?"coherent":DetailVariant==EDetailVariant::Smooth?"smooth":"per-voxel", DetailSupremum,DetailBand,static_cast(Stats.DetailFlips),Correlation, bFlips&&bFamilies?"PASS":"FAIL",bCoherent?"PASS":"FAIL"); return bFlips&&bCoherent&&bFamilies; } bool AuditSurfaceBandFamilies(bool bPassageOnly=false) const { struct FPatchKey { int32 X=0,Y=0,Z=0;uint8 Family=0; bool operator==(const FPatchKey&)const=default; }; struct FPatchHash { size_t operator()(const FPatchKey& K)const noexcept { uint64 H=static_cast(K.X)*0x9E3779B185EBCA87ull; H^=static_cast(static_cast(K.Y))*0xC2B2AE3D27D4EB4Full; H^=static_cast(static_cast(K.Z))*0x165667B19E3779F9ull; H^=static_cast(K.Family)*0x94D049BB133111EBull; H^=H>>29;H*=0xBF58476D1CE4E5B9ull;H^=H>>31; return static_cast(H); } }; struct FPatchStats {uint64 Count=0;double Sum=0.0,SumSq=0.0;}; struct FFamilyStats {uint64 Count=0,NearZero=0;double Sum=0.0,SumSq=0.0;}; std::unordered_map Patches; std::array Families{}; FFamilyStats PassageWalls,OrdinaryCaveWalls; for(size_t Line=0;Line(Size[0])*Size[1];++Line) { const int32 X=Min[0]+static_cast(Line%Size[0]); const int32 Y=Min[1]+static_cast(Line/Size[0]); const size_t WordBase=Line*WordsPerZ; for(size_t W=0;W=static_cast(Size[2]))continue; const int32 Z=Min[2]+static_cast(Zi); ESurfaceFamily Family=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; bool bFloorSurface=false;float CaveInfluence=0.0f,FloorInfluence=0.0f; float PassageInfluence=0.0f; const float Raw=EffectiveRockDistance(X,Y,Z,&Family,&SurfaceSDF, &bFloorSurface,&CaveInfluence,&FloorInfluence,nullptr,nullptr,false, &PassageInfluence); if(std::abs(Raw)>=0.5f)continue; const float Fade=DetailBandFadeAt(X,Y,Z,Raw,SurfaceSDF, CaveInfluence,FloorInfluence); const float Detail=Fade>0.0f ?DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence, PassageInfluence)*Fade:0.0f; const size_t FamilyIndex=static_cast(Family); FFamilyStats& F=Families[FamilyIndex];++F.Count;F.Sum+=Detail;F.SumSq+=Detail*Detail; if(std::abs(Detail)<0.05f)++F.NearZero; if(PassageInfluence>=0.5f) {++PassageWalls.Count;PassageWalls.Sum+=Detail;PassageWalls.SumSq+=Detail*Detail;} else if(Family==ESurfaceFamily::Cave&&PassageInfluence<0.1f) {++OrdinaryCaveWalls.Count;OrdinaryCaveWalls.Sum+=Detail;OrdinaryCaveWalls.SumSq+=Detail*Detail;} const FPatchKey Key{FloorDiv(X,16),FloorDiv(Y,16),FloorDiv(Z,16), static_cast(FamilyIndex)}; FPatchStats& P=Patches[Key];++P.Count;P.Sum+=Detail;P.SumSq+=Detail*Detail; } } } std::array FlatCounts{};uint64 LargestFlatPatch=0; for(const auto& Entry:Patches) { const FPatchStats& P=Entry.second;if(P.Count<16)continue; const double Mean=P.Sum/P.Count; const double StdDev=std::sqrt(std::max(0.0,P.SumSq/P.Count-Mean*Mean)); if(StdDev<0.10) { FlatCounts[Entry.first.Family]+=P.Count; LargestFlatPatch=std::max(LargestFlatPatch,P.Count); } } bool bPass=true;uint64 Tested=0,FlatTotal=0,Total=0; for(size_t I=0;I(F.NearZero)/F.Count; const bool FamilyPass=StdDev>=0.15&&ZeroShare<0.60&&FlatCounts[I](F.Count),StdDev,ZeroShare*100.0, static_cast(FlatCounts[I]),FamilyPass?"PASS":"FAIL"); } const double FlatShare=Total?100.0*static_cast(FlatTotal)/Total:100.0; std::printf("[FlatPatchAudit] patch_width=16 vox min_patch_samples=16 flat_stddev_lt=0.10 flat_share=%.1f%% largest_flat_patch=%llu status=%s\n", FlatShare,static_cast(LargestFlatPatch), Tested>0&&FlatShare<20.0?"PASS":"FAIL"); const double PassageRms=PassageWalls.Count ?std::sqrt(PassageWalls.SumSq/PassageWalls.Count):0.0; const double OrdinaryCaveRms=OrdinaryCaveWalls.Count ?std::sqrt(OrdinaryCaveWalls.SumSq/OrdinaryCaveWalls.Count):0.0; const double PassageRmsRatio=OrdinaryCaveRms>1.0e-9 ?PassageRms/OrdinaryCaveRms:0.0; const bool bHasPassageSamples=PassageWalls.Count>0; const bool bRatioPass=PassageWalls.Count>=32&&OrdinaryCaveWalls.Count>=32 &&PassageRmsRatio>=0.75&&PassageRmsRatio<=1.25; const bool bPassageDetail=bPassageOnly?bRatioPass:(!bHasPassageSamples||bRatioPass); const char* PassageStatus=!bHasPassageSamples ?(bPassageOnly?"FAIL":"SKIP"):(bPassageDetail?"PASS":"FAIL"); std::printf("[PassageWallDetailAudit] passage_samples=%llu passage_rms=%.4f cave_samples=%llu cave_rms=%.4f ratio=%.3f range=(0.750,1.250) status=%s\n", static_cast(PassageWalls.Count),PassageRms, static_cast(OrdinaryCaveWalls.Count),OrdinaryCaveRms, PassageRmsRatio,PassageStatus); return bPassageOnly?bPassageDetail:(Tested>0&&FlatShare<20.0&&bPass&&bPassageDetail); } bool AuditGridStairIntegrity() const { uint64 CheckedEdges=0,MissingAny=0,MissingBoth=0; std::array,8> Examples{}; size_t ExampleCount=0; const auto Record=[&](uint64 EdgeMask,uint64 CandidateA,uint64 CandidateB, int32 X,int32 Y,int32 Z,int32 Axis) { CheckedEdges+=std::popcount(EdgeMask); const uint64 Missing=EdgeMask&~(CandidateA&CandidateB); uint64 AnyMissing=Missing; MissingAny+=std::popcount(Missing); MissingBoth+=std::popcount(EdgeMask&~(CandidateA|CandidateB)); while(AnyMissing&&ExampleCount(Bit),Axis}; } }; const size_t Columns=static_cast(Size[0])*Size[1]; for(size_t Line=0;Line(Line%Size[0]); const int32 Y=Min[1]+static_cast(Line/Size[0]); const size_t Base=Line*WordsPerZ; for(size_t W=0;W>63:0; const uint64 PreviousCandidates=W?CandidateWords[Word-1]>>63:0; uint64 Valid=(Size[2]-W*64)>=64?~uint64{0} :((uint64{1}<<(Size[2]-W*64))-1); if(W==0)Valid&=~uint64{1}; const uint64 ShiftedRock=(Rock<<1)|PreviousRock; const uint64 ShiftedCandidates=(Candidates<<1)|PreviousCandidates; Record((Rock^ShiftedRock)&Valid,Candidates,ShiftedCandidates, X,Y,Min[2]+static_cast(W*64),2); if(X+1(W*64),0); } if(Y+1(Size[0])*WordsPerZ; Record(Rock^RockWords[Other],Candidates,CandidateWords[Other], X,Y,Min[2]+static_cast(W*64),1); } } } std::printf("[GridStairDistanceAudit] region=(%d,%d,%d)..(%d,%d,%d) sign_edges=%llu candidate_fallback_edges=%llu both_fallback_edges=%llu status=%s\n", Min[0],Min[1],Min[2],Max[0],Max[1],Max[2], static_cast(CheckedEdges), static_cast(MissingAny), static_cast(MissingBoth),MissingAny==0?"PASS":"FAIL"); const auto CandidateAt=[&](int32 X,int32 Y,int32 Z) { if(!Contains(X,Y,Z))return false; const size_t Column=static_cast(Y-Min[1])*Size[0]+static_cast(X-Min[0]); const size_t Zi=static_cast(Z-Min[2]); return (CandidateWords[Column*WordsPerZ+(Zi>>6)]&(uint64{1}<<(Zi&63u)))!=0; }; for(size_t I=0;I(AX),static_cast(AY),static_cast(AZ)); const float BExact=AnalyticDensityAt(static_cast(BX),static_cast(BY),static_cast(BZ)); std::printf("[GridStairDistanceCandidate] a=(%d,%d,%d) b=(%d,%d,%d) axis=%d candidate=(%d,%d) solid=(%d,%d) density=(%.4f,%.4f) exact=(%.4f,%.4f) critical_air=(%d,%d)\n", AX,AY,AZ,BX,BY,BZ,Examples[I][3],CandidateAt(AX,AY,AZ),CandidateAt(BX,BY,BZ), IsRock(AX,AY,AZ),IsRock(BX,BY,BZ), AD,BD,AExact,BExact, IsInsideCriticalAir(AX,AY,AZ),IsInsideCriticalAir(BX,BY,BZ)); } return MissingAny==0; } bool AuditViewSurface(double CameraX,double CameraY,double CameraZ, float Yaw,float Pitch) const { struct FFamilyStats { uint64 Hits=0,NearZero=0,AxisLocked=0; double Sum=0.0,SumSq=0.0; }; std::array FamilyStats{}; constexpr std::array YawOffsets={-36.0f,-30.0f,-24.0f,-18.0f,-12.0f,-6.0f, 0.0f,6.0f,12.0f,18.0f,24.0f,30.0f,36.0f}; constexpr std::array PitchOffsets={-80.0f,-70.0f,-60.0f,-50.0f,-40.0f, -30.0f,-20.0f,-10.0f,0.0f,10.0f,20.0f}; const double OX=CameraX*4.0,OY=CameraY*4.0,OZ=CameraZ*4.0; auto Distance=[&](double X,double Y,double Z,ESurfaceFamily* OutFamily=nullptr, float* OutRaw=nullptr,float* OutDetail=nullptr, bool* OutFloorSurface=nullptr,float* OutPassageInfluence=nullptr, float* OutCaveInfluence=nullptr,float* OutFloorInfluence=nullptr, float* OutSurfaceSDF=nullptr) { ESurfaceFamily Family=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; bool bFloorSurface=false;float CaveInfluence=0.0f,FloorInfluence=0.0f; float PassageInfluence=0.0f; float D=EffectiveRockDistance(X,Y,Z,&Family,&SurfaceSDF, &bFloorSurface,&CaveInfluence,&FloorInfluence,nullptr,nullptr,false, &PassageInfluence); float Detail=0.0f; const bool bInside=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1] &&Z>=Min[2]&&Z<=Max[2]; const bool bCriticalAir=IsInsideCriticalAir(X,Y,Z); if(bInside&&DetailVariant!=EDetailVariant::Smooth&&std::abs(D)<=DetailSupremum+0.125f) { const float Fade=DetailBandFadeAt(X,Y,Z,D,SurfaceSDF,CaveInfluence,FloorInfluence); if(Fade>0.0f) {Detail=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence, PassageInfluence)*Fade;D+=Detail;} } if(bCriticalAir)D=std::min(D,-1.0f/16.0f); if(OutFamily)*OutFamily=Family; if(OutRaw)*OutRaw=D-Detail; if(OutDetail)*OutDetail=Detail; if(OutFloorSurface)*OutFloorSurface=bFloorSurface; if(OutPassageInfluence)*OutPassageInfluence=PassageInfluence; if(OutCaveInfluence)*OutCaveInfluence=CaveInfluence; if(OutFloorInfluence)*OutFloorInfluence=FloorInfluence; if(OutSurfaceSDF)*OutSurfaceSDF=SurfaceSDF; return D; }; uint64 TotalHits=0,TotalAxisLocked=0,TotalNearZero=0,FloorTaggedHits=0,FloorAxisLocked=0; uint64 FamilyTransitionPairs=0,StableFamilyPairs=0; uint64 ThinDetachedSheets=0; double ThinnestDetachedSheet=std::numeric_limits::max(); double FamilyTransitionGradientSq=0.0,StableFamilyGradientSq=0.0; for(float YawOffset:YawOffsets)for(float PitchOffset:PitchOffsets) { const double YawRad=Yaw+YawOffset*0.017453292519943295; const double PitchRad=Pitch+PitchOffset*0.017453292519943295; const double CP=std::cos(PitchRad),DX=std::sin(YawRad)*CP, DY=std::cos(YawRad)*CP,DZ=std::sin(PitchRad); double PreviousT=0.0; float PreviousD=Distance(OX,OY,OZ+0.001); bool bFound=false; double PrimaryHitT=-1.0; for(double T=0.5;T<=256.0;T+=0.5) { const double X=OX+DX*T,Y=OY+DY*T,Z=OZ+DZ*T; const float D=Distance(X,Y,Z); if(PreviousD<0.0f&&D>=0.0f) { double Lo=PreviousT,Hi=T; for(int32 It=0;It<10;++It) { const double Mid=(Lo+Hi)*0.5; if(Distance(OX+DX*Mid,OY+DY*Mid,OZ+DZ*Mid)<0.0f)Lo=Mid;else Hi=Mid; } const double HitT=(Lo+Hi)*0.5; PrimaryHitT=HitT; const double XH=OX+DX*HitT,YH=OY+DY*HitT,ZH=OZ+DZ*HitT; ESurfaceFamily Family=ESurfaceFamily::Strata;float Raw=0.0f,Detail=0.0f; bool bFloorSurface=false;float PassageInfluence=0.0f; float CaveInfluence=0.0f,FloorInfluence=0.0f,SurfaceSDF=100000.0f; Distance(XH,YH,ZH,&Family,&Raw,&Detail,&bFloorSurface,&PassageInfluence, &CaveInfluence,&FloorInfluence,&SurfaceSDF); const double GX=Distance(XH+0.5,YH,ZH)-Distance(XH-0.5,YH,ZH); const double GY=Distance(XH,YH+0.5,ZH)-Distance(XH,YH-0.5,ZH); const double GZ=Distance(XH,YH,ZH+0.5)-Distance(XH,YH,ZH-0.5); const double GL=std::sqrt(GX*GX+GY*GY+GZ*GZ); const double AxisDot=GL>1.0e-8?std::max({std::abs(GX),std::abs(GY),std::abs(GZ)})/GL:1.0; for(int32 Axis=0;Axis<3;++Axis) { double AP[3]={XH,YH,ZH},AM[3]={XH,YH,ZH}; AP[Axis]+=0.5;AM[Axis]-=0.5; ESurfaceFamily FamilyP=ESurfaceFamily::Strata,FamilyM=ESurfaceFamily::Strata; float DetailP=0.0f,DetailM=0.0f;bool bFloorP=false,bFloorM=false; Distance(AP[0],AP[1],AP[2],&FamilyP,nullptr,&DetailP,&bFloorP); Distance(AM[0],AM[1],AM[2],&FamilyM,nullptr,&DetailM,&bFloorM); const double Gradient=static_cast(DetailP-DetailM); if(FamilyP!=FamilyM||bFloorP!=bFloorM) {FamilyTransitionGradientSq+=Gradient*Gradient;++FamilyTransitionPairs;} else {StableFamilyGradientSq+=Gradient*Gradient;++StableFamilyPairs;} } constexpr double ScanRadius=16.0; constexpr int32 ScanSteps=128; double ScanT=std::max(0.0,HitT-ScanRadius); ESurfaceFamily ScanFamily=ESurfaceFamily::Strata; bool bScanFloor=false;float ScanRaw=0.0f,ScanDetail=0.0f; Distance(OX+DX*ScanT,OY+DY*ScanT,OZ+DZ*ScanT, &ScanFamily,&ScanRaw,&ScanDetail,&bScanFloor); for(int32 Step=1;Step<=ScanSteps;++Step) { const double T=std::min(HitT+ScanRadius,ScanT+0.25); ESurfaceFamily NextFamily=ESurfaceFamily::Strata; bool bNextFloor=false;float NextRaw=0.0f,NextDetail=0.0f; Distance(OX+DX*T,OY+DY*T,OZ+DZ*T, &NextFamily,&NextRaw,&NextDetail,&bNextFloor); if(std::abs(ScanRaw)<=5.0f&&std::abs(NextRaw)<=5.0f) { const double Gradient=static_cast(NextDetail-ScanDetail); if(ScanFamily!=NextFamily||bScanFloor!=bNextFloor) {FamilyTransitionGradientSq+=Gradient*Gradient;++FamilyTransitionPairs;} else {StableFamilyGradientSq+=Gradient*Gradient;++StableFamilyPairs;} } ScanT=T;ScanFamily=NextFamily;bScanFloor=bNextFloor; ScanRaw=NextRaw;ScanDetail=NextDetail; } FFamilyStats& S=FamilyStats[static_cast(Family)]; ++S.Hits;++TotalHits;S.Sum+=Detail;S.SumSq+=Detail*Detail; if(std::abs(Detail)<0.05f){++S.NearZero;++TotalNearZero;} if(AxisDot>0.985){++S.AxisLocked;++TotalAxisLocked;} if(bFloorSurface){++FloorTaggedHits;if(AxisDot>0.985)++FloorAxisLocked;} if(YawOffset==0.0f&&PitchOffset==0.0f) { const char* Name=Family==ESurfaceFamily::Strata?"strata": Family==ESurfaceFamily::Cave?"cave":"solid_overlay"; const FLayerField* Field=FindLayerField(ZH); FOwnerNoiseLineContext OwnerNoise8,OwnerNoise2; const float Owner8=Field?OwnerCaveRoughness(XH,YH,ZH,SurfaceSDF, *Field,CaveInfluence,OwnerNoise8,nullptr,8):0.0f; const float Owner2=Field?OwnerCaveRoughness(XH,YH,ZH,SurfaceSDF, *Field,CaveInfluence,OwnerNoise2,nullptr,2):0.0f; const float Relief=SurfaceRelief(static_cast(XH), static_cast(YH),static_cast(ZH), static_cast(Owner->Seed)); std::printf("[SurfaceRay] t=%.2f vox hit=(%.2f,%.2f,%.2f) family=%s floor_surface=%s cave_influence=%.3f floor_influence=%.3f passage_influence=%.3f surface_sdf=%.4f raw_sdf=%.4f detail=%.4f relief=%.4f owner8=%.4f owner2=%.4f axis_dot=%.4f\n", HitT,XH,YH,ZH,Name,bFloorSurface?"yes":"no",CaveInfluence, FloorInfluence,PassageInfluence,SurfaceSDF,Raw,Detail,Relief, Owner8,Owner2,AxisDot); } bFound=true;break; } PreviousT=T;PreviousD=D; } if(PrimaryHitT>=0.0) { constexpr double SheetStep=0.25; constexpr double MinimumAirGap=3.0; constexpr double MaximumSheetThickness=1.0; double T=PrimaryHitT+SheetStep; float Prev=Distance(OX+DX*T,OY+DY*T,OZ+DZ*T); bool bInSolid=Prev>=0.0f; double SolidStart=T,LastSolidExit=PrimaryHitT; bool bSeparatedFromPrevious=false; for(T+=SheetStep;T<=256.0;T+=SheetStep) { const float D=Distance(OX+DX*T,OY+DY*T,OZ+DZ*T); if(!bInSolid&&Prev<0.0f&&D>=0.0f) { SolidStart=T; bSeparatedFromPrevious=(T-LastSolidExit)>=MinimumAirGap; bInSolid=true; } else if(bInSolid&&Prev>=0.0f&&D<0.0f) { const double Thickness=T-SolidStart; if(bSeparatedFromPrevious&&Thickness(TotalNearZero)/TotalHits:1.0; const double AxisShare=TotalHits?static_cast(TotalAxisLocked)/TotalHits:1.0; const double FloorAxisShare=FloorTaggedHits?static_cast(FloorAxisLocked)/FloorTaggedHits:1.0; const double TransitionRMS=FamilyTransitionPairs ?std::sqrt(FamilyTransitionGradientSq/FamilyTransitionPairs):0.0; const double StableRMS=StableFamilyPairs ?std::sqrt(StableFamilyGradientSq/StableFamilyPairs):0.0; const double TransitionRatio=StableRMS>1.0e-6?TransitionRMS/StableRMS:1000000.0; double Sum=0.0,SumSq=0.0; for(const FFamilyStats& S:FamilyStats){Sum+=S.Sum;SumSq+=S.SumSq;} const double Mean=TotalHits?Sum/TotalHits:0.0; const double Variance=TotalHits?std::max(0.0,SumSq/TotalHits-Mean*Mean):0.0; const double SurfaceDetailRMS=std::sqrt(Variance); const double RelativeJoinStep=SurfaceDetailRMS>1.0e-6?TransitionRMS/SurfaceDetailRMS:1000000.0; const bool bDetailPass=TotalHits>=24&&std::sqrt(Variance)>=0.15&&NearZeroShare<0.50; constexpr double GridStairThreshold=0.40; const bool bJoinCoverage=FloorTaggedHits>=8 &&(FamilyTransitionPairs==0||(FamilyTransitionPairs>=8&&RelativeJoinStep=24&&AxisShare<0.60 &&bJoinCoverage; const bool bThinSheetPass=ThinDetachedSheets==0; std::printf("[FlatSurfaceAudit] rays=%llu detail_stddev=%.4f vox near_zero_share=%.1f%% threshold=50.0%% status=%s\n", static_cast(TotalHits),std::sqrt(Variance),NearZeroShare*100.0, bDetailPass?"PASS":"FAIL"); std::printf("[GridStairAudit] rays=%llu axis_locked_normals=%.1f%% threshold=60.0%% floor_tagged_hits=%llu floor_axis_locked=%.1f%% family_transition_pairs=%llu transition_rms=%.4f vox stable_rms=%.4f vox surface_detail_rms=%.4f vox relative_join_step=%.3f threshold=%.3f legacy_join_ratio=%.3f status=%s\n", static_cast(TotalHits),AxisShare*100.0, static_cast(FloorTaggedHits),FloorAxisShare*100.0, static_cast(FamilyTransitionPairs),TransitionRMS,StableRMS, SurfaceDetailRMS,RelativeJoinStep,GridStairThreshold,TransitionRatio,bGridPass?"PASS":"FAIL"); std::printf("[ThinSheetAudit] rays=%llu isolated_sheets=%llu thickness_lt=1.000 vox air_gap_ge=3.000 vox thinnest=%.3f vox status=%s\n", static_cast(TotalHits),static_cast(ThinDetachedSheets), ThinDetachedSheets?ThinnestDetachedSheet:0.0,bThinSheetPass?"PASS":"FAIL"); for(size_t I=0;I(S.Hits),M,V, static_cast(S.NearZero),static_cast(S.AxisLocked)); } const bool bFloorProfilePass=Stats.MaxUnmarkedFloorSlopeDelta<=0.75 &&Stats.MaxFloorProfileEndpointValueError<=0.01 &&Stats.MaxFloorProfileEndpointSlopeError<=0.001 &&Stats.FloorProfileEndpointStepControlDetected &&Stats.FloorProfileEndpointSlopeControlDetected; return bDetailPass&&bGridPass&&bThinSheetPass&&bFloorProfilePass; } private: bool NativeOnlyContainsColumn(float X,float Y) const { return std::isfinite(X)&&std::isfinite(Y) &&X>=NativeOnlyMinimum.X&&X<=NativeOnlyMaximum.X &&Y>=NativeOnlyMinimum.Y&&Y<=NativeOnlyMaximum.Y; } bool NativeOnlyContains(float X,float Y,float Z) const { return std::isfinite(X)&&std::isfinite(Y)&&std::isfinite(Z) &&X>=NativeOnlyMinimum.X&&X<=NativeOnlyMaximum.X &&Y>=NativeOnlyMinimum.Y&&Y<=NativeOnlyMaximum.Y &&Z>=NativeOnlyMinimum.Z&&Z<=NativeOnlyMaximum.Z; } static FClassificationThreadStats& ThreadClassificationStats() { static thread_local FClassificationThreadStats Stats; return Stats; } const VoxelForgeCore::WorldAssemblyStratumPlanV1* FindWorldAssemblyStratum( int32 StrateIndex,int32 ChunkZ) const { if(!WorldAssembly||StrateIndex<0)return nullptr; const auto It=std::find_if(WorldAssembly->plan.strates.begin(),WorldAssembly->plan.strates.end(), [StrateIndex,ChunkZ](const auto& Entry) {return Entry.strateIndex==StrateIndex&&ChunkZ<=Entry.topChunkZ&&ChunkZ>=Entry.bottomChunkZ;}); return It==WorldAssembly->plan.strates.end()?nullptr:&*It; } const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1* FindWorldAssemblyFloatingIslandStratum( int32 StrateIndex,int32 ChunkZ) const { if(!WorldAssembly||StrateIndex<0)return nullptr; const auto It=std::find_if(WorldAssembly->floatingIslands.begin(), WorldAssembly->floatingIslands.end(),[StrateIndex,ChunkZ](const auto& Entry) {return Entry.strateIndex==StrateIndex&&ChunkZ<=Entry.topChunkZ&&ChunkZ>=Entry.bottomChunkZ;}); return It==WorldAssembly->floatingIslands.end()?nullptr:&*It; } bool NativeSurfaceDensityAt(float X,float Y,float Z,float& OutDensity) const { if(!bUseNativeManifestFields||!WorldAssembly||!ResolvedLayout||!Owner||!Owner->Manager||!Owner->Generator ||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))return false; const float LayoutWidth=static_cast(GetResolvedLayoutChunkWidthVoxels()); if(!(LayoutWidth>0.0f))return false; const int32 ChunkZ=FMath::FloorToInt(Z/LayoutWidth); if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ)) !=ECaveGeneratorType::SurfaceWorld)return false; const int32 StrateIndex=Owner->Manager->GetStrateIndex( static_cast((static_cast(ChunkZ)+0.5) *ResolvedLayout->strateLayoutChunkSizeMicrometres/10000.0)); const auto* Plan=FindWorldAssemblyStratum(StrateIndex,ChunkZ); if(!Plan)return false; constexpr double Scale=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double MaxVoxel=static_cast(std::numeric_limits::max())/Scale; if(std::abs(static_cast(X))>MaxVoxel||std::abs(static_cast(Y))>MaxVoxel ||std::abs(static_cast(Z))>MaxVoxel)return false; const auto ToFixed=[](float Value) {return static_cast(std::llround(static_cast(Value) *VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));}; const VoxelForgeCore::Heightfield2D Block(WorldAssembly->recipe,*Plan); if(!Block.isValid())return false; OutDensity=static_cast(Block.point(ToFixed(X),ToFixed(Y),ToFixed(Z)).densitySubvoxels /Scale); return true; } bool NativeFloatingIslandDensityAt(float X,float Y,float Z,float& OutDensity) const { if(!bUseNativeManifestFields||!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator ||!ResolvedLayout||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z)) return false; // Convert Core query Z back to the owner's legacy chunk coordinate. // Convertir Z du Core vers la coordonnee de chunk legacy du proprietaire. const double LayoutChunkWidth=GetResolvedLayoutChunkWidthVoxels(); if(!(LayoutChunkWidth>0.0))return false; const int32 ChunkZ=FMath::FloorToInt(Z/static_cast(LayoutChunkWidth)); if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ)) !=ECaveGeneratorType::FloatingIslands)return false; const int32 StrateIndex=Owner->Manager->GetStrateIndex( static_cast((static_cast(ChunkZ)+0.5) *ResolvedLayout->strateLayoutChunkSizeMicrometres/10000.0)); const auto* Plan=FindWorldAssemblyFloatingIslandStratum(StrateIndex,ChunkZ); if(!Plan)return false; constexpr double Scale=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); constexpr double Maximum=1000000000000.0/Scale; if(std::abs(static_cast(X))>Maximum||std::abs(static_cast(Y))>Maximum ||std::abs(static_cast(Z))>Maximum)return false; const auto ToFixed=[](float Value) {return static_cast(std::llround(static_cast(Value) *VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));}; const VoxelForgeCore::FloatingIslandFieldV1 Field( Plan->manifest.recipe,Plan->manifest.plan); if(!Field.isValid())return false; std::int64_t Density=Field.densitySubvoxels(ToFixed(X),ToFixed(Y),ToFixed(Z)); const std::int64_t Bottom=Plan->manifest.recipe.layoutMinimumZSubvoxels; const std::int64_t Top=Plan->manifest.recipe.layoutMaximumZSubvoxels; const std::int64_t Seal=Plan->manifest.recipe.boundarySealSubvoxels; const std::int64_t Zq=ToFixed(Z); if(Plan->manifest.recipe.geometryVersion<3) { if(Zq>=Bottom&&Zq(1,Zq-Bottom)); if(Zq<=Top&&Zq>Top-Seal) Density=std::min(Density,-std::max(1,Top-Zq)); OutDensity=static_cast(static_cast(Density)/Scale); } else if(Plan->manifest.recipe.geometryVersion<4) { OutDensity=static_cast(static_cast(Density)/Scale); FResolvedStratumBounds Bounds; if(!MakeResolvedStratumBounds(static_cast(Bottom)/Scale, static_cast(Top)/Scale,static_cast(Seal)/Scale,Bounds)) return false; const FFloatingIslandParams Params=Owner->Manager->GetFloatingIslandParamsForChunk( FIntVector(0,0,ChunkZ)); float InternalDensity=-OutDensity; VF_ApplyUpperBoundarySeal(InternalDensity,Z,static_cast(Bounds.TopZ), static_cast(Bounds.SealThickness),Params.BaseDensity); OutDensity=-InternalDensity; } else OutDensity=static_cast(static_cast(Density)/Scale); return std::isfinite(OutDensity); } bool UsesLegacySurfaceField(float X,float Y,float Z) const { if(!Owner||!Owner->Manager||!Owner->Generator)return false; const double LayoutChunkWidth=GetResolvedLayoutChunkWidthVoxels(); if(!(LayoutChunkWidth>0.0))return false; const FIntVector ChunkCoord(0,0,FMath::FloorToInt(Z/static_cast(LayoutChunkWidth))); return Owner->Manager->GetGeneratorTypeForChunk(ChunkCoord) ==ECaveGeneratorType::SurfaceWorld; } float LegacySurfaceDensityAt(float X,float Y,float Z) const { // SurfaceWorld consumes biome overrides, terrain ops, water level and its sky cap in // the legacy stack. Reuse that immutable evaluator as an exact temporary bridge. // SurfaceWorld lit biomes, ops de terrain, niveau d'eau et plafond; reutiliser ce champ. const float density=ApplyResolvedStrateComposition(Owner->Generator->GetDensityAt(X,Y,Z),X,Y,Z); return ApplyEditBrushes(density,X,Y,Z); } float ApplyEditBrushes(float Density,double X,double Y,double Z) const { if(EditBrushes.empty()||XEditBoundsMax.X ||YEditBoundsMax.Y||ZEditBoundsMax.Z) return Density; const double P[3]={X,Y,Z}; for(const FEditBrush& Brush:EditBrushes) { double Distance=0.0; if(Brush.Shape==FEditBrush::EShape::Sphere) { const double DX=X-Brush.A[0],DY=Y-Brush.A[1],DZ=Z-Brush.A[2]; Distance=DeterministicSqrt(DX*DX+DY*DY+DZ*DZ)-Brush.Radius; } else if(Brush.Shape==FEditBrush::EShape::Capsule) { double AB[3]{},AP[3]{},LengthSq=0.0,Dot=0.0; for(int32 Axis=0;Axis<3;++Axis) {AB[Axis]=Brush.B[Axis]-Brush.A[Axis];AP[Axis]=P[Axis]-Brush.A[Axis];LengthSq+=AB[Axis]*AB[Axis];Dot+=AB[Axis]*AP[Axis];} const double T=LengthSq>1.0e-12?std::clamp(Dot/LengthSq,0.0,1.0):0.0; double D2=0.0; for(int32 Axis=0;Axis<3;++Axis) {const double D=AP[Axis]-T*AB[Axis];D2+=D*D;} Distance=DeterministicSqrt(D2)-Brush.Radius; } else { double OutsideSq=0.0,MaxQ=-std::numeric_limits::infinity(); for(int32 Axis=0;Axis<3;++Axis) { const double Q=std::abs(P[Axis]-Brush.A[Axis])-Brush.HalfExtent[Axis]; const double Outside=std::max(Q,0.0); OutsideSq+=Outside*Outside;MaxQ=std::max(MaxQ,Q); } Distance=DeterministicSqrt(OutsideSq)+std::min(MaxQ,0.0)-Brush.Rounding; } if(Brush.Operation==FEditBrush::EOperation::DensityOffset) { double Influence=0.0; if(Brush.Shape==FEditBrush::EShape::Sphere) { const double Radius=std::max(Brush.Radius,1.0e-12); const double T=std::clamp((Distance+Brush.Radius)/Radius,0.0,1.0); if(Distance+Brush.Radius>=Brush.Radius)continue; Influence=1.0-T*T*(3.0-2.0*T); } else { const double Band=std::max(static_cast(Brush.Falloff),0.01); if(Distance>=Band)continue; const double T=std::clamp(std::max(Distance,0.0)/Band,0.0,1.0); Influence=1.0-T*T*(3.0-2.0*T); } Density+=Brush.Strength*static_cast(Influence); continue; } if(Brush.Operation==FEditBrush::EOperation::Carve) Density=std::max(Density,static_cast(-Distance)); else Density=std::min(Density,static_cast(Distance)); } return Density; } struct FAxisBins { int32 MinA=0,MinB=0,CountA=0,CountB=0; std::vector> Items; }; struct FLayerField { int32 Lo=0,Hi=-1; int32 StrateIndex=INDEX_NONE; FStrateGenerationParams Params; }; enum class ESurfaceFamily : uint8 { Strata, Cave, SolidOverlay }; static bool NearInteger(float V,int32& Out) { const float R=std::round(V); if(std::abs(V-R)>0.0001f||R(MIN_int32)||R>static_cast(MAX_int32))return false; Out=static_cast(R);return true; } static double DeterministicSqrt(double Value) { if(!(Value>0.0))return 0.0; return _mm_cvtsd_f64(_mm_sqrt_sd(_mm_setzero_pd(),_mm_set_sd(Value))); } bool Contains(int32 X,int32 Y,int32 Z) const {return X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]&&Z>=Min[2]&&Z<=Max[2];} static int32 FloorDiv(int32 A,int32 B) {int32 Q=A/B,R=A%B;return Q-((R!=0&&R<0)?1:0);} static int64 RoundDiv(int64 N,int64 D) {return N>=0?(N+D/2)/D:-((-N+D/2)/D);} static float SmoothMinDistance(float A,float B,float Width) { if(Width<=0.0f)return std::min(A,B); const float H=std::max(Width-std::abs(A-B),0.0f)/Width; return std::min(A,B)-(H*H*H)*(Width/6.0f); } static float SmoothMaxDistance(float A,float B,float Width) {return -SmoothMinDistance(-A,-B,Width);} static float SmoothUnit(float V) { V=std::clamp(V,0.0f,1.0f); return V*V*(3.0f-2.0f*V); } static float DetailBandFade(float SignedDistance) { // Keep the larger candidate mask conservative around smooth family unions; // visible relief still fades to zero beyond its 4.125-voxel support radius. constexpr float Edge=DetailSupremum+0.125f; constexpr float Width=1.5f; return SmoothUnit((Edge-std::abs(SignedDistance))/Width); } static float DetailContrast(float CaveInfluence,float FloorInfluence) { const float Cave=std::clamp(CaveInfluence,0.0f,1.0f); const float Floor=std::min(Cave,std::clamp(FloorInfluence,0.0f,1.0f)); const float StrataWeight=1.0f-Cave; const float CaveWeight=Cave-Floor; const float FloorWeight=Floor; // The three influences are a partition of unity. Cave floors blend at the // cave family's amplitude instead of multiplying both family contributions. return 1.35f*StrataWeight+1.00f*CaveWeight+0.55f*FloorWeight; } static float BlendDetailFamilies(float Relief,float PassageBandRelief,float OwnerRelief, float CaveInfluence,float FloorInfluence,float PassageInfluence) { const float Cave=std::clamp(CaveInfluence,0.0f,1.0f); const float Floor=std::min(Cave,std::clamp(FloorInfluence,0.0f,1.0f)); const float Passage=std::clamp(PassageInfluence,0.0f,1.0f); const float NonPassageWeight=1.0f-Passage; const float StrataWeight=NonPassageWeight*(1.0f-Cave); const float CaveWeight=NonPassageWeight*(Cave-Floor); const float FloorWeight=NonPassageWeight*Floor; const float StrataRelief=1.35f*(Relief+0.5f*OwnerRelief); const float CaveRelief=1.00f*Relief; const float FloorRelief=0.55f*Relief; // Passage walls keep the broad coherent relief bands but omit the shortest // octave, which makes repeated rings on the authored shafts. const float PassageRelief=0.75f*PassageBandRelief; return StrataWeight*StrataRelief+CaveWeight*CaveRelief +FloorWeight*FloorRelief+Passage*PassageRelief; } static constexpr float DetailBandInnerRadius=1.5f; static float DetailBandFade(float SignedDistance,float Edge) { const float Distance=std::abs(SignedDistance); if(Distance<=DetailBandInnerRadius)return 1.0f; const float Width=Edge-DetailBandInnerRadius; return Width>0.0f?SmoothUnit((Edge-Distance)/Width):0.0f; } static float LocalDetailBandEdge(float MacroValue,float SurfaceSDF, float CaveInfluence,float FloorInfluence,const FLayerField* Field) { float OwnerBound=0.0f; if(Field) { const FStrateGenerationParams& Params=Field->Params; const float Depth=Params.SurfaceRoughness*2.0f; if(Depth>0.0f&&std::abs(SurfaceSDF)0.0f) { const float Fade=1.0f-std::abs(SurfaceSDF)/Depth; // Perlin is bounded by 2; the squared ridged fold by 2.25. This // uses the largest type bound and remains valid under domain warp. OwnerBound=3.15f*VOXEL_NOISE_SCALE*Params.SurfaceRoughness*Fade*Fade; } } const float ReliefBound=1.20f*std::abs(MacroValue) +(0.80f+0.25f*MacroValue)*(0.40f+0.60f); const float Contrast=DetailContrast(CaveInfluence,FloorInfluence); const float LocalSupremum=std::min(3.9f,(ReliefBound+0.5f*OwnerBound)*Contrast); return std::clamp(LocalSupremum+0.125f,DetailBandInnerRadius,DetailSupremum+0.125f); } float DetailBandFadeAt(double X,double Y,double Z,float SignedDistance,float SurfaceSDF, float CaveInfluence,float FloorInfluence) const { if(DetailVariant!=EDetailVariant::Coherent ||std::abs(SignedDistance)<=DetailBandInnerRadius) return DetailBandFade(SignedDistance); const float MacroValue=FStandaloneResearchTunnels::ValueNoise( static_cast(X),static_cast(Y),static_cast(Z),32, static_cast(Owner->Seed)^0xB5297A4Du); const FLayerField* Field=FindLayerField(Z); return DetailBandFade(SignedDistance,LocalDetailBandEdge(MacroValue,SurfaceSDF, CaveInfluence,FloorInfluence,Field)); } void BuildLayerBounds() { LayerRanges.clear();LayerFields.clear(); const auto& Layout=Owner->Manager->GetLayout(); int32 Top=MIN_int32,Bottom=MAX_int32; for(const FStrateSlot& Slot:Layout) { FResolvedStratumBounds Bounds; if(!GetResolvedStratumBounds(Slot,Bounds))continue; const int32 ResolvedBottom=FMath::FloorToInt(static_cast(Bounds.BottomZ)); const int32 ResolvedTop=FMath::CeilToInt(static_cast(Bounds.TopZ)); Top=std::max(Top,ResolvedTop);Bottom=std::min(Bottom,ResolvedBottom); if(!Slot.Definition)continue; const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2; FLayerField Field; Field.Lo=ResolvedBottom; Field.Hi=ResolvedTop; Field.StrateIndex=Slot.StrateIndex; Field.Params=Owner->Manager->GetGenerationParams(FIntVector(0,0,MidChunkZ)); LayerFields.push_back(std::move(Field)); } if(Top>=Bottom) { LayerRanges.push_back({Bottom,Top}); } } static std::array V3(const FVector& V){return {V.X,V.Y,V.Z};} static double Dot(const std::array& A,const std::array& B) {return A[0]*B[0]+A[1]*B[1]+A[2]*B[2];} static std::array Sub(const FVector& A,const FVector& B) {return {A.X-B.X,A.Y-B.Y,A.Z-B.Z};} static std::array Normalize(std::array V) { const double L=std::sqrt(std::max(1.0e-24,Dot(V,V))); for(double& C:V)C/=L;return V; } static std::array Cross(const std::array& A,const std::array& B) {return {A[1]*B[2]-A[2]*B[1],A[2]*B[0]-A[0]*B[2],A[0]*B[1]-A[1]*B[0]};} static VF_FieldPrimitive MakeSharedFieldPrimitive(const FPrimitive& P) { VF_FieldPrimitive Out{}; if(P.bCapsule)Out.flags|=VF_FIELD_PRIMITIVE_CAPSULE; if(P.bSmoothCapsuleRadius)Out.flags|=VF_FIELD_PRIMITIVE_SMOOTH_CAPSULE; if(P.bRoundedBox)Out.flags|=VF_FIELD_PRIMITIVE_ROUNDED_BOX; if(P.bHasFloorClip)Out.flags|=VF_FIELD_PRIMITIVE_HAS_FLOOR; if(P.bCurvedFloorClip)Out.flags|=VF_FIELD_PRIMITIVE_CURVED_FLOOR; if(P.bCarve)Out.flags|=VF_FIELD_PRIMITIVE_CARVE; if(P.bCritical)Out.flags|=VF_FIELD_PRIMITIVE_CRITICAL; if(P.bPassageSurface)Out.flags|=VF_FIELD_PRIMITIVE_PASSAGE; for(int32 A=0;A<3;++A) { Out.minVoxel[A]=P.Min[A];Out.maxVoxel[A]=P.Max[A]; Out.center[A]=P.CenterSub[A]/16.0; Out.capsuleStart[A]=P.CapsuleStartWorld[A]; Out.capsuleDelta[A]=P.CapsuleAxisDelta[A]; Out.roundedHalfExtent[A]=P.RoundedHalfExtent[A]; } for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J) Out.q[I*3+J]=static_cast(P.Q[I][J])/QScale; Out.minRadius=P.MinRadius; Out.capsuleAxisLengthSquared=P.CapsuleAxisLengthSquared; Out.capsuleAxisLength=P.CapsuleAxisLength; Out.capsuleStartRadius=P.CapsuleStartRadius; Out.capsuleEndRadius=P.CapsuleEndRadius; Out.profileStartT=P.CapsuleProfileStartT; Out.profileEndT=P.CapsuleProfileEndT; Out.profileStartRadius=P.CapsuleProfileStartRadius; Out.profileEndRadius=P.CapsuleProfileEndRadius; Out.radiusSlopeStart=P.CapsuleRadiusSlopeStart; Out.radiusSlopeEnd=P.CapsuleRadiusSlopeEnd; Out.rounding=P.Rounding; Out.blendRadius=P.BlendRadius; Out.floorBlendRadius=P.FloorBlendRadius; Out.floorCenterZ=P.FloorCenterZ; Out.floorSlopeX=P.FloorSlopeX; Out.floorSlopeY=P.FloorSlopeY; Out.floorCurveOriginX=P.FloorCurveOriginX; Out.floorCurveOriginY=P.FloorCurveOriginY; Out.floorCurveDeltaX=P.FloorCurveDeltaX; Out.floorCurveDeltaY=P.FloorCurveDeltaY; Out.floorCurveLength=P.FloorCurveLength; Out.floorCurveStartZ=P.FloorCurveStartZ; Out.floorCurveEndZ=P.FloorCurveEndZ; Out.floorCurveTangentStart=P.FloorCurveTangentStart; Out.floorCurveTangentEnd=P.FloorCurveTangentEnd; Out.floorCurveStartBlend=P.FloorCurveStartBlend; Out.floorCurveEndBlend=P.FloorCurveEndBlend; return Out; } void RebuildSharedFieldPrimitives() { InvalidateFarFieldGpuInput(); SharedFieldPrimitives.clear();SharedFieldPrimitives.reserve(Primitives.size()); for(const FPrimitive& P:Primitives) SharedFieldPrimitives.push_back(MakeSharedFieldPrimitive(P)); } void AddEllipsoid(const FVector& Center,const std::array& Radii, const std::array& Axis0={1,0,0}, const std::array& Axis1={0,1,0}, const std::array& Axis2={0,0,1},bool bCarve=true) { FPrimitive P;const auto C=V3(Center); for(int32 A=0;A<3;++A)P.CenterSub[A]=static_cast(std::llround(C[A]*SubPerVoxel)); const std::array,3> Axes={Axis0,Axis1,Axis2}; int32 RSub[3]; for(int32 K=0;K<3;++K)RSub[K]=std::max(16,static_cast(std::llround(std::max(1.0,Radii[K])*SubPerVoxel))); for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J) { int64 Sum=0; for(int32 K=0;K<3;++K) { const int64 RI=static_cast(std::llround(Axes[K][I]*32768.0)); const int64 RJ=static_cast(std::llround(Axes[K][J]*32768.0)); const int64 Den=static_cast(32768)*32768*RSub[K]*RSub[K]; Sum+=RoundDiv(RI*RJ*QScale,Den); } P.Q[I][J]=Sum; } for(int32 A=0;A<3;++A) { double E=0.0;for(int32 K=0;K<3;++K)E+=std::abs(Axes[K][A])*Radii[K]; P.Min[A]=static_cast(std::floor(C[A]-E))-1; P.Max[A]=static_cast(std::ceil(C[A]+E))+1; } P.MinRadius=static_cast(std::min({Radii[0],Radii[1],Radii[2]})); P.BlendRadius=ActiveBlendRadius; P.bCarve=bCarve;Primitives.push_back(P); } void AddRoundedBox(const FVector& Center,const std::array& HalfExtent, double Rounding,bool bCarve=true) { const std::array Outer={HalfExtent[0]+Rounding,HalfExtent[1]+Rounding,HalfExtent[2]+Rounding}; AddEllipsoid(Center,Outer,{1,0,0},{0,1,0},{0,0,1},bCarve); FPrimitive& P=Primitives.back();P.bRoundedBox=true;P.Rounding=Rounding; for(int32 A=0;A<3;++A)P.RoundedHalfExtent[A]=HalfExtent[A]; } static void ApplyFloorClip(FPrimitive& P,double FloorCenterZ,double SlopeX=0.0,double SlopeY=0.0, double FloorBlendRadius=0.0) { P.bHasFloorClip=true;P.FloorCenterZ=FloorCenterZ-0.5; P.FloorSlopeX=SlopeX;P.FloorSlopeY=SlopeY;P.FloorBlendRadius=FloorBlendRadius; } void AddCapsule(const FVector& A,const FVector& B,float Radius,bool bCarve=true,bool bCritical=false, bool bFloorClip=false,double FloorA=0.0,double FloorB=0.0) { AddLegacyEllipsoidCapsule(A,B,Radius,bCarve,bCritical,bFloorClip,FloorA,FloorB); } static FVector ApproximateUnwarp(const FVector& Target,const FStrateGenerationParams& Params, uint32 Seed) { FVector WorldPoint=Target; const float ZScale=Params.VerticalScale>0.0f?Params.VerticalScale:1.0f; if(ZScale!=1.0f)WorldPoint.Z*=ZScale; if(Params.CaveWarpStrength<=0.0f)return WorldPoint; for(int32 Iteration=0;Iteration<8;++Iteration) { const FVector Mapped=VoxelCaveMorphology::ApplyCaveWarp(WorldPoint,Params,Seed); const FVector Error=Target-Mapped; if(!Error.ContainsNaN()&&VoxelMath::IsFinite(Error.X) &&VoxelMath::IsFinite(Error.Y)&&VoxelMath::IsFinite(Error.Z)) { WorldPoint.X+=Error.X;WorldPoint.Y+=Error.Y;WorldPoint.Z+=Error.Z*ZScale; } } return WorldPoint; } void AddLegacyEllipsoidCapsule(const FVector& A,const FVector& B,float Radius,bool bCarve, bool bCritical,bool bFloorClip,double FloorA,double FloorB) { const auto D=Sub(B,A);const double Length=std::sqrt(Dot(D,D)); const double PreviousBlendRadius=ActiveBlendRadius;ActiveBlendRadius=0.0; if(!(Length>1.0e-4)) { const size_t Before=Primitives.size(); AddEllipsoid(A,{Radius,Radius,Radius},{1,0,0},{0,1,0},{0,0,1},bCarve); if(bFloorClip)ApplyFloorClip(Primitives.back(),std::min(FloorA,FloorB),0.0,0.0, 0.0); if(bCritical)for(size_t I=Before;I Ref=std::abs(U[2])<0.90?std::array{0,0,1}:std::array{0,1,0}; const auto V=Normalize(Cross(U,Ref));const auto W=Normalize(Cross(U,V)); const double SegmentStep=std::max(8.0,static_cast(Radius)*2.0); const int32 Segments=std::max(1,static_cast(std::ceil(Length/SegmentStep))); for(int32 I=0;I(I)/Segments,T1=static_cast(I+1)/Segments; const FVector P0(A.X+D[0]*T0,A.Y+D[1]*T0,A.Z+D[2]*T0); const FVector P1(A.X+D[0]*T1,A.Y+D[1]*T1,A.Z+D[2]*T1); const FVector C((P0.X+P1.X)*0.5,(P0.Y+P1.Y)*0.5,(P0.Z+P1.Z)*0.5); const double Half=Length*(T1-T0)*0.5+Radius; const size_t Before=Primitives.size();AddEllipsoid(C,{Half,Radius,Radius},U,V,W,bCarve); if(bFloorClip) { const double Mid=(T0+T1)*0.5; const double Floor=(1.0-Mid)*FloorA+Mid*FloorB; const double DX=P1.X-P0.X,DY=P1.Y-P0.Y,HorizontalSq=DX*DX+DY*DY; const double FloorStep=(FloorB-FloorA)*(T1-T0); if(HorizontalSq>1.0e-6) ApplyFloorClip(Primitives.back(),Floor,FloorStep*DX/HorizontalSq, FloorStep*DY/HorizontalSq,0.0); else ApplyFloorClip(Primitives.back(),std::min(FloorA,FloorB),0.0,0.0, 0.0); } if(bCritical)for(size_t J=Before;J1.0e-4)) { AddEllipsoid(A,{RadiusA,RadiusA,RadiusA},{1,0,0},{0,1,0},{0,0,1},bCarve); Primitives.back().bCritical=bCritical; if(bFloorClip)ApplyFloorClip(Primitives.back(),std::min(FloorA,FloorB),0.0,0.0, ActiveFloorBlendRadius); return; } ExtendStart=std::clamp(ExtendStart,0.0,Length*0.4); ExtendEnd=std::clamp(ExtendEnd,0.0,Length*0.4); const double RadiusSlope=(static_cast(RadiusB)-RadiusA)/Length; const float ExtendedRadiusA=static_cast(std::max(1.0,RadiusA-RadiusSlope*ExtendStart)); const float ExtendedRadiusB=static_cast(std::max(1.0,RadiusB+RadiusSlope*ExtendEnd)); const double Start[3]={A.X-D[0]*(ExtendStart/Length),A.Y-D[1]*(ExtendStart/Length),A.Z-D[2]*(ExtendStart/Length)}; const double End[3]={B.X+D[0]*(ExtendEnd/Length),B.Y+D[1]*(ExtendEnd/Length),B.Z+D[2]*(ExtendEnd/Length)}; FPrimitive P; P.bCapsule=true;P.bCarve=bCarve;P.bCritical=bCritical; P.CapsuleStartRadius=ExtendedRadiusA;P.CapsuleEndRadius=ExtendedRadiusB; P.MinRadius=std::min(ExtendedRadiusA,ExtendedRadiusB);P.BlendRadius=ActiveBlendRadius; const double R=std::max(ExtendedRadiusA,ExtendedRadiusB); double AxisLengthSquared=0.0; for(int32 Axis=0;Axis<3;++Axis) { P.CapsuleStartSub[Axis]=static_cast(std::llround(Start[Axis]*SubPerVoxel)); P.CapsuleEndSub[Axis]=static_cast(std::llround(End[Axis]*SubPerVoxel)); P.CapsuleStartWorld[Axis]=P.CapsuleStartSub[Axis]/static_cast(SubPerVoxel); const double CapsuleEndWorld=P.CapsuleEndSub[Axis]/static_cast(SubPerVoxel); P.CapsuleAxisDelta[Axis]=CapsuleEndWorld-P.CapsuleStartWorld[Axis]; AxisLengthSquared+=P.CapsuleAxisDelta[Axis]*P.CapsuleAxisDelta[Axis]; P.CenterSub[Axis]=static_cast(std::llround((Start[Axis]+End[Axis])*0.5*SubPerVoxel)); P.Min[Axis]=static_cast(std::floor(std::min(Start[Axis],End[Axis])-R))-1; P.Max[Axis]=static_cast(std::ceil(std::max(Start[Axis],End[Axis])+R))+1; } P.CapsuleAxisLengthSquared=AxisLengthSquared; P.CapsuleAxisLength=std::sqrt(std::max(0.0,AxisLengthSquared)); if(bFloorClip) { const double FloorSlope=(FloorB-FloorA)/Length; const double ExtendedFloorA=FloorA-FloorSlope*ExtendStart; const double ExtendedFloorB=FloorB+FloorSlope*ExtendEnd; const double ExtendedD[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]}; const double HorizontalSq=ExtendedD[0]*ExtendedD[0]+ExtendedD[1]*ExtendedD[1]; if(HorizontalSq>1.0e-6) ApplyFloorClip(P,(ExtendedFloorA+ExtendedFloorB)*0.5, (ExtendedFloorB-ExtendedFloorA)*ExtendedD[0]/HorizontalSq, (ExtendedFloorB-ExtendedFloorA)*ExtendedD[1]/HorizontalSq,ActiveFloorBlendRadius); else ApplyFloorClip(P,std::min(ExtendedFloorA,ExtendedFloorB),0.0,0.0,ActiveFloorBlendRadius); } Primitives.push_back(P); } static double ContinuousJoinExtension(float RadiusA,float RadiusB,double SegmentLength) { return std::min(std::max(0.0,SegmentLength*0.35), JoinBlendRadius+0.5*static_cast(std::max(RadiusA,RadiusB))); } static float CapsuleRadiusAt(const FPrimitive& P,const FVector& Point) { if(!P.bCapsule)return P.MinRadius; const double Start[3]={P.CapsuleStartSub[0]/static_cast(SubPerVoxel), P.CapsuleStartSub[1]/static_cast(SubPerVoxel), P.CapsuleStartSub[2]/static_cast(SubPerVoxel)}; const double End[3]={P.CapsuleEndSub[0]/static_cast(SubPerVoxel), P.CapsuleEndSub[1]/static_cast(SubPerVoxel), P.CapsuleEndSub[2]/static_cast(SubPerVoxel)}; const double V[3]={Point.X-Start[0],Point.Y-Start[1],Point.Z-Start[2]}; const double D[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]}; const double D2=D[0]*D[0]+D[1]*D[1]+D[2]*D[2]; const double T=D2>1.0e-12?(V[0]*D[0]+V[1]*D[1]+V[2]*D[2])/D2:0.0; return CapsuleRadiusAtT(P,T,std::sqrt(std::max(0.0,D2))); } static void FitCapsuleRadiiAtEndpoints(FPrimitive& P,const FVector& PointA,float RadiusA, const FVector& PointB,float RadiusB) { const double Start[3]={P.CapsuleStartSub[0]/static_cast(SubPerVoxel), P.CapsuleStartSub[1]/static_cast(SubPerVoxel), P.CapsuleStartSub[2]/static_cast(SubPerVoxel)}; const double End[3]={P.CapsuleEndSub[0]/static_cast(SubPerVoxel), P.CapsuleEndSub[1]/static_cast(SubPerVoxel), P.CapsuleEndSub[2]/static_cast(SubPerVoxel)}; const double D[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]}; const double D2=D[0]*D[0]+D[1]*D[1]+D[2]*D[2]; if(D2<=1.0e-12)return; const auto Parameter=[&](const FVector& Point) { const double V[3]={Point.X-Start[0],Point.Y-Start[1],Point.Z-Start[2]}; return (V[0]*D[0]+V[1]*D[1]+V[2]*D[2])/D2; }; const double TA=Parameter(PointA),TB=Parameter(PointB); if(std::abs(TB-TA)<=1.0e-9)return; const double Slope=(RadiusB-RadiusA)/(TB-TA); P.CapsuleStartRadius=static_cast(RadiusA-Slope*TA); P.CapsuleEndRadius=static_cast(P.CapsuleStartRadius+Slope); P.MinRadius=std::min(P.CapsuleStartRadius,P.CapsuleEndRadius); } static void SetCapsuleRadiusProfile(FPrimitive& P,const FVector& PointA,float RadiusA, double SlopeA,const FVector& PointB,float RadiusB,double SlopeB) { const double Start[3]={P.CapsuleStartSub[0]/static_cast(SubPerVoxel), P.CapsuleStartSub[1]/static_cast(SubPerVoxel), P.CapsuleStartSub[2]/static_cast(SubPerVoxel)}; const double End[3]={P.CapsuleEndSub[0]/static_cast(SubPerVoxel), P.CapsuleEndSub[1]/static_cast(SubPerVoxel), P.CapsuleEndSub[2]/static_cast(SubPerVoxel)}; const double D[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]}; const double D2=D[0]*D[0]+D[1]*D[1]+D[2]*D[2]; if(D2<=1.0e-12)return; const auto Parameter=[&](const FVector& Point) { const double V[3]={Point.X-Start[0],Point.Y-Start[1],Point.Z-Start[2]}; return std::clamp((V[0]*D[0]+V[1]*D[1]+V[2]*D[2])/D2,0.0,1.0); }; P.CapsuleProfileStartT=Parameter(PointA); P.CapsuleProfileEndT=Parameter(PointB); P.CapsuleProfileStartRadius=RadiusA; P.CapsuleProfileEndRadius=RadiusB; P.CapsuleRadiusSlopeStart=SlopeA; P.CapsuleRadiusSlopeEnd=SlopeB; P.bSmoothCapsuleRadius=P.CapsuleProfileEndT-P.CapsuleProfileStartT>1.0e-6; } static float SmoothedControlRadius(const TArray& Radii,int32 Index,float Fallback, float Minimum) { const int32 Count=Radii.Num(); const float Current=std::max(Minimum,Radii.IsValidIndex(Index)?Radii[Index]:Fallback); if(Index<=0||Index+1>=Count)return Current; const auto At=[&](int32 I) {return std::max(Minimum,Radii[std::clamp(I,0,Count-1)]);}; return (At(Index-4)+8.0f*At(Index-3)+28.0f*At(Index-2)+56.0f*At(Index-1) +70.0f*Current+56.0f*At(Index+1)+28.0f*At(Index+2)+8.0f*At(Index+3) +At(Index+4))*(1.0f/256.0f); } static double ControlRadiusSlopeAt(const TArray& Points,const TArray& Radii, int32 Index,float Fallback,float Minimum) { if(Points.Num()<2||Index<0||Index>=Points.Num())return 0.0; const auto RadiusAt=[&](int32 I) {return static_cast(SmoothedControlRadius(Radii,I,Fallback,Minimum));}; const auto LengthAt=[&](int32 I) { const FVector& A=Points[I];const FVector& B=Points[I+1]; const double DX=B.X-A.X,DY=B.Y-A.Y,DZ=B.Z-A.Z; return std::sqrt(DX*DX+DY*DY+DZ*DZ); }; if(Index==0) { const double Length=LengthAt(0); return Length>1.0e-9?(RadiusAt(1)-RadiusAt(0))/Length:0.0; } if(Index+1==Points.Num()) { const double Length=LengthAt(Index-1); return Length>1.0e-9?(RadiusAt(Index)-RadiusAt(Index-1))/Length:0.0; } const double PrevLength=LengthAt(Index-1),NextLength=LengthAt(Index); if(PrevLength<=1.0e-9||NextLength<=1.0e-9)return 0.0; const double PrevSlope=(RadiusAt(Index)-RadiusAt(Index-1))/PrevLength; const double NextSlope=(RadiusAt(Index+1)-RadiusAt(Index))/NextLength; if(PrevSlope*NextSlope<=0.0)return 0.0; const double W1=2.0*NextLength+PrevLength,W2=NextLength+2.0*PrevLength; const double Harmonic=(W1+W2)/(W1/PrevSlope+W2/NextSlope); const double Limit=3.0*std::min(std::abs(PrevSlope),std::abs(NextSlope)); return std::copysign(std::min(std::abs(Harmonic),Limit),Harmonic); } static double EvaluateFloorProfile(double A,double B,double TangentA,double TangentB, double Length,double StartBlend,double EndBlend,double S) { const double Secant=(B-A)/Length; if(S<0.0)return A+TangentA*S; if(S>Length)return B+TangentB*(S-Length); double Result=std::lerp(A,B,S/Length); const auto Correction=[](double DeltaSlope,double Span,double T) { if(!(Span>0.0)||T<0.0||T>=1.0)return 0.0; return DeltaSlope*Span*(T-2.0*T*T+T*T*T); }; if(SLength)return TangentB; const double Secant=(B-A)/Length; double Result=Secant; const auto CorrectionSlope=[](double DeltaSlope,double T) {return DeltaSlope*(1.0-4.0*T+3.0*T*T);}; if(SValueThreshold; Stats.FloorProfileEndpointSlopeControlDetected= std::abs((startSlope+SlopeStep)-TangentA)>SlopeThreshold; std::printf("[FloorProfileBlendNegativeControls] endpoint_step=%.3f_vox tangent_step=%.3f_vox_per_m detected=%s/%s status=%s\n", Step,SlopeStep, Stats.FloorProfileEndpointStepControlDetected?"true":"false", Stats.FloorProfileEndpointSlopeControlDetected?"true":"false", Stats.FloorProfileEndpointStepControlDetected &&Stats.FloorProfileEndpointSlopeControlDetected?"PASS":"FAIL"); } static double FloorSurfaceRelief(double X,double Y,uint32 Seed) { // A shared XY field keeps independently authored floor clips from ending on // separate flat planes. Its peak displacement stays under 1.2 voxels. const float Broad=FStandaloneResearchTunnels::ValueNoise( static_cast(X),static_cast(Y),0.0f,32,Seed^0x7F4A7C15u); const float Mid=FStandaloneResearchTunnels::ValueNoise( static_cast(X),static_cast(Y),0.0f,16,Seed^0x94D049BBu); return 0.80*Broad+0.35*Mid; } static std::vector BuildFloorTangents(const TArray& Points, const std::vector& Floor,const std::vector& MarkedSegments) { const size_t Count=Floor.size();std::vector Tangent(Count,0.0); if(Count<2||Count!=static_cast(Points.Num())||MarkedSegments.size()+1!=Count)return Tangent; std::vector Length(Count-1,0.0),Slope(Count-1,0.0); for(size_t I=0;I+1(I)];const FVector& B=Points[static_cast(I+1)]; Length[I]=std::hypot(B.X-A.X,B.Y-A.Y); if(Length[I]>1.0e-6)Slope[I]=(Floor[I+1]-Floor[I])/Length[I]; } for(size_t I=0;I0&&Length[I-1]>1.0e-6; const bool bHasNext=I+11.0e-6; if(I==0){Tangent[I]=MarkedSegments[0]?0.0:Slope[0];continue;} if(I+1==Count){Tangent[I]=MarkedSegments[I-1]?0.0:Slope[I-1];continue;} const bool bPrevMarked=MarkedSegments[I-1]!=0,bNextMarked=MarkedSegments[I]!=0; if(bPrevMarked||bNextMarked) { if(!bPrevMarked&&bHasPrev)Tangent[I]=Slope[I-1]; else if(!bNextMarked&&bHasNext)Tangent[I]=Slope[I]; continue; } if(!bHasPrev||!bHasNext||Slope[I-1]*Slope[I]<=0.0)continue; const double W0=2.0*Length[I]+Length[I-1],W1=Length[I]+2.0*Length[I-1]; const double Denominator=W0/Slope[I-1]+W1/Slope[I]; if(std::abs(Denominator)>1.0e-12) Tangent[I]=(W0+W1)/Denominator; } return Tangent; } static void SetCurvedFloorClip(FPrimitive& P,const FVector& A,const FVector& B, double FloorA,double FloorB,double TangentA,double TangentB) { if(!P.bHasFloorClip)return; const double DX=B.X-A.X,DY=B.Y-A.Y,Length=std::hypot(DX,DY); if(Length<=1.0e-6)return; P.bCurvedFloorClip=true;P.FloorCurveOriginX=A.X;P.FloorCurveOriginY=A.Y; P.FloorCurveDeltaX=DX;P.FloorCurveDeltaY=DY;P.FloorCurveLength=Length; P.FloorCurveStartZ=FloorA;P.FloorCurveEndZ=FloorB; P.FloorCurveTangentStart=TangentA;P.FloorCurveTangentEnd=TangentB; P.FloorCurveStartBlend=std::min(8.0,Length*0.35); P.FloorCurveEndBlend=std::min(8.0,Length*0.35); for(int32 I=1;I<64;++I) { const double T=I/64.0; const double Curve=EvaluateFloorProfile(FloorA,FloorB,TangentA,TangentB, Length,P.FloorCurveStartBlend,P.FloorCurveEndBlend,Length*T); const double Linear=std::lerp(FloorA,FloorB,T); P.FloorCurveMaxDeviation=std::max(P.FloorCurveMaxDeviation,std::abs(Curve-Linear)); } P.FloorCurveEndpointValueError=std::max( std::abs(EvaluateFloorProfile(FloorA,FloorB,TangentA,TangentB,Length, P.FloorCurveStartBlend,P.FloorCurveEndBlend,0.0)-FloorA), std::abs(EvaluateFloorProfile(FloorA,FloorB,TangentA,TangentB,Length, P.FloorCurveStartBlend,P.FloorCurveEndBlend,Length)-FloorB)); P.FloorCurveEndpointSlopeError=std::max( std::abs(EvaluateFloorProfileSlope(FloorA,FloorB,TangentA,TangentB,Length, P.FloorCurveStartBlend,P.FloorCurveEndBlend,0.0)-TangentA), std::abs(EvaluateFloorProfileSlope(FloorA,FloorB,TangentA,TangentB,Length, P.FloorCurveStartBlend,P.FloorCurveEndBlend,Length)-TangentB)); } double FloorAt(const FPrimitive& P,double X,double Y) const { double Base=0.0; if(P.bCurvedFloorClip) { const double T=((X-P.FloorCurveOriginX)*P.FloorCurveDeltaX +(Y-P.FloorCurveOriginY)*P.FloorCurveDeltaY) /(P.FloorCurveLength*P.FloorCurveLength); Base=EvaluateFloorProfile(P.FloorCurveStartZ,P.FloorCurveEndZ, P.FloorCurveTangentStart,P.FloorCurveTangentEnd,P.FloorCurveLength, P.FloorCurveStartBlend,P.FloorCurveEndBlend,T*P.FloorCurveLength); } else Base=P.FloorCenterZ+P.FloorSlopeX*(X-P.CenterSub[0]/16.0) +P.FloorSlopeY*(Y-P.CenterSub[1]/16.0); return Base; } double FloorSlopeAtEndpoint(const FPrimitive& P,bool bEnd) const { const double DX=P.bCurvedFloorClip?P.FloorCurveDeltaX :P.CapsuleEndSub[0]-P.CapsuleStartSub[0]; const double DY=P.bCurvedFloorClip?P.FloorCurveDeltaY :P.CapsuleEndSub[1]-P.CapsuleStartSub[1]; const double Length=P.bCurvedFloorClip?P.FloorCurveLength :std::hypot(DX,DY)/SubPerVoxel; if(Length<=1.0e-9)return 0.0; const double OriginX=P.bCurvedFloorClip?P.FloorCurveOriginX :P.CapsuleStartSub[0]/static_cast(SubPerVoxel); const double OriginY=P.bCurvedFloorClip?P.FloorCurveOriginY :P.CapsuleStartSub[1]/static_cast(SubPerVoxel); const double X=OriginX+(bEnd?DX/(P.bCurvedFloorClip?1.0:SubPerVoxel):0.0); const double Y=OriginY+(bEnd?DY/(P.bCurvedFloorClip?1.0:SubPerVoxel):0.0); constexpr double Epsilon=0.125; const double UX=DX/(Length*(P.bCurvedFloorClip?1.0:SubPerVoxel)); const double UY=DY/(Length*(P.bCurvedFloorClip?1.0:SubPerVoxel)); return (FloorAt(P,X+UX*Epsilon,Y+UY*Epsilon) -FloorAt(P,X-UX*Epsilon,Y-UY*Epsilon))/(2.0*Epsilon); } void BuildOwnerPrimitives() { const auto& Layout=Owner->Manager->GetLayout(); const float MinX=static_cast(Min[0]-256),MinY=static_cast(Min[1]-256); const float MaxX=static_cast(Max[0]+256),MaxY=static_cast(Max[1]+256); for(const FStrateSlot& Slot:Layout) { if(!Slot.Definition)continue; const FIntVector MidChunk(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2); const FStrateGenerationParams Params=Owner->Manager->GetGenerationParams(MidChunk); ActiveBlendRadius=0.0; ActiveFloorBlendRadius=0.0; FChunkSDFCache Cache; VoxelCaveMorphology::BuildChunkCache(Cache,MinX,MinY,MaxX,MaxY,Params, static_cast(Owner->Seed), Slot.StrateIndex,&Slot.Definition->TerrainOperations,ERoomGraphBuildSite::Unknown); for(const FCachedRoom& R:Cache.Rooms) { ++Stats.RoomCount; const size_t RoomPrimitiveStart=Primitives.size(); const FVector RoomCenter=ApproximateUnwarp(R.Center,Params, static_cast(Owner->Seed)); if(R.ShapeType==2) AddCapsule(ApproximateUnwarp(R.ShapeA,Params,static_cast(Owner->Seed)), ApproximateUnwarp(R.ShapeB,Params,static_cast(Owner->Seed)), std::max(1.0f,R.ShapeR),true); else if(R.ShapeType==1) { AddRoundedBox(RoomCenter,{std::abs(R.ShapeA.X),std::abs(R.ShapeA.Y),std::abs(R.ShapeA.Z)}, std::max(0.0f,R.ShapeR),true); } else AddEllipsoid(RoomCenter,{std::abs(R.ShapeA.X),std::abs(R.ShapeA.Y),std::abs(R.ShapeA.Z)}, {1,0,0},{0,1,0},{0,0,1},true); if(R.FloorCutZ>-FLT_MAX*0.5f) { const double Relief=std::max(0.0f,R.FloorReliefStrength); const double FloorZ=ApproximateUnwarp( FVector(R.Center.X,R.Center.Y,R.FloorCutZ-Relief),Params, static_cast(Owner->Seed)).Z; for(size_t I=RoomPrimitiveStart;I(Cache.RoomFloorJoins.Num()); const double RoomBlendRadius=ActiveBlendRadius; ActiveBlendRadius=JoinBlendRadius; ActiveFloorBlendRadius=FloorClipBlendRadius; for(const FCachedTunnel& T:Cache.Tunnels) { ++Stats.TunnelCount; const bool bHasWorldChain=T.WorldControlPoints.Num()>1 &&T.WorldControlRadii.Num()==T.WorldControlPoints.Num(); const TArray& Points=bHasWorldChain?T.WorldControlPoints:T.ControlPoints; const TArray& Radii=bHasWorldChain?T.WorldControlRadii:T.ControlRadii; const TArray& FloorProfiles=bHasWorldChain &&T.WorldFloorProfiles.Num()==Points.Num()-1?T.WorldFloorProfiles:T.FloorProfiles; std::vector RawFloor(static_cast(Points.Num())); std::vector MarkedFloorSegments(Points.Num()>0?static_cast(Points.Num()-1):0,0); for(int32 I=0;I(I)]=static_cast(Points[I].Z -SmoothedControlRadius(Radii,I,T.RadiusA,2.0f)); if(FloorProfiles.Num()==Points.Num()-1) { for(int32 I=0;I+1(I)]=0.5f*(FloorProfiles[I-1].EndFloorZ+Profile.StartFloorZ); RawFloor[static_cast(I+1)]=Profile.EndFloorZ; MarkedFloorSegments[static_cast(I)]=Profile.bLedgeTransition?1:0; } } for(int32 I=0;I(I-1)]; const bool bNextUnmarked=I+1>=Points.Num() ||!MarkedFloorSegments[static_cast(I)]; if(bPreviousUnmarked&&bNextUnmarked) RawFloor[static_cast(I)]+=static_cast(FloorSurfaceRelief( Points[I].X,Points[I].Y,static_cast(Owner->Seed))); } const std::vector FloorTangents=BuildFloorTangents( Points,RawFloor,MarkedFloorSegments); for(int32 Join=1;Join+11.0e-6&&BL>1.0e-6) { const double Dot=(A.X*B.X+A.Y*B.Y+A.Z*B.Z)/(AL*BL); const double Angle=std::acos(std::clamp(Dot,-1.0,1.0))*57.29577951308232; if(Angle>Stats.MaxTunnelBendDegrees) { Stats.MaxTunnelBendDegrees=Angle;Stats.MaxTunnelBendVoxels=Points[Join]; const double DX=A.X/AL+B.X/BL,DY=A.Y/AL+B.Y/BL,DZ=A.Z/AL+B.Z/BL; const double DL=std::sqrt(DX*DX+DY*DY+DZ*DZ); if(DL>1.0e-6)Stats.MaxTunnelBendDirection=FVector(DX/DL,DY/DL,DZ/DL); } } const float RPrev=std::max(2.0f,Radii.IsValidIndex(Join-1)?Radii[Join-1]:T.RadiusA); const float RHere=std::max(2.0f,Radii.IsValidIndex(Join)?Radii[Join]:T.RadiusA); const float RNext=std::max(2.0f,Radii.IsValidIndex(Join+1)?Radii[Join+1]:T.RadiusB); Stats.LegacySegmentRadiusStep=std::max(Stats.LegacySegmentRadiusStep, static_cast(std::abs(std::max(RPrev,RHere)-std::max(RHere,RNext)))); if(FloorProfiles.IsValidIndex(Join-1)&&FloorProfiles.IsValidIndex(Join)) { Stats.MaxCapsuleJoinFloorDelta=std::max(Stats.MaxCapsuleJoinFloorDelta, static_cast(std::abs(FloorProfiles[Join-1].EndFloorZ -FloorProfiles[Join].StartFloorZ))); const FVector PrevD=Points[Join]-Points[Join-1]; const FVector NextD=Points[Join+1]-Points[Join]; const double PrevXY=std::sqrt(PrevD.X*PrevD.X+PrevD.Y*PrevD.Y); const double NextXY=std::sqrt(NextD.X*NextD.X+NextD.Y*NextD.Y); const double RawPrevSlope=PrevXY>1.0e-6 ?(FloorProfiles[Join-1].EndFloorZ-FloorProfiles[Join-1].StartFloorZ)/PrevXY:0.0; const double RawNextSlope=NextXY>1.0e-6 ?(FloorProfiles[Join].EndFloorZ-FloorProfiles[Join].StartFloorZ)/NextXY:0.0; const double RawSlopeDelta=std::abs(RawPrevSlope-RawNextSlope); if(FloorProfiles[Join-1].bLedgeTransition||FloorProfiles[Join].bLedgeTransition) { ++Stats.MarkedFloorSlopeJoins; Stats.MaxMarkedFloorSlopeDelta=std::max(Stats.MaxMarkedFloorSlopeDelta,RawSlopeDelta); } else { Stats.MaxRawUnmarkedFloorSlopeDelta=std::max(Stats.MaxRawUnmarkedFloorSlopeDelta,RawSlopeDelta); } } ++Stats.CapsuleJoinCount; } if(Points.Num()>1) { const size_t SegmentPrimitiveStart=Primitives.size(); for(int32 I=0;I+1(std::abs(R1-R0))); const FVector Segment=Points[I+1]-Points[I]; const double SegmentLength=std::sqrt(Segment.X*Segment.X+Segment.Y*Segment.Y+Segment.Z*Segment.Z); const double Extension=ContinuousJoinExtension(R0,R1,SegmentLength); const float ZA=RawFloor[static_cast(I)]; const float ZB=RawFloor[static_cast(I+1)]; AddTaperedCapsule(Points[I],Points[I+1],R0,R1,true,false,true,ZA,ZB, I>0?Extension:0.0,I+2(I)]) { SetCurvedFloorClip(Primitives.back(),Points[I],Points[I+1],ZA,ZB, FloorTangents[static_cast(I)],FloorTangents[static_cast(I+1)]); ++Stats.CurvedFloorSegments; Stats.MaxFloorCurveDeviation=std::max(Stats.MaxFloorCurveDeviation, Primitives.back().FloorCurveMaxDeviation); Stats.MaxFloorProfileEndpointValueError=std::max( Stats.MaxFloorProfileEndpointValueError, Primitives.back().FloorCurveEndpointValueError); Stats.MaxFloorProfileEndpointSlopeError=std::max( Stats.MaxFloorProfileEndpointSlopeError, Primitives.back().FloorCurveEndpointSlopeError); } FitCapsuleRadiiAtEndpoints(Primitives.back(),Points[I],R0,Points[I+1],R1); SetCapsuleRadiusProfile(Primitives.back(),Points[I],R0,RadiusSlope0, Points[I+1],R1,RadiusSlope1); } for(int32 Join=1;Join+1(Join-1)]; const FPrimitive& Next=Primitives[SegmentPrimitiveStart+static_cast(Join)]; const float PreviousEnd=CapsuleRadiusAt(Previous,Points[Join]); const float NextStart=CapsuleRadiusAt(Next,Points[Join]); Stats.MaxCapsuleJoinRadiusDelta=std::max(Stats.MaxCapsuleJoinRadiusDelta, static_cast(std::abs(PreviousEnd-NextStart))); Stats.MaxCapsuleJoinRadiusSlopeDelta=std::max( Stats.MaxCapsuleJoinRadiusSlopeDelta, std::abs(Previous.CapsuleRadiusSlopeEnd-Next.CapsuleRadiusSlopeStart)); if(!MarkedFloorSegments[static_cast(Join-1)] &&!MarkedFloorSegments[static_cast(Join)]) { const double TangentDelta=Previous.bCurvedFloorClip&&Next.bCurvedFloorClip ?std::abs(Previous.FloorCurveTangentEnd-Next.FloorCurveTangentStart) :std::abs(FloorSlopeAtEndpoint(Previous,true)-FloorSlopeAtEndpoint(Next,false)); const double DirectionalSlopeChange=std::abs(FloorSlopeAtEndpoint(Previous,true) -FloorSlopeAtEndpoint(Next,false)); Stats.MaxCapsuleJoinFloorSlopeDelta=std::max( Stats.MaxCapsuleJoinFloorSlopeDelta,TangentDelta); Stats.MaxUnmarkedFloorSlopeDelta=std::max( Stats.MaxUnmarkedFloorSlopeDelta,TangentDelta); Stats.MaxUnmarkedFloorDirectionSlopeDelta=std::max( Stats.MaxUnmarkedFloorDirectionSlopeDelta,DirectionalSlopeChange); } } } else AddCapsule(T.EndpointA,T.EndpointB,std::max(T.RadiusA,T.RadiusB),true); } ActiveBlendRadius=RoomBlendRadius; ActiveFloorBlendRadius=0.0; for(const FCachedPit& P:Cache.Pits) AddCapsule(FVector(P.CenterX,P.CenterY,P.TopZ-P.Depth), FVector(P.CenterX,P.CenterY,P.TopZ),std::max(2.0f,P.Radius),true); for(const FCachedChimney& C:Cache.Chimneys) AddCapsule(FVector(C.CenterX,C.CenterY,C.BottomZ), FVector(C.CenterX,C.CenterY,C.BottomZ+C.Height),std::max(2.0f,C.Radius),true); for(const FCachedColumn& C:Cache.Columns) { FVector Center(C.CenterX,C.CenterY,(LayerRanges[0].Lo+LayerRanges[0].Hi)*0.5); const double HalfZ=std::max(8.0,(LayerRanges[0].Hi-LayerRanges[0].Lo)*0.48); AddEllipsoid(Center,{std::max(2.0f,C.Radius),std::max(2.0f,C.Radius),HalfZ}, {1,0,0},{0,1,0},{0,0,1},false); } // Reproduce the owner's finite, top-anchored origin room in every strate. // Reproduire dans chaque strate la salle d'origine finie et ancrée en haut. AddOriginLandingRoom(FieldForStrate(Slot.StrateIndex)); } const double PassageJoinBlend=ActiveBlendRadius;ActiveBlendRadius=JoinBlendRadius; ActiveFloorBlendRadius=FloorClipBlendRadius; for(const FVoxelPassage& P:Owner->Manager->GetPassages()) { ++Stats.PassageCount; const TArray& Points=P.ControlPoints; const TArray& Radii=P.ControlRadii; const size_t PassagePrimitiveStart=Primitives.size(); std::vector RawFloor(static_cast(Points.Num())); std::vector MarkedFloorSegments(Points.Num()>0?static_cast(Points.Num()-1):0,0); for(int32 I=0;I(I)]=static_cast(Points[I].Z -SmoothedControlRadius(Radii,I,P.Radius,6.0f)); if(P.NativeFloorProfileZ.Num()==Points.Num()) for(int32 I=0;I(I)]=P.NativeFloorProfileZ[I]; for(int32 I=0;I(I)]+=static_cast(FloorSurfaceRelief( Points[I].X,Points[I].Y,static_cast(Owner->Seed))); const std::vector FloorTangents=BuildFloorTangents( Points,RawFloor,MarkedFloorSegments); for(int32 Join=1;Join+1(std::abs(std::max(RPrev,RHere)-std::max(RHere,RNext)))); if(P.NativeFloorProfileZ.IsValidIndex(Join-1)&&P.NativeFloorProfileZ.IsValidIndex(Join)) { const FVector PrevD=Points[Join]-Points[Join-1]; const FVector NextD=Points[Join+1]-Points[Join]; const double PrevXY=std::sqrt(PrevD.X*PrevD.X+PrevD.Y*PrevD.Y); const double NextXY=std::sqrt(NextD.X*NextD.X+NextD.Y*NextD.Y); const double PrevSlope=PrevXY>1.0e-6 ?(P.NativeFloorProfileZ[Join]-P.NativeFloorProfileZ[Join-1])/PrevXY:0.0; const double NextSlope=NextXY>1.0e-6 ?(P.NativeFloorProfileZ[Join+1]-P.NativeFloorProfileZ[Join])/NextXY:0.0; const double RawSlopeDelta=std::abs(PrevSlope-NextSlope); Stats.MaxRawUnmarkedFloorSlopeDelta=std::max(Stats.MaxRawUnmarkedFloorSlopeDelta,RawSlopeDelta); } ++Stats.CapsuleJoinCount; } if(Points.Num()>1) { for(int32 I=0;I+1(std::abs(RadiusB-RadiusA))); const FVector Segment=Points[I+1]-Points[I]; const double SegmentLength=std::sqrt(Segment.X*Segment.X+Segment.Y*Segment.Y+Segment.Z*Segment.Z); const float ZA=RawFloor[static_cast(I)]; const float ZB=RawFloor[static_cast(I+1)]; const double Extension=ContinuousJoinExtension(RadiusA,RadiusB,SegmentLength); const double CoreExtension=ContinuousJoinExtension(std::min(2.5f,RadiusA*0.45f), std::min(2.5f,RadiusB*0.45f),SegmentLength); const double ExtendStart=I>0?Extension:0.0,ExtendEnd=I+20?CoreExtension:0.0,CoreExtendEnd=I+2(I)],FloorTangents[static_cast(I+1)]); ++Stats.CurvedFloorSegments; Stats.MaxFloorCurveDeviation=std::max(Stats.MaxFloorCurveDeviation, Primitives.back().FloorCurveMaxDeviation); Stats.MaxFloorProfileEndpointValueError=std::max( Stats.MaxFloorProfileEndpointValueError, Primitives.back().FloorCurveEndpointValueError); Stats.MaxFloorProfileEndpointSlopeError=std::max( Stats.MaxFloorProfileEndpointSlopeError, Primitives.back().FloorCurveEndpointSlopeError); FitCapsuleRadiiAtEndpoints(Primitives.back(),Points[I],RadiusA,Points[I+1],RadiusB); SetCapsuleRadiusProfile(Primitives.back(),Points[I],RadiusA,RadiusSlopeA, Points[I+1],RadiusB,RadiusSlopeB); const float CoreRadiusA=std::min(2.5f,RadiusA*0.45f); const float CoreRadiusB=std::min(2.5f,RadiusB*0.45f); const double CoreSlopeA=CoreRadiusA<2.5f-1.0e-4?RadiusSlopeA*0.45:0.0; const double CoreSlopeB=CoreRadiusB<2.5f-1.0e-4?RadiusSlopeB*0.45:0.0; // Keep a narrow, un-clipped air core through every authored control segment. // The larger outer tube owns the D-section floor; its interpolated floor curve // can bow above the path on sharp drops, so clipping this safety bore would // close the passage precisely where Cascading/Worm need the continuity repair. // Garder un petit noyau d'air sans coupe de plancher dans chaque segment. // Le tube large porte le plancher en D; sa courbe peut depasser l'axe dans les // chutes brusques et fermer la route si ce noyau herite de la meme coupe. AddTaperedCapsule(Points[I],Points[I+1],CoreRadiusA, CoreRadiusB,true,true,false,ZA,ZB, CoreExtendStart,CoreExtendEnd); FitCapsuleRadiiAtEndpoints(Primitives.back(),Points[I],CoreRadiusA, Points[I+1],CoreRadiusB); SetCapsuleRadiusProfile(Primitives.back(),Points[I],CoreRadiusA,CoreSlopeA, Points[I+1],CoreRadiusB,CoreSlopeB); } for(int32 Join=1;Join+1(Join-1)*2; const size_t NextBase=PassagePrimitiveStart+static_cast(Join)*2; for(size_t Tube=0;Tube<2;++Tube) { const FPrimitive& Previous=Primitives[PreviousBase+Tube]; const FPrimitive& Next=Primitives[NextBase+Tube]; const float PreviousEnd=CapsuleRadiusAt(Previous,Points[Join]); const float NextStart=CapsuleRadiusAt(Next,Points[Join]); Stats.MaxCapsuleJoinRadiusDelta=std::max(Stats.MaxCapsuleJoinRadiusDelta, static_cast(std::abs(PreviousEnd-NextStart))); Stats.MaxCapsuleJoinRadiusSlopeDelta=std::max( Stats.MaxCapsuleJoinRadiusSlopeDelta, std::abs(Previous.CapsuleRadiusSlopeEnd-Next.CapsuleRadiusSlopeStart)); if(Tube==0) { const double TangentDelta=Previous.bCurvedFloorClip&&Next.bCurvedFloorClip ?std::abs(Previous.FloorCurveTangentEnd-Next.FloorCurveTangentStart) :std::abs(FloorSlopeAtEndpoint(Previous,true)-FloorSlopeAtEndpoint(Next,false)); const double DirectionalSlopeChange=std::abs(FloorSlopeAtEndpoint(Previous,true) -FloorSlopeAtEndpoint(Next,false)); Stats.MaxCapsuleJoinFloorSlopeDelta=std::max( Stats.MaxCapsuleJoinFloorSlopeDelta,TangentDelta); Stats.MaxUnmarkedFloorSlopeDelta=std::max( Stats.MaxUnmarkedFloorSlopeDelta,TangentDelta); Stats.MaxUnmarkedFloorDirectionSlopeDelta=std::max( Stats.MaxUnmarkedFloorDirectionSlopeDelta,DirectionalSlopeChange); } } } } else { AddCapsule(P.UpperPoint,P.LowerPoint,std::max(6.0f,P.Radius),true,true); Primitives.back().bPassageSurface=true; } AddLanding(P.UpperLanding,true); AddLanding(P.LowerLanding,true); } ActiveBlendRadius=PassageJoinBlend;ActiveFloorBlendRadius=0.0; } const FLayerField* FieldForStrate(int32 StrateIndex) const { for(const FLayerField& Field:LayerFields) if(Field.StrateIndex==StrateIndex)return &Field; return nullptr; } void AddOriginLandingRoom(const FLayerField* Field) { if(Field==nullptr||Owner==nullptr||Owner->Settings==nullptr)return; const double Radius=Owner->Settings->OriginSpineRadius; if(!(Radius>0.0)||!std::isfinite(Radius))return; // These constants mirror VoxelPassageGeometry in legacy voxel units. // Ces constantes reprennent VoxelPassageGeometry en voxels historiques. constexpr double MinimumHalfWidth=12.0; constexpr double MinimumRoomHeight=24.0; constexpr double PlayerHeight=14.08; constexpr double Headroom=8.0; constexpr double FloorThickness=6.0; constexpr double SealSafetyMargin=8.0; constexpr double RoomRounding=3.0; constexpr double ShaftFloorClearance=8.0; const double SafeRadius=std::max(std::abs(Radius),2.0); const double HalfWidth=std::max({MinimumHalfWidth,SafeRadius+4.0, SafeRadius+ShaftFloorClearance}); const double DesiredHeight=std::max({MinimumRoomHeight,PlayerHeight+Headroom, 2.0*SafeRadius+4.0}); const double Seal=std::max(0.0,static_cast(Field->Params.BoundarySealThickness)); const double InnerBottom=static_cast(Field->Lo)+Seal; const double InnerTop=static_cast(Field->Hi)-Seal; const double MinimumFloor=InnerBottom+FloorThickness+SealSafetyMargin; const double MaximumFloor=InnerTop-DesiredHeight-SealSafetyMargin; if(!(InnerTop>InnerBottom)||MaximumFloor0.0f)||!(L.CeilingZ>L.FloorZ))return; const double HalfZ=std::max(2.0,(L.CeilingZ-L.FloorZ)*0.5); const FVector Center(L.StandingPoint.X,L.StandingPoint.Y,(L.CeilingZ+L.FloorZ)*0.5); const double Width=std::max(6.0f,L.HalfWidth); AddRoundedBox(Center,{Width,Width,HalfZ},2.0,true); if(bCritical) { const size_t Before=Primitives.size(); AddEllipsoid(Center,{2.5,2.5,HalfZ},{1,0,0},{0,1,0},{0,0,1},true); for(size_t I=Before;I(Owner->Seed); auto Unit=[](uint32 H){return static_cast(FStandaloneResearchTunnels::Unit(H));}; for(int32 CY=CY0;CY<=CY1;++CY)for(int32 CX=CX0;CX<=CX1;++CX) for(int32 Axis=0;Axis<2;++Axis) { const uint32 H=FStandaloneResearchTunnels::EdgeHash(CX,CY,Axis,Seed); if(Unit(H^0x68E31DA4u)>0.56)continue; const int32 NX=CX+(Axis==0?1:0),NY=CY+(Axis==1?1:0); auto Cell=[&](int32 X,int32 Y,double& OX,double& OY) { const uint32 CH=FStandaloneResearchTunnels::EdgeHash(X,Y,3,Seed); OX=(X+0.5)*Pitch+(Unit(CH^0xA511E9B3u)-0.5)*Pitch*0.18; OY=(Y+0.5)*Pitch+(Unit(CH^0x63D83595u)-0.5)*Pitch*0.18; }; double AX,AY,BX,BY;Cell(CX,CY,AX,AY);Cell(NX,NY,BX,BY); const double DX=BX-AX,DY=BY-AY,L2=DX*DX+DY*DY,L=std::sqrt(std::max(1.0e-8,L2)); const double NXrm=-DY/L,NYrm=DX/L; const double BA=(Unit(H^0xB5297A4Du)*2.0-1.0)*Pitch*0.16; const double BB=(Unit(H^0x1B56C4E9u)*2.0-1.0)*Pitch*0.16; const std::array P0={AX,AY,FStandaloneResearchTunnels::WorldCenterZVoxels}; const std::array C1={AX+DX/3.0+NXrm*BA,AY+DY/3.0+NYrm*BA,-80.0}; const std::array C2={AX+DX*2.0/3.0+NXrm*BB,AY+DY*2.0/3.0+NYrm*BB,-80.0}; const std::array P3={BX,BY,FStandaloneResearchTunnels::WorldCenterZVoxels}; const uint32 PhaseHash=H^0xA511E9B3u; const double Phase=Unit(PhaseHash)*6.2831853071795864769; auto Bezier=[&](double T) { const double O=1.0-T; std::array P{}; for(int32 A=0;A<3;++A) P[A]=O*O*O*P0[A]+3*O*O*T*C1[A]+3*O*T*T*C2[A]+T*T*T*P3[A]; const double R=RadiusBase*(0.82+0.36*Unit(H^0x63D83595u)) *(1.0+0.22*VoxelMath::DetSin(static_cast(6.2831853071795864769*T+Phase))); const double VW=4.0*VoxelMath::DetSin(static_cast(3.141592653589793*T)) *VoxelMath::DetSin(static_cast(6.2831853071795864769*T+Phase)); P[2]+=VW; return std::pair{P,R}; }; constexpr int32 Segments=12; auto Last=Bezier(0.0); for(int32 I=1;I<=Segments;++I) { const auto Next=Bezier(static_cast(I)/Segments); const FVector A(Last.first[0],Last.first[1],Last.first[2]); const FVector B(Next.first[0],Next.first[1],Next.first[2]); const FVector Segment=B-A; const double SegmentLength=std::sqrt(Segment.X*Segment.X+Segment.Y*Segment.Y+Segment.Z*Segment.Z); const double Extension=ContinuousJoinExtension(static_cast(Last.second), static_cast(Next.second),SegmentLength); AddTaperedCapsule(A,B,static_cast(Last.second),static_cast(Next.second), true,false,false,0.0,0.0,I>1?Extension:0.0,I(Primitives.size()*BinsPerPrimitive,1u)); if(!bBuildAxisBins) { CriticalAirBins.reserve(Primitives.size()/16+1); } if(bBuildAxisBins)for(int32 Axis=0;Axis<3;++Axis) { FAxisBins& B=AxisBins[Axis]; const int32 A=(Axis+1)%3,C=(Axis+2)%3; B.MinA=FloorDiv(Min[A],LineBinSize);B.MinB=FloorDiv(Min[C],LineBinSize); const int32 MaxA=FloorDiv(Max[A],LineBinSize),MaxB=FloorDiv(Max[C],LineBinSize); B.CountA=MaxA-B.MinA+1;B.CountB=MaxB-B.MinB+1; B.Items.resize(static_cast(B.CountA)*B.CountB); for(uint32 Id=0;IdA1||B0>B1)continue; for(int32 IB=B0;IB<=B1;++IB)for(int32 IA=A0;IA<=A1;++IA) B.Items[static_cast(IB-B.MinB)*B.CountA+(IA-B.MinA)].push_back(Id); } } for(uint32 Id=0;Id(X1-X0+1)*(Y1-Y0+1)*(Z1-Z0+1); if(BoundingBinCount>100000) { // Long diagonal passages have a huge AABB even though their capsule is narrow. // Rasterize the centerline with a conservative bin halo instead of dropping the // primitive from analytic queries outside the packed near region. // Les longues galeries diagonales ont une grande AABB mais un tube fin. // Echantillonner l'axe avec une marge prudente garde les requetes analytiques. if(!P.bCapsule)continue; const double BinWidth=static_cast(BinResolution); const double AxisStep=std::max(1.0,BinWidth*0.5); const int32 AxisSamples=std::max(1,static_cast(std::ceil( P.CapsuleAxisLength/AxisStep))); const double CapsuleRadius=std::max(P.CapsuleStartRadius,P.CapsuleEndRadius); const int32 Halo=std::clamp(static_cast(std::ceil( (CapsuleRadius+DetailBand)/BinWidth))+1,1,8); std::vector PrimitiveBins; const int64 HaloWidth=2*Halo+1; const uint64 Reserve=std::min( static_cast(AxisSamples+1)*HaloWidth*HaloWidth*HaloWidth, 1000000ull); PrimitiveBins.reserve(static_cast(Reserve)); for(int32 Sample=0;Sample<=AxisSamples;++Sample) { const double T=static_cast(Sample)/AxisSamples; const int32 CX=static_cast(std::floor(P.CapsuleStartWorld[0] +P.CapsuleAxisDelta[0]*T)); const int32 CY=static_cast(std::floor(P.CapsuleStartWorld[1] +P.CapsuleAxisDelta[1]*T)); const int32 CZ=static_cast(std::floor(P.CapsuleStartWorld[2] +P.CapsuleAxisDelta[2]*T)); const int32 BX=FloorDiv(CX,BinResolution),BY=FloorDiv(CY,BinResolution); const int32 BZ=FloorDiv(CZ,BinResolution); for(int32 DZ=-Halo;DZ<=Halo;++DZ) for(int32 DY=-Halo;DY<=Halo;++DY) for(int32 DX=-Halo;DX<=Halo;++DX) PrimitiveBins.push_back({BX+DX,BY+DY,BZ+DZ}); } std::sort(PrimitiveBins.begin(),PrimitiveBins.end(),[](const FBinKey& A,const FBinKey& B) { if(A.X!=B.X)return A.X Parents; std::unordered_map ByCell; const auto& Layout=Owner->Manager->GetLayout(); const uint32 Seed=static_cast(Owner->Seed); const auto Key=[](int32 GX,int32 GY,int32 Strate) {return (static_cast(static_cast(GX+524288)&0xFFFFFu)<<44) |(static_cast(static_cast(GY+524288)&0xFFFFFu)<<24) |(static_cast(static_cast(Strate)&0xFFFFu)<<8);}; auto Hash=[](uint32 X) {X^=X>>16;X*=0x7FEB352Du;X^=X>>15;X*=0x846CA68Bu;X^=X>>16;return X;}; auto Unit=[&](uint32 H){return static_cast(Hash(H)>>8)*(1.0/16777216.0);}; for(const FStrateSlot& Slot:Layout) { const double LayoutChunkWidth=GetResolvedLayoutChunkWidthVoxels(); const double ZBottom=static_cast(Slot.BottomChunkZ)*LayoutChunkWidth; const double ZTop=static_cast(Slot.TopChunkZ+1)*LayoutChunkWidth; for(int32 GY=GY0;GY<=GY1;++GY)for(int32 GX=GX0;GX<=GX1;++GX) { uint32 H=Hash(Seed^static_cast(GX)*0x9E3779B9u ^static_cast(GY)*0x85EBCA6Bu ^static_cast(Slot.StrateIndex)*0xC2B2AE35u); const double X=GX*static_cast(ParentPitch)+ParentPitch*0.5 +(Unit(H^0xA511E9B3u)-0.5)*192.0; const double Y=GY*static_cast(ParentPitch)+ParentPitch*0.5 +(Unit(H^0x63D83595u)-0.5)*192.0; const double Rx=300.0+Unit(H^0xB5297A4Du)*100.0; const double Ry=280.0+Unit(H^0x1B56C4E9u)*120.0; const double Rz=100.0+Unit(H^0x68E31DA4u)*60.0; if(X+Rx>=Min[0]&&X-Rx<=Max[0]&&Y+Ry>=Min[1]&&Y-Ry<=Max[1])continue; const double Z=ZBottom+(ZTop-ZBottom)*(0.35+0.30*Unit(H^0x27D4EB2Fu)); FParent P{GX,GY,Slot.StrateIndex,FVector(X,Y,Z),Rx,Ry,Rz}; const size_t Index=Parents.size();Parents.push_back(P); ByCell.emplace(Key(GX,GY,Slot.StrateIndex),Index); const std::array Axis0={1,0,0},Axis1={0,1,0},Axis2={0,0,1}; AddEllipsoid(P.Center,{P.Rx,P.Ry,P.Rz},Axis0,Axis1,Axis2,true); } } for(const FParent& P:Parents) { for(const auto [DX,DY]:std::array,2>{{{1,0},{0,1}}}) { const auto It=ByCell.find(Key(P.GX+DX,P.GY+DY,P.Strate)); if(It==ByCell.end())continue; const FVector A(P.Center.X,P.Center.Y,P.Center.Z); const FVector B=Parents[It->second].Center; const int32 MinX=static_cast(std::floor(std::min(A.X,B.X)-64.0)); const int32 MaxX=static_cast(std::ceil(std::max(A.X,B.X)+64.0)); const int32 MinY=static_cast(std::floor(std::min(A.Y,B.Y)-64.0)); const int32 MaxY=static_cast(std::ceil(std::max(A.Y,B.Y)+64.0)); if(MaxX>=Min[0]&&MinX<=Max[0]&&MaxY>=Min[1]&&MinY<=Max[1])continue; AddTaperedCapsule(A,B,64.0f,64.0f,true); Primitives.back().BlendRadius=0.0; FarConnectorSegments.push_back({A,B,64.0f}); } } BuildFarConnectorSpatialBins(); if(!Parents.empty()) std::printf("[FarParents] pitch=%d rooms=%zu links=%llu clip=(%d,%d)..(%d,%d)\n", ParentPitch,Parents.size(),static_cast(Primitives.size()), ClipMin[0],ClipMin[1],ClipMax[0],ClipMax[1]); } void BuildFarConnectorSpatialBins() { constexpr int32 BinSize=2048; FarConnectorSpatialBins.clear(); FarConnectorSpatialBins.reserve(FarConnectorSegments.size()); for(uint32 Index=0;Index(std::floor( (std::min(Connector.Start.X,Connector.End.X)-Connector.Radius)/BinSize)); const int32 MaxX=static_cast(std::floor( (std::max(Connector.Start.X,Connector.End.X)+Connector.Radius)/BinSize)); const int32 MinY=static_cast(std::floor( (std::min(Connector.Start.Y,Connector.End.Y)-Connector.Radius)/BinSize)); const int32 MaxY=static_cast(std::floor( (std::max(Connector.Start.Y,Connector.End.Y)+Connector.Radius)/BinSize)); const int32 MinZ=static_cast(std::floor( (std::min(Connector.Start.Z,Connector.End.Z)-Connector.Radius)/BinSize)); const int32 MaxZ=static_cast(std::floor( (std::max(Connector.Start.Z,Connector.End.Z)+Connector.Radius)/BinSize)); for(int32 Z=MinZ;Z<=MaxZ;++Z)for(int32 Y=MinY;Y<=MaxY;++Y) for(int32 X=MinX;X<=MaxX;++X) FarConnectorSpatialBins[{X,Y,Z}].push_back(Index); } } const std::vector& GetLineCandidates(int32 Axis,int32 A,int32 B) const { static const std::vector Empty; const FAxisBins& G=AxisBins[Axis]; const int32 IA=FloorDiv(A,LineBinSize)-G.MinA,IB=FloorDiv(B,LineBinSize)-G.MinB; if(IA<0||IB<0||IA>=G.CountA||IB>=G.CountB)return Empty; return G.Items[static_cast(IB)*G.CountA+IA]; } FBinKey PointBin(double X,double Y,double Z) const {return {FloorDiv(static_cast(std::floor(X)),ActiveSpatialBinSize),FloorDiv(static_cast(std::floor(Y)),ActiveSpatialBinSize),FloorDiv(static_cast(std::floor(Z)),ActiveSpatialBinSize)};} bool ShapeContains(const FPrimitive& P,int32 X,int32 Y,int32 Z) const { bool bInside=false; if(P.bCapsule) { const double Position[3]={static_cast(X),static_cast(Y),static_cast(Z)}; double V[3],D[3];double D2=0.0,VD=0.0,V2=0.0; for(int32 A=0;A<3;++A) { const double Start=P.CapsuleStartSub[A]/static_cast(SubPerVoxel); const double End=P.CapsuleEndSub[A]/static_cast(SubPerVoxel); V[A]=Position[A]-Start;D[A]=End-Start; D2+=D[A]*D[A];VD+=V[A]*D[A];V2+=V[A]*V[A]; } const double R0=P.CapsuleStartRadius,R1=P.CapsuleEndRadius,DR=R1-R0; const double QA=D2-DR*DR,QB=-2.0*(VD+R0*DR),QC=V2-R0*R0; double Minimum=std::min(QC,QA+QB+QC); if(QA>1.0e-12) { const double T=std::clamp(-QB/(2.0*QA),0.0,1.0); Minimum=std::min(Minimum,(QA*T+QB)*T+QC); } bInside=Minimum<=0.0; } else if(P.bRoundedBox) { const double D[3]={std::abs(X-P.CenterSub[0]/16.0)-P.RoundedHalfExtent[0], std::abs(Y-P.CenterSub[1]/16.0)-P.RoundedHalfExtent[1], std::abs(Z-P.CenterSub[2]/16.0)-P.RoundedHalfExtent[2]}; const double Ox=std::max(D[0],0.0),Oy=std::max(D[1],0.0),Oz=std::max(D[2],0.0); const double Outside=std::sqrt(Ox*Ox+Oy*Oy+Oz*Oz); const double Inside=std::min(std::max({D[0],D[1],D[2]}),0.0); bInside=Outside+Inside-P.Rounding<=0.0; } else { const int32 World[3]={X,Y,Z};int64 D[3]; for(int32 A=0;A<3;++A)D[A]=static_cast(World[A])*SubPerVoxel-P.CenterSub[A]; int64 Q=0; for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J)Q+=P.Q[I][J]*D[I]*D[J]; bInside=Q<=static_cast(QScale); } if(!bInside)return false; if(P.bHasFloorClip) { const double Floor=FloorAt(P,X,Y); if(P.FloorBlendRadius<=0.0)return Z>=Floor; return SmoothMaxDistance(ShapeDistance(P,X,Y,Z),static_cast(Floor-Z), static_cast(P.FloorBlendRadius))<=0.0f; } return true; } static float CapsuleRadiusAtT(const FPrimitive& P,double T,double AxisLength) { T=std::clamp(T,0.0,1.0); if(!P.bSmoothCapsuleRadius) return static_cast(P.CapsuleStartRadius +(P.CapsuleEndRadius-P.CapsuleStartRadius)*T); const double T0=P.CapsuleProfileStartT,T1=P.CapsuleProfileEndT; if(T(P.CapsuleProfileStartRadius +P.CapsuleRadiusSlopeStart*AxisLength*(T-T0)); if(T>T1)return static_cast(P.CapsuleProfileEndRadius +P.CapsuleRadiusSlopeEnd*AxisLength*(T-T1)); const double SpanT=std::max(1.0e-9,T1-T0); const double U=std::clamp((T-T0)/SpanT,0.0,1.0),U2=U*U,U3=U2*U; const double SpanDistance=AxisLength*SpanT; const double H00=2.0*U3-3.0*U2+1.0; const double H10=U3-2.0*U2+U; const double H01=-2.0*U3+3.0*U2; const double H11=U3-U2; return static_cast(H00*P.CapsuleProfileStartRadius +H10*SpanDistance*P.CapsuleRadiusSlopeStart +H01*P.CapsuleProfileEndRadius +H11*SpanDistance*P.CapsuleRadiusSlopeEnd); } float ShapeDistance(const FPrimitive& P,double X,double Y,double Z) const { if(P.bCapsule) { const double Position[3]={X,Y,Z}; double V[3],D[3],D2=0.0,VD=0.0; for(int32 A=0;A<3;++A) { V[A]=Position[A]-P.CapsuleStartWorld[A]; D[A]=P.CapsuleAxisDelta[A]; VD+=V[A]*D[A]; } D2=P.CapsuleAxisLengthSquared; const double T=D2>1.0e-12?std::clamp(VD/D2,0.0,1.0):0.0; double DistanceSquared=0.0; for(int32 A=0;A<3;++A) {const double E=V[A]-T*D[A];DistanceSquared+=E*E;} const double Radius=CapsuleRadiusAtT(P,T,P.CapsuleAxisLength); return static_cast(std::sqrt(DistanceSquared)-Radius); } if(P.bRoundedBox) { const double D[3]={std::abs(X-P.CenterSub[0]/16.0)-P.RoundedHalfExtent[0], std::abs(Y-P.CenterSub[1]/16.0)-P.RoundedHalfExtent[1], std::abs(Z-P.CenterSub[2]/16.0)-P.RoundedHalfExtent[2]}; const double Ox=std::max(D[0],0.0),Oy=std::max(D[1],0.0),Oz=std::max(D[2],0.0); const double Outside=std::sqrt(Ox*Ox+Oy*Oy+Oz*Oz); const double Inside=std::min(std::max({D[0],D[1],D[2]}),0.0); return static_cast(Outside+Inside-P.Rounding); } const double D[3]={X-P.CenterSub[0]/16.0,Y-P.CenterSub[1]/16.0,Z-P.CenterSub[2]/16.0}; double Q=0.0; for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J)Q+=static_cast(P.Q[I][J])/QScale*D[I]*D[J]*256.0; return static_cast((std::sqrt(std::max(0.0,Q))-1.0)*P.MinRadius); } bool SolveLine(const FPrimitive& P,int32 Axis,const int32 Fixed[3],FInterval& Out) const { const int32 A=(Axis+1)%3,B=(Axis+2)%3; const int32 Lo=P.Min[Axis],Hi=P.Max[Axis]; if(Fixed[A]P.Max[A]||Fixed[B]P.Max[B])return false; int32 L=Lo,H=Hi; if(!P.bRoundedBox&&!P.bCapsule) { double CoefA=static_cast(P.Q[Axis][Axis]); double CoefB=0.0,CoefC=-static_cast(QScale); int64 D[3];D[Axis]=0; for(int32 J=0;J<3;++J)if(J!=Axis)D[J]=static_cast(Fixed[J])*SubPerVoxel-P.CenterSub[J]; for(int32 J=0;J<3;++J)if(J!=Axis) { CoefB+=2.0*P.Q[Axis][J]*D[J]; for(int32 K=0;K<3;++K)if(K!=Axis)CoefC+=static_cast(P.Q[J][K])*D[J]*D[K]; } if(!(CoefA>0.0))return false; const double Disc=CoefB*CoefB-4.0*CoefA*CoefC; if(Disc<0.0)return false; const double S=std::sqrt(Disc); const double U0=(-CoefB-S)/(2.0*CoefA),U1=(-CoefB+S)/(2.0*CoefA); const double T0=(U0+P.CenterSub[Axis])/SubPerVoxel; const double T1=(U1+P.CenterSub[Axis])/SubPerVoxel; L=std::max(Lo,static_cast(std::floor(T0))-1); H=std::min(Hi,static_cast(std::ceil(T1))+1); } if(L>H)return false; if(P.bCarve&&P.bHasFloorClip&&(!P.bCurvedFloorClip||Axis==2)) { double X0=Fixed[0],Y0=Fixed[1],Z0=Fixed[2]; if(Axis==0)X0=0.0;else if(Axis==1)Y0=0.0;else Z0=0.0; const double Floor0=FloorAt(P,X0,Y0); const double SmoothAllowance=P.FloorBlendRadius>0.0?P.FloorBlendRadius/6.0:0.0; const double Margin0=Z0-(Floor0-SmoothAllowance); const double MarginSlope=Axis==0?-P.FloorSlopeX:Axis==1?-P.FloorSlopeY:1.0; if(std::abs(MarginSlope)<1.0e-12) {if(Margin0<0.0)return false;} else { const double Boundary=-Margin0/MarginSlope; if(MarginSlope>0.0)L=std::max(L,static_cast(std::ceil(Boundary))); else H=std::min(H,static_cast(std::floor(Boundary))); } } if(L>H)return false; int32 Mid=L+(H-L)/2; if(P.bCapsule) { double V2=0.0,VD=0.0,D2=0.0; for(int32 J=0;J<3;++J)if(J!=Axis) { const double Start=P.CapsuleStartSub[J]/static_cast(SubPerVoxel); const double End=P.CapsuleEndSub[J]/static_cast(SubPerVoxel); const double V=Fixed[J]-Start,D=End-Start; V2+=V*V;VD+=V*D;D2+=D*D; } const double R0=P.CapsuleStartRadius,DR=P.CapsuleEndRadius-R0; const double QA=D2-DR*DR,QB=-2.0*(VD+R0*DR),QC=V2-R0*R0; const auto Value=[&](double T){return (QA*T+QB)*T+QC;}; double TBest=Value(1.0)1.0e-12) { const double T=std::clamp(-QB/(2.0*QA),0.0,1.0); if(Value(T)0.0)return false; const double Start=P.CapsuleStartSub[Axis]/static_cast(SubPerVoxel); const double End=P.CapsuleEndSub[Axis]/static_cast(SubPerVoxel); Mid=std::clamp(static_cast(std::llround(Start+(End-Start)*TBest)),L,H); } if(!ShapeContains(P,Axis==0?Mid:Fixed[0],Axis==1?Mid:Fixed[1],Axis==2?Mid:Fixed[2])) { bool Found=false; const int32 SearchLimit=P.bCapsule?std::min(H-L,8):2; for(int32 Delta=1;Delta<=SearchLimit&&!Found;++Delta) { if(Mid-Delta>=L&&ShapeContains(P,Axis==0?Mid-Delta:Fixed[0],Axis==1?Mid-Delta:Fixed[1],Axis==2?Mid-Delta:Fixed[2])){Mid-=Delta;Found=true;} else if(Mid+Delta<=H&&ShapeContains(P,Axis==0?Mid+Delta:Fixed[0],Axis==1?Mid+Delta:Fixed[1],Axis==2?Mid+Delta:Fixed[2])){Mid+=Delta;Found=true;} } if(!Found&&P.bCapsule) { for(int32 Sample=L;Sample<=H;Sample+=2) if(ShapeContains(P,Axis==0?Sample:Fixed[0],Axis==1?Sample:Fixed[1],Axis==2?Sample:Fixed[2])) {Mid=Sample;Found=true;break;} if(!Found&&((H-L)&1) &&ShapeContains(P,Axis==0?H:Fixed[0],Axis==1?H:Fixed[1],Axis==2?H:Fixed[2])) {Mid=H;Found=true;} } if(!Found)return false; } int32 Left=L,Right=Mid; while(Left& Ranges) { if(Ranges.size()<2)return; std::sort(Ranges.begin(),Ranges.end(),[](const FInterval& A,const FInterval& B){return A.Lo==B.Lo?A.Hi& Words,size_t Base,int32 Lo,int32 Hi,bool bSet) { if(Lo>Hi)return; const size_t First=static_cast(Lo)>>6,Last=static_cast(Hi)>>6; for(size_t W=First;W<=Last;++W) { const uint32 Bit0=W==First?(static_cast(Lo)&63u):0u; const uint32 Bit1=W==Last?(static_cast(Hi)&63u):63u; const uint64 Mask=(~uint64{0}<>(63u-Bit1)); if(bSet)Words[Base+W]|=Mask;else Words[Base+W]&=~Mask; } } void BuildPackedRockIntervals(FParallelTiming& Timing) { const int32 NX=Size[0],NY=Size[1],NZ=Size[2]; CandidateAxisWords[2].assign(static_cast(NX)*NY*WordsPerZ,0); struct FIntervalScratch { std::vector Carve,Solid,ProtectedAir; }; std::array WorkerScratch; for(int32 Worker=0;Worker(Worker)].Carve.reserve(24); WorkerScratch[static_cast(Worker)].Solid.reserve(8); WorkerScratch[static_cast(Worker)].ProtectedAir.reserve(4); } ParallelLines(static_cast(NX)*NY,[&](size_t Line,int32 Worker) { const int32 X=Min[0]+static_cast(Line%NX),Y=Min[1]+static_cast(Line/NX); FIntervalScratch& Scratch=WorkerScratch[static_cast(Worker)]; std::vector& Carve=Scratch.Carve; std::vector& Solid=Scratch.Solid; std::vector& ProtectedAir=Scratch.ProtectedAir; Carve.clear();Solid.clear();ProtectedAir.clear(); const int32 Fixed[3]={X,Y,0}; for(uint32 Id:GetLineCandidates(2,X,Y)) { const FPrimitive& P=Primitives[Id];FInterval R; if(SolveLine(P,2,Fixed,R)) { if(!P.bCritical) { const size_t CandidateBase=Line*WordsPerZ; const int32 CandidateLo=std::max(Min[2],R.Lo-DetailBand); const int32 CandidateHi=std::min(Max[2],R.Lo+DetailBand); const int32 CandidateLo2=std::max(Min[2],R.Hi-DetailBand); const int32 CandidateHi2=std::min(Max[2],R.Hi+DetailBand); FillBits(CandidateAxisWords[2],CandidateBase, CandidateLo-Min[2],CandidateHi-Min[2],true); FillBits(CandidateAxisWords[2],CandidateBase, CandidateLo2-Min[2],CandidateHi2-Min[2],true); } R.Lo=std::max(R.Lo,Min[2])-Min[2];R.Hi=std::min(R.Hi,Max[2])-Min[2]; if(R.Lo<=R.Hi) { if(!P.bCarve)Solid.push_back(R); else if(P.bCritical)ProtectedAir.push_back(R); else Carve.push_back(R); } } } MergeIntervals(Carve);MergeIntervals(Solid);MergeIntervals(ProtectedAir); const size_t Base=Line*WordsPerZ; const int32 ZLo=LayerRanges[0].Lo, ZHi=LayerRanges[0].Hi; const int32 BaseLo=std::max(ZLo,Min[2])-Min[2],BaseHi=std::min(ZHi,Max[2])-Min[2]; if(BaseLo<=BaseHi)FillBits(RockWords,Base,BaseLo,BaseHi,true); for(const FInterval& R:Carve)FillBits(RockWords,Base,R.Lo,R.Hi,false); for(const FInterval& R:Solid)FillBits(RockWords,Base,R.Lo,R.Hi,true); // Critical passage air is the final authored override, including at landing floors. for(const FInterval& R:ProtectedAir)FillBits(RockWords,Base,R.Lo,R.Hi,false); },&Timing,nullptr,16); } void BuildCandidateBands(FParallelTiming& AxisTiming,FParallelTiming& MergeTiming, double& MergeSeconds) { const int32 N[3]={Size[0],Size[1],Size[2]}; for(int32 Axis=0;Axis<3;++Axis) { if(Axis==2)continue; const int32 A=(Axis+1)%3,B=(Axis+2)%3; const size_t Lines=static_cast(Size[A])*Size[B]; const size_t Words=(static_cast(N[Axis])+63u)/64u; CandidateAxisWords[Axis].assign(Lines*Words,0); ParallelLines(Lines,[&,Axis,A,B,Words](size_t Line) { const int32 CA=Min[A]+static_cast(Line%Size[A]); const int32 CB=Min[B]+static_cast(Line/Size[A]); int32 Fixed[3]{};Fixed[A]=CA;Fixed[B]=CB; for(uint32 Id:GetLineCandidates(Axis,CA,CB)) { const FPrimitive& P=Primitives[Id];if(P.bCritical)continue; FInterval R;if(!SolveLine(P,Axis,Fixed,R))continue; const int32 Lo=std::max(Min[Axis],R.Lo-DetailBand); const int32 Hi=std::min(Max[Axis],R.Lo+DetailBand); const int32 Lo2=std::max(Min[Axis],R.Hi-DetailBand); const int32 Hi2=std::min(Max[Axis],R.Hi+DetailBand); const size_t Base=Line*Words; FillBits(CandidateAxisWords[Axis],Base,Lo-Min[Axis],Hi-Min[Axis],true); FillBits(CandidateAxisWords[Axis],Base,Lo2-Min[Axis],Hi2-Min[Axis],true); } },&AxisTiming,nullptr,16); } const auto MergeStart=std::chrono::steady_clock::now(); CandidateWords=std::move(CandidateAxisWords[2]); for(int32 Axis=0;Axis<2;++Axis) { const int32 A=(Axis+1)%3,B=(Axis+2)%3; const size_t Words=(static_cast(Size[Axis])+63u)/64u; ParallelLines(static_cast(Size[A])*Size[B],[&,Axis,A,B,Words](size_t Line) { const int32 CA=Min[A]+static_cast(Line%Size[A]); const int32 CB=Min[B]+static_cast(Line/Size[A]); for(size_t W=0;W(W*64+Bit); if(T>Max[Axis])continue; int32 Coord[3]{};Coord[Axis]=T;Coord[A]=CA;Coord[B]=CB; const size_t Column=static_cast(Coord[1]-Min[1])*Size[0] +static_cast(Coord[0]-Min[0]); const size_t Zi=static_cast(Coord[2]-Min[2]); std::atomic_ref(CandidateWords[Column*WordsPerZ+(Zi>>6)]) .fetch_or(uint64{1}<<(Zi&63u),std::memory_order_relaxed); } } },&MergeTiming,nullptr,16); } const int32 LayerLo=LayerRanges.empty()?1:LayerRanges[0].Lo; const int32 LayerHi=LayerRanges.empty()?0:LayerRanges[0].Hi; if(LayerLo<=LayerHi) { for(size_t Line=0;Line(Size[0])*Size[1];++Line) { const size_t Base=Line*WordsPerZ; const int32 Lo0=std::max(Min[2],LayerLo-DetailBand)-Min[2]; const int32 Hi0=std::min(Max[2],LayerLo+DetailBand)-Min[2]; const int32 Lo1=std::max(Min[2],LayerHi-DetailBand)-Min[2]; const int32 Hi1=std::min(Max[2],LayerHi+DetailBand)-Min[2]; FillBits(CandidateWords,Base,Lo0,Hi0,true); FillBits(CandidateWords,Base,Lo1,Hi1,true); } } const double MergeElapsed=std::chrono::duration( std::chrono::steady_clock::now()-MergeStart).count(); MergeSeconds=std::max(0.0,MergeElapsed-MergeTiming.WallSeconds); } bool BuildGpuVisualDetailNodeSets( const std::vector& GpuTiles, const std::vector& CpuTiles, std::string& Error) { if(GpuTiles.empty()&&CpuTiles.empty()) { GpuVisualDetailWords.clear();CpuVisualDetailWords.clear(); if(bProfileDetailBand) { GpuVisualDetailWords.assign(CandidateWords.size(),0u); CpuVisualDetailWords.assign(CandidateWords.size(),0u); } GpuVisualDetailTileCount=CpuVisualDetailTileCount=0; GpuVisualDetailGridSamples=CpuVisualDetailGridSamples=0; GpuVisualDetailNodeCount=CpuVisualDetailNodeCount=0; return true; } const auto BuildSet=[&](const std::vector& Tiles, std::vector& Nodes,uint64& GridSamples)->bool { Nodes.clear();GridSamples=0; uint64 ReserveSamples=0; for(const auto& Tile:Tiles) { if(Tile.cells<=0||Tile.cells>256||Tile.step<=0) {Error="GPU visual detail-band tile region has invalid cells or step";return false;} const uint64 Dim=static_cast(Tile.cells)+3u; const uint64 TileSamples=Dim*Dim*Dim; if(TileSamples>std::numeric_limits::max()-ReserveSamples) {Error="GPU visual detail-band tile sample count overflowed 64 bits";return false;} ReserveSamples+=TileSamples; } GridSamples=ReserveSamples; const auto IsCandidate=[&](int32 X,int32 Y,int32 Z) { if(XMax[0]||YMax[1]||ZMax[2]) return false; const size_t Column=static_cast(Y-Min[1])*Size[0] +static_cast(X-Min[0]); const size_t Zi=static_cast(Z-Min[2]); return (CandidateWords[Column*WordsPerZ+(Zi>>6)] &(uint64{1}<<(Zi&63u)))!=0; }; Nodes.assign(CandidateWords.size(),0u); for(const auto& Tile:Tiles) for(int32 Z=-1;Z<=Tile.cells+1;++Z) for(int32 Y=-1;Y<=Tile.cells+1;++Y) for(int32 X=-1;X<=Tile.cells+1;++X) { const int32 WX=Tile.originVoxel.x+X*Tile.step; const int32 WY=Tile.originVoxel.y+Y*Tile.step; const int32 WZ=Tile.originVoxel.z+Z*Tile.step; if(!IsCandidate(WX,WY,WZ))continue; const size_t Column=static_cast(WY-Min[1])*Size[0] +static_cast(WX-Min[0]); const size_t Zi=static_cast(WZ-Min[2]); Nodes[Column*WordsPerZ+(Zi>>6)]|=uint64{1}<<(Zi&63u); } return true; }; if(!BuildSet(GpuTiles,GpuVisualDetailWords,GpuVisualDetailGridSamples) ||!BuildSet(CpuTiles,CpuVisualDetailWords,CpuVisualDetailGridSamples))return false; GpuVisualDetailNodeCount=0;CpuVisualDetailNodeCount=0; for(const uint64 Bits:GpuVisualDetailWords)GpuVisualDetailNodeCount+=std::popcount(Bits); for(const uint64 Bits:CpuVisualDetailWords)CpuVisualDetailNodeCount+=std::popcount(Bits); GpuVisualDetailTileCount=GpuTiles.size();CpuVisualDetailTileCount=CpuTiles.size(); if(!GpuTiles.empty()||!CpuTiles.empty()) { std::printf("[GpuVisualDetailBandPlan] gpu_tiles=%llu gpu_sample_nodes=%llu gpu_unique_candidate_nodes=%llu cpu_near_tiles=%llu cpu_sample_nodes=%llu cpu_unique_candidate_nodes=%llu\n", static_cast(GpuVisualDetailTileCount), static_cast(GpuVisualDetailGridSamples), static_cast(GpuVisualDetailNodeCount), static_cast(CpuVisualDetailTileCount), static_cast(CpuVisualDetailGridSamples), static_cast(CpuVisualDetailNodeCount)); } return true; } bool IsGpuVisualDetailNode(int32 X,int32 Y,int32 Z) const { if(!Contains(X,Y,Z)||GpuVisualDetailWords.empty())return false; const size_t Column=static_cast(Y-Min[1])*Size[0] +static_cast(X-Min[0]); const size_t Zi=static_cast(Z-Min[2]); return (GpuVisualDetailWords[Column*WordsPerZ+(Zi>>6)] &(uint64{1}<<(Zi&63u)))!=0; } bool IsCpuVisualDetailNode(int32 X,int32 Y,int32 Z) const { if(!Contains(X,Y,Z)||CpuVisualDetailWords.empty())return false; const size_t Column=static_cast(Y-Min[1])*Size[0] +static_cast(X-Min[0]); const size_t Zi=static_cast(Z-Min[2]); return (CpuVisualDetailWords[Column*WordsPerZ+(Zi>>6)] &(uint64{1}<<(Zi&63u)))!=0; } void ApplyDetailBand(FParallelTiming& Timing) { using Clock=std::chrono::steady_clock; const bool bProfile=bProfileDetailBand; std::array WorkerProfile{}; std::array WorkerActiveSeconds{}; std::atomic CandidateCount{0},ProxyCount{0},FlipCount{0}; ParallelLines(static_cast(Size[0])*Size[1],[&](size_t Line,int32 Worker) { const int32 X=Min[0]+static_cast(Line%Size[0]); const int32 Y=Min[1]+static_cast(Line/Size[0]); FDetailNoiseLineContext NoiseLine(static_cast(X),static_cast(Y), static_cast(Owner->Seed)); const FLayerField* LineField=nullptr; bool bLineFieldKnown=false; int32 LineFieldLo=0,LineFieldHi=-1; const auto FindLineLayerField=[&](double Z)->const FLayerField* { if(bLineFieldKnown&&LineField&&Z>=LineFieldLo&&Z<=LineFieldHi) return LineField; LineField=FindLayerField(Z); bLineFieldKnown=LineField!=nullptr; if(LineField){LineFieldLo=LineField->Lo;LineFieldHi=LineField->Hi;} return LineField; }; const int32 QueryBinX=FloorDiv(X,ActiveSpatialBinSize); const int32 QueryBinY=FloorDiv(Y,ActiveSpatialBinSize); int32 CachedQueryBinZ=std::numeric_limits::min(); const FBinItems* CachedQueryBinItems=nullptr; FDetailWorkerProfile& Profile=WorkerProfile[static_cast(Worker)]; if(bProfile)++Profile.Lines; uint64 LocalCandidates=0,LocalProxies=0,LocalFlips=0; const size_t WordBase=Line*WordsPerZ; for(size_t W=0;W::min(); const FBinItems* BatchBinItems=nullptr; const auto ProcessBatch=[&]() { if(BatchCount==0)return; FEffectiveRockSample Samples[8]; int32 ProfileDistanceSamples=0; if(bProfile) { for(int32 Lane=0;Lane0 ?Clock::now():Clock::time_point{}; EffectiveRockDistanceZ8(X,Y,BatchZ,BatchCount,BatchBinItems,Samples); if(ProfileDistanceSamples>0) { Profile.DistanceSampleNanoseconds+=static_cast( std::chrono::duration_cast( Clock::now()-DistanceStart).count()); Profile.DistanceSamples+=static_cast(BatchCount); } for(int32 Lane=0;Lane(Z-Min[2]); ++LocalCandidates; const FEffectiveRockSample& Sample=Samples[Lane]; const float SDF=Sample.Distance; if(std::abs(SDF)>DetailSupremum+0.125f)continue; const size_t VisualWordIndex=WordBase+(Zi>>6); const uint64 VisualMask=uint64{1}<<(Zi&63u); const bool bGpuVisualNode=bProfile &&(GpuVisualDetailWords[VisualWordIndex]&VisualMask)!=0; const bool bCpuVisualNode=bProfile &&(CpuVisualDetailWords[VisualWordIndex]&VisualMask)!=0; if(bProfile) { if(bGpuVisualNode)++Profile.GpuVisualCandidates; if(bCpuVisualNode)++Profile.CpuVisualCandidates; if(bGpuVisualNode&&!bCpuVisualNode)++Profile.GpuOnlyCandidates; } const float SurfaceSDF=Sample.SurfaceSDF; const float CaveInfluence=Sample.CaveInfluence; const float FloorInfluence=Sample.FloorInfluence; const float PassageInfluence=Sample.PassageInfluence; const bool bCriticalAir=Sample.bCriticalAir; float DetailFade=DetailBandFade(SDF); float MacroValue=0.0f; bool bMacroKnown=false; const FLayerField* DetailField=nullptr; bool bDetailFieldKnown=false; if(DetailVariant==EDetailVariant::Coherent &&std::abs(SDF)>DetailBandInnerRadius) { MacroValue=NoiseLine.Macro.Sample(static_cast(Z)); bMacroKnown=true; DetailField=FindLineLayerField(Z); bDetailFieldKnown=true; DetailFade=DetailBandFade(SDF,LocalDetailBandEdge(MacroValue, SurfaceSDF,CaveInfluence,FloorInfluence,DetailField)); } if(DetailFade<=0.0f&&bProfile)++Profile.LocalBandSkips; const bool bTimeDetail=DetailFade>0.0f&&bProfile &&((Profile.DetailEvaluations&63u)==0); const auto DetailStart=bTimeDetail?Clock::now():Clock::time_point{}; float Displaced=SDF; if(DetailFade>0.0f) { if(bProfile)++Profile.DetailEvaluations; if(bProfile&&bGpuVisualNode)++Profile.GpuVisualDetailEvaluations; if(bProfile&&bGpuVisualNode&&!bCpuVisualNode) ++Profile.GpuOnlyDetailEvaluations; Displaced+=DetailNoiseOnLine(X,Y,Z,Sample.SurfaceSDF, CaveInfluence,FloorInfluence,PassageInfluence,NoiseLine,bTimeDetail?&Profile:nullptr, DetailField,bDetailFieldKnown,bMacroKnown,MacroValue)*DetailFade; } if(bTimeDetail) { Profile.DetailSampleNanoseconds+=static_cast( std::chrono::duration_cast( Clock::now()-DetailStart).count()); ++Profile.DetailSamples; } if(bCriticalAir)Displaced=std::min(Displaced,-1.0f/16.0f); const bool bNewRock=Displaced>=0.0f; const bool bOldRock=((RockWords[WordBase+(Zi>>6)]>>(Zi&63u))&1u)!=0; if(bNewRock!=bOldRock) { RockWords[WordBase+(Zi>>6)]^=uint64{1}<<(Zi&63u); ++LocalFlips; if(bProfile)++Profile.Flips; } const size_t Node=(Zi*static_cast(Size[1]) +static_cast(Y-Min[1]))*Size[0] +static_cast(X-Min[0]); StoreSurfaceDistance(Node,-Displaced); ValidBits|=uint64{1}<<(Zi&63u); ++LocalProxies; } BatchCount=0; }; while(Bits) { const uint32 Bit=std::countr_zero(Bits);Bits&=Bits-1; const size_t Zi=W*64+Bit;if(Zi>=static_cast(Size[2]))continue; const int32 Z=Min[2]+static_cast(Zi); const int32 QueryBinZ=FloorDiv(Z,ActiveSpatialBinSize); if(BatchCount>0&&QueryBinZ!=BatchQueryBinZ)ProcessBatch(); if(QueryBinZ!=CachedQueryBinZ) { const auto Bin=SpatialBins.find({QueryBinX,QueryBinY,QueryBinZ}); CachedQueryBinItems=Bin==SpatialBins.end()?nullptr:&Bin->second; CachedQueryBinZ=QueryBinZ; } if(BatchCount==0)BatchQueryBinZ=QueryBinZ,BatchBinItems=CachedQueryBinItems; BatchZ[BatchCount++]=Z; if(BatchCount==8)ProcessBatch(); } ProcessBatch(); if(OriginalBits!=ValidBits)CandidateWords[WordBase+W]=ValidBits; } CandidateCount.fetch_add(LocalCandidates,std::memory_order_relaxed); ProxyCount.fetch_add(LocalProxies,std::memory_order_relaxed); FlipCount.fetch_add(LocalFlips,std::memory_order_relaxed); // Each X/Y column is independent. Claim eight adjacent columns at a time to // reduce scheduler atomics while preserving per-column scalar evaluation order. },&Timing,&WorkerActiveSeconds,8); Stats.SurfaceCandidates=CandidateCount.load(std::memory_order_relaxed); Stats.SurfaceProxyNodes=ProxyCount.load(std::memory_order_relaxed); Stats.DetailFlips=FlipCount.load(std::memory_order_relaxed); if(bProfile) { uint64 DistanceCalls=0,DetailCalls=0,DistanceSamples=0,DistanceSampleNs=0; uint64 DetailSamples=0,DetailSampleNs=0,ReliefSampleNs=0,LocalBandSkips=0; uint64 LayerLookupSampleNs=0,OwnerSampleNs=0,OwnerFieldCalls=0,OwnerActiveCalls=0; uint64 OwnerCacheHits=0,OwnerCacheMisses=0; uint64 GpuVisualCandidates=0,CpuVisualCandidates=0,GpuOnlyCandidates=0; uint64 GpuVisualDetailCalls=0,GpuOnlyDetailCalls=0; for(int32 Worker=0;Worker(Worker)]; DistanceCalls+=P.Candidates;DetailCalls+=P.DetailEvaluations; DistanceSamples+=P.DistanceSamples;DistanceSampleNs+=P.DistanceSampleNanoseconds; DetailSamples+=P.DetailSamples;DetailSampleNs+=P.DetailSampleNanoseconds; LocalBandSkips+=P.LocalBandSkips; ReliefSampleNs+=P.ReliefSampleNanoseconds; LayerLookupSampleNs+=P.LayerLookupSampleNanoseconds; OwnerSampleNs+=P.OwnerRoughnessSampleNanoseconds; OwnerFieldCalls+=P.OwnerRoughnessFieldCalls; OwnerActiveCalls+=P.OwnerRoughnessActiveCalls; OwnerCacheHits+=P.OwnerNoiseCacheHits; OwnerCacheMisses+=P.OwnerNoiseCacheMisses; GpuVisualCandidates+=P.GpuVisualCandidates; CpuVisualCandidates+=P.CpuVisualCandidates; GpuOnlyCandidates+=P.GpuOnlyCandidates; GpuVisualDetailCalls+=P.GpuVisualDetailEvaluations; GpuOnlyDetailCalls+=P.GpuOnlyDetailEvaluations; } const double DistanceAverageNs=DistanceSamples?static_cast(DistanceSampleNs)/DistanceSamples:0.0; const double DetailAverageNs=DetailSamples?static_cast(DetailSampleNs)/DetailSamples:0.0; const double CandidateWorkerNs=Stats.SurfaceCandidates ?Timing.WorkerSeconds*1.0e9/static_cast(Stats.SurfaceCandidates):0.0; std::printf("[DetailBandProfile] candidates=%llu distance_evaluations=%llu detail_noise_evaluations=%llu proxies=%llu flips=%llu candidate_worker_ns=%.1f distance_sample_ns=%.1f distance_estimated_worker_ms=%.1f detail_sample_ns=%.1f detail_estimated_worker_ms=%.1f worker_sum_ms=%.3f wall_ms=%.3f sampling=(distance_1_of_256,detail_1_of_64)\n", static_cast(Stats.SurfaceCandidates), static_cast(DistanceCalls),static_cast(DetailCalls), static_cast(Stats.SurfaceProxyNodes),static_cast(Stats.DetailFlips), CandidateWorkerNs,DistanceAverageNs,DistanceAverageNs*static_cast(DistanceCalls)/1.0e6, DetailAverageNs,DetailAverageNs*static_cast(DetailCalls)/1.0e6, Timing.WorkerSeconds*1000.0,Timing.WallSeconds*1000.0); std::printf("[DetailBandSubprofile] relief_sample_ns=%.1f relief_estimated_worker_ms=%.1f layer_lookup_sample_ns=%.1f layer_lookup_estimated_worker_ms=%.1f owner_sample_ns=%.1f owner_estimated_worker_ms=%.1f owner_sampled_field_calls=%llu owner_sampled_noise_calls=%llu owner_sampled_core_cache_hits=%llu owner_sampled_core_cache_misses=%llu local_band_skipped=%llu\n", DetailSamples?static_cast(ReliefSampleNs)/DetailSamples:0.0, DetailSamples?static_cast(ReliefSampleNs)/DetailSamples*DetailCalls/1.0e6:0.0, DetailSamples?static_cast(LayerLookupSampleNs)/DetailSamples:0.0, DetailSamples?static_cast(LayerLookupSampleNs)/DetailSamples*DetailCalls/1.0e6:0.0, DetailSamples?static_cast(OwnerSampleNs)/DetailSamples:0.0, DetailSamples?static_cast(OwnerSampleNs)/DetailSamples*DetailCalls/1.0e6:0.0, static_cast(OwnerFieldCalls), static_cast(OwnerActiveCalls), static_cast(OwnerCacheHits), static_cast(OwnerCacheMisses), static_cast(LocalBandSkips)); std::printf("[GpuDetailConsumerProfile] gpu_near_proxy_nodes=%llu cpu_near_mesh_nodes=%llu gpu_only_nodes=%llu gpu_near_detail_noise_evaluations=%llu gpu_only_detail_noise_evaluations=%llu\n", static_cast(GpuVisualCandidates), static_cast(CpuVisualCandidates), static_cast(GpuOnlyCandidates), static_cast(GpuVisualDetailCalls), static_cast(GpuOnlyDetailCalls)); for(int32 Worker=0;Worker(Worker)]; std::printf("[DetailBandWorker] id=%d active_ms=%.3f lines=%llu candidates=%llu detail_evaluations=%llu flips=%llu\n", Worker,WorkerActiveSeconds[static_cast(Worker)]*1000.0, static_cast(P.Lines),static_cast(P.Candidates), static_cast(P.DetailEvaluations),static_cast(P.Flips)); } } } uint64 CompleteSignEdgeDistances() { uint64 AddedTotal=0,SignCorrections=0; const size_t Columns=static_cast(Size[0])*Size[1]; for(int32 Pass=0;Pass<4;++Pass) { uint64 AddedThisPass=0; bool bSignChanged=false; const auto EnsureDistance=[&](int32 X,int32 Y,int32 Z) { const size_t Column=static_cast(Y-Min[1])*Size[0] +static_cast(X-Min[0]); const size_t Zi=static_cast(Z-Min[2]); const size_t Word=Column*WordsPerZ+(Zi>>6); const uint64 Mask=uint64{1}<<(Zi&63u); if(CandidateWords[Word]&Mask)return; const float Density=AnalyticDensityAt(static_cast(X), static_cast(Y),static_cast(Z)); const size_t Node=(Zi*static_cast(Size[1]) +static_cast(Y-Min[1]))*Size[0] +static_cast(X-Min[0]); StoreSurfaceDistance(Node,Density); CandidateWords[Word]|=Mask; ++AddedThisPass; const bool bExactRock=Density<0.0f; const bool bPackedRock=(RockWords[Word]&Mask)!=0; if(bExactRock!=bPackedRock) { if(bExactRock)RockWords[Word]|=Mask; else RockWords[Word]&=~Mask; ++SignCorrections; bSignChanged=true; } }; for(size_t Line=0;Line(Line%Size[0]); const int32 Y=Min[1]+static_cast(Line/Size[0]); const size_t Base=Line*WordsPerZ; for(size_t W=0;W>63:0; const uint64 PreviousCandidates=W?CandidateWords[Word-1]>>63:0; uint64 Valid=(Size[2]-W*64)>=64?~uint64{0} :((uint64{1}<<(Size[2]-W*64))-1); if(W==0)Valid&=~uint64{1}; const uint64 ShiftedRock=(Rock<<1)|PreviousRock; const uint64 ShiftedCandidates=(Candidates<<1)|PreviousCandidates; uint64 Missing=((Rock^ShiftedRock)&Valid) &~(Candidates&ShiftedCandidates); while(Missing) { const uint32 Bit=std::countr_zero(Missing);Missing&=Missing-1; const int32 Z=Min[2]+static_cast(W*64+Bit); EnsureDistance(X,Y,Z); EnsureDistance(X,Y,Z-1); } if(X(W*64+Bit); EnsureDistance(X,Y,Z); EnsureDistance(X+1,Y,Z); } } if(Y(Size[0])*WordsPerZ; Missing=(Rock^RockWords[Other])&Valid &~(Candidates&CandidateWords[Other]); while(Missing) { const uint32 Bit=std::countr_zero(Missing);Missing&=Missing-1; const int32 Z=Min[2]+static_cast(W*64+Bit); EnsureDistance(X,Y,Z); EnsureDistance(X,Y+1,Z); } } } } AddedTotal+=AddedThisPass; if(!bSignChanged)break; } std::printf("[GridStairDistanceRepair] exact_endpoint_proxies=%llu occupancy_corrections=%llu\n", static_cast(AddedTotal), static_cast(SignCorrections)); return AddedTotal; } void StoreSurfaceDistance(size_t Node,float Density) { int32 Q=static_cast(std::lround(Density*256.0f)); Q=std::clamp(Q,-32767,32767); if(Q==0)Q=Density<0.0f?-1:1; SurfaceDistanceQ[Node]=static_cast(Q); } bool IsInsideSolidOverlay(int32 X,int32 Y,int32 Z) const { const FBinKey K=PointBin(X,Y,Z);const auto It=SpatialBins.find(K); if(It==SpatialBins.end())return false; for(uint32 Id:It->second) { const FPrimitive& P=Primitives[Id];if(P.bCarve)continue; if(XP.Max[0]||YP.Max[1]||ZP.Max[2])continue; if(ShapeContains(P,X,Y,Z))return true; } return false; } bool IsInsideCriticalAir(int32 X,int32 Y,int32 Z) const { const auto It=CriticalAirBins.find(PointBin(X,Y,Z)); if(It==CriticalAirBins.end())return false; for(uint32 Id:It->second) { const FPrimitive& P=Primitives[Id];if(!P.bCarve||!P.bCritical)continue; if(XP.Max[0]||YP.Max[1]||ZP.Max[2])continue; if(ShapeContains(P,X,Y,Z))return true; } return false; } bool IsNearCriticalAirBoundary(int32 X,int32 Y,int32 Z,double Margin) const { const auto It=CriticalAirBins.find(PointBin(X,Y,Z)); if(It==CriticalAirBins.end())return false; for(uint32 Id:It->second) { const FPrimitive& P=Primitives[Id];if(!P.bCarve||!P.bCritical)continue; if(XP.Max[0]+Margin ||YP.Max[1]+Margin ||ZP.Max[2]+Margin)continue; float Distance=ShapeDistance(P,X,Y,Z); if(P.bHasFloorClip) { const float FloorDistance=static_cast(FloorAt(P,X,Y)-Z); Distance=SmoothMaxDistance(Distance,FloorDistance,static_cast(P.FloorBlendRadius)); } if(std::abs(Distance)<=Margin)return true; } return false; } bool IsInsideSolidOverlay(double X,double Y,double Z) const { const auto It=SpatialBins.find(PointBin(X,Y,Z)); if(It==SpatialBins.end())return false; for(uint32 Id:It->second) { const FPrimitive& P=Primitives[Id];if(P.bCarve)continue; if(XP.Max[0]||YP.Max[1]||ZP.Max[2])continue; if(ShapeDistance(P,X,Y,Z)<=0.0f)return true; } return false; } float SolidOverlayDistance(double X,double Y,double Z) const { const auto It=SpatialBins.find(PointBin(X,Y,Z)); if(It==SpatialBins.end())return -100000.0f; float Best=-100000.0f; for(uint32 Id:It->second) { const FPrimitive& P=Primitives[Id];if(P.bCarve)continue; if(XP.Max[0]||YP.Max[1]||ZP.Max[2])continue; Best=std::max(Best,-ShapeDistance(P,X,Y,Z)); } return Best; } bool IsInsideCriticalAir(double X,double Y,double Z) const { const auto It=CriticalAirBins.find(PointBin(X,Y,Z)); if(It==CriticalAirBins.end())return false; for(uint32 Id:It->second) { const FPrimitive& P=Primitives[Id];if(!P.bCarve||!P.bCritical)continue; if(XP.Max[0]||YP.Max[1]||ZP.Max[2])continue; if(ShapeContains(P,X,Y,Z))return true; } return false; } float CaveDistance(double X,double Y,double Z,bool* OutFloorSurface=nullptr, float* OutFloorInfluence=nullptr) const { const auto It=SpatialBins.find(PointBin(X,Y,Z)); if(It==SpatialBins.end()) { if(OutFloorSurface)*OutFloorSurface=false; if(OutFloorInfluence)*OutFloorInfluence=0.0f; return 100000.0f; } float Best=100000.0f; float BestFloorInfluence=0.0f; for(uint32 Id:It->second) { const FPrimitive& P=Primitives[Id];if(!P.bCarve)continue; if(XP.Max[0]+DetailBand||YP.Max[1]+DetailBand ||ZP.Max[2]+DetailBand)continue; float Distance=ShapeDistance(P,X,Y,Z); float PrimitiveFloorInfluence=0.0f; if(P.bHasFloorClip) { const double Floor=FloorAt(P,X,Y); const float FloorT=(static_cast(std::abs(Z-Floor))-static_cast(P.FloorBlendRadius)) /static_cast(std::max(DetailBand*2,1)); PrimitiveFloorInfluence=1.0f-SmoothUnit(FloorT); Distance=SmoothMaxDistance(Distance,static_cast(Floor-Z), static_cast(P.FloorBlendRadius)); } const float MetadataWidth=static_cast(std::max(P.BlendRadius,6.0)); const float PrimitiveWeight=SmoothUnit(0.5f+(Best-Distance)/(2.0f*MetadataWidth)); const float Combined=SmoothMinDistance(Best,Distance,static_cast(P.BlendRadius)); BestFloorInfluence=std::lerp(BestFloorInfluence,PrimitiveFloorInfluence,PrimitiveWeight); Best=Combined; } if(OutFloorInfluence)*OutFloorInfluence=BestFloorInfluence; if(OutFloorSurface)*OutFloorSurface=BestFloorInfluence>=0.5f; return Best; } float StrataDistance(double Z) const { if(LayerRanges.empty())return -100000.0f; return static_cast(std::min(Z-LayerRanges[0].Lo,LayerRanges[0].Hi-Z)); } const FLayerField* FindLayerField(double Z) const { for(const FLayerField& Field:LayerFields) if(Z>=Field.Lo&&Z<=Field.Hi)return &Field; return nullptr; } float EffectiveRockDistance(double X,double Y,double Z, ESurfaceFamily* OutFamily=nullptr, float* OutSurfaceSDF=nullptr, bool* OutFloorSurface=nullptr, float* OutCaveInfluence=nullptr, float* OutFloorInfluence=nullptr, bool* OutCriticalAir=nullptr, const FBinItems* KnownBinItems=nullptr, bool bKnownBin=false, float* OutPassageInfluence=nullptr) const { const float Base=StrataDistance(Z); float Cave=100000.0f; float Overlay=-100000.0f; float CaveFloorInfluence=0.0f; float CavePassageInfluence=0.0f; bool bCriticalAir=false; const FBinItems* BinItems=KnownBinItems; if(!bKnownBin) { const auto Bin=SpatialBins.find(PointBin(X,Y,Z)); if(Bin!=SpatialBins.end())BinItems=&Bin->second; } if(BinItems) { for(uint32 Id:*BinItems) { const FPrimitive& P=Primitives[Id]; if(P.bCarve) { if(P.bCritical&&X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1] &&Z>=P.Min[2]&&Z<=P.Max[2]&&ShapeContains(P, static_cast(X),static_cast(Y),static_cast(Z))) bCriticalAir=true; if(XP.Max[0]+DetailBand ||YP.Max[1]+DetailBand ||ZP.Max[2]+DetailBand)continue; float Distance=ShapeDistance(P,X,Y,Z); float PrimitiveFloorInfluence=0.0f; if(P.bHasFloorClip) { const double Floor=FloorAt(P,X,Y); const float FloorT=(static_cast(std::abs(Z-Floor)) -static_cast(P.FloorBlendRadius)) /static_cast(std::max(DetailBand*2,1)); PrimitiveFloorInfluence=1.0f-SmoothUnit(FloorT); Distance=SmoothMaxDistance(Distance,static_cast(Floor-Z), static_cast(P.FloorBlendRadius)); } const float MetadataWidth=static_cast(std::max(P.BlendRadius,6.0)); const float PrimitiveWeight=SmoothUnit(0.5f+(Cave-Distance)/(2.0f*MetadataWidth)); const float Combined=SmoothMinDistance(Cave,Distance,static_cast(P.BlendRadius)); CaveFloorInfluence=std::lerp(CaveFloorInfluence,PrimitiveFloorInfluence,PrimitiveWeight); if(OutPassageInfluence&&(CavePassageInfluence>0.0f||P.bPassageSurface)) CavePassageInfluence=std::lerp(CavePassageInfluence, P.bPassageSurface?1.0f:0.0f,PrimitiveWeight); Cave=Combined; } else if(X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1] &&Z>=P.Min[2]&&Z<=P.Max[2]) Overlay=std::max(Overlay,-ShapeDistance(P,X,Y,Z)); } } if(OutCriticalAir)*OutCriticalAir=bCriticalAir; const float CarveOrOverlay=std::max(Cave,Overlay); const ESurfaceFamily Family=Base<=CarveOrOverlay?ESurfaceFamily::Strata: (Cave>=Overlay?ESurfaceFamily::Cave:ESurfaceFamily::SolidOverlay); constexpr float FamilyBlendWidth=6.0f; const float CaveVsOverlay=SmoothUnit(0.5f+(Cave-Overlay)/(2.0f*FamilyBlendWidth)); const float CarveInfluence=SmoothUnit(0.5f+(Base-CarveOrOverlay)/(2.0f*FamilyBlendWidth)); const float CaveInfluence=CarveInfluence*CaveVsOverlay; const float FloorInfluence=CaveInfluence*CaveFloorInfluence; const float PassageInfluence=CaveInfluence*CavePassageInfluence; const float RockDistance=SmoothMinDistance(Base,CarveOrOverlay,StrataCaveBlendRadius); if(OutFamily) *OutFamily=Family; if(OutSurfaceSDF) *OutSurfaceSDF=Family==ESurfaceFamily::Strata?Base: (Family==ESurfaceFamily::Cave?Cave:Overlay); if(OutCaveInfluence)*OutCaveInfluence=CaveInfluence; if(OutFloorInfluence)*OutFloorInfluence=FloorInfluence; if(OutPassageInfluence)*OutPassageInfluence=PassageInfluence; if(OutFloorSurface)*OutFloorSurface=FloorInfluence>=0.5f; return RockDistance; } struct FEffectiveRockSample { float Distance=0.0f; float SurfaceSDF=0.0f; float CaveInfluence=0.0f; float FloorInfluence=0.0f; float PassageInfluence=0.0f; ESurfaceFamily Family=ESurfaceFamily::Strata; bool bFloorSurface=false; bool bCriticalAir=false; }; void ShapeDistanceZ8(const FPrimitive& P,int32 X,int32 Y,const int32 Z[8],int32 Count, float OutDistance[8]) const { const __m256d Zero=_mm256_setzero_pd(); const __m256d One=_mm256_set1_pd(1.0); const __m256d SignMask=_mm256_set1_pd(-0.0); for(int32 Base=0;Base(Z[Base+std::min(Lane,Lanes-1)]); const __m256d VZ=_mm256_setr_pd(ZLane[0],ZLane[1],ZLane[2],ZLane[3]); __m256d Distance; if(P.bCapsule) { const double VX=static_cast(X)-P.CapsuleStartWorld[0]; const double VY=static_cast(Y)-P.CapsuleStartWorld[1]; const __m256d VXv=_mm256_set1_pd(VX); const __m256d VYv=_mm256_set1_pd(VY); const __m256d VZv=_mm256_sub_pd(VZ,_mm256_set1_pd(P.CapsuleStartWorld[2])); __m256d VD=_mm256_mul_pd(VXv,_mm256_set1_pd(P.CapsuleAxisDelta[0])); VD=_mm256_add_pd(VD,_mm256_mul_pd(VYv,_mm256_set1_pd(P.CapsuleAxisDelta[1]))); VD=_mm256_add_pd(VD,_mm256_mul_pd(VZv,_mm256_set1_pd(P.CapsuleAxisDelta[2]))); const __m256d T=P.CapsuleAxisLengthSquared>1.0e-12 ?_mm256_max_pd(Zero,_mm256_min_pd(One, _mm256_div_pd(VD,_mm256_set1_pd(P.CapsuleAxisLengthSquared)))) :Zero; const __m256d EX=_mm256_sub_pd(VXv, _mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[0]))); const __m256d EY=_mm256_sub_pd(VYv, _mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[1]))); const __m256d EZ=_mm256_sub_pd(VZv, _mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[2]))); __m256d DistanceSquared=_mm256_mul_pd(EX,EX); DistanceSquared=_mm256_add_pd(DistanceSquared,_mm256_mul_pd(EY,EY)); DistanceSquared=_mm256_add_pd(DistanceSquared,_mm256_mul_pd(EZ,EZ)); const __m256d Root=_mm256_sqrt_pd(DistanceSquared); double TValues[4],RootValues[4]; _mm256_storeu_pd(TValues,T); _mm256_storeu_pd(RootValues,Root); for(int32 Lane=0;Lane(RootValues[Lane] -CapsuleRadiusAtT(P,TValues[Lane],P.CapsuleAxisLength)); continue; } if(P.bRoundedBox) { const double DX=std::abs(static_cast(X)-P.CenterSub[0]/16.0) -P.RoundedHalfExtent[0]; const double DY=std::abs(static_cast(Y)-P.CenterSub[1]/16.0) -P.RoundedHalfExtent[1]; const double DZCenter=P.CenterSub[2]/16.0; const __m256d DZv=_mm256_sub_pd(_mm256_andnot_pd(SignMask, _mm256_sub_pd(VZ,_mm256_set1_pd(DZCenter))), _mm256_set1_pd(P.RoundedHalfExtent[2])); const __m256d DXv=_mm256_set1_pd(DX),DYv=_mm256_set1_pd(DY); const __m256d Ox=_mm256_max_pd(DXv,Zero),Oy=_mm256_max_pd(DYv,Zero); const __m256d Oz=_mm256_max_pd(DZv,Zero); __m256d OutsideSquared=_mm256_mul_pd(Ox,Ox); OutsideSquared=_mm256_add_pd(OutsideSquared,_mm256_mul_pd(Oy,Oy)); OutsideSquared=_mm256_add_pd(OutsideSquared,_mm256_mul_pd(Oz,Oz)); const __m256d MaxXY=_mm256_max_pd(DXv,DYv); const __m256d MaxQ=_mm256_max_pd(MaxXY,DZv); const __m256d Inside=_mm256_min_pd(MaxQ,Zero); Distance=_mm256_sub_pd(_mm256_add_pd(_mm256_sqrt_pd(OutsideSquared),Inside), _mm256_set1_pd(P.Rounding)); } else { const __m256d D[3]={ _mm256_set1_pd(static_cast(X)-P.CenterSub[0]/16.0), _mm256_set1_pd(static_cast(Y)-P.CenterSub[1]/16.0), _mm256_sub_pd(VZ,_mm256_set1_pd(P.CenterSub[2]/16.0))}; __m256d Q=_mm256_setzero_pd(); for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J) { const double Coefficient=static_cast(P.Q[I][J])/QScale; __m256d Term=_mm256_mul_pd(_mm256_set1_pd(Coefficient),D[I]); Term=_mm256_mul_pd(Term,D[J]); Term=_mm256_mul_pd(Term,_mm256_set1_pd(256.0)); Q=_mm256_add_pd(Q,Term); } Distance=_mm256_mul_pd(_mm256_sub_pd( _mm256_sqrt_pd(_mm256_max_pd(Q,Zero)),_mm256_set1_pd(1.0)), _mm256_set1_pd(P.MinRadius)); } double Values[4]; _mm256_storeu_pd(Values,Distance); for(int32 Lane=0;Lane(Values[Lane]); } } void EffectiveRockDistanceZ8(int32 X,int32 Y,const int32 Z[8],int32 Count, const FBinItems* BinItems,FEffectiveRockSample Out[8]) const { float Base[8],Cave[8],Overlay[8],CaveFloorInfluence[8],CavePassageInfluence[8]; bool CriticalAir[8]{}; for(int32 Lane=0;Lane=P.Min[0]&&X<=P.Max[0] &&Y>=P.Min[1]&&Y<=P.Max[1] &&ZValue>=P.Min[2]&&ZValue<=P.Max[2] &&ShapeContains(P,X,Y,ZValue)) CriticalAir[Lane]=true; if(XP.Max[0]+DetailBand ||YP.Max[1]+DetailBand ||ZValueP.Max[2]+DetailBand) continue; Eligible[Lane]=true; AnyEligible=true; } } else { for(int32 Lane=0;Lane=P.Min[0]&&X<=P.Max[0] &&Y>=P.Min[1]&&Y<=P.Max[1] &&ZValue>=P.Min[2]&&ZValue<=P.Max[2]; AnyEligible=AnyEligible||Eligible[Lane]; } } if(!AnyEligible)continue; float Distances[8]; ShapeDistanceZ8(P,X,Y,Z,Count,Distances); if(P.bCarve) { const double Floor=P.bHasFloorClip?FloorAt(P,X,Y):0.0; const float MetadataWidth=static_cast(std::max(P.BlendRadius,6.0)); for(int32 Lane=0;Lane(std::abs(Z[Lane]-Floor)) -static_cast(P.FloorBlendRadius)) /static_cast(std::max(DetailBand*2,1)); PrimitiveFloorInfluence=1.0f-SmoothUnit(FloorT); Distance=SmoothMaxDistance(Distance,static_cast(Floor-Z[Lane]), static_cast(P.FloorBlendRadius)); } const float PrimitiveWeight=SmoothUnit(0.5f +(Cave[Lane]-Distance)/(2.0f*MetadataWidth)); const float Combined=SmoothMinDistance(Cave[Lane],Distance, static_cast(P.BlendRadius)); CaveFloorInfluence[Lane]=std::lerp(CaveFloorInfluence[Lane], PrimitiveFloorInfluence,PrimitiveWeight); if(CavePassageInfluence[Lane]>0.0f||P.bPassageSurface) CavePassageInfluence[Lane]=std::lerp(CavePassageInfluence[Lane], P.bPassageSurface?1.0f:0.0f,PrimitiveWeight); Cave[Lane]=Combined; } } else { for(int32 Lane=0;Lane=Overlay[Lane]?ESurfaceFamily::Cave:ESurfaceFamily::SolidOverlay); constexpr float FamilyBlendWidth=6.0f; const float CaveVsOverlay=SmoothUnit(0.5f +(Cave[Lane]-Overlay[Lane])/(2.0f*FamilyBlendWidth)); const float CarveInfluence=SmoothUnit(0.5f +(Base[Lane]-CarveOrOverlay)/(2.0f*FamilyBlendWidth)); const float CaveInfluence=CarveInfluence*CaveVsOverlay; const float FloorInfluence=CaveInfluence*CaveFloorInfluence[Lane]; const float PassageInfluence=CaveInfluence*CavePassageInfluence[Lane]; Out[Lane].Distance=SmoothMinDistance(Base[Lane],CarveOrOverlay,StrataCaveBlendRadius); Out[Lane].Family=Family; Out[Lane].SurfaceSDF=Family==ESurfaceFamily::Strata?Base[Lane]: (Family==ESurfaceFamily::Cave?Cave[Lane]:Overlay[Lane]); Out[Lane].CaveInfluence=CaveInfluence; Out[Lane].FloorInfluence=FloorInfluence; Out[Lane].PassageInfluence=PassageInfluence; Out[Lane].bFloorSurface=FloorInfluence>=0.5f; Out[Lane].bCriticalAir=CriticalAir[Lane]; } } public: void DensityAtLatticeZRun(int32 X,int32 Y,int32 ZStart,int32 ZStep, int32 Count,float* OutDensity) const { if(SharedDensitySource) { SharedDensitySource->DensityAtLatticeZRun(X,Y,ZStart,ZStep,Count,OutDensity); return; } if(Count<=0||OutDensity==nullptr)return; if(bNativeOnly) { for(int32 Lane=0;Lane(ZStart+Lane*ZStep)); if(bHasResolvedBoundaryBand) { for(int32 Lane=0;Lane(X), static_cast(Y),static_cast(ZStart+Lane*ZStep)); if(bHasLegacySurfaceSample) { for(int32 Lane=0;Lane(X),static_cast(Y),static_cast(Owner->Seed)); for(int32 Offset=0;Offsetsecond; int32 BatchCount=1; while(BatchCount=Min[0]&&X<=Max[0] &&Y>=Min[1]&&Y<=Max[1]&&SampleZ>=Min[2]&&SampleZ<=Max[2]; float RockDistance=Samples[Lane].Distance; if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth &&std::abs(RockDistance)<=DetailSupremum+0.125f) { const float Fade=DetailBandFadeAt(static_cast(X), static_cast(Y),static_cast(SampleZ),RockDistance, Samples[Lane].SurfaceSDF,Samples[Lane].CaveInfluence, Samples[Lane].FloorInfluence); if(Fade>0.0f)RockDistance+=DetailNoiseOnLine(static_cast(X), static_cast(Y),static_cast(SampleZ), Samples[Lane].SurfaceSDF,Samples[Lane].CaveInfluence, Samples[Lane].FloorInfluence,Samples[Lane].PassageInfluence, DetailNoiseLine)*Fade; } if(Samples[Lane].bCriticalAir) RockDistance=std::min(RockDistance,-1.0f/16.0f); const bool bRock=RockDistance>=0.0f; float Density=-RockDistance; if((Density<0.0f)!=bRock||std::abs(Density)<0.05f) Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),0.05f); OutDensity[Offset+Lane]=ApplyEditBrushes(Density,X,Y,SampleZ); } Offset+=BatchCount; } } void DensityAtLatticeZ8(int32 X,int32 Y,int32 ZStart,int32 ZStep, int32 Count,float OutDensity[8]) const { DensityAtLatticeZRun(X,Y,ZStart,ZStep,std::clamp(Count,0,8),OutDensity); } private: float CoarseRockDistance(double X,double Y,double Z,bool* OutCriticalAir) const { const float Base=StrataDistance(Z); float Cave=100000.0f,Overlay=-100000.0f; bool bCriticalAir=false; const FBinKey QueryKey=PointBin(X,Y,Z); const auto QueryBin=SpatialBins.find(QueryKey); const FBinItems* QueryItems=QueryBin==SpatialBins.end()?nullptr:&QueryBin->second; if(QueryItems) { for(uint32 Id:*QueryItems) { const FPrimitive& P=Primitives[Id]; const VF_FieldPrimitive& Shared=SharedFieldPrimitives[Id]; if(P.bCarve) { if(P.bCritical&&X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1] &&Z>=P.Min[2]&&Z<=P.Max[2]&&ShapeContains(P, static_cast(X),static_cast(Y),static_cast(Z))) bCriticalAir=true; if(XP.Max[0]+DetailBand ||YP.Max[1]+DetailBand ||ZP.Max[2]+DetailBand)continue; float Distance=VF_FieldPrimitiveDistance(Shared,X,Y,Z); if(P.bHasFloorClip) { const double Floor=VF_FieldPrimitiveFloorAt(Shared,X,Y); Distance=VF_FieldSmoothMax(Distance,static_cast(Floor-Z), static_cast(P.FloorBlendRadius)); } Cave=VF_FieldSmoothMin(Cave,Distance,static_cast(P.BlendRadius)); } else if(X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1] &&Z>=P.Min[2]&&Z<=P.Max[2]) Overlay=std::max(Overlay,-VF_FieldPrimitiveDistance(Shared,X,Y,Z)); } } if(OutCriticalAir)*OutCriticalAir=bCriticalAir; return VF_FieldSmoothMin(Base,std::max(Cave,Overlay),StrataCaveBlendRadius); } struct FOwnerNoiseLineContext { struct FAnchorCache { const FLayerField* Field=nullptr; int32 GridZ0=MIN_int32; int32 SampleZ0=MIN_int32,SampleZ1=MIN_int32; float LowerCore=0.0f,UpperCore=0.0f; } Step8,Step2; }; float OwnerCaveRoughnessCore(double X,double Y,double Z, const FStrateGenerationParams& Params) const { // Keep CPU prediction and the near-field HLSL on one scalar source. The helper // preserves the legacy seed offsets, octave order, and roughness families. return VF_FieldOwnerRoughnessCore(static_cast(X),static_cast(Y), static_cast(Z),static_cast(Owner->Seed),Params.SurfaceRoughness, Params.RoughnessFrequency,Params.VerticalScale,Params.DomainWarpStrength, Params.DomainWarpFrequency,static_cast(Params.RoughnessNoiseType)); } float OwnerCaveRoughness(double X,double Y,double Z,float SurfaceSDF, const FLayerField& Field,float CaveInfluence, FOwnerNoiseLineContext& LineContext, FDetailWorkerProfile* Profile=nullptr, int32 AnchorStep=8) const { const FStrateGenerationParams& Params=Field.Params; const float Depth=Params.SurfaceRoughness*2.0f; if(!(Depth>0.0f)||std::abs(SurfaceSDF)>=Depth||!(Params.RoughnessFrequency>0.0f))return 0.0f; if(Profile)++Profile->OwnerRoughnessActiveCalls; const int32 ZStep=std::max(1,AnchorStep); const int32 GridZ0=static_cast(std::floor(Z/static_cast(ZStep)))*ZStep; const int32 SampleZ0=std::max(GridZ0,Field.Lo); const int32 SampleZ1=std::min(GridZ0+ZStep,Field.Hi); float Total=0.0f; if(ZStep==1) Total=OwnerCaveRoughnessCore(X,Y,Z,Params); else if(SampleZ1>SampleZ0) { FOwnerNoiseLineContext::FAnchorCache& Cache=ZStep==2 ?LineContext.Step2:LineContext.Step8; const bool bCacheHit=Cache.Field==&Field&&Cache.GridZ0==GridZ0 &&Cache.SampleZ0==SampleZ0&&Cache.SampleZ1==SampleZ1; if(Profile) { if(bCacheHit)++Profile->OwnerNoiseCacheHits; else ++Profile->OwnerNoiseCacheMisses; } if(!bCacheHit) { const bool bReusesPreviousUpper=Cache.Field==&Field &&Cache.SampleZ1==SampleZ0; Cache.Field=&Field; Cache.GridZ0=GridZ0; Cache.SampleZ0=SampleZ0; Cache.SampleZ1=SampleZ1; Cache.LowerCore=bReusesPreviousUpper?Cache.UpperCore :OwnerCaveRoughnessCore(X,Y,SampleZ0,Params); Cache.UpperCore=OwnerCaveRoughnessCore(X,Y,SampleZ1,Params); } const float Alpha=static_cast((Z-SampleZ0)/(SampleZ1-SampleZ0)); Total=FMath::Lerp(Cache.LowerCore,Cache.UpperCore,Alpha); } else Total=OwnerCaveRoughnessCore(X,Y,Z,Params); if(SurfaceSDF<0.0f) Total=std::lerp(Total,std::min(Total,0.0f),std::clamp(CaveInfluence,0.0f,1.0f)); const float Fade=1.0f-std::abs(SurfaceSDF)/Depth; return Total*Fade*Fade; } float OwnerCaveRoughnessForDetail(double X,double Y,double Z,float SurfaceSDF, const FLayerField& Field,float CaveInfluence, float PassageInfluence, FOwnerNoiseLineContext& LineContext, FDetailWorkerProfile* Profile=nullptr) const { constexpr float DirectStart=0.40f,DirectEnd=0.60f; constexpr int32 PassageAnchorStep=2; const float PassageWeight=std::clamp(PassageInfluence,0.0f,1.0f); if(PassageWeight>=DirectEnd) return OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field,CaveInfluence, LineContext,Profile,PassageAnchorStep); const float Anchored=OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field, CaveInfluence,LineContext,Profile); if(PassageWeight<=DirectStart)return Anchored; const float Direct=OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field, CaveInfluence,LineContext,Profile,PassageAnchorStep); const float DirectWeight=SmoothUnit((PassageWeight-DirectStart) /(DirectEnd-DirectStart)); const float AnchoredWeight=1.0f-DirectWeight; const float Normalization=1.0f/std::sqrt( AnchoredWeight*AnchoredWeight+DirectWeight*DirectWeight); return (AnchoredWeight*Anchored+DirectWeight*Direct)*Normalization; } static float SurfaceRelief(float X,float Y,float Z,uint32 Seed, float* OutPassageBandRelief=nullptr) { // Keep all active bands at four voxels or wider: the full-detail lattice can // resolve them without turning the surface into per-voxel steps. The broad // owner comparison showed that the shortest octave needed more usable relief. const float Macro=FStandaloneResearchTunnels::ValueNoise(X,Y,Z,32,Seed^0xB5297A4Du); const float Mid=FStandaloneResearchTunnels::ValueNoise(X,Y,Z,16,Seed^0x63D83595u); const float Fine=FStandaloneResearchTunnels::ValueNoise(X,Y,Z,4,Seed^0xA511E9B3u); if(OutPassageBandRelief) *OutPassageBandRelief=1.20f*Macro+(0.80f+0.25f*Macro)*(0.40f*Mid); return 1.20f*Macro+(0.80f+0.25f*Macro)*(0.40f*Mid+0.60f*Fine); } struct FValueNoiseLineContext { int32 X0=0,Y0=0,Z0=std::numeric_limits::min(); int32 Wavelength=1; uint32 Seed=0; float Inv=1.0f,TX=0.0f,TY=0.0f; std::array Corners{}; FValueNoiseLineContext(float X,float Y,int32 InWavelength,uint32 InSeed) :Wavelength(InWavelength),Seed(InSeed),Inv(1.0f/static_cast(InWavelength)) { const float FX=X*Inv,FY=Y*Inv; X0=FMath::FloorToInt(FX);Y0=FMath::FloorToInt(FY); TX=FStandaloneResearchTunnels::Fade(FX-static_cast(X0)); TY=FStandaloneResearchTunnels::Fade(FY-static_cast(Y0)); } void SetPlane(int32 Z,bool bUpper) { const size_t I=bUpper?4u:0u; Corners[I+0]=FStandaloneResearchTunnels::NoiseValue(X0,Y0,Z,Seed); Corners[I+1]=FStandaloneResearchTunnels::NoiseValue(X0+1,Y0,Z,Seed); Corners[I+2]=FStandaloneResearchTunnels::NoiseValue(X0,Y0+1,Z,Seed); Corners[I+3]=FStandaloneResearchTunnels::NoiseValue(X0+1,Y0+1,Z,Seed); } float Sample(float Z) { const float FZ=Z*Inv; const int32 NewZ0=FMath::FloorToInt(FZ); const float TZ=FStandaloneResearchTunnels::Fade(FZ-static_cast(NewZ0)); if(NewZ0!=Z0) { if(NewZ0==Z0+1) { Corners[0]=Corners[4];Corners[2]=Corners[6]; Corners[1]=Corners[5];Corners[3]=Corners[7]; SetPlane(NewZ0+1,true); } else { SetPlane(NewZ0,false);SetPlane(NewZ0+1,true); } Z0=NewZ0; } const float Z00=FMath::Lerp(Corners[0],Corners[1],TX); const float Z10=FMath::Lerp(Corners[2],Corners[3],TX); const float Z01=FMath::Lerp(Corners[4],Corners[5],TX); const float Z11=FMath::Lerp(Corners[6],Corners[7],TX); return FMath::Lerp(FMath::Lerp(Z00,Z10,TY),FMath::Lerp(Z01,Z11,TY),TZ); } }; struct FDetailNoiseLineContext { FValueNoiseLineContext Macro,Mid,Fine; FOwnerNoiseLineContext OwnerNoise; FDetailNoiseLineContext(float X,float Y,uint32 Seed) :Macro(X,Y,32,Seed^0xB5297A4Du),Mid(X,Y,16,Seed^0x63D83595u), Fine(X,Y,4,Seed^0xA511E9B3u) {} float SurfaceReliefAt(float Z,bool bMacroKnown=false,float KnownMacro=0.0f, float* OutPassageBandRelief=nullptr) { const float MacroValue=bMacroKnown?KnownMacro:Macro.Sample(Z); const float MidValue=Mid.Sample(Z); const float FineValue=Fine.Sample(Z); if(OutPassageBandRelief) *OutPassageBandRelief=1.20f*MacroValue +(0.80f+0.25f*MacroValue)*(0.40f*MidValue); return 1.20f*MacroValue+(0.80f+0.25f*MacroValue) *(0.40f*MidValue+0.60f*FineValue); } }; float DetailNoise(double X,double Y,double Z,float SurfaceSDF, float CaveInfluence,float FloorInfluence,float PassageInfluence) const { if(DetailVariant==EDetailVariant::Smooth)return 0.0f; if(DetailVariant==EDetailVariant::PerVoxel) { const int32 IX=static_cast(std::floor(X)),IY=static_cast(std::floor(Y)),IZ=static_cast(std::floor(Z)); return FStandaloneResearchTunnels::NoiseValue(IX,IY,IZ,static_cast(Owner->Seed)) *DetailSupremum; } float PassageBandRelief=0.0f; const float Relief=SurfaceRelief(static_cast(X),static_cast(Y), static_cast(Z),static_cast(Owner->Seed),&PassageBandRelief); const float PassageWeight=std::clamp(PassageInfluence,0.0f,1.0f); const FLayerField* Field=FindLayerField(Z); FOwnerNoiseLineContext OwnerNoise; float OwnerRelief=0.0f; if(Field) OwnerRelief=OwnerCaveRoughnessForDetail(X,Y,Z,SurfaceSDF,*Field, CaveInfluence,PassageWeight,OwnerNoise); const float Detail=std::clamp(BlendDetailFamilies(Relief,PassageBandRelief, OwnerRelief,CaveInfluence,FloorInfluence,PassageWeight),-3.9f,3.9f); return Detail; } float DetailNoiseOnLine(double X,double Y,double Z,float SurfaceSDF, float CaveInfluence,float FloorInfluence,float PassageInfluence, FDetailNoiseLineContext& NoiseLine, FDetailWorkerProfile* Profile=nullptr, const FLayerField* KnownField=nullptr, bool bFieldKnown=false,bool bMacroKnown=false, float KnownMacro=0.0f) const { if(DetailVariant!=EDetailVariant::Coherent) return DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence,PassageInfluence); using Clock=std::chrono::steady_clock; const auto ReliefStart=Profile?Clock::now():Clock::time_point{}; float PassageBandRelief=0.0f; const float Relief=NoiseLine.SurfaceReliefAt(static_cast(Z), bMacroKnown,KnownMacro,&PassageBandRelief); if(Profile) Profile->ReliefSampleNanoseconds+=static_cast( std::chrono::duration_cast(Clock::now()-ReliefStart).count()); const auto LookupStart=Profile?Clock::now():Clock::time_point{}; const FLayerField* Field=bFieldKnown?KnownField:FindLayerField(Z); if(Profile) Profile->LayerLookupSampleNanoseconds+=static_cast( std::chrono::duration_cast(Clock::now()-LookupStart).count()); const auto OwnerStart=Profile?Clock::now():Clock::time_point{}; if(Profile&&Field)++Profile->OwnerRoughnessFieldCalls; const float PassageWeight=std::clamp(PassageInfluence,0.0f,1.0f); float OwnerRelief=0.0f; if(Field) OwnerRelief=OwnerCaveRoughnessForDetail(X,Y,Z,SurfaceSDF,*Field, CaveInfluence,PassageWeight,NoiseLine.OwnerNoise,Profile); if(Profile) Profile->OwnerRoughnessSampleNanoseconds+=static_cast( std::chrono::duration_cast(Clock::now()-OwnerStart).count()); const float Detail=std::clamp(BlendDetailFamilies(Relief,PassageBandRelief, OwnerRelief,CaveInfluence,FloorInfluence,PassageWeight),-3.9f,3.9f); return Detail; } bool EvaluateFast(double X,double Y,double Z) const { ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f; float PassageInfluence=0.0f; float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF, &bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir,nullptr,false, &PassageInfluence); const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]&&Z>=Min[2]&&Z<=Max[2]; if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth &&std::abs(RockDistance)<=DetailSupremum+0.125f) { const float Fade=DetailBandFadeAt(X,Y,Z,RockDistance,SurfaceSDF, CaveInfluence,FloorInfluence); if(Fade>0.0f)RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence, FloorInfluence,PassageInfluence)*Fade; } if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f); return RockDistance>=0.0f; } bool ValidatePassageRoutes() { constexpr double SampleStep=0.5; uint32 PassageIndex=0; for(const FVoxelPassage& P:Owner->Manager->GetPassages()) { const auto& Points=P.ControlPoints; if(Points.Num()<2){++PassageIndex;continue;} uint64 RouteSamples=0,RouteFailures=0,RouteCritical=0; int32 FirstFailure[3]={0,0,0};bool bFirstFailureCritical=false; for(int32 I=0;I+1(std::ceil(L/SampleStep))); uint64 SegmentSamples=0,SegmentFailures=0,SegmentCritical=0; for(int32 J=0;J<=Count;++J) { const double T=static_cast(J)/Count; const double X=A.X+(B.X-A.X)*T,Y=A.Y+(B.Y-A.Y)*T,Z=A.Z+(B.Z-A.Z)*T; ++Stats.WalkSamples; ++RouteSamples;++SegmentSamples; const int32 VX=static_cast(std::lround(X)); const int32 VY=static_cast(std::lround(Y)); const int32 VZ=static_cast(std::lround(Z)); const bool bCritical=IsInsideCriticalAir(VX,VY,VZ); if(bCritical){++RouteCritical;++SegmentCritical;} if(IsRock(VX,VY,VZ)) { ++Stats.WalkFailures;++RouteFailures;++SegmentFailures; if(RouteFailures==1) {FirstFailure[0]=VX;FirstFailure[1]=VY;FirstFailure[2]=VZ;bFirstFailureCritical=bCritical;} } } if(SegmentFailures) std::fprintf(stderr,"[WalkabilitySegment] passage=%u segment=%d failures=%llu/%llu critical=%llu length=%.2f start=(%.2f,%.2f,%.2f) end=(%.2f,%.2f,%.2f)\n", PassageIndex,I,static_cast(SegmentFailures), static_cast(SegmentSamples), static_cast(SegmentCritical),L, A.X,A.Y,A.Z,B.X,B.Y,B.Z); } if(RouteFailures) std::fprintf(stderr,"[WalkabilityRoute] passage=%u type=%u points=%d failures=%llu/%llu critical_centerline=%llu first_solid=(%d,%d,%d) critical=%u\n", PassageIndex,static_cast(P.PassageType),Points.Num(), static_cast(RouteFailures), static_cast(RouteSamples), static_cast(RouteCritical),FirstFailure[0], FirstFailure[1],FirstFailure[2],bFirstFailureCritical?1u:0u); ++PassageIndex; } if(Stats.WalkFailures) std::fprintf(stderr,"[Walkability] FAIL %llu/%llu passage centerline samples are solid\n", static_cast(Stats.WalkFailures),static_cast(Stats.WalkSamples)); else std::printf("[Walkability] PASS %llu/%llu passage centerline samples remain open; native landing floors retained\n", static_cast(Stats.WalkSamples),static_cast(Stats.WalkSamples)); return Stats.WalkFailures==0; } static double DetailAutocorrelation(EDetailVariant Variant) { constexpr int32 N=24;double Sum=0.0,SumSq=0.0,PairA=0.0,PairB=0.0,PairAA=0.0,PairBB=0.0,PairAB=0.0;uint64 Count=0; auto Value=[&](int32 X,int32 Y,int32 Z)->double { if(Variant==EDetailVariant::PerVoxel)return FStandaloneResearchTunnels::NoiseValue(X,Y,Z,11); if(Variant==EDetailVariant::Smooth)return 0.0; return SurfaceRelief(static_cast(X),static_cast(Y),static_cast(Z),11); }; for(int32 Z=0;Z void ParallelLines(size_t Count,Fn&& Work,FParallelTiming* Timing=nullptr, std::array* WorkerActiveSeconds=nullptr,size_t WorkBatch=1) const { const auto WallStart=std::chrono::steady_clock::now(); if(WorkerCount<=1||Count<512||!Scheduler) { const auto WorkerStart=std::chrono::steady_clock::now(); for(size_t I=0;I)Work(I,0); else Work(I); } if(WorkerActiveSeconds) (*WorkerActiveSeconds)[0]=std::chrono::duration( std::chrono::steady_clock::now()-WorkerStart).count(); if(Timing) { const double WorkerSeconds=std::chrono::duration(std::chrono::steady_clock::now()-WorkerStart).count(); Timing->WallSeconds+=std::chrono::duration(std::chrono::steady_clock::now()-WallStart).count(); Timing->WorkerSeconds+=WorkerSeconds; } return; } WorkBatch=std::max(WorkBatch,1); using WorkType=std::remove_reference_t; struct FDispatchContext { WorkType* Work; } Dispatch{&Work}; const auto Invoke=[](void* Opaque,size_t Index,size_t Worker) { auto& Context=*static_cast(Opaque); if constexpr(std::is_invocable_v) (*Context.Work)(Index,static_cast(Worker)); else (*Context.Work)(Index); }; std::array ActiveSeconds{}; const double WorkerSeconds=Scheduler->parallel_for(Count,WorkBatch, static_cast(WorkerCount),&Dispatch,Invoke,ActiveSeconds.data(),ActiveSeconds.size()); if(WorkerActiveSeconds)*WorkerActiveSeconds=ActiveSeconds; if(Timing) { Timing->WallSeconds+=std::chrono::duration(std::chrono::steady_clock::now()-WallStart).count(); Timing->WorkerSeconds+=WorkerSeconds; } } void InvalidateFarFieldGpuInput() { std::lock_guard Lock(FarFieldGpuInputMutex); CachedFarFieldGpuInput.reset(); } FStandaloneOwnerWorld* Owner=nullptr; const VoxelForgeCore::WorldAssemblyManifest* WorldAssembly=nullptr; const FStandaloneShapeWorld* SharedDensitySource=nullptr; int32 WorkerCount=6; const VoxelForgeCore::JobScheduler* Scheduler=nullptr; bool RoundTunnelsEnabled=false; bool bProfileDetailBand=false; int32 Min[3]{},Max[3]{},ClipMin[3]{},ClipMax[3]{},Size[3]{}; FIntVector EditBoundsMin=FIntVector(MAX_int32,MAX_int32,MAX_int32); FIntVector EditBoundsMax=FIntVector(MIN_int32,MIN_int32,MIN_int32); int32 ActiveSpatialBinSize=LineBinSize; double ActiveBlendRadius=0.0; double ActiveFloorBlendRadius=0.0; size_t WordsPerZ=0; EDetailVariant DetailVariant=EDetailVariant::Coherent; bool Built=false; bool bNativeOnly=false; bool bUseNativeManifestFields=false; FIntVector NativeOnlyMinimum=FIntVector::ZeroValue; FIntVector NativeOnlyMaximum=FIntVector::ZeroValue; mutable std::atomic NativeOnlyViolations{0}; bool FarBuilt=false; std::vector> ResolvedBoundaryZIntervalsMicrometres; std::vector LayerRanges; std::vector LayerFields; std::vector Primitives; std::vector SharedFieldPrimitives; std::vector FarConnectorSegments; std::unordered_map,FBinHash> FarConnectorSpatialBins; std::vector EditBrushes; std::vector PassageVoidCrossings; std::array AxisBins; std::unordered_map SpatialBins; std::unordered_map CriticalAirBins; mutable std::mutex FarFieldGpuInputMutex; mutable std::shared_ptr CachedFarFieldGpuInput; std::vector RockWords; std::unique_ptr SurfaceDistanceQ; std::array,3> CandidateAxisWords; std::vector CandidateWords; std::vector GpuVisualDetailWords,CpuVisualDetailWords; uint64 GpuVisualDetailTileCount=0,CpuVisualDetailTileCount=0; uint64 GpuVisualDetailGridSamples=0,CpuVisualDetailGridSamples=0; uint64 GpuVisualDetailNodeCount=0,CpuVisualDetailNodeCount=0; FBuildStats Stats; bool bAnalyticOnly=false; inline static thread_local const FScopedPassageTileQueries* ActivePassageTileQueries=nullptr; std::vector PassageFeatureBounds; std::vector PassageTileFeatures; std::vector PassageFeatureOrder; std::vector PassageFeatureNodes; bool PassageFeatureBoundsFailOpen=false; std::vector> PassageNearestNodes; std::vector PassageProofBounds; std::vector PassageProofNodes; std::vector> PassageColumnSegmentFlags,PassageShortSpanSegmentFlags; std::vector PassageHasShortSpanFlags; };