diff --git a/Standalone/Research/OwnerShapeWorld/round2/chamber_final.png b/Standalone/Research/OwnerShapeWorld/round2/chamber_final.png new file mode 100644 index 0000000..64229ad Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/round2/chamber_final.png differ diff --git a/Standalone/Research/OwnerShapeWorld/round2/chamber_tuned_final.png b/Standalone/Research/OwnerShapeWorld/round2/chamber_tuned_final.png new file mode 100644 index 0000000..65d657d Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/round2/chamber_tuned_final.png differ diff --git a/Standalone/Research/OwnerShapeWorld/round2/far_final.png b/Standalone/Research/OwnerShapeWorld/round2/far_final.png new file mode 100644 index 0000000..837b13e Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/round2/far_final.png differ diff --git a/Standalone/Research/OwnerShapeWorld/round2/pass_floor_closeup_final.png b/Standalone/Research/OwnerShapeWorld/round2/pass_floor_closeup_final.png new file mode 100644 index 0000000..ab3223f Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/round2/pass_floor_closeup_final.png differ diff --git a/Standalone/Research/OwnerShapeWorld/round2/pass_floor_closeup_medium_final.png b/Standalone/Research/OwnerShapeWorld/round2/pass_floor_closeup_medium_final.png new file mode 100644 index 0000000..b2c197f Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/round2/pass_floor_closeup_medium_final.png differ diff --git a/Standalone/Research/OwnerShapeWorld/round2/passage_final.png b/Standalone/Research/OwnerShapeWorld/round2/passage_final.png new file mode 100644 index 0000000..a811994 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/round2/passage_final.png differ diff --git a/Standalone/Research/OwnerShapeWorld/round2/passage_floor_closeup_final.png b/Standalone/Research/OwnerShapeWorld/round2/passage_floor_closeup_final.png new file mode 100644 index 0000000..9064db5 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/round2/passage_floor_closeup_final.png differ diff --git a/Standalone/Research/OwnerShapeWorld/round2/passage_floor_closeup_near_final.png b/Standalone/Research/OwnerShapeWorld/round2/passage_floor_closeup_near_final.png new file mode 100644 index 0000000..f4a725d Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/round2/passage_floor_closeup_near_final.png differ diff --git a/Standalone/Research/OwnerShapeWorld/round2/passage_floor_natural_final.png b/Standalone/Research/OwnerShapeWorld/round2/passage_floor_natural_final.png new file mode 100644 index 0000000..1b687d6 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/round2/passage_floor_natural_final.png differ diff --git a/Standalone/Research/OwnerShapeWorld/round2/passage_floor_tuned_final.png b/Standalone/Research/OwnerShapeWorld/round2/passage_floor_tuned_final.png new file mode 100644 index 0000000..180a16a Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/round2/passage_floor_tuned_final.png differ diff --git a/Standalone/Research/OwnerShapeWorld/round2/tunnel_bend_closeup_final.png b/Standalone/Research/OwnerShapeWorld/round2/tunnel_bend_closeup_final.png new file mode 100644 index 0000000..1a7e88c Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/round2/tunnel_bend_closeup_final.png differ diff --git a/Standalone/Research/OwnerShapeWorld/round2/tunnel_final.png b/Standalone/Research/OwnerShapeWorld/round2/tunnel_final.png new file mode 100644 index 0000000..8fcffba Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/round2/tunnel_final.png differ diff --git a/Standalone/Source/StandaloneShapeWorld.h b/Standalone/Source/StandaloneShapeWorld.h index 2e96599..1d72f65 100644 --- a/Standalone/Source/StandaloneShapeWorld.h +++ b/Standalone/Source/StandaloneShapeWorld.h @@ -3,6 +3,7 @@ #include "OwnerWorldLoader.h" #include "StandaloneResearchTunnels.h" #include "VoxelCaveMorphology.h" +#include "VoxelNoise.h" #include "VoxelStrateManager.h" #include @@ -44,17 +45,32 @@ public: uint64 DetailFlips = 0; uint64 WalkSamples = 0; uint64 WalkFailures = 0; + double MaxCapsuleJoinRadiusDelta = 0.0; + double MaxCapsuleJoinFloorDelta = 0.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]{}; int64 Q[3][3]{}; int32 Min[3]{}; int32 Max[3]{}; float MinRadius = 1.0f; + float CapsuleStartRadius = 1.0f; + float CapsuleEndRadius = 1.0f; + double BlendRadius = 0.0; + double FloorBlendRadius = 0.0; bool bCarve = true; bool bCritical = false; + bool bCapsule = false; bool bRoundedBox = false; bool bHasFloorClip = false; double RoundedHalfExtent[3]{}; @@ -81,7 +97,8 @@ public: static constexpr int32 QScale = 1 << 28; static constexpr int32 SubPerVoxel = 16; static constexpr int32 DetailBand = 4; - static constexpr float DetailSupremum = 0.70f + 1.05f + 1.45f; + static constexpr float DetailSupremum = 4.0f; + static constexpr double JoinBlendRadius = 0.0; static constexpr int32 LineBinSize = 32; static constexpr int32 SpatialBinSize = 256; @@ -152,6 +169,22 @@ public: static_cast(Stats.DetailFlips), static_cast(Stats.WalkSamples-Stats.WalkFailures), static_cast(Stats.WalkSamples)); + const double SmoothUnionGrowthBound=JoinBlendRadius/6.0; + 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 floor_endpoint_max_delta=%.6f vox smooth_union_growth_bound=%.4f vox thresholds=(0.005,0.005,0.25) status=%s\n", + static_cast(Stats.CapsuleJoinCount),Stats.LegacySegmentRadiusStep, + Stats.SmoothedProfileRadiusStep,Stats.MaxCapsuleJoinRadiusDelta, + Stats.MaxCapsuleJoinFloorDelta,SmoothUnionGrowthBound, + Stats.MaxCapsuleJoinRadiusDelta<=0.005&&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; } @@ -179,7 +212,6 @@ public: // edge interpolation and normals to the existing mesher. float DensityAt(float X,float Y,float Z) const { - if(LayerRanges.empty()||ZLayerRanges[0].Hi)return 1.0f; int32 IX=0,IY=0,IZ=0; const bool bInteger=NearInteger(X,IX)&&NearInteger(Y,IY)&&NearInteger(Z,IZ); const bool bPacked=bInteger&&Contains(IX,IY,IZ); @@ -201,21 +233,17 @@ public: } return bRock?-(DetailBand+2.0f):(DetailBand+2.0f); } - const bool bSolid=IsInsideSolidOverlay(X,Y,Z); - const bool bCritical=IsInsideCriticalAir(X,Y,Z); - float Cave=CaveDistance(X,Y,Z); - if(Cave>=100000.0f)Cave=100000.0f; - if(bCritical)Cave=-std::max(1.0f,std::abs(Cave)); - else if(bSolid)Cave=std::max(1.0f,std::abs(Cave)); - else - { - 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(Cave)<=DetailSupremum+0.125f) - Cave+=DetailNoise(X,Y,Z); - } - float Density=-Cave; + ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; + bool bFloorSurface=false; + float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,&bFloorSurface); + const bool bInsideFine=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(bInsideFine&&DetailVariant!=EDetailVariant::Smooth + &&std::abs(RockDistance)<=DetailSupremum+0.125f) + RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,SurfaceFamily,bFloorSurface); + if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.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); return Density; @@ -262,15 +290,215 @@ public: 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?"PASS":"FAIL",bCoherent?"PASS":"FAIL"); - return bFlips&&bCoherent; + bFlips&&bFamilies?"PASS":"FAIL",bCoherent?"PASS":"FAIL"); + return bFlips&&bCoherent&&bFamilies; + } + + bool AuditSurfaceBandFamilies() 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{}; + 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; + const float Raw=EffectiveRockDistance(X,Y,Z,&Family,&SurfaceSDF,&bFloorSurface); + if(std::abs(Raw)>=0.5f)continue; + const float Detail=DetailNoise(X,Y,Z,SurfaceSDF,Family,bFloorSurface); + 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; + 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"); + return Tested>0&&FlatShare<20.0&&bPass; + } + + 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={-32.0f,-24.0f,-16.0f,-8.0f,0.0f,8.0f,16.0f,24.0f,32.0f}; + constexpr std::array PitchOffsets={-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) + { + ESurfaceFamily Family=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; + bool bFloorSurface=false; + float D=EffectiveRockDistance(X,Y,Z,&Family,&SurfaceSDF,&bFloorSurface); + 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) + {Detail=DetailNoise(X,Y,Z,SurfaceSDF,Family,bFloorSurface);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; + return D; + }; + uint64 TotalHits=0,TotalAxisLocked=0,TotalNearZero=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; + 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; + 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; + Distance(XH,YH,ZH,&Family,&Raw,&Detail,&bFloorSurface); + 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; + 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(YawOffset==0.0f&&PitchOffset==0.0f) + { + const char* Name=Family==ESurfaceFamily::Strata?"strata": + Family==ESurfaceFamily::Cave?"cave":"solid_overlay"; + std::printf("[SurfaceRay] t=%.2f vox hit=(%.2f,%.2f,%.2f) family=%s floor_surface=%s raw_sdf=%.4f detail=%.4f axis_dot=%.4f\n", + HitT,XH,YH,ZH,Name,bFloorSurface?"yes":"no",Raw,Detail,AxisDot); + } + bFound=true;break; + } + PreviousT=T;PreviousD=D; + } + (void)bFound; + } + const double NearZeroShare=TotalHits?static_cast(TotalNearZero)/TotalHits:1.0; + const double AxisShare=TotalHits?static_cast(TotalAxisLocked)/TotalHits:1.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 bool bDetailPass=TotalHits>=24&&std::sqrt(Variance)>=0.15&&NearZeroShare<0.50; + const bool bGridPass=TotalHits>=24&&AxisShare<0.60; + 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%% status=%s\n", + static_cast(TotalHits),AxisShare*100.0,bGridPass?"PASS":"FAIL"); + for(size_t I=0;I(S.Hits),M,V, + static_cast(S.NearZero),static_cast(S.AxisLocked)); + } + return bDetailPass&&bGridPass; } private: 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) { @@ -287,14 +515,32 @@ private: 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);} void BuildLayerBounds() { - LayerRanges.clear(); + LayerRanges.clear();LayerFields.clear(); const auto& Layout=Owner->Manager->GetLayout(); int32 Top=MIN_int32,Bottom=MAX_int32; for(const FStrateSlot& Slot:Layout) - {Top=std::max(Top,Slot.TopChunkZ);Bottom=std::min(Bottom,Slot.BottomChunkZ);} + { + Top=std::max(Top,Slot.TopChunkZ);Bottom=std::min(Bottom,Slot.BottomChunkZ); + if(!Slot.Definition)continue; + const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2; + FLayerField Field; + Field.Lo=Slot.BottomChunkZ*CHUNK_SIZE; + Field.Hi=(Slot.TopChunkZ+1)*CHUNK_SIZE; + 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*CHUNK_SIZE,(Top+1)*CHUNK_SIZE}); @@ -343,6 +589,7 @@ private: 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); } @@ -355,22 +602,52 @@ private: 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) + 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.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)); + 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}; @@ -393,11 +670,125 @@ private: const double FloorStep=(FloorB-FloorA)*(T1-T0); if(HorizontalSq>1.0e-6) ApplyFloorClip(Primitives.back(),Floor,FloorStep*DX/HorizontalSq, - FloorStep*DY/HorizontalSq); - else ApplyFloorClip(Primitives.back(),std::min(FloorA,FloorB)); + 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); + 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.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; + } + 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 static_cast(P.CapsuleStartRadius + +(P.CapsuleEndRadius-P.CapsuleStartRadius)*T); + } + + 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 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); } void AddFloorSegment(const FVector& A,const FVector& B,float Width) @@ -422,6 +813,8 @@ private: 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), @@ -430,21 +823,28 @@ private: { ++Stats.RoomCount; const size_t RoomPrimitiveStart=Primitives.size(); + const FVector RoomCenter=ApproximateUnwarp(R.Center,Params, + static_cast(Owner->Seed)); if(R.ShapeType==2) - AddCapsule(R.ShapeA,R.ShapeB,std::max(1.0f,R.ShapeR),true); + 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(R.Center,{std::abs(R.ShapeA.X),std::abs(R.ShapeA.Y),std::abs(R.ShapeA.Z)}, + 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(R.Center,{std::abs(R.ShapeA.X),std::abs(R.ShapeA.Y),std::abs(R.ShapeA.Z)}, + 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& Points=T.ControlPoints; - const TArray& Radii=T.ControlRadii; + 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; + 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))); + ++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); + float ZA=Points[I].Z-R0, ZB=Points[I+1].Z-R1; + if(FloorProfiles.IsValidIndex(I))ZA=FloorProfiles[I].StartFloorZ; + if(FloorProfiles.IsValidIndex(I))ZB=FloorProfiles[I].EndFloorZ; + AddTaperedCapsule(Points[I],Points[I+1],R0,R1,true,false,true,ZA,ZB, + I>0?Extension:0.0,I+2(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))); } } 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); @@ -485,29 +939,72 @@ private: {1,0,0},{0,1,0},{0,0,1},false); } } + const double PassageJoinBlend=ActiveBlendRadius;ActiveBlendRadius=JoinBlendRadius; + ActiveFloorBlendRadius=1.5; 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(); + for(int32 Join=1;Join+1(std::abs(std::max(RPrev,RHere)-std::max(RHere,RNext)))); + ++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); + float ZA=Points[I].Z-RadiusA,ZB=Points[I+1].Z-RadiusB; if(P.NativeFloorProfileZ.IsValidIndex(I))ZA=P.NativeFloorProfileZ[I]; if(P.NativeFloorProfileZ.IsValidIndex(I+1))ZB=P.NativeFloorProfileZ[I+1]; - AddCapsule(Points[I],Points[I+1],Radius,true,false,true,ZA,ZB); - AddCapsule(Points[I],Points[I+1],std::min(2.5f,Radius*0.45f), - true,true,true,ZA,ZB); + 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(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))); + } } } else AddCapsule(P.UpperPoint,P.LowerPoint,std::max(6.0f,P.Radius),true,true); AddLanding(P.UpperLanding,true); AddLanding(P.LowerLanding,true); } + ActiveBlendRadius=PassageJoinBlend;ActiveFloorBlendRadius=0.0; if(Owner->Settings->bOpenSurfaceEntry) { const double Radius=std::max(8.0f,Owner->Settings->OriginSpineRadius); @@ -535,6 +1032,7 @@ private: void BuildRoundTunnelPrimitives() { + const double PreviousBlendRadius=ActiveBlendRadius;ActiveBlendRadius=0.0; constexpr double Pitch=144.0,RadiusBase=28.0,HalfHeight=26.0; const int32 CX0=FloorDiv(Min[0],144)-1,CY0=FloorDiv(Min[1],144)-1; const int32 CX1=FloorDiv(Max[0],144)+1,CY1=FloorDiv(Max[1],144)+1; @@ -581,17 +1079,24 @@ private: for(int32 I=1;I<=Segments;++I) { const auto Next=Bezier(static_cast(I)/Segments); - AddCapsule(FVector(Last.first[0],Last.first[1],Last.first[2]), - FVector(Next.first[0],Next.first[1],Next.first[2]), - static_cast((Last.second+Next.second)*0.5),true); + 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(X1-X0+1)*(Y1-Y0+1)*(Z1-Z0+1)>100000)continue; for(int32 Z=Z0;Z<=Z1;++Z)for(int32 Y=Y0;Y<=Y1;++Y)for(int32 X=X0;X<=X1;++X) - SpatialBins[{X,Y,Z}].push_back(Id); + { + const FBinKey Key{X,Y,Z}; + SpatialBins[Key].push_back(Id); + if(bCriticalAir)CriticalAirBins[Key].push_back(Id); + } } } @@ -704,7 +1214,28 @@ private: bool ShapeContains(const FPrimitive& P,int32 X,int32 Y,int32 Z) const { bool bInside=false; - if(P.bRoundedBox) + 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], @@ -727,13 +1258,33 @@ private: { const double Floor=P.FloorCenterZ+P.FloorSlopeX*(X-P.CenterSub[0]/16.0) +P.FloorSlopeY*(Y-P.CenterSub[1]/16.0); - return Z>=Floor; + 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; } 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) + { + 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]; + } + 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=P.CapsuleStartRadius + +(P.CapsuleEndRadius-P.CapsuleStartRadius)*T; + 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], @@ -756,7 +1307,7 @@ private: 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) + if(!P.bRoundedBox&&!P.bCapsule) { double CoefA=static_cast(P.Q[Axis][Axis]); double CoefB=0.0,CoefC=-static_cast(QScale); @@ -797,14 +1348,48 @@ private: } 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; - for(int32 Delta=1;Delta<=2&&!Found;++Delta) + 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; @@ -901,7 +1486,7 @@ private: int32 Fixed[3]{};Fixed[A]=CA;Fixed[B]=CB; for(uint32 Id:GetLineCandidates(Axis,CA,CB)) { - const FPrimitive& P=Primitives[Id];if(!P.bCarve)continue; + 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); @@ -939,6 +1524,21 @@ private: } } } + 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); + } + } } void ApplyDetailBand() @@ -960,11 +1560,13 @@ private: const size_t Zi=W*64+Bit;if(Zi>=static_cast(Size[2]))continue; const int32 Z=Min[2]+static_cast(Zi); ++LocalCandidates; - if(ZLayerRanges[0].Hi)continue; - if(IsInsideSolidOverlay(X,Y,Z)||IsInsideCriticalAir(X,Y,Z))continue; - const float SDF=CaveDistance(X,Y,Z); + const bool bCriticalAir=IsInsideCriticalAir(X,Y,Z); + ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; + bool bFloorSurface=false; + const float SDF=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,&bFloorSurface); if(std::abs(SDF)>DetailSupremum+0.125f)continue; - const float Displaced=SDF+DetailNoise(X,Y,Z); + float Displaced=SDF+DetailNoise(X,Y,Z,SurfaceSDF,SurfaceFamily,bFloorSurface); + 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) @@ -1012,8 +1614,8 @@ private: bool IsInsideCriticalAir(int32 X,int32 Y,int32 Z) const { - const auto It=SpatialBins.find(PointBin(X,Y,Z)); - if(It==SpatialBins.end())return false; + 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; @@ -1036,10 +1638,24 @@ private: return false; } - bool IsInsideCriticalAir(double X,double Y,double Z) const + float SolidOverlayDistance(double X,double Y,double Z) const { const auto It=SpatialBins.find(PointBin(X,Y,Z)); - if(It==SpatialBins.end())return false; + 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; @@ -1056,29 +1672,143 @@ private: return false; } - float CaveDistance(double X,double Y,double Z) const + float CaveDistance(double X,double Y,double Z,bool* OutFloorSurface=nullptr) const { const auto It=SpatialBins.find(PointBin(X,Y,Z)); - if(It==SpatialBins.end())return 100000.0f; + if(It==SpatialBins.end()) + { + if(OutFloorSurface)*OutFloorSurface=false; + return 100000.0f; + } float Best=100000.0f; + bool bBestFloorSurface=false; 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); + bool bFloorInfluencesSurface=false; if(P.bHasFloorClip) { const double Floor=P.FloorCenterZ+P.FloorSlopeX*(X-P.CenterSub[0]/16.0) +P.FloorSlopeY*(Y-P.CenterSub[1]/16.0); - Distance=std::max(Distance,static_cast(Floor-Z)); + bFloorInfluencesSurface=OutFloorSurface!=nullptr + &&std::abs(Z-Floor)<=DetailBand+P.FloorBlendRadius; + Distance=SmoothMaxDistance(Distance,static_cast(Floor-Z), + static_cast(P.FloorBlendRadius)); + } + const float Combined=SmoothMinDistance(Best,Distance,static_cast(P.BlendRadius)); + if(Combined(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) const + { + const float Base=StrataDistance(Z); + bool bCaveFloorSurface=false; + const float Cave=CaveDistance(X,Y,Z,OutFloorSurface?&bCaveFloorSurface:nullptr); + const float Overlay=SolidOverlayDistance(X,Y,Z); + const float CarveOrOverlay=std::max(Cave,Overlay); + const ESurfaceFamily Family=Base<=CarveOrOverlay?ESurfaceFamily::Strata: + (Cave>=Overlay?ESurfaceFamily::Cave:ESurfaceFamily::SolidOverlay); + if(OutFamily) + *OutFamily=Family; + if(OutSurfaceSDF) + *OutSurfaceSDF=Family==ESurfaceFamily::Strata?Base: + (Family==ESurfaceFamily::Cave?Cave:Overlay); + if(OutFloorSurface)*OutFloorSurface=Family==ESurfaceFamily::Cave&&bCaveFloorSurface; + return std::min(Base,CarveOrOverlay); + } + + float OwnerCaveRoughness(double X,double Y,double Z,float SurfaceSDF, + const FStrateGenerationParams& Params, + bool bCaveSurface) const + { + const float Depth=Params.SurfaceRoughness*2.0f; + if(!(Depth>0.0f)||std::abs(SurfaceSDF)>=Depth||!(Params.RoughnessFrequency>0.0f))return 0.0f; + const uint32 Seed=static_cast(Owner->Seed); + const float RF=Params.RoughnessFrequency; + const float EffectiveZ=(Params.VerticalScale>0.0f&&Params.VerticalScale!=1.0f) + ?static_cast(Z)/Params.VerticalScale:static_cast(Z); + FVector3f Main(static_cast(X)*RF+VoxelHash::SeedOffset(Seed,11.3f), + static_cast(Y)*RF+VoxelHash::SeedOffset(Seed,13.7f), + EffectiveZ*RF+VoxelHash::SeedOffset(Seed,17.1f)); + FVector3f Fine(static_cast(X)*RF*3.0f+VoxelHash::SeedOffset(Seed,19.1f)+2000.0f, + static_cast(Y)*RF*3.0f+VoxelHash::SeedOffset(Seed,23.7f)+2500.0f, + EffectiveZ*RF*3.0f+VoxelHash::SeedOffset(Seed,29.3f)+3000.0f); + if(Params.DomainWarpStrength>0.0f) + { + const float WF=Params.DomainWarpFrequency,WS=Params.DomainWarpStrength; + const float WX=VoxelNoise::Perlin3D(FVector3f( + static_cast(X)*WF+VoxelHash::SeedOffset(Seed,5.2f), + static_cast(Y)*WF+VoxelHash::SeedOffset(Seed,1.3f), + EffectiveZ*WF+VoxelHash::SeedOffset(Seed,9.7f)))*VOXEL_NOISE_SCALE*WS; + const float WY=VoxelNoise::Perlin3D(FVector3f( + static_cast(X)*WF+100.0f+VoxelHash::SeedOffset(Seed,7.7f), + static_cast(Y)*WF+200.0f+VoxelHash::SeedOffset(Seed,3.1f), + EffectiveZ*WF+300.0f))*VOXEL_NOISE_SCALE*WS; + const float WZ=VoxelNoise::Perlin3D(FVector3f( + static_cast(X)*WF+400.0f, + static_cast(Y)*WF+500.0f+VoxelHash::SeedOffset(Seed,11.9f), + EffectiveZ*WF+600.0f+VoxelHash::SeedOffset(Seed,13.3f)))*VOXEL_NOISE_SCALE*WS; + Main+=FVector3f(WX,WY,WZ);Fine+=FVector3f(WX,WY,WZ); + } + float Rough=0.0f,FineValue=0.0f; + switch(Params.RoughnessNoiseType) + { + case EVoxelNoiseType::Ridged: + Rough=VoxelNoise::Ridged(Main.X,Main.Y,Main.Z,3); + FineValue=VoxelNoise::Ridged(Fine.X,Fine.Y,Fine.Z,2);break; + case EVoxelNoiseType::Mixed: + Rough=0.5f*(VoxelNoise::FBM(Main.X,Main.Y,Main.Z,3) + +VoxelNoise::Ridged(Main.X,Main.Y,Main.Z,3)); + FineValue=0.5f*(VoxelNoise::FBM(Fine.X,Fine.Y,Fine.Z,2) + +VoxelNoise::Ridged(Fine.X,Fine.Y,Fine.Z,2));break; + case EVoxelNoiseType::Cellular: + Rough=VoxelNoise::Cellular3D(Main);FineValue=VoxelNoise::Cellular3D(Fine);break; + case EVoxelNoiseType::FBM: + default: + Rough=VoxelNoise::FBM(Main.X,Main.Y,Main.Z,3); + FineValue=VoxelNoise::FBM(Fine.X,Fine.Y,Fine.Z,2);break; + } + float Total=(Rough+FineValue*0.4f)*VOXEL_NOISE_SCALE*Params.SurfaceRoughness; + if(bCaveSurface&&SurfaceSDF<0.0f)Total=std::min(Total,0.0f); + const float Fade=1.0f-std::abs(SurfaceSDF)/Depth; + return Total*Fade*Fade; + } + + static float SurfaceRelief(float X,float Y,float Z,uint32 Seed) + { + return 0.70f*FStandaloneResearchTunnels::ValueNoise(X,Y,Z,3,Seed^0xA511E9B3u) + +1.00f*FStandaloneResearchTunnels::ValueNoise(X,Y,Z,6,Seed^0x63D83595u) + +1.00f*FStandaloneResearchTunnels::ValueNoise(X,Y,Z,12,Seed^0xB5297A4Du); + } + + float DetailNoise(double X,double Y,double Z,float SurfaceSDF, + ESurfaceFamily SurfaceFamily,bool bFloorSurface=false) const { if(DetailVariant==EDetailVariant::Smooth)return 0.0f; if(DetailVariant==EDetailVariant::PerVoxel) @@ -1086,21 +1816,28 @@ private: 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; } - return FStandaloneResearchTunnels::WallRelief(static_cast(X),static_cast(Y), - static_cast(Z),static_cast(Owner->Seed)); + const float Relief=SurfaceRelief(static_cast(X), + static_cast(Y),static_cast(Z),static_cast(Owner->Seed)); + const FLayerField* Field=FindLayerField(Z); + const float OwnerRelief=Field?OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field->Params, + SurfaceFamily==ESurfaceFamily::Cave):0.0f; + const float Contrast=bFloorSurface?2.40f: + (SurfaceFamily==ESurfaceFamily::Cave?0.55f:1.35f); + return std::clamp((Relief+OwnerRelief*0.5f)*Contrast,-3.9f,3.9f); } bool EvaluateFast(double X,double Y,double Z) const { - if(LayerRanges.empty()||ZLayerRanges[0].Hi)return false; - if(IsInsideCriticalAir(X,Y,Z))return false; - if(IsInsideSolidOverlay(X,Y,Z))return true; - const float Cave=CaveDistance(X,Y,Z); - if(Cave>=100000.0f)return true; + const bool bCriticalAir=IsInsideCriticalAir(X,Y,Z); + ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; + bool bFloorSurface=false; + float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,&bFloorSurface); 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(Cave)<=DetailBand - &&DetailNoise(X,Y,Z)>-Cave)return true; - return Cave>=0.0f; + if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth + &&std::abs(RockDistance)<=DetailSupremum+0.125f) + RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,SurfaceFamily,bFloorSurface); + if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f); + return RockDistance>=0.0f; } void ValidatePassageRoutes() @@ -1141,7 +1878,7 @@ private: { if(Variant==EDetailVariant::PerVoxel)return FStandaloneResearchTunnels::NoiseValue(X,Y,Z,11); if(Variant==EDetailVariant::Smooth)return 0.0; - return FStandaloneResearchTunnels::WallRelief(static_cast(X),static_cast(Y),static_cast(Z),11); + return SurfaceRelief(static_cast(X),static_cast(Y),static_cast(Z),11); }; for(int32 Z=0;Z LayerRanges; + std::vector LayerFields; std::vector Primitives; std::array AxisBins; std::unordered_map,FBinHash> SpatialBins; + std::unordered_map,FBinHash> CriticalAirBins; std::vector RockWords; std::unique_ptr SurfaceDistanceQ; std::array,3> CandidateAxisWords; diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp index ea1883e..b369e56 100644 --- a/Standalone/Source/StandaloneViewer.cpp +++ b/Standalone/Source/StandaloneViewer.cpp @@ -1286,13 +1286,16 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; } int RunOwnerShapeComparison(FStandaloneOwnerWorld& World,const FStandaloneShapeWorld& Shape, - V3 CameraPosition,int32 Workers,int32 LevelFilter,int32 TileLimit) + V3 CameraPosition,float CameraYaw,float CameraPitch, + int32 Workers,int32 LevelFilter,int32 TileLimit) { if(FStandaloneShapeWorldAccess::GetActive()!=&Shape) {std::fprintf(stderr,"Shape comparison requires the compiled interval world to be active.\n");return 11;} if(LevelFilter>=World.FullResClipLevels&&!Shape.HasFarWorld()) const_cast(Shape).BuildFarWorld(); const FVector CameraVoxels(CameraPosition.X*4.0,CameraPosition.Y*4.0,CameraPosition.Z*4.0); + Shape.AuditViewSurface(CameraPosition.X,CameraPosition.Y,CameraPosition.Z, + CameraYaw,CameraPitch); { const int32 Offsets[][3]={{0,0,0},{4,0,0},{-4,0,0},{0,4,0},{0,-4,0},{0,0,4},{0,0,-4}}; for(const auto& Offset:Offsets) @@ -1779,9 +1782,14 @@ int main(int Argc,char** Argv) if(App.bShapeWorld&&(App.bShapeAuditOnly||App.bShapeCompare||bStreamBenchmark||BenchmarkLevel>=0) &&!App.BuildShapeWorldForCamera(Error,bNeedFarShapeWorld)) {std::fprintf(stderr,"Shape interval build failed: %s\n",Error.c_str());return 2;} - if(App.bShapeAuditOnly)return App.ShapeWorld->IsDetailAuditPassing()?0:7; + if(App.bShapeAuditOnly) + { + const bool bSurfacePassing=App.ShapeWorld->AuditViewSurface(App.Position.X,App.Position.Y, + App.Position.Z,App.Yaw,App.Pitch); + return App.ShapeWorld->IsDetailAuditPassing()&&bSurfacePassing?0:7; + } if(App.bShapeCompare) - return RunOwnerShapeComparison(*App.World,*App.ShapeWorld,App.Position,App.WorkerCount, + return RunOwnerShapeComparison(*App.World,*App.ShapeWorld,App.Position,App.Yaw,App.Pitch,App.WorkerCount, ShapeCompareLevel,ShapeCompareTileLimit); if(bRoundWallCompare) return RunRoundWallComparison(*App.World,App.Position,RoundCompareTileCount);