From b3af0e589e2578cc9f92952be679f748cc791b94 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Tue, 6 Oct 2026 00:17:14 +0200 Subject: [PATCH] wip(standalone): owner LOD0 defects -- skirt curtains clipped, exact edge roots; interrupted (OpenAI capacity) Not verified as a whole. In the tree when Luna's session ended ("model at capacity"): - fins: LOD seam skirts could hang into tunnel air; skirt triangles are now clipped against the solid half-space and carry a per-face mask so a host can hide skirts between same-LOD neighbours. - choppy LOD0 stairs: marching cubes' linear edge interpolation misplaced vertices where the relief bends within one cell; edge crossings corrected with the exact field (82 patches, midpoint-edge errors 18,818 -> 3,288); normal halo now ignores the +-4.125 fallback samples. - an explicit unclamped Core distance query for audits. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- .../include/VoxelForgeCore/VoxelForgeCore.h | 13 + Standalone/Core/src/ShapeWorldInternal.h | 126 +- Standalone/Core/src/VoxelForgeCore.cpp | 10 + Standalone/Source/StandaloneViewer.cpp | 1025 ++++++++++++++++- Standalone/Source/StreamingWorld.h | 14 + .../Private/VoxelMarchingCubesMesher.cpp | 174 ++- .../Source/VoxelForge/Public/VoxelTypes.h | 4 + 7 files changed, 1304 insertions(+), 62 deletions(-) diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index d976f1e..61ba896 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -56,6 +56,9 @@ namespace VoxelForgeCore TileKey key; std::vector vertices; std::vector indices; + // One entry per triangle. Zero is a surface triangle; skirt entries contain the + // boundary-face bit mask (bit order: -X,+X,-Y,+Y,-Z,+Z). + std::vector skirtFaceMasks; std::vector debugDensitySamples; MeshGroups groups; std::uint64_t densitySampleCount = 0; @@ -148,6 +151,13 @@ namespace VoxelForgeCore std::uint64_t walkFailures = 0; }; + struct DetailFieldSample + { + float displacement = 0.0f; + float caveInfluence = 0.0f; + float floorInfluence = 0.0f; + }; + struct TileRegionCompileStats { std::size_t regionsBuilt = 0; @@ -229,6 +239,9 @@ namespace VoxelForgeCore ShapeBuildStats buildStats() const; float densityAt(Vec3d voxelPosition) const; + // Analytic source distance without the occupancy-preserving near-zero clamp or packed proxy. + float unclampedDensityAt(Vec3d voxelPosition) const; + DetailFieldSample detailSampleAt(Vec3d voxelPosition) const; float densityAtLattice(Vec3i voxelPosition) const; bool isSolidAt(Vec3d voxelPosition) const; bool isSolidAlongSegment(Vec3d start, Vec3d end, double sampleSpacingVoxels = 0.5) const; diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 7202558..4102725 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -313,7 +313,7 @@ public: 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)); + SurfaceDistanceQ=std::make_unique_for_overwrite(static_cast(NodeCount)); BuildPackedRockIntervals(Stats.RockIntervalLines); BuildCandidateBands(Stats.CandidateAxisLines,Stats.CandidateMergeLines, Stats.CandidateMergeSeconds); @@ -348,7 +348,7 @@ public: Stats.DetailBandLines.WorkerSeconds*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(int8))/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), @@ -364,7 +364,7 @@ public: static_cast(Stats.SignNodes), static_cast(WordBytes)/1048576.0, static_cast(Stats.SurfaceProxyNodes), - static_cast(Stats.SignNodes*sizeof(int8))/1048576.0,WorkerCount); + 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, @@ -539,6 +539,24 @@ public: static_cast(Z)); } + float DensityAtUnclamped(float X,float Y,float Z) const + { + return AnalyticDensityAt(X,Y,Z,false); + } + + VoxelForgeCore::DetailFieldSample DetailSampleAt(float X,float Y,float Z) const + { + VoxelForgeCore::DetailFieldSample 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, @@ -578,11 +596,11 @@ public: float Density; if(CandidateWords[Word]&Mask) { - const int8 Proxy=SurfaceDistanceQ[NodeBase + const int16 Proxy=SurfaceDistanceQ[NodeBase +static_cast(Lane*XStep)]; - Density=static_cast(Proxy)*(1.0f/16.0f); + 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/16.0f); + Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),1.0f/256.0f); } else Density=bRock?-ProxyBandEdge:ProxyBandEdge; @@ -607,10 +625,10 @@ private: if(CandidateWords[Word]&Mask) { const size_t Node=Zi*static_cast(Size[1])*Size[0]+Column; - const int8 Proxy=SurfaceDistanceQ[Node]; - float Density=static_cast(Proxy)*(1.0f/16.0f); + 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/16.0f); + Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),1.0f/256.0f); return ApplyEditBrushes(Density,FX,FY,FZ); } // Match the fixed fallback to the edge of the stored proxy band. The @@ -620,8 +638,14 @@ private: return ApplyEditBrushes(bRock?-ProxyBandEdge:ProxyBandEdge,FX,FY,FZ); } - float AnalyticDensityAt(float X,float Y,float Z) const + float AnalyticDensityAt(float X,float Y,float Z,bool bClampNearZero=true, + float* OutDetailDisplacement=nullptr, + float* OutCaveInfluence=nullptr, + float* OutFloorInfluence=nullptr) const { + if(OutDetailDisplacement)*OutDetailDisplacement=0.0f; + if(OutCaveInfluence)*OutCaveInfluence=0.0f; + if(OutFloorInfluence)*OutFloorInfluence=0.0f; const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1] &&Z>=Min[2]&&Z<=Max[2]; if(!bInsideFine) @@ -631,8 +655,9 @@ private: if(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); + 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); return ApplyEditBrushes(Density,X,Y,Z); } ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; @@ -641,19 +666,27 @@ private: 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)RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence, - FloorInfluence,PassageInfluence)*Fade; + 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||std::abs(Density)<0.05f) - Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),0.05f); + 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); return ApplyEditBrushes(Density,X,Y,Z); } @@ -923,7 +956,9 @@ public: 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) + 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; @@ -948,6 +983,9 @@ public: 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; @@ -982,7 +1020,9 @@ public: 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; - Distance(XH,YH,ZH,&Family,&Raw,&Detail,&bFloorSurface,&PassageInfluence); + 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); @@ -1036,8 +1076,19 @@ public: { 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 passage_influence=%.3f raw_sdf=%.4f detail=%.4f axis_dot=%.4f\n", - HitT,XH,YH,ZH,Name,bFloorSurface?"yes":"no",PassageInfluence,Raw,Detail,AxisDot); + 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; } @@ -1245,6 +1296,12 @@ private: 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::clamp(FloorInfluence,0.0f,1.0f); + return 1.35f-0.80f*Cave+1.85f*Floor; + } static constexpr float DetailBandInnerRadius=1.5f; static float DetailBandFade(float SignedDistance,float Edge) { @@ -1271,8 +1328,7 @@ private: } const float ReliefBound=std::abs(MacroValue) +(0.80f+0.25f*MacroValue)*(0.85f+0.18f); - const float Contrast=1.35f-0.80f*std::clamp(CaveInfluence,0.0f,1.0f) - +1.85f*std::clamp(FloorInfluence,0.0f,1.0f); + 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); } @@ -3040,10 +3096,10 @@ private: void StoreSurfaceDistance(size_t Node,float Density) { - int32 Q=static_cast(std::lround(Density*16.0f)); - Q=std::clamp(Q,-127,127); + 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); + SurfaceDistanceQ[Node]=static_cast(Q); } bool IsInsideSolidOverlay(int32 X,int32 Y,int32 Z) const @@ -3727,8 +3783,8 @@ private: static float SurfaceRelief(float X,float Y,float Z,uint32 Seed) { // The low band carries the broad rock shapes and also gently varies mid-band - // amplitude. The quieter 6-voxel octave keeps fine relief without restoring - // the same tight bubble pattern on every exposed surface. + // amplitude. Keep the finest octave wider than a few marching-cubes cells so + // it does not alias into voxel-sized ledges on the full-detail surface. 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,6,Seed^0xA511E9B3u); @@ -3794,7 +3850,7 @@ private: FOwnerNoiseLineContext OwnerNoise; FDetailNoiseLineContext(float X,float Y,uint32 Seed) :Macro(X,Y,32,Seed^0xB5297A4Du),Mid(X,Y,16,Seed^0x63D83595u), - Fine(X,Y,6,Seed^0xA511E9B3u) + Fine(X,Y,6,Seed^0xA511E9B3u) {} float SurfaceReliefAt(float Z,bool bMacroKnown=false,float KnownMacro=0.0f) @@ -3827,9 +3883,9 @@ private: if(Field) OwnerRelief=OwnerCaveRoughnessForDetail(X,Y,Z,SurfaceSDF,*Field, CaveInfluence,PassageWeight,OwnerNoise); - const float Contrast=1.35f-0.80f*std::clamp(CaveInfluence,0.0f,1.0f) - +1.85f*std::clamp(FloorInfluence,0.0f,1.0f); - return std::clamp((Relief+OwnerRelief*0.5f)*Contrast,-3.9f,3.9f); + const float Contrast=DetailContrast(CaveInfluence,FloorInfluence); + const float Detail=std::clamp((Relief+OwnerRelief*0.5f)*Contrast,-3.9f,3.9f); + return Detail; } float DetailNoiseOnLine(double X,double Y,double Z,float SurfaceSDF, @@ -3864,9 +3920,9 @@ private: if(Profile) Profile->OwnerRoughnessSampleNanoseconds+=static_cast( std::chrono::duration_cast(Clock::now()-OwnerStart).count()); - const float Contrast=1.35f-0.80f*std::clamp(CaveInfluence,0.0f,1.0f) - +1.85f*std::clamp(FloorInfluence,0.0f,1.0f); - return std::clamp((Relief+OwnerRelief*0.5f)*Contrast,-3.9f,3.9f); + const float Contrast=DetailContrast(CaveInfluence,FloorInfluence); + const float Detail=std::clamp((Relief+OwnerRelief*0.5f)*Contrast,-3.9f,3.9f); + return Detail; } bool EvaluateFast(double X,double Y,double Z) const @@ -4012,7 +4068,7 @@ private: std::unordered_map SpatialBins; std::unordered_map CriticalAirBins; std::vector RockWords; - std::unique_ptr SurfaceDistanceQ; + std::unique_ptr SurfaceDistanceQ; std::array,3> CandidateAxisWords; std::vector CandidateWords; FBuildStats Stats; diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index 8fd7bd1..398544a 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -128,6 +128,8 @@ namespace VoxelForgeCore { AddWord(hash,color.R,1); AddWord(hash,color.G,1); AddWord(hash,color.B,1); AddWord(hash,color.A,1); } AddWord(hash,static_cast(mesh.Triangles.Num()),8); for(const int32 index:mesh.Triangles)AddWord(hash,static_cast(index),4); + AddWord(hash,static_cast(mesh.TriangleSkirtFaceMasks.Num()),8); + for(const uint8 faces:mesh.TriangleSkirtFaceMasks)AddWord(hash,faces,1); AddWord(hash,static_cast(mesh.NumSurfaceVertices),4); AddWord(hash,static_cast(mesh.NumSkirtTriangles),4); AddWord(hash,static_cast(mesh.NumCeilingTriangles),4); @@ -502,6 +504,10 @@ namespace VoxelForgeCore float World::densityAt(Vec3d position) const { return impl_&&impl_->shape?impl_->shape->DensityAt(static_cast(position.x),static_cast(position.y),static_cast(position.z)):0.0f; } + float World::unclampedDensityAt(Vec3d position) const + { return impl_&&impl_->shape?impl_->shape->DensityAtUnclamped(static_cast(position.x),static_cast(position.y),static_cast(position.z)):0.0f; } + DetailFieldSample World::detailSampleAt(Vec3d position) const + { return impl_&&impl_->shape?impl_->shape->DetailSampleAt(static_cast(position.x),static_cast(position.y),static_cast(position.z)):DetailFieldSample{}; } float World::densityAtLattice(Vec3i position) const { return impl_&&impl_->shape?impl_->shape->DensityAtLattice(position.x,position.y,position.z):0.0f; } bool World::isSolidAt(Vec3d position) const { return densityAt(position)<0.0f; } @@ -614,12 +620,16 @@ namespace VoxelForgeCore +static_cast(mesh.Normals.GetAllocatedSize()) +static_cast(mesh.UVs.GetAllocatedSize()) +static_cast(mesh.Colors.GetAllocatedSize()) + +static_cast(mesh.TriangleSkirtFaceMasks.GetAllocatedSize()) +static_cast(mesh.Triangles.GetAllocatedSize()); out.groups.surfaceVertexCount=static_cast(mesh.NumSurfaceVertices); out.groups.ceilingTriangleCount=static_cast(mesh.NumCeilingTriangles); out.groups.groundTriangleCount=static_cast( std::max(0,mesh.Triangles.Num()/3-mesh.NumCeilingTriangles)); out.groups.skirtTriangleCount=static_cast(mesh.NumSkirtTriangles); + out.skirtFaceMasks.assign(mesh.TriangleSkirtFaceMasks.begin(), + mesh.TriangleSkirtFaceMasks.end()); + out.skirtFaceMasks.resize(mesh.Triangles.Num()/3,0); out.vertices.reserve(static_cast(mesh.Vertices.Num())); for(int32 i=0;i::max(); }; + struct FMeshArtifactAudit + { + uint64 Tiles=0,SurfaceVertices=0,EdgeVertices=0,MidpointVertices=0; + uint64 SkirtTriangles=0,HiddenSameLodSkirtTriangles=0,ExposedSkirtTriangles=0, + ReverseSideSolidSkirtTriangles=0,RepeatedOffsetPatches=0,FieldMismatchPatches=0, + NormalMismatchPatches=0,GradientFallbackPatches=0,GradientFallbackSamples=0, + DetailDisplacementSamples=0,HighDetailDisplacementSamples=0,DetailDisplacementPatches=0, + CurvedEdgeChecks=0,CurvedEdgeRootPatches=0,CurvedEdgeRootBadVertices=0; + double MaximumAirSkirtDensity=0.0,MaximumPatchDominance=0.0; + double MaximumDetailDisplacement=0.0; + double MaximumCurvedEdgeRootError=0.0; + uint64 FieldEdgeChecks=0,AnalyticCrossingMisses=0,OldInt8RangeEdges=0,NormalChecks=0; + double SumAbsSourceResidual=0.0,MaximumAbsSourceResidual=0.0, + SumAnalyticEdgeShiftVoxels=0.0,MaximumAnalyticEdgeShiftVoxels=0.0, + SumNormalAgreement=0.0,MinimumNormalAgreement=1.0; + std::array WorstShiftPosition{},WorstResidualPosition{}; + double WorstShiftFraction=0.0,WorstShiftD0=0.0,WorstShiftD1=0.0, + WorstShiftExactD0=0.0,WorstShiftExactD1=0.0; + int32 WorstShiftAxis=-1; + std::array OffsetHistogram{}; + }; + + uint8 ComputeVisibleSkirtFaceMask(const FVoxelTileKey& Tile, + const std::unordered_set& Desired) + { + const FIntVector Origin=Tile.OriginVoxels(); + const int32 Extent=Tile.ExtentVoxels(); + const auto FloorDiv=[](int64 Value,int64 Divisor) + { + int64 Quotient=Value/Divisor; + if(Value%Divisor<0)--Quotient; + return static_cast(Quotient); + }; + uint8 Mask=0; + for(int32 Axis=0;Axis<3;++Axis) + for(int32 Side=0;Side<2;++Side) + { + int64 Probe[3]={Origin.X+Extent/2,Origin.Y+Extent/2,Origin.Z+Extent/2}; + Probe[Axis]=Side?static_cast((&Origin.X)[Axis])+Extent + :static_cast((&Origin.X)[Axis])-1; + int32 NeighborLevel=-1; + for(int32 Level=0;Level<=8;++Level) + { + const int64 NeighborExtent=static_cast(CHUNK_SIZE)<(1u<<(Axis*2+Side)); + } + return Mask; + } + + FMeshArtifactAudit AuditMeshArtifacts(const FStandaloneCpuTile& Tile, + const VoxelForgeCore::World& Shape,double SkirtDepthMeters,uint8 VisibleSkirtFaceMask, + bool bPrintSkirts,bool bPrintPatches) + { + FMeshArtifactAudit Result; + if(Tile.Key.Level<0||Tile.Key.Level>1||Tile.Vertices.empty()||Tile.Indices.size()<3) + return Result; + ++Result.Tiles; + const size_t VertexCount=Tile.Vertices.size(); + const size_t SurfaceVertices=std::min(Tile.SurfaceVertexCount,VertexCount); + Result.SurfaceVertices=SurfaceVertices; + const int32 Step=1<; // interpolated axis, then the 8-voxel block xyz + struct FFieldBlock + {uint64 Vertices=0,BadVertices=0;double MaximumShift=0.0,MaximumResidual=0.0; + std::array Position{};}; + struct FNormalBlock + {uint64 Vertices=0,BadVertices=0;double MinimumAgreement=1.0; + std::array Position{},MeshNormal{};}; + struct FGradientFallbackBlock + {uint64 Vertices=0,BadVertices=0,BadSamples=0;double MaximumError=0.0; + std::array Position{};}; + struct FCurvedEdgeRootBlock + {uint64 Vertices=0,BadVertices=0;double MaximumError=0.0; + std::array Position{};}; + std::map> OffsetBlocks; + std::map FieldBlocks; + std::map NormalBlocks; + std::map GradientFallbackBlocks; + std::map CurvedEdgeRootBlocks; + struct FDetailDisplacementBlock + {uint64 Vertices=0,HighVertices=0;double Maximum=0.0;std::array Position{};}; + std::map,FDetailDisplacementBlock> DetailDisplacementBlocks; + std::map> FirstPositionByBlock; + std::map> FirstNormalByBlock; + uint64 RayTileResidualSamples=0,RayTileResidualGt01=0, + RayTileResidualGt025=0,RayTileResidualGt05=0; + double RayTileResidualSum=0.0,RayTileResidualMaximum=0.0; + std::array RayTileResidualMaximumPosition{}; + uint64 RayTileInteriorSamples=0,RayTileInteriorGt0125=0, + RayTileInteriorGt025=0,RayTileInteriorGt05=0,RayTileInteriorGt1=0; + double RayTileInteriorResidualSum=0.0,RayTileInteriorResidualMaximum=0.0; + std::array RayTileInteriorMaximumPosition{}; + for(size_t I=0;I=0.60f&&DetailSample.floorInfluence>=0.60f) + { + const double DetailDisplacement=std::abs(DetailSample.displacement); + ++Result.DetailDisplacementSamples; + Result.MaximumDetailDisplacement=std::max( + Result.MaximumDetailDisplacement,DetailDisplacement); + const std::array DetailKey{ + static_cast(std::floor((P[0]-Origin.X)/8.0)), + static_cast(std::floor((P[1]-Origin.Y)/8.0)), + static_cast(std::floor((P[2]-Origin.Z)/8.0))}; + FDetailDisplacementBlock& DetailBlock=DetailDisplacementBlocks[DetailKey]; + ++DetailBlock.Vertices; + if(DetailDisplacement>0.65) + { + ++DetailBlock.HighVertices; + if(DetailDisplacement>DetailBlock.Maximum) + { + DetailBlock.Maximum=DetailDisplacement; + DetailBlock.Position={P[0],P[1],P[2]}; + } + ++Result.HighDetailDisplacementSamples; + } + } + } + double Grid[3];int32 VariableAxis=-1; + for(int32 Axis=0;Axis<3;++Axis) + { + Grid[Axis]=(P[Axis]-(&Origin.X)[Axis])/Step; + if(std::abs(Grid[Axis]-std::round(Grid[Axis]))>0.003) + { + if(VariableAxis>=0){VariableAxis=-2;break;} + VariableAxis=Axis; + } + } + if(VariableAxis<0)continue; + double Fraction=Grid[VariableAxis]-std::floor(Grid[VariableAxis]); + if(Fraction<0.003||Fraction>0.997)continue; + ++Result.EdgeVertices; + const int32 CoarseBin=std::clamp(static_cast(Fraction*16.0),0,15); + const int32 PreciseBin=std::clamp(static_cast(std::lround(Fraction*256.0)),0,255); + ++Result.OffsetHistogram[CoarseBin]; + if(CoarseBin==7||CoarseBin==8)++Result.MidpointVertices; + double EdgeA[3],EdgeB[3]; + for(int32 Axis=0;Axis<3;++Axis) + { + const int32 Cell=static_cast(std::floor(Grid[Axis])); + EdgeA[Axis]=(&Origin.X)[Axis]+Cell*Step; + EdgeB[Axis]=EdgeA[Axis]; + } + EdgeB[VariableAxis]+=Step; + const double D0=Shape.densityAt({EdgeA[0],EdgeA[1],EdgeA[2]}); + const double D1=Shape.densityAt({EdgeB[0],EdgeB[1],EdgeB[2]}); + const double ExactD0=Shape.unclampedDensityAt( + {EdgeA[0]+0.013,EdgeA[1]+0.013,EdgeA[2]+0.013}); + const double ExactD1=Shape.unclampedDensityAt( + {EdgeB[0]+0.013,EdgeB[1]+0.013,EdgeB[2]+0.013}); + const double ExactDenominator=ExactD1-ExactD0; + ++Result.FieldEdgeChecks; + if(std::abs(D0)>127.0/256.0||std::abs(D1)>127.0/256.0) + ++Result.OldInt8RangeEdges; + const bool bExactCrossing=(ExactD0<0.0&&ExactD1>0.0) + ||(ExactD0>0.0&&ExactD1<0.0); + double AnalyticShift=0.0; + if(bExactCrossing&&std::abs(ExactDenominator)>1.0e-9) + { + const double ExactT=std::clamp(-ExactD0/ExactDenominator,0.0,1.0); + AnalyticShift=std::abs(Fraction-ExactT)*Step; + Result.SumAnalyticEdgeShiftVoxels+=AnalyticShift; + if(AnalyticShift>Result.MaximumAnalyticEdgeShiftVoxels) + { + Result.MaximumAnalyticEdgeShiftVoxels=AnalyticShift; + Result.WorstShiftPosition={P[0],P[1],P[2]}; + Result.WorstShiftFraction=Fraction;Result.WorstShiftD0=D0; + Result.WorstShiftD1=D1;Result.WorstShiftExactD0=ExactD0; + Result.WorstShiftExactD1=ExactD1;Result.WorstShiftAxis=VariableAxis; + } + } + else ++Result.AnalyticCrossingMisses; + if(Tile.Key.Level==0&&Step==1&&bExactCrossing + &&Fraction>=0.375&&Fraction<=0.625) + { + const double MidExact=Shape.unclampedDensityAt({ + (EdgeA[0]+EdgeB[0])*0.5+0.013, + (EdgeA[1]+EdgeB[1])*0.5+0.013, + (EdgeA[2]+EdgeB[2])*0.5+0.013}); + double CurveRootT=0.5; + if(std::abs(MidExact)>1.0e-8) + { + if((MidExact<0.0)==(ExactD0<0.0)) + { + const double Denominator=ExactD1-MidExact; + if(std::abs(Denominator)>1.0e-9) + CurveRootT=0.5+0.5*std::clamp(-MidExact/Denominator,0.0,1.0); + } + else + { + const double Denominator=MidExact-ExactD0; + if(std::abs(Denominator)>1.0e-9) + CurveRootT=0.5*std::clamp(-ExactD0/Denominator,0.0,1.0); + } + } + const double CurveError=std::abs(Fraction-CurveRootT); + ++Result.CurvedEdgeChecks; + Result.MaximumCurvedEdgeRootError=std::max( + Result.MaximumCurvedEdgeRootError,CurveError); + FBlockKey RootKey{};RootKey[0]=VariableAxis; + for(int32 Axis=0;Axis<3;++Axis) + RootKey[Axis+1]=static_cast(std::floor( + (P[Axis]-(&Origin.X)[Axis])/8.0)); + FCurvedEdgeRootBlock& RootBlock=CurvedEdgeRootBlocks[RootKey]; + ++RootBlock.Vertices; + if(CurveError>0.08) + { + ++RootBlock.BadVertices; + if(CurveError>RootBlock.MaximumError) + { + RootBlock.MaximumError=CurveError; + RootBlock.Position={P[0],P[1],P[2]}; + } + ++Result.CurvedEdgeRootBadVertices; + } + } + const double SourceResidual=std::abs(Shape.unclampedDensityAt({P[0],P[1],P[2]})); + if(Tile.Key.Level==0&&Tile.Key.Coord.X==1 + &&Tile.Key.Coord.Y==-16&&Tile.Key.Coord.Z==-3) + { + ++RayTileResidualSamples; + RayTileResidualSum+=SourceResidual; + if(SourceResidual>0.1)++RayTileResidualGt01; + if(SourceResidual>0.25)++RayTileResidualGt025; + if(SourceResidual>0.5)++RayTileResidualGt05; + if(SourceResidual>RayTileResidualMaximum) + { + RayTileResidualMaximum=SourceResidual; + RayTileResidualMaximumPosition={P[0],P[1],P[2]}; + } + } + Result.SumAbsSourceResidual+=SourceResidual; + if(SourceResidual>Result.MaximumAbsSourceResidual) + { + Result.MaximumAbsSourceResidual=SourceResidual; + Result.WorstResidualPosition={P[0],P[1],P[2]}; + } + const double H=0.5; + const double GX=Shape.unclampedDensityAt({P[0]+H+0.013,P[1]+0.013,P[2]+0.013}) + -Shape.unclampedDensityAt({P[0]-H+0.013,P[1]+0.013,P[2]+0.013}); + const double GY=Shape.unclampedDensityAt({P[0]+0.013,P[1]+H+0.013,P[2]+0.013}) + -Shape.unclampedDensityAt({P[0]+0.013,P[1]-H+0.013,P[2]+0.013}); + const double GZ=Shape.unclampedDensityAt({P[0]+0.013,P[1]+0.013,P[2]+H+0.013}) + -Shape.unclampedDensityAt({P[0]+0.013,P[1]+0.013,P[2]-H+0.013}); + const double GL=std::sqrt(GX*GX+GY*GY+GZ*GZ); + double NormalAgreement=1.0; + if(Tile.Key.Level==0&&GL>1.0e-8) + { + const auto& N=Tile.Vertices[I].Normal; + NormalAgreement=std::clamp((N[0]*GX+N[1]*GY+N[2]*GZ)/GL,-1.0,1.0); + ++Result.NormalChecks;Result.SumNormalAgreement+=NormalAgreement; + Result.MinimumNormalAgreement=std::min(Result.MinimumNormalAgreement,NormalAgreement); + } + FBlockKey Key{};Key[0]=VariableAxis; + for(int32 Axis=0;Axis<3;++Axis) + Key[Axis+1]=static_cast(std::floor((P[Axis]-(&Origin.X)[Axis])/8.0)); + if(Tile.Key.Level==0&&GL>1.0e-8) + { + FNormalBlock& NormalBlock=NormalBlocks[Key]; + ++NormalBlock.Vertices; + if(NormalAgreement0.5; + if(bBadFieldEdge) + { + ++FieldBlock.BadVertices; + if(FieldBlock.BadVertices==1)FieldBlock.Position={P[0],P[1],P[2]}; + } + + FGradientFallbackBlock& FallbackBlock=GradientFallbackBlocks[Key]; + ++FallbackBlock.Vertices; + uint64 VertexFallbackSamples=0; + for(int32 Endpoint=0;Endpoint<2;++Endpoint) + { + const double* EdgePoint=Endpoint?EdgeB:EdgeA; + for(int32 Axis=0;Axis<3;++Axis) + for(int32 Side=-1;Side<=1;Side+=2) + { + double Probe[3]={EdgePoint[0],EdgePoint[1],EdgePoint[2]}; + Probe[Axis]+=Side; + const double Packed=Shape.densityAt({Probe[0],Probe[1],Probe[2]}); + if(std::abs(std::abs(Packed)-4.125)>0.001)continue; + double ExactProbe[3]={Probe[0]+0.013,Probe[1]+0.013,Probe[2]+0.013}; + const double Exact=Shape.unclampedDensityAt( + {ExactProbe[0],ExactProbe[1],ExactProbe[2]}); + const double Error=std::abs(Packed-Exact); + if(Error<=0.25)continue; + ++VertexFallbackSamples; + FallbackBlock.MaximumError=std::max(FallbackBlock.MaximumError,Error); + } + } + if(VertexFallbackSamples>0&&NormalAgreement<0.98) + { + ++FallbackBlock.BadVertices; + FallbackBlock.BadSamples+=VertexFallbackSamples; + if(FallbackBlock.BadVertices==1)FallbackBlock.Position={P[0],P[1],P[2]}; + } + } + FirstPositionByBlock.try_emplace(Key,std::array{P[0],P[1],P[2]}); + const auto& Normal=Tile.Vertices[I].Normal; + FirstNormalByBlock.try_emplace(Key,std::array{Normal[0],Normal[1],Normal[2]}); + ++OffsetBlocks[Key][PreciseBin]; + } + + struct FHighDetailBlock + {std::array Key{};uint64 Vertices=0;double Maximum=0.0; + std::array Position{};}; + std::vector HighDetailBlocks; + for(const auto& [Key,Block]:DetailDisplacementBlocks) + { + if(Block.Vertices<12||Block.HighVertices<6 + ||static_cast(Block.HighVertices)/Block.Vertices<0.25)continue; + HighDetailBlocks.push_back({Key,Block.HighVertices,Block.Maximum,Block.Position}); + } + std::vector HighDetailVisited(HighDetailBlocks.size(),0); + for(size_t I=0;I Pending{I};HighDetailVisited[I]=1; + uint64 BlockCount=0,VertexCountInPatch=0;double Maximum=0.0; + size_t Representative=I; + while(!Pending.empty()) + { + const size_t Current=Pending.back();Pending.pop_back(); + const FHighDetailBlock& A=HighDetailBlocks[Current]; + ++BlockCount;VertexCountInPatch+=A.Vertices; + if(A.Maximum>Maximum){Maximum=A.Maximum;Representative=Current;} + for(size_t J=0;J(BlockCount), + static_cast(VertexCountInPatch),Maximum, + Sample.Position[0],Sample.Position[1],Sample.Position[2]); + } + } + + struct FBadFieldBlock + {FBlockKey Key{};uint64 Vertices=0,BadVertices=0;double Share=0.0,MaximumShift=0.0, + MaximumResidual=0.0;std::array Position{};}; + std::vector BadFieldBlocks; + for(const auto& [Key,Block]:FieldBlocks) + { + if(Block.Vertices<12||Block.BadVertices<6)continue; + const double Share=static_cast(Block.BadVertices)/Block.Vertices; + if(Share<0.25)continue; + BadFieldBlocks.push_back({Key,Block.Vertices,Block.BadVertices,Share, + Block.MaximumShift,Block.MaximumResidual,Block.Position}); + } + std::vector BadFieldVisited(BadFieldBlocks.size(),0); + for(size_t I=0;I Pending{I};BadFieldVisited[I]=1; + uint64 BlockCount=0,BadVertexCount=0;double MaxShift=0.0,MaxResidual=0.0; + size_t Representative=I; + while(!Pending.empty()) + { + const size_t Current=Pending.back();Pending.pop_back(); + const FBadFieldBlock& A=BadFieldBlocks[Current]; + ++BlockCount;BadVertexCount+=A.BadVertices; + MaxShift=std::max(MaxShift,A.MaximumShift); + MaxResidual=std::max(MaxResidual,A.MaximumResidual); + if(A.MaximumShift>=MaxShift||A.MaximumResidual>=MaxResidual)Representative=Current; + for(size_t J=0;J(BlockCount), + static_cast(BadVertexCount),MaxShift,MaxResidual, + Sample.Position[0],Sample.Position[1],Sample.Position[2]); + } + } + + struct FBadGradientFallbackBlock + {FBlockKey Key{};uint64 Vertices=0,BadVertices=0,BadSamples=0; + double MaximumError=0.0;std::array Position{};}; + std::vector BadGradientFallbackBlocks; + for(const auto& [Key,Block]:GradientFallbackBlocks) + { + if(Block.Vertices<12||Block.BadVertices<6)continue; + if(static_cast(Block.BadVertices)/Block.Vertices<0.25)continue; + BadGradientFallbackBlocks.push_back({Key,Block.Vertices,Block.BadVertices, + Block.BadSamples,Block.MaximumError,Block.Position}); + } + std::vector GradientFallbackVisited(BadGradientFallbackBlocks.size(),0); + for(size_t I=0;I Pending{I};GradientFallbackVisited[I]=1; + uint64 BlockCount=0,BadVertexCount=0,BadSamples=0;double MaximumError=0.0; + size_t Representative=I; + while(!Pending.empty()) + { + const size_t Current=Pending.back();Pending.pop_back(); + const FBadGradientFallbackBlock& A=BadGradientFallbackBlocks[Current]; + ++BlockCount;BadVertexCount+=A.BadVertices;BadSamples+=A.BadSamples; + MaximumError=std::max(MaximumError,A.MaximumError); + if(A.MaximumError>=MaximumError)Representative=Current; + for(size_t J=0;J(BlockCount), + static_cast(BadVertexCount), + static_cast(BadSamples),MaximumError, + Sample.Position[0],Sample.Position[1],Sample.Position[2]); + } + } + + struct FBadNormalBlock + {FBlockKey Key{};uint64 Vertices=0,BadVertices=0;double MinimumAgreement=1.0; + std::array Position{},MeshNormal{};}; + std::vector BadNormalBlocks; + for(const auto& [Key,Block]:NormalBlocks) + { + if(Block.Vertices<12||Block.BadVertices<6)continue; + if(static_cast(Block.BadVertices)/Block.Vertices<0.25)continue; + BadNormalBlocks.push_back({Key,Block.Vertices,Block.BadVertices, + Block.MinimumAgreement,Block.Position,Block.MeshNormal}); + } + std::vector BadNormalVisited(BadNormalBlocks.size(),0); + for(size_t I=0;I Pending{I};BadNormalVisited[I]=1; + uint64 BlockCount=0,BadVertexCount=0;double MinAgreement=1.0;size_t Representative=I; + while(!Pending.empty()) + { + const size_t Current=Pending.back();Pending.pop_back(); + const FBadNormalBlock& A=BadNormalBlocks[Current]; + ++BlockCount;BadVertexCount+=A.BadVertices; + MinAgreement=std::min(MinAgreement,A.MinimumAgreement); + if(A.MinimumAgreement(BlockCount), + static_cast(BadVertexCount),MinAgreement, + Sample.Position[0],Sample.Position[1],Sample.Position[2]); + double EdgeA[3],EdgeB[3]; + for(int32 Axis=0;Axis<3;++Axis) + { + EdgeA[Axis]=(&Origin.X)[Axis] + +std::floor((Sample.Position[Axis]-(&Origin.X)[Axis])/Step)*Step; + EdgeB[Axis]=EdgeA[Axis]; + } + EdgeB[Sample.Key[0]]+=Step; + double GradPacked[2][3]{},GradExact[2][3]{}; + for(int32 Endpoint=0;Endpoint<2;++Endpoint) + { + const double* P=Endpoint?EdgeB:EdgeA; + for(int32 Axis=0;Axis<3;++Axis) + { + double Minus[3]={P[0],P[1],P[2]},Plus[3]={P[0],P[1],P[2]}; + Minus[Axis]-=1.0;Plus[Axis]+=1.0; + GradPacked[Endpoint][Axis]=Shape.densityAt({Plus[0],Plus[1],Plus[2]}) + -Shape.densityAt({Minus[0],Minus[1],Minus[2]}); + for(int32 A=0;A<3;++A){Minus[A]+=0.013;Plus[A]+=0.013;} + GradExact[Endpoint][Axis]=Shape.unclampedDensityAt( + {Plus[0],Plus[1],Plus[2]}) + -Shape.unclampedDensityAt({Minus[0],Minus[1],Minus[2]}); + } + } + const double T=std::clamp(Sample.Position[Sample.Key[0]]-EdgeA[Sample.Key[0]],0.0,1.0); + double PackedInterpolated[3]{},ExactInterpolated[3]{}; + for(int32 Axis=0;Axis<3;++Axis) + { + PackedInterpolated[Axis]=GradPacked[0][Axis]+T*(GradPacked[1][Axis]-GradPacked[0][Axis]); + ExactInterpolated[Axis]=GradExact[0][Axis]+T*(GradExact[1][Axis]-GradExact[0][Axis]); + } + const auto NormalDot=[](const double A[3],const double B[3]) + { + const double AL=std::sqrt(A[0]*A[0]+A[1]*A[1]+A[2]*A[2]); + const double BL=std::sqrt(B[0]*B[0]+B[1]*B[1]+B[2]*B[2]); + return AL>1.0e-10&&BL>1.0e-10 + ?(A[0]*B[0]+A[1]*B[1]+A[2]*B[2])/(AL*BL):0.0; + }; + std::printf("[MeshNormalProbe] t=%.3f edge_axis=%d mesh=(%.3f,%.3f,%.3f) packed_grid=(%.3f,%.3f,%.3f) exact_grid=(%.3f,%.3f,%.3f) packed_dot=%.3f exact_dot=%.3f endpoints=(%.0f,%.0f,%.0f)->(%.0f,%.0f,%.0f)\n", + T,Sample.Key[0],Sample.MeshNormal[0],Sample.MeshNormal[1],Sample.MeshNormal[2], + PackedInterpolated[0],PackedInterpolated[1],PackedInterpolated[2], + ExactInterpolated[0],ExactInterpolated[1],ExactInterpolated[2], + NormalDot(Sample.MeshNormal.data(),PackedInterpolated), + NormalDot(Sample.MeshNormal.data(),ExactInterpolated), + EdgeA[0],EdgeA[1],EdgeA[2],EdgeB[0],EdgeB[1],EdgeB[2]); + for(int32 Endpoint=0;Endpoint<2;++Endpoint) + for(int32 Axis=0;Axis<3;++Axis) + { + const double* P=Endpoint?EdgeB:EdgeA; + for(int32 Side=-1;Side<=1;Side+=2) + { + double Q[3]={P[0],P[1],P[2]};Q[Axis]+=Side; + const double Packed=Shape.densityAt({Q[0],Q[1],Q[2]}); + Q[0]+=0.013;Q[1]+=0.013;Q[2]+=0.013; + const double Exact=Shape.unclampedDensityAt({Q[0],Q[1],Q[2]}); + if(std::abs(Packed-Exact)>0.02) + std::printf("[MeshNormalProbeNode] endpoint=%d axis=%d side=%d packed=%.4f exact=%.4f delta=%.4f\n", + Endpoint,Axis,Side,Packed,Exact,Packed-Exact); + } + } + } + } + + if(Tile.Key.Level==0&&Tile.Key.Coord.X==1 + &&Tile.Key.Coord.Y==-16&&Tile.Key.Coord.Z==-3) + { + struct FConcentratedBlock + {FBlockKey Key{};uint64 Count=0;double TopOne=0.0,TopThree=0.0; + std::array Bins{};std::array Position{};}; + std::vector ConcentratedBlocks; + for(const auto& [Key,Bins]:OffsetBlocks) + { + const uint64 Count=std::accumulate(Bins.begin(),Bins.end(),uint64{0}); + if(Count<12)continue; + std::array Order{}; + std::iota(Order.begin(),Order.end(),0); + std::partial_sort(Order.begin(),Order.begin()+3,Order.end(), + [&](int32 A,int32 B){return Bins[A]>Bins[B];}); + const uint64 TopThree=Bins[Order[0]]+Bins[Order[1]]+Bins[Order[2]]; + ConcentratedBlocks.push_back({Key,Count, + static_cast(Bins[Order[0]])/Count, + static_cast(TopThree)/Count, + {Order[0],Order[1],Order[2]},FirstPositionByBlock[Key]}); + } + std::sort(ConcentratedBlocks.begin(),ConcentratedBlocks.end(), + [](const auto& A,const auto& B){return A.TopThree>B.TopThree;}); + for(size_t I=0;I(8,ConcentratedBlocks.size());++I) + { + const auto& B=ConcentratedBlocks[I]; + std::printf("[MeshOffsetConcentration] tile=(1,-16,-3) axis=%d block=(%d,%d,%d) vertices=%llu top1=%.3f top3=%.3f bins=(%d,%d,%d) sample=(%.3f,%.3f,%.3f)\n", + B.Key[0],B.Key[1],B.Key[2],B.Key[3], + static_cast(B.Count),B.TopOne,B.TopThree, + B.Bins[0],B.Bins[1],B.Bins[2], + B.Position[0],B.Position[1],B.Position[2]); + } + } + + struct FRepeatBlock + {FBlockKey Key{};int32 Bin=0;uint64 Count=0;double Share=0.0; + std::array Position{},Normal{};}; + std::vector RepeatBlocks; + for(const auto& [Key,Bins]:OffsetBlocks) + { + const uint64 Count=std::accumulate(Bins.begin(),Bins.end(),uint64{0}); + if(Count<12)continue; + const auto Best=std::max_element(Bins.begin(),Bins.end()); + const int32 Bin=static_cast(Best-Bins.begin()); + const double Share=static_cast(*Best)/Count; + if(Bin<80||Bin>176||Share<0.75)continue; + RepeatBlocks.push_back({Key,Bin,Count,Share,FirstPositionByBlock[Key],FirstNormalByBlock[Key]}); + } + std::vector Visited(RepeatBlocks.size(),0); + for(size_t I=0;I Pending{I};Visited[I]=1; + uint64 BlockCount=0,VertexCountInPatch=0;double Dominance=0.0; + while(!Pending.empty()) + { + const size_t Current=Pending.back();Pending.pop_back(); + const FRepeatBlock& A=RepeatBlocks[Current]; + ++BlockCount;VertexCountInPatch+=A.Count;Dominance=std::max(Dominance,A.Share); + for(size_t J=0;J1)continue; + int32 Manhattan=0; + for(int32 Axis=1;Axis<4;++Axis) + Manhattan+=std::abs(A.Key[Axis]-RepeatBlocks[J].Key[Axis]); + if(Manhattan==1){Visited[J]=1;Pending.push_back(J);} + } + } + if(BlockCount<3||VertexCountInPatch<48)continue; + ++Result.RepeatedOffsetPatches; + Result.MaximumPatchDominance=std::max(Result.MaximumPatchDominance,Dominance); + if(bPrintPatches&&Result.RepeatedOffsetPatches<=8) + { + const auto& P=RepeatBlocks[I].Position;const int32 Axis=RepeatBlocks[I].Key[0]; + double EdgeA[3],EdgeB[3]; + for(int32 A=0;A<3;++A) + { + const int32 Cell=static_cast(std::floor((P[A]-(&Origin.X)[A])/Step)); + EdgeA[A]=(&Origin.X)[A]+Cell*Step;EdgeB[A]=EdgeA[A]; + } + EdgeB[Axis]+=Step; + const double D0=Shape.densityAt({EdgeA[0],EdgeA[1],EdgeA[2]}); + const double D1=Shape.densityAt({EdgeB[0],EdgeB[1],EdgeB[2]}); + const double ExactD0=Shape.densityAt({EdgeA[0]+0.013,EdgeA[1]+0.013,EdgeA[2]+0.013}); + const double ExactD1=Shape.densityAt({EdgeB[0]+0.013,EdgeB[1]+0.013,EdgeB[2]+0.013}); + const double InterpolatedT=std::abs(D1-D0)>1.0e-9 + ?std::clamp(-D0/(D1-D0),0.0,1.0):0.5; + const double SourceDensity=Shape.densityAt({P[0],P[1],P[2]}); + const auto& N=RepeatBlocks[I].Normal; + const double PlusNormal=Shape.densityAt({P[0]+N[0],P[1]+N[1],P[2]+N[2]}); + const double MinusNormal=Shape.densityAt({P[0]-N[0],P[1]-N[1],P[2]-N[2]}); + std::printf("[MeshLatticePatch] level=%d tile=(%d,%d,%d) connected_8vox_blocks=%llu edge_vertices=%llu dominant_share=%.3f midpoint_bin_cluster=1 axis=%d sample=(%.3f,%.3f,%.3f) offset=%.4f normal=(%.3f,%.3f,%.3f) endpoint_density=(%.4f,%.4f) analytic_probe=(%.4f,%.4f) predicted_t=%.4f source_density=%.4f normal_probe=(plus1=%.4f,minus1=%.4f)\n", + Tile.Key.Level,Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z, + static_cast(BlockCount), + static_cast(VertexCountInPatch),Dominance,Axis, + P[0],P[1],P[2],(P[Axis]-EdgeA[Axis])/Step,N[0],N[1],N[2],D0,D1, + ExactD0,ExactD1,InterpolatedT,SourceDensity,PlusNormal,MinusNormal); + } + } + + const size_t TriangleCount=Tile.Indices.size()/3; + for(size_t T=0;T=VertexCount||B>=VertexCount||C>=VertexCount)continue; + if(A0.125)++RayTileInteriorGt0125; + if(Residual>0.25)++RayTileInteriorGt025; + if(Residual>0.5)++RayTileInteriorGt05; + if(Residual>1.0)++RayTileInteriorGt1; + if(Residual>RayTileInteriorResidualMaximum) + { + RayTileInteriorResidualMaximum=Residual; + RayTileInteriorMaximumPosition={Sample[0],Sample[1],Sample[2]}; + } + } + } + continue; + } + const uint8 SkirtFaces=T(PA[0])+PB[0]+PC[0])*4.0/3.0, + (static_cast(PA[1])+PB[1]+PC[1])*4.0/3.0, + (static_cast(PA[2])+PB[2]+PC[2])*4.0/3.0}; + const double Density=Shape.densityAt(Center); + double AlternativeCenter[3]={0.0,0.0,0.0}; + const uint32 TriangleIndices[3]={A,B,C}; + for(uint32 Index:TriangleIndices) + for(int32 Axis=0;Axis<3;++Axis) + { + double Position=Tile.Vertices[Index].Position[Axis]; + if(Index>=SurfaceVertices) + Position+=2.0*SkirtDepthMeters*Tile.Vertices[Index].Normal[Axis]; + AlternativeCenter[Axis]+=Position*4.0/3.0; + } + const double AlternativeDensity=Shape.densityAt({ + AlternativeCenter[0],AlternativeCenter[1],AlternativeCenter[2]}); + double Normal[3]={0.0,0.0,0.0}; + for(uint32 Index:TriangleIndices)if(Index>=SurfaceVertices) + for(int32 Axis=0;Axis<3;++Axis)Normal[Axis]+=Tile.Vertices[Index].Normal[Axis]; + const double NormalLength=std::sqrt(Normal[0]*Normal[0]+Normal[1]*Normal[1]+Normal[2]*Normal[2]); + if(NormalLength>1.0e-8)for(double& Value:Normal)Value/=NormalLength; + const double Inward1=Shape.densityAt({Center.x-Normal[0],Center.y-Normal[1],Center.z-Normal[2]}); + const double Inward2=Shape.densityAt({Center.x-Normal[0]*2.0,Center.y-Normal[1]*2.0,Center.z-Normal[2]*2.0}); + if(Density>0.25) + { + ++Result.ExposedSkirtTriangles; + Result.MaximumAirSkirtDensity=std::max(Result.MaximumAirSkirtDensity,Density); + if(AlternativeDensity<-0.25)++Result.ReverseSideSolidSkirtTriangles; + if(bPrintSkirts&&Result.ExposedSkirtTriangles<=8) + std::printf("[ExposedTileSkirt] level=%d tile=(%d,%d,%d) center=(%.2f,%.2f,%.2f) density=%.3f reverse_density=%.3f inward=(%.3f,%.3f) normal=(%.3f,%.3f,%.3f)\n", + Tile.Key.Level,Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z, + Center.x,Center.y,Center.z,Density,AlternativeDensity,Inward1,Inward2, + Normal[0],Normal[1],Normal[2]); + } + } + if(Tile.Key.Level==0&&Tile.Key.Coord.X==1 + &&Tile.Key.Coord.Y==-16&&Tile.Key.Coord.Z==-3) + { + std::printf("[MeshRayTileResidual] tile=(1,-16,-3) vertices=%llu mean=%.4f max=%.4f gt0.1=%llu gt0.25=%llu gt0.5=%llu max_vertex=(%.3f,%.3f,%.3f)\n", + static_cast(RayTileResidualSamples), + RayTileResidualSamples?RayTileResidualSum/RayTileResidualSamples:0.0, + RayTileResidualMaximum, + static_cast(RayTileResidualGt01), + static_cast(RayTileResidualGt025), + static_cast(RayTileResidualGt05), + RayTileResidualMaximumPosition[0],RayTileResidualMaximumPosition[1], + RayTileResidualMaximumPosition[2]); + std::printf("[MeshRayTileInteriorResidual] tile=(1,-16,-3) samples=%llu mean=%.4f max=%.4f gt0.125=%llu gt0.25=%llu gt0.5=%llu gt1=%llu max_probe=(%.3f,%.3f,%.3f)\n", + static_cast(RayTileInteriorSamples), + RayTileInteriorSamples?RayTileInteriorResidualSum/RayTileInteriorSamples:0.0, + RayTileInteriorResidualMaximum, + static_cast(RayTileInteriorGt0125), + static_cast(RayTileInteriorGt025), + static_cast(RayTileInteriorGt05), + static_cast(RayTileInteriorGt1), + RayTileInteriorMaximumPosition[0],RayTileInteriorMaximumPosition[1], + RayTileInteriorMaximumPosition[2]); + } + struct FBadCurvedEdgeRootBlock + {FBlockKey Key{};uint64 Vertices=0,BadVertices=0;double Share=0.0,MaximumError=0.0; + std::array Position{};}; + std::vector BadCurvedEdgeRootBlocks; + for(const auto& [Key,Block]:CurvedEdgeRootBlocks) + { + if(Block.Vertices<12||Block.BadVertices<6)continue; + const double Share=static_cast(Block.BadVertices)/Block.Vertices; + if(Share<0.25)continue; + BadCurvedEdgeRootBlocks.push_back({Key,Block.Vertices,Block.BadVertices,Share, + Block.MaximumError,Block.Position}); + } + std::vector CurvedEdgeRootVisited(BadCurvedEdgeRootBlocks.size(),0); + for(size_t I=0;I Pending{I};CurvedEdgeRootVisited[I]=1; + uint64 BlockCount=0,BadVertexCount=0;double MaximumError=0.0;size_t Representative=I; + while(!Pending.empty()) + { + const size_t Current=Pending.back();Pending.pop_back(); + const FBadCurvedEdgeRootBlock& A=BadCurvedEdgeRootBlocks[Current]; + ++BlockCount;BadVertexCount+=A.BadVertices; + if(A.MaximumError>MaximumError) + {MaximumError=A.MaximumError;Representative=Current;} + for(size_t J=0;J(BlockCount), + static_cast(BadVertexCount),MaximumError, + Sample.Position[0],Sample.Position[1],Sample.Position[2]); + } + } + return Result; + } + FMeshSheetAudit AuditThinMeshSheets(const FStandaloneCpuTile& Tile, const FVector& CameraVoxels,float Yaw,float Pitch,bool bPrintCandidates) { @@ -421,6 +1262,8 @@ namespace { std::vector Vertices; std::vector Indices; + std::vector TriangleSkirtFaceMasks; + uint8 ActiveSkirtFaceMask=0x3f; uint64 Samples=0; double WorkerSeconds=0.0; }; @@ -534,6 +1377,7 @@ namespace float CameraYawOverride=0.0f,CameraPitchOverride=0.0f; std::unordered_map ResidentTiles; std::unordered_set DeferredLeaverTiles; + std::unordered_set DesiredTileSet; std::unordered_map,FStandaloneBatchKeyHash> BatchMembers; std::unordered_map StreamingBatches; std::unordered_set DirtyBatchSet; @@ -622,7 +1466,7 @@ namespace if(Pair.second.IndexBuffer)Pair.second.IndexBuffer->Release(); } StreamingBatches.clear();BatchMembers.clear();ResidentTiles.clear(); - DirtyBatchSet.clear();DesiredTiles.clear();CrossingWork.clear(); + DirtyBatchSet.clear();DesiredTiles.clear();DesiredTileSet.clear();CrossingWork.clear(); DeferredLeaverTiles.clear(); for(int32 I=0;I<=MaxLevels;++I) {DesiredByLevel[I]=0;GeneratedByLevel[I]=0;SamplesByLevel[I]=0;WorkerSecondsByLevel[I]=0.0; @@ -1041,6 +1885,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; for(const auto& Key:CoreTiles) DesiredTiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod); } + DesiredTileSet.clear();DesiredTileSet.reserve(DesiredTiles.size()*2+1); + for(const FVoxelTileKey& Key:DesiredTiles)DesiredTileSet.insert(Key); for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0; for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level]; const int32 StreamWorkers=bShapePipelineActive?(WorkerCount>=8?2:1):WorkerCount; @@ -1173,6 +2019,18 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; void MarkBatchDirty(const FStandaloneBatchKey& Key) {DirtyBatchSet.insert(Key);} + void RefreshResidentSkirtMasks() + { + for(auto& [Key,Resident]:ResidentTiles) + { + if(!DesiredTileSet.contains(Key))continue; + const uint8 NewMask=ComputeVisibleSkirtFaceMask(Key,DesiredTileSet); + if(NewMask==Resident.ActiveSkirtFaceMask)continue; + Resident.ActiveSkirtFaceMask=NewMask; + MarkBatchDirty(BatchForTile(Key)); + } + } + static bool TileKeyLess(const FVoxelTileKey& A,const FVoxelTileKey& B) { if(A.Level!=B.Level)return A.Level ResidentKeys;ResidentKeys.reserve(ResidentTiles.size()); @@ -1331,7 +2191,14 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; for(const FVoxelTileKey& Tile:Members) { const auto It=ResidentTiles.find(Tile);if(It==ResidentTiles.end())continue; - VertexCount+=It->second.Vertices.size();IndexCount+=It->second.Indices.size(); + const FResidentTileMesh& Resident=It->second; + VertexCount+=Resident.Vertices.size(); + if(Resident.TriangleSkirtFaceMasks.size()==Resident.Indices.size()/3) + { + for(const uint8 Faces:Resident.TriangleSkirtFaceMasks) + if(Faces==0||(Faces&Resident.ActiveSkirtFaceMask)!=0)IndexCount+=3; + } + else IndexCount+=Resident.Indices.size(); } if(VertexCount==0||IndexCount==0) { @@ -1352,9 +2219,22 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; { const auto It=ResidentTiles.find(Tile);if(It==ResidentTiles.end())continue; const uint32 Base=static_cast(Vertices.size()); - for(const FStandaloneCpuVertex& V:It->second.Vertices) + const FResidentTileMesh& Resident=It->second; + for(const FStandaloneCpuVertex& V:Resident.Vertices) Vertices.push_back({{V.Position[0],V.Position[1],V.Position[2]},{V.Normal[0],V.Normal[1],V.Normal[2]}}); - for(const uint32 Index:It->second.Indices)Indices.push_back(Base+Index); + const bool bHasFaceMasks=Resident.TriangleSkirtFaceMasks.size()==Resident.Indices.size()/3; + for(size_t Triangle=0;Triangle=0) Desired.Tiles.erase(std::remove_if(Desired.Tiles.begin(),Desired.Tiles.end(), [&](const FVoxelTileKey& Tile){return Tile.Level>MaxLevel;}),Desired.Tiles.end()); + std::unordered_set DesiredTileSet; + DesiredTileSet.reserve(Desired.Tiles.size()*2+1); + for(const FVoxelTileKey& Tile:Desired.Tiles)DesiredTileSet.insert(Tile); if(Desired.Tiles.size()>1000000) { std::fprintf(stderr,"Stream benchmark rejected an unsafe desired set of %zu tiles.\n", @@ -2556,6 +3441,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; double FullDetailSeconds=0.0; const int32 DesiredTotal=Update.DesiredTiles; FMeshSheetAudit MeshSheetStats; + FMeshArtifactAudit MeshArtifactStats; FCoarsePanelAudit CoarsePanelStats; uint64 MeshAuditedTiles=0; while(Finished(Tile.Vertices.capacity())*sizeof(FStandaloneCpuVertex) - +static_cast(Tile.Indices.capacity())*sizeof(uint32); + +static_cast(Tile.Indices.capacity())*sizeof(uint32) + +static_cast(Tile.TriangleSkirtFaceMasks.capacity())*sizeof(uint8); MeshPayloadAllocatedBytesByLevel[Level]+=Tile.MeshPayloadAllocatedBytes; if(Tile.bHasDensityHash)HashRows[Level].push_back({Tile.Key,Tile.DensityHash}); if(Tile.bHasMeshHash)MeshHashRows[Level].push_back({Tile.Key,Tile.MeshHash}); @@ -2613,6 +3500,70 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; TileAudit.MinimumThicknessVoxels); ++MeshAuditedTiles; } + if(bShardAudit&&Level<=1&&CoreWorld) + { + const FMeshArtifactAudit TileAudit=AuditMeshArtifacts( + Tile,*CoreWorld,std::max(1,World.SkirtCells)*(1<MeshArtifactStats.MaximumAnalyticEdgeShiftVoxels) + { + MeshArtifactStats.MaximumAnalyticEdgeShiftVoxels=TileAudit.MaximumAnalyticEdgeShiftVoxels; + MeshArtifactStats.WorstShiftPosition=TileAudit.WorstShiftPosition; + MeshArtifactStats.WorstShiftFraction=TileAudit.WorstShiftFraction; + MeshArtifactStats.WorstShiftD0=TileAudit.WorstShiftD0; + MeshArtifactStats.WorstShiftD1=TileAudit.WorstShiftD1; + MeshArtifactStats.WorstShiftExactD0=TileAudit.WorstShiftExactD0; + MeshArtifactStats.WorstShiftExactD1=TileAudit.WorstShiftExactD1; + MeshArtifactStats.WorstShiftAxis=TileAudit.WorstShiftAxis; + } + if(TileAudit.MaximumAbsSourceResidual>MeshArtifactStats.MaximumAbsSourceResidual) + { + MeshArtifactStats.MaximumAbsSourceResidual=TileAudit.MaximumAbsSourceResidual; + MeshArtifactStats.WorstResidualPosition=TileAudit.WorstResidualPosition; + } + MeshArtifactStats.SumNormalAgreement+=TileAudit.SumNormalAgreement; + MeshArtifactStats.MinimumNormalAgreement=std::min( + MeshArtifactStats.MinimumNormalAgreement,TileAudit.MinimumNormalAgreement); + MeshArtifactStats.MaximumAirSkirtDensity=std::max( + MeshArtifactStats.MaximumAirSkirtDensity,TileAudit.MaximumAirSkirtDensity); + MeshArtifactStats.MaximumPatchDominance=std::max( + MeshArtifactStats.MaximumPatchDominance,TileAudit.MaximumPatchDominance); + for(size_t Bin=0;Bin=1&&CoreWorld) { const FCoarsePanelAudit TileAudit=AuditCoarseSurfacePanels( @@ -2712,6 +3663,65 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; static_cast(MeshSheetStats.TrianglesInThinSheets), MeshSheetStats.ThinSheets==0&&MeshSheetStats.SmallComponentsInView==0?"PASS":"FAIL"); if(bShardAudit&&CoreWorld) + { + const double MidpointShare=MeshArtifactStats.EdgeVertices + ?100.0*static_cast(MeshArtifactStats.MidpointVertices) + /MeshArtifactStats.EdgeVertices:0.0; + const double MeanSourceResidual=MeshArtifactStats.FieldEdgeChecks + ?MeshArtifactStats.SumAbsSourceResidual/MeshArtifactStats.FieldEdgeChecks:0.0; + const double MeanAnalyticEdgeShift=MeshArtifactStats.FieldEdgeChecks + ?MeshArtifactStats.SumAnalyticEdgeShiftVoxels + /std::max(1,MeshArtifactStats.FieldEdgeChecks-MeshArtifactStats.AnalyticCrossingMisses):0.0; + const double MeanNormalAgreement=MeshArtifactStats.NormalChecks + ?MeshArtifactStats.SumNormalAgreement/MeshArtifactStats.NormalChecks:1.0; + std::printf("[MeshFieldFitAudit] edge_checks=%llu analytic_crossing_misses=%llu old_int8_range_edges=%llu source_residual_mean=%.4f max=%.4f vox analytic_edge_shift_mean=%.4f max=%.4f vox sampled_normal_dot_mean=%.4f min=%.4f\n", + static_cast(MeshArtifactStats.FieldEdgeChecks), + static_cast(MeshArtifactStats.AnalyticCrossingMisses), + static_cast(MeshArtifactStats.OldInt8RangeEdges), + MeanSourceResidual,MeshArtifactStats.MaximumAbsSourceResidual, + MeanAnalyticEdgeShift,MeshArtifactStats.MaximumAnalyticEdgeShiftVoxels, + MeanNormalAgreement,MeshArtifactStats.MinimumNormalAgreement); + std::printf("[MeshFieldFitWorst] shift=%.4f vox vertex=(%.3f,%.3f,%.3f) axis=%d fraction=%.4f packed=(%.4f,%.4f) analytic=(%.4f,%.4f) max_source_residual=%.4f vox vertex=(%.3f,%.3f,%.3f)\n", + MeshArtifactStats.MaximumAnalyticEdgeShiftVoxels, + MeshArtifactStats.WorstShiftPosition[0],MeshArtifactStats.WorstShiftPosition[1], + MeshArtifactStats.WorstShiftPosition[2],MeshArtifactStats.WorstShiftAxis, + MeshArtifactStats.WorstShiftFraction,MeshArtifactStats.WorstShiftD0, + MeshArtifactStats.WorstShiftD1,MeshArtifactStats.WorstShiftExactD0, + MeshArtifactStats.WorstShiftExactD1,MeshArtifactStats.MaximumAbsSourceResidual, + MeshArtifactStats.WorstResidualPosition[0],MeshArtifactStats.WorstResidualPosition[1], + MeshArtifactStats.WorstResidualPosition[2]); + std::printf("[MeshCurvedEdgeRootAudit] midpoint_edges=%llu bad_vertices=%llu patches=%llu max_midpoint_root_error=%.4f vox error_threshold=0.080 status=%s\n", + static_cast(MeshArtifactStats.CurvedEdgeChecks), + static_cast(MeshArtifactStats.CurvedEdgeRootBadVertices), + static_cast(MeshArtifactStats.CurvedEdgeRootPatches), + MeshArtifactStats.MaximumCurvedEdgeRootError, + MeshArtifactStats.CurvedEdgeRootPatches==0?"PASS":"FAIL"); + std::printf("[MeshSurfaceArtifactAudit] levels=0,1 tiles=%llu surface_vertices=%llu edge_vertices=%llu midpoint_band_vertices=%llu midpoint_share=%.2f%% visible_skirt_triangles=%llu hidden_same_lod_skirt_triangles=%llu exposed_skirt_triangles=%llu reverse_side_solid=%llu max_air_density=%.3f vox repeated_offset_patches=%llu field_residual_patches=%llu normal_mismatch_patches=%llu gradient_fallback_patches=%llu fallback_gradient_samples=%llu high_detail_vertices=%llu/%llu high_detail_patches=%llu max_detail_displacement=%.3f vox max_patch_dominance=%.3f status=%s\n", + static_cast(MeshArtifactStats.Tiles), + static_cast(MeshArtifactStats.SurfaceVertices), + static_cast(MeshArtifactStats.EdgeVertices), + static_cast(MeshArtifactStats.MidpointVertices),MidpointShare, + static_cast(MeshArtifactStats.SkirtTriangles), + static_cast(MeshArtifactStats.HiddenSameLodSkirtTriangles), + static_cast(MeshArtifactStats.ExposedSkirtTriangles), + static_cast(MeshArtifactStats.ReverseSideSolidSkirtTriangles), + MeshArtifactStats.MaximumAirSkirtDensity, + static_cast(MeshArtifactStats.RepeatedOffsetPatches), + static_cast(MeshArtifactStats.FieldMismatchPatches), + static_cast(MeshArtifactStats.NormalMismatchPatches), + static_cast(MeshArtifactStats.GradientFallbackPatches), + static_cast(MeshArtifactStats.GradientFallbackSamples), + static_cast(MeshArtifactStats.HighDetailDisplacementSamples), + static_cast(MeshArtifactStats.DetailDisplacementSamples), + static_cast(MeshArtifactStats.DetailDisplacementPatches), + MeshArtifactStats.MaximumDetailDisplacement, + MeshArtifactStats.MaximumPatchDominance, + MeshArtifactStats.ExposedSkirtTriangles==0&&MeshArtifactStats.RepeatedOffsetPatches==0 + &&MeshArtifactStats.FieldMismatchPatches==0 + &&MeshArtifactStats.NormalMismatchPatches==0 + &&MeshArtifactStats.GradientFallbackPatches==0 + &&MeshArtifactStats.DetailDisplacementPatches==0 + &&MeshArtifactStats.CurvedEdgeRootPatches==0?"PASS":"FAIL"); std::printf("[CoarsePanelAudit] tiles=%llu planar_open_components=%llu off_surface_panels=%llu panel_triangles=%llu max_source_error=%.3f vox status=%s\n", static_cast(CoarsePanelStats.Tiles), static_cast(CoarsePanelStats.PlanarOpenComponents), @@ -2719,6 +3729,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; static_cast(CoarsePanelStats.Triangles), CoarsePanelStats.MaximumSourceErrorVoxels, CoarsePanelStats.OffSurfacePanels==0?"PASS":"FAIL"); + } for(int32 Level=0;Level<=MaxLevels;++Level) { if(TilesByLevel[Level]==0)continue; @@ -2761,6 +3772,10 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; } if(Finished!=DesiredTotal||FullDetailFinished!=FullDetailExpected)return 6; if(bShardAudit&&(MeshSheetStats.ThinSheets!=0||MeshSheetStats.SmallComponentsInView!=0))return 13; + if(bShardAudit&&CoreWorld&&(MeshArtifactStats.ExposedSkirtTriangles!=0 + ||MeshArtifactStats.RepeatedOffsetPatches!=0||MeshArtifactStats.FieldMismatchPatches!=0 + ||MeshArtifactStats.NormalMismatchPatches!=0||MeshArtifactStats.GradientFallbackPatches!=0 + ||MeshArtifactStats.CurvedEdgeRootPatches!=0))return 13; if(bShardAudit&&CoreWorld&&CoarsePanelStats.OffSurfacePanels!=0)return 13; if(FarAudit) { diff --git a/Standalone/Source/StreamingWorld.h b/Standalone/Source/StreamingWorld.h index 6e8b363..1590e8f 100644 --- a/Standalone/Source/StreamingWorld.h +++ b/Standalone/Source/StreamingWorld.h @@ -36,6 +36,9 @@ struct FStandaloneCpuTile uint64 RequestedCrossing=0; std::vector Vertices; std::vector Indices; + std::vector TriangleSkirtFaceMasks; + uint32 SurfaceVertexCount=0; + uint32 SkirtTriangleCount=0; uint64 DensitySamples=0; double WorkerSeconds=0.0; std::chrono::steady_clock::time_point CompletedAt{}; @@ -79,6 +82,8 @@ inline uint64 HashStandaloneMeshPayload(const FVoxelMeshData& Mesh) { AddWord(Color.R,1);AddWord(Color.G,1);AddWord(Color.B,1);AddWord(Color.A,1); } AddWord(static_cast(Mesh.Triangles.Num()),8); for(const int32 Index:Mesh.Triangles)AddWord(static_cast(Index),4); + AddWord(static_cast(Mesh.TriangleSkirtFaceMasks.Num()),8); + for(const uint8 Faces:Mesh.TriangleSkirtFaceMasks)AddWord(Faces,1); AddWord(static_cast(Mesh.NumSurfaceVertices),4); AddWord(static_cast(Mesh.NumSkirtTriangles),4); AddWord(static_cast(Mesh.NumCeilingTriangles),4); @@ -614,6 +619,8 @@ private: Out.CompletedAt=FClock::now();return Out; } Out.DensitySamples=Result->densitySampleCount; + Out.SurfaceVertexCount=Result->groups.surfaceVertexCount; + Out.SkirtTriangleCount=Result->groups.skirtTriangleCount; Out.MeshPayloadAllocatedBytes=Result->meshPayloadAllocatedBytes; Out.DensityHash=Result->densityHash;Out.MeshHash=Result->meshHash; Out.PayloadHash=Result->payloadHash;Out.TopologyHash=Result->topologyHash; @@ -627,6 +634,7 @@ private: {static_cast(Vertex.normal.x),static_cast(Vertex.normal.y), static_cast(Vertex.normal.z)}}); Out.Indices=std::move(Result->indices); + Out.TriangleSkirtFaceMasks=std::move(Result->skirtFaceMasks); if(bProfile) { const VoxelDensityProfile::FSnapshot After=VoxelDensityProfile::SnapshotCurrentThread(); @@ -660,10 +668,13 @@ private: } } Out.DensitySamples=static_cast(FMath::Max(0,SampleCount)); + Out.SurfaceVertexCount=static_cast(std::clamp(Mesh.NumSurfaceVertices,0,Mesh.Vertices.Num())); + Out.SkirtTriangleCount=static_cast(std::max(0,Mesh.NumSkirtTriangles)); Out.MeshPayloadAllocatedBytes=static_cast(Mesh.Vertices.GetAllocatedSize()) +static_cast(Mesh.Normals.GetAllocatedSize()) +static_cast(Mesh.UVs.GetAllocatedSize()) +static_cast(Mesh.Colors.GetAllocatedSize()) + +static_cast(Mesh.TriangleSkirtFaceMasks.GetAllocatedSize()) +static_cast(Mesh.Triangles.GetAllocatedSize()); if(bHashDensity) { @@ -680,6 +691,9 @@ private: } Out.Indices.reserve(static_cast(Mesh.Triangles.Num())); for(const int32 Index:Mesh.Triangles)Out.Indices.push_back(static_cast(Index)); + Out.TriangleSkirtFaceMasks.assign(Mesh.TriangleSkirtFaceMasks.begin(), + Mesh.TriangleSkirtFaceMasks.end()); + Out.TriangleSkirtFaceMasks.resize(Out.Indices.size()/3,0); if(bHashDensity) {Out.MeshHash=HashStandaloneCpuTileMesh(Out);Out.bHasMeshHash=true;} Out.WorkerSeconds=std::chrono::duration(FClock::now()-StartTime).count(); diff --git a/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp b/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp index dbad2c0..a9d3de6 100644 --- a/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp +++ b/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp @@ -214,9 +214,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, return BiomeTileGridMemo.Add(ChunkZ, Samples); }; - // Normale fournie par l'appelant (gradient lu dans la grille de densité, T1.b) — - // plus d'échantillonnage de densité par vertex. RawNormal pointe solide→air ; on la - // normalise ici (fallback up si dégénérée). + // Normale fournie par l'appelant (gradient lu dans la grille de densité, T1.b). auto GetOrCreateVertex = [&](const FVector& WorldPos, const FVector& RawNormal) -> int32 { const FIntVector Key( @@ -1299,13 +1297,26 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, { return DensityGrid[((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1)]; }; + auto SampleGradientG = [&](int32 gx, int32 gy, int32 gz) -> float + { + const float Cached = SampleG(gx, gy, gz); + if (!ShapeWorldContext || Step != 1 + || !ShapeWorldContext->IsProxyBandFallback(Cached)) + { + return Cached; + } + return ShapeWorldContext->DensityAtUnclamped( + static_cast(OriginVoxels.X + gx * Step), + static_cast(OriginVoxels.Y + gy * Step), + static_cast(OriginVoxels.Z + gz * Step)); + }; auto GradAt = [&](int32 gx, int32 gy, int32 gz) -> FVector { // Densité négative=solide, positive=air → le gradient pointe vers l'air (sortant). return FVector( - SampleG(gx + 1, gy, gz) - SampleG(gx - 1, gy, gz), - SampleG(gx, gy + 1, gz) - SampleG(gx, gy - 1, gz), - SampleG(gx, gy, gz + 1) - SampleG(gx, gy, gz - 1)); + SampleGradientG(gx + 1, gy, gz) - SampleGradientG(gx - 1, gy, gz), + SampleGradientG(gx, gy + 1, gz) - SampleGradientG(gx, gy - 1, gz), + SampleGradientG(gx, gy, gz + 1) - SampleGradientG(gx, gy, gz - 1)); }; //========================================================================= @@ -1477,9 +1488,32 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, const int32 A = EdgeCorners[i][0]; const int32 B = EdgeCorners[i][1]; const float D1 = Densities[A], D2 = Densities[B]; - const float T = (FMath::Abs(D2 - D1) < KINDA_SMALL_NUMBER) + float T = (FMath::Abs(D2 - D1) < KINDA_SMALL_NUMBER) ? 0.5f : FMath::Clamp((IsoLevel - D1) / (D2 - D1), 0.0f, 1.0f); + if(ShapeWorldContext&&Step==1&&T>=0.375f&&T<=0.625f) + { + const FVector Midpoint=(Positions[A]+Positions[B])*0.5f; + const float MidDensity=ShapeWorldContext->DensityAtUnclamped( + Midpoint.X/VOXEL_SIZE,Midpoint.Y/VOXEL_SIZE, + Midpoint.Z/VOXEL_SIZE); + if(FMath::Abs(MidDensity-IsoLevel)<1.0e-6f) + T=0.5f; + else if((MidDensityKINDA_SMALL_NUMBER) + T=0.5f+0.5f*FMath::Clamp( + (IsoLevel-MidDensity)/Denominator,0.0f,1.0f); + } + else + { + const float Denominator=MidDensity-D1; + if(FMath::Abs(Denominator)>KINDA_SMALL_NUMBER) + T=0.5f*FMath::Clamp( + (IsoLevel-D1)/Denominator,0.0f,1.0f); + } + } EdgeVertices[i] = Positions[A] + T * (Positions[B] - Positions[A]); EdgeNormals[i] = Gradients[A] + T * (Gradients[B] - Gradients[A]); } @@ -1559,6 +1593,9 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, // matériau. Une arête de surface posée sur une face de frontière a sa coordonnée d'axe EXACTE // (l'interpolation MC garde fixe l'axe de la face) → comparaison flottante exacte fiable. MeshData.NumSurfaceVertices = MeshData.Vertices.Num(); + const int32 GroundSurfaceTriangleCount=GroundTris.Num()/3; + const int32 CapSurfaceTriangleCount=CapTris.Num()/3; + TArray GroundSkirtFaceMasks,CapSkirtFaceMasks; if (bGenerateSkirts && (GroundTris.Num() + CapTris.Num()) > 0) { const float ExtentCm = (float)(CellsPerAxis * Step) * VOXEL_SIZE; @@ -1566,11 +1603,16 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, const float MaxX = MinX + ExtentCm, MaxY = MinY + ExtentCm, MaxZ = MinZ + ExtentCm; const float SkirtDepth = FMath::Max(1.0f, SkirtCells) * (float)Step * VOXEL_SIZE; - auto OnBoundaryPlane = [&](const FVector& A, const FVector& B) -> bool + auto BoundaryFaceMask = [&](const FVector& A, const FVector& B) -> uint8 { - return (A.X == MinX && B.X == MinX) || (A.X == MaxX && B.X == MaxX) - || (A.Y == MinY && B.Y == MinY) || (A.Y == MaxY && B.Y == MaxY) - || (A.Z == MinZ && B.Z == MinZ) || (A.Z == MaxZ && B.Z == MaxZ); + uint8 Mask=0; + if(A.X==MinX&&B.X==MinX)Mask|=1u<<0; + if(A.X==MaxX&&B.X==MaxX)Mask|=1u<<1; + if(A.Y==MinY&&B.Y==MinY)Mask|=1u<<2; + if(A.Y==MaxY&&B.Y==MaxY)Mask|=1u<<3; + if(A.Z==MinZ&&B.Z==MinZ)Mask|=1u<<4; + if(A.Z==MaxZ&&B.Z==MaxZ)Mask|=1u<<5; + return Mask; }; auto AddSkirtVert = [&](const FVector& Pos, const FVector& Nrm, const FColor& Col) -> int32 @@ -1592,10 +1634,87 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, // F17 — jupes émises PAR SEAU : chaque jupe hérite la classe (donc le polygroup / // matériau) de son triangle source ; ajouter les jupes après coup casserait les runs // d'indices contigus par groupe qu'exige RMC. - auto EmitSkirts = [&](TArray& Tris) + auto EmitSkirts = [&](TArray& Tris,TArray& FaceMasks) { // On ajoute en itérant : on fige le nombre de triangles de surface et on n'ajoute qu'au-delà. const int32 BaseTriNum = Tris.Num(); + struct FSkirtVertex + { + FVector Position; + FVector Normal; + FColor Color; + float Density=0.0f; + int32 OriginalIndex=INDEX_NONE; + }; + const auto MakeSkirtVertex=[&](const FVector& Position,const FVector& Normal, + const FColor& Color,bool bOnSurface,int32 OriginalIndex) + { + FSkirtVertex Vertex{Position,Normal,Color,0.0f,OriginalIndex}; + if(!bOnSurface&&ShapeWorldContext) + Vertex.Density=ShapeWorldContext->DensityAt(Position.X/VOXEL_SIZE, + Position.Y/VOXEL_SIZE,Position.Z/VOXEL_SIZE); + return Vertex; + }; + const auto InterpolateVertex=[](const FSkirtVertex& A,const FSkirtVertex& B,float T) + { + if(T<=1.0e-6f)return A; + if(T>=1.0f-1.0e-6f)return B; + FSkirtVertex Out; + Out.Position=FMath::Lerp(A.Position,B.Position,T); + Out.Normal=FMath::Lerp(A.Normal,B.Normal,T).GetSafeNormal(); + Out.Color=FColor( + static_cast(FMath::Clamp(FMath::RoundToInt(FMath::Lerp(static_cast(A.Color.R),static_cast(B.Color.R),T)),0,255)), + static_cast(FMath::Clamp(FMath::RoundToInt(FMath::Lerp(static_cast(A.Color.G),static_cast(B.Color.G),T)),0,255)), + static_cast(FMath::Clamp(FMath::RoundToInt(FMath::Lerp(static_cast(A.Color.B),static_cast(B.Color.B),T)),0,255)), + static_cast(FMath::Clamp(FMath::RoundToInt(FMath::Lerp(static_cast(A.Color.A),static_cast(B.Color.A),T)),0,255))); + Out.Density=FMath::Lerp(A.Density,B.Density,T); + Out.OriginalIndex=INDEX_NONE; + return Out; + }; + const auto EmitClippedTriangle=[&](const FSkirtVertex& A,const FSkirtVertex& B, + const FSkirtVertex& C,uint8 FaceMask) + { + FSkirtVertex Input[3]={A,B,C}; + FSkirtVertex Polygon[5]; + int32 PolygonCount=0; + for(int32 Edge=0;Edge<3;++Edge) + { + const FSkirtVertex& Start=Input[Edge]; + const FSkirtVertex& End=Input[(Edge+1)%3]; + const bool bStartSolid=Start.Density<=0.0f; + const bool bEndSolid=End.Density<=0.0f; + if(bStartSolid!=bEndSolid) + { + const float Denominator=Start.Density-End.Density; + const float T=FMath::Abs(Denominator)>1.0e-8f + ?FMath::Clamp(Start.Density/Denominator,0.0f,1.0f):0.5f; + Polygon[PolygonCount++]=InterpolateVertex(Start,End,T); + } + if(bEndSolid)Polygon[PolygonCount++]=End; + } + if(PolygonCount<3)return; + int32 PolygonIndices[5]; + for(int32 Vertex=0;Vertex=0 + ?Polygon[Vertex].OriginalIndex + :AddSkirtVert(Polygon[Vertex].Position,Polygon[Vertex].Normal, + Polygon[Vertex].Color); + for(int32 Fan=1;Fan+1DensityAt(Center.X,Center.Y,Center.Z)>0.0f)continue; + } + Tris.Add(PolygonIndices[0]);Tris.Add(PolygonIndices[Fan]);Tris.Add(PolygonIndices[Fan+1]); + Tris.Add(PolygonIndices[0]);Tris.Add(PolygonIndices[Fan+1]);Tris.Add(PolygonIndices[Fan]); + FaceMasks.Add(FaceMask);FaceMasks.Add(FaceMask); + } + }; for (int32 t = 0; t + 2 < BaseTriNum; t += 3) { const int32 Tri[3] = { Tris[t], Tris[t + 1], Tris[t + 2] }; @@ -1605,26 +1724,29 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, // COPIES par valeur — AddSkirtVert réalloue Vertices/Normals (invaliderait des refs). const FVector PA = MeshData.Vertices[iA]; const FVector PB = MeshData.Vertices[iB]; - if (!OnBoundaryPlane(PA, PB)) continue; + const uint8 FaceMask=BoundaryFaceMask(PA,PB); + if (!FaceMask) continue; const FVector NA = MeshData.Normals[iA]; const FVector NB = MeshData.Normals[iB]; const FColor CA = bEmitColors ? MeshData.Colors[iA] : FColor::White; const FColor CB = bEmitColors ? MeshData.Colors[iB] : FColor::White; - const int32 iA2 = AddSkirtVert(PA - NA * SkirtDepth, NA, CA); - const int32 iB2 = AddSkirtVert(PB - NB * SkirtDepth, NB, CB); + const FSkirtVertex A=MakeSkirtVertex(PA,NA,CA,true,iA); + const FSkirtVertex B=MakeSkirtVertex(PB,NB,CB,true,iB); + const FSkirtVertex A2=MakeSkirtVertex(PA-NA*SkirtDepth,NA,CA,false,INDEX_NONE); + const FSkirtVertex B2=MakeSkirtVertex(PB-NB*SkirtDepth,NB,CB,false,INDEX_NONE); - // Quad (iA, iB, iB2, iA2) → 2 triangles, émis dans LES DEUX orientations. - Tris.Add(iA); Tris.Add(iB); Tris.Add(iB2); - Tris.Add(iA); Tris.Add(iB2); Tris.Add(iA2); - Tris.Add(iA); Tris.Add(iB2); Tris.Add(iB); - Tris.Add(iA); Tris.Add(iA2); Tris.Add(iB2); + // Clip both source triangles against the solid half-space before emitting + // each orientation. A curtain that points into tunnel air must not become + // visible geometry beyond the sampled isosurface. + EmitClippedTriangle(A,B,B2,FaceMask); + EmitClippedTriangle(A,B2,A2,FaceMask); } } MeshData.NumSkirtTriangles += (Tris.Num() - BaseTriNum) / 3; }; - EmitSkirts(GroundTris); - EmitSkirts(CapTris); + EmitSkirts(GroundTris,GroundSkirtFaceMasks); + EmitSkirts(CapTris,CapSkirtFaceMasks); } // F17 — concatène sol PUIS sky-cap : un run d'indices contigu par polygroup (voir @@ -1633,6 +1755,14 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, MeshData.Triangles.Append(GroundTris); MeshData.Triangles.Append(CapTris); MeshData.NumCeilingTriangles = CapTris.Num() / 3; + MeshData.TriangleSkirtFaceMasks.Reset(); + MeshData.TriangleSkirtFaceMasks.Reserve(MeshData.Triangles.Num()/3); + for(int32 Triangle=0;Triangle Normals; // Normale lissée (gradient de densité) TArray Colors; // Masques matériau F6 (R=palette biome dominant, G=pente, // B=poids de fondu de bordure, A=palette biome voisin) + // Parallel to Triangles/3. Zero marks isosurface geometry; skirt triangles carry a + // six-face mask so a streaming host can hide skirts where adjacent tiles use the same LOD. + TArray TriangleSkirtFaceMasks; // Number of vertices on the actual density isosurface. LOD seam skirts are appended after // this prefix and must not be used as witnesses for terrain visibility audits. int32 NumSurfaceVertices = 0; @@ -392,6 +395,7 @@ struct FVoxelMeshData UVs.Empty(); Normals.Empty(); Colors.Empty(); + TriangleSkirtFaceMasks.Empty(); NumSurfaceVertices = 0; NumSkirtTriangles = 0; NumCeilingTriangles = 0;