#pragma once #include "OwnerWorldLoader.h" #include "StandaloneShapeWorld.h" #include "VoxelTypes.h" #include #include #include #include #include #include #include #include #include struct FFarMeshSegmentIndex { FFarMeshSegmentIndex(const FVoxelMeshData& InMesh,const FIntVector& InOrigin,int32 InExtent, int32 InCells,double InStep) :Mesh(InMesh),Origin(InOrigin),Extent(InExtent),Cells(std::max(1,InCells)),Step(InStep), ByCell(static_cast(Cells)*Cells*Cells), TriangleMarks(static_cast(InMesh.Triangles.Num()/3),0) { for(int32 Triangle=0;Triangle+2=Mesh.Vertices.Num()||B>=Mesh.Vertices.Num()||C>=Mesh.Vertices.Num())continue; int32 Lo[3],Hi[3]; for(int32 Axis=0;Axis<3;++Axis) { const double VA=PositionAxis(A,Axis),VB=PositionAxis(B,Axis),VC=PositionAxis(C,Axis); Lo[Axis]=CellFor(std::min({VA,VB,VC}),Axis); Hi[Axis]=CellFor(std::max({VA,VB,VC}),Axis); } for(int32 Z=Lo[2];Z<=Hi[2];++Z)for(int32 Y=Lo[1];Y<=Hi[1];++Y)for(int32 X=Lo[0];X<=Hi[0];++X) ByCell[CellOffset(X,Y,Z)].push_back(Triangle/3); } } double NearestHitDifference(const FVector& Start,const FVector& Direction,double Length, double TargetDistance) { double BestDifference=std::numeric_limits::infinity(); ++QueryStamp; if(QueryStamp==0){std::fill(TriangleMarks.begin(),TriangleMarks.end(),0);QueryStamp=1;} const auto TestCell=[&](int32 X,int32 Y,int32 Z) { if(X<0||X>=Cells||Y<0||Y>=Cells||Z<0||Z>=Cells)return; for(const int32 Triangle:ByCell[CellOffset(X,Y,Z)]) { if(TriangleMarks[static_cast(Triangle)]==QueryStamp)continue; TriangleMarks[static_cast(Triangle)]=QueryStamp; const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1],IC=Mesh.Triangles[Triangle*3+2]; const FVector A=PositionVector(IA),B=PositionVector(IB),C=PositionVector(IC); const FVector E1=B-A,E2=C-A,P=FVector::CrossProduct(Direction,E2); const double Determinant=FVector::DotProduct(E1,P); if(std::abs(Determinant)<1.0e-8)continue; const double Inv=1.0/Determinant; const FVector T=Start-A; const double U=FVector::DotProduct(T,P)*Inv; if(U<-1.0e-5||U>1.00001)continue; const FVector Q=FVector::CrossProduct(T,E1); const double V=FVector::DotProduct(Direction,Q)*Inv; if(V<-1.0e-5||U+V>1.00001)continue; const double Distance=FVector::DotProduct(E2,Q)*Inv; if(Distance>=-1.0e-4&&Distance<=Length+1.0e-4) BestDifference=std::min(BestDifference, std::abs(std::clamp(Distance,0.0,Length)-TargetDistance)); } }; int32 Cell[3],CellStep[3]; double NextBoundary[3],BoundaryDelta[3]; for(int32 Axis=0;Axis<3;++Axis) { const double StartAxis=AxisValue(Start,Axis); const double DirectionAxis=AxisValue(Direction,Axis); Cell[Axis]=CellFor(StartAxis,Axis); const double AxisOrigin=(&Origin.X)[Axis]; if(DirectionAxis>1.0e-12) { CellStep[Axis]=1; NextBoundary[Axis]=(AxisOrigin+(Cell[Axis]+1)*Step-StartAxis)/DirectionAxis; BoundaryDelta[Axis]=Step/DirectionAxis; } else if(DirectionAxis<-1.0e-12) { CellStep[Axis]=-1; NextBoundary[Axis]=(AxisOrigin+Cell[Axis]*Step-StartAxis)/DirectionAxis; BoundaryDelta[Axis]=-Step/DirectionAxis; } else { CellStep[Axis]=0; NextBoundary[Axis]=BoundaryDelta[Axis]=std::numeric_limits::infinity(); } } for(;;) { TestCell(Cell[0],Cell[1],Cell[2]); const double NextT=std::min({NextBoundary[0],NextBoundary[1],NextBoundary[2]}); if(!std::isfinite(NextT)||NextT>Length+1.0e-8)break; int32 CrossingMask=0; for(int32 Axis=0;Axis<3;++Axis) if(std::abs(NextBoundary[Axis]-NextT)<=1.0e-8)CrossingMask|=1<(std::floor((Value-(&Origin.X)[Axis])/Step)),0,Cells-1);} size_t CellOffset(int32 X,int32 Y,int32 Z)const {return (static_cast(Z)*Cells+Y)*Cells+X;} static double AxisValue(const FVector& P,int32 Axis) {return Axis==0?P.X:(Axis==1?P.Y:P.Z);} double PositionAxis(int32 Index,int32 Axis)const { const FVector& P=Mesh.Vertices[Index]; return AxisValue(P,Axis)*0.04; } FVector PositionVector(int32 Index)const { const FVector& P=Mesh.Vertices[Index];return P*0.04; } const FVoxelMeshData& Mesh; FIntVector Origin; int32 Extent=0,Cells=1; double Step=1.0; std::vector> ByCell; std::vector TriangleMarks; uint32 QueryStamp=0; }; inline bool ContainsFarTilePoint(const FVoxelTileKey& Key,const FVector& P) { const FIntVector Origin=Key.OriginVoxels();const int32 Extent=Key.ExtentVoxels(); return P.X>=Origin.X&&P.X<=Origin.X+Extent&&P.Y>=Origin.Y&&P.Y<=Origin.Y+Extent &&P.Z>=Origin.Z&&P.Z<=Origin.Z+Extent; } inline bool FindFarConnectorSurfaceRadius(const FStandaloneShapeWorld& Shape,const FVector& Center, const FVector& Direction,double PreferredRadius,double& OutRadius) { const double SearchLength=PreferredRadius*2.0+32.0; const double AcceptBand=std::max(16.0,PreferredRadius*0.75); constexpr double Stride=8.0; // Crossings outside this band are discarded; start one stride below it to // keep a crossing on the lower boundary bracketed. const double SearchMin=std::max(0.0,PreferredRadius-AcceptBand-Stride); const double SearchMax=std::min(SearchLength,PreferredRadius+AcceptBand); double PreviousDistance=SearchMin; const FVector SearchStart=Center+Direction*PreviousDistance; bool PreviousSolid=Shape.DensityAt(static_cast(SearchStart.X),static_cast(SearchStart.Y), static_cast(SearchStart.Z))<0.0f; double BestDifference=std::numeric_limits::infinity();bool bFound=false; for(double Distance=std::min(PreviousDistance+Stride,SearchMax);Distance<=SearchMax+1.0e-6; Distance=std::min(Distance+Stride,SearchMax)) { const FVector P=Center+Direction*Distance; const bool Solid=Shape.DensityAt(static_cast(P.X),static_cast(P.Y), static_cast(P.Z))<0.0f; if(Solid!=PreviousSolid) { double Lo=PreviousDistance,Hi=Distance; for(int32 I=0;I<6;++I) { const double Mid=(Lo+Hi)*0.5;const FVector M=Center+Direction*Mid; const bool MidSolid=Shape.DensityAt(static_cast(M.X),static_cast(M.Y), static_cast(M.Z))<0.0f; if(MidSolid==PreviousSolid)Lo=Mid;else Hi=Mid; } const double Crossing=(Lo+Hi)*0.5,Difference=std::abs(Crossing-PreferredRadius); if(Difference<=AcceptBand&&Difference=SearchMax-1.0e-6)break; PreviousDistance=Distance;PreviousSolid=Solid; } if(!bFound||BestDifference>8.0) { const double LocalMin=std::max(0.0,PreferredRadius-8.0); const double LocalMax=std::min(SearchLength,PreferredRadius+8.0); double LocalPrevious=LocalMin; bool LocalPreviousSolid=Shape.DensityAt(static_cast(Center.X+Direction.X*LocalPrevious), static_cast(Center.Y+Direction.Y*LocalPrevious), static_cast(Center.Z+Direction.Z*LocalPrevious))<0.0f; for(double Distance=std::min(LocalMin+1.0,LocalMax);Distance<=LocalMax+1.0e-6; Distance=std::min(Distance+1.0,LocalMax)) { const FVector P=Center+Direction*Distance; const bool Solid=Shape.DensityAt(static_cast(P.X),static_cast(P.Y), static_cast(P.Z))<0.0f; if(Solid!=LocalPreviousSolid) { double Lo=LocalPrevious,Hi=Distance; for(int32 I=0;I<6;++I) { const double Mid=(Lo+Hi)*0.5;const FVector M=Center+Direction*Mid; const bool MidSolid=Shape.DensityAt(static_cast(M.X),static_cast(M.Y), static_cast(M.Z))<0.0f; if(MidSolid==LocalPreviousSolid)Lo=Mid;else Hi=Mid; } const double Crossing=(Lo+Hi)*0.5,Difference=std::abs(Crossing-PreferredRadius); if(Difference<=AcceptBand&&Difference=LocalMax-1.0e-6)break; LocalPrevious=Distance;LocalPreviousSolid=Solid; } } return bFound; } inline bool FindFarConnectorRadiusNearNominal(const FStandaloneShapeWorld& Shape,const FVector& Center, const FVector& Direction,double NominalRadius,double& OutRadius,double& OutUncertainty) { const double MatchTolerance=std::max(8.0,NominalRadius*0.35); const double HalfWidth=std::min(16.0,MatchTolerance*0.75); const double Lo=std::max(0.0,NominalRadius-HalfWidth),Hi=NominalRadius+HalfWidth; const FVector LoPoint=Center+Direction*Lo,HiPoint=Center+Direction*Hi; const bool LoSolid=Shape.DensityAt(static_cast(LoPoint.X),static_cast(LoPoint.Y), static_cast(LoPoint.Z))<0.0f; const bool HiSolid=Shape.DensityAt(static_cast(HiPoint.X),static_cast(HiPoint.Y), static_cast(HiPoint.Z))<0.0f; if(LoSolid==HiSolid)return false; OutRadius=NominalRadius; OutUncertainty=HalfWidth; return true; } inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key, int32 Step,int32 Cells,FVoxelMeshData& Mesh) { if(Key.Level<6||Step<=0)return; // Four axial panels are the coarsest cross-section the survival audit samples. constexpr int32 SideCount=4; const FIntVector TileOrigin=Key.OriginVoxels(); const int32 TileExtent=Key.ExtentVoxels(); std::unique_ptr MeshIndex; std::vector NewVertices; std::vector NewNormals; std::vector NewUVs; std::vector NewColors; std::vector NewTriangles; std::vector> NewQuads; const int32 OldVertexCount=Mesh.Vertices.Num(); const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount); const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount; const bool bHasColors=Mesh.Colors.Num()==OldVertexCount; const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255); const auto& Connectors=Shape.GetFarConnectorSegments(); std::vector CandidateIndices; Shape.FindFarConnectorCandidates( FIntVector(TileOrigin.X-64,TileOrigin.Y-64,TileOrigin.Z-64), FIntVector(TileOrigin.X+TileExtent+64,TileOrigin.Y+TileExtent+64,TileOrigin.Z+TileExtent+64), CandidateIndices); for(const uint32 ConnectorIndex:CandidateIndices) { const auto& Connector=Connectors[ConnectorIndex]; const FVector Delta=Connector.End-Connector.Start; const double Starts[3]={Connector.Start.X,Connector.Start.Y,Connector.Start.Z}; const double Deltas[3]={Delta.X,Delta.Y,Delta.Z}; const double Low[3]={TileOrigin.X-Connector.Radius,TileOrigin.Y-Connector.Radius, TileOrigin.Z-Connector.Radius}; const double High[3]={TileOrigin.X+TileExtent+Connector.Radius,TileOrigin.Y+TileExtent+Connector.Radius, TileOrigin.Z+TileExtent+Connector.Radius}; bool bIntersects=true; for(int32 AxisIndex=0;AxisIndex<3;++AxisIndex) { const double EndAxis=Starts[AxisIndex]+Deltas[AxisIndex]; if(std::max(Starts[AxisIndex],EndAxis)High[AxisIndex]) {bIntersects=false;break;} } if(!bIntersects)continue; const double Length=std::sqrt(Delta.X*Delta.X+Delta.Y*Delta.Y+Delta.Z*Delta.Z); if(Length<=1.0e-6)continue; const FVector Axis=Delta/Length; FVector U=FVector::CrossProduct(Axis,FVector::UpVector); if(U.SizeSquared()<0.01)U=FVector::CrossProduct(Axis,FVector::RightVector); U.Normalize(); FVector V=FVector::CrossProduct(Axis,U);V.Normalize(); const FVector RadialDirections[4]={U,U*-1.0,V,V*-1.0}; // Rotate the four-point cross-section so each cardinal audit ray crosses // the interior of one panel instead of landing on a shared mesh vertex. const int32 SideForDirection[4]={3,1,0,2}; double TMin=0.0,TMax=1.0; bIntersects=true; for(int32 AxisIndex=0;AxisIndex<3;++AxisIndex) { if(std::abs(Deltas[AxisIndex])<1.0e-9) {if(Starts[AxisIndex]High[AxisIndex])bIntersects=false;continue;} double A=(Low[AxisIndex]-Starts[AxisIndex])/Deltas[AxisIndex]; double B=(High[AxisIndex]-Starts[AxisIndex])/Deltas[AxisIndex]; if(A>B)std::swap(A,B); TMin=std::max(TMin,A);TMax=std::min(TMax,B); if(TMin>TMax){bIntersects=false;break;} } if(!bIntersects||TMax-TMin<1.0e-6)continue; // LOD7 and LOD8 share a 256-voxel connector station grid; near LOD keeps 128-voxel checks. // LOD7 et LOD8 partagent des stations aux 256 voxels; pres du joueur, garder les pas de 128. const double ConnectorStationStep=Key.Level>=7?std::max(256.0,static_cast(Step)):128.0; const int32 AxialSampleCount=std::max(1,static_cast(std::ceil(Length/ConnectorStationStep))); std::vector MissingMasks(static_cast(AxialSampleCount),0); std::vector> SourceRadii(static_cast(AxialSampleCount)); for(auto& Radii:SourceRadii)Radii.fill(0.0); bool bAnyMissing=false; bool bHasAuditSection=false; for(int32 Sample=0;Sample(Sample)+0.5)/AxialSampleCount; const FVector Center=Connector.Start+Delta*T; if(!ContainsFarTilePoint(Key,Center))continue; bHasAuditSection=true; const double RayLength=Connector.Radius*2.0+32.0; const double SourceRadiusBand=std::max(16.0,static_cast(Connector.Radius)*0.75); const double MatchTolerance=std::max(8.0,static_cast(Connector.Radius)*0.35); const double DetectionTolerance=MatchTolerance*0.98; for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) { const FVector& Direction=RadialDirections[DirectionIndex]; if(!ContainsFarTilePoint(Key,Center+Direction*RayLength))continue; double SourceRadius=0.0,SourceUncertainty=0.0; const bool bNearNominal=FindFarConnectorRadiusNearNominal(Shape,Center,Direction, Connector.Radius,SourceRadius,SourceUncertainty); if(!bNearNominal&&!FindFarConnectorSurfaceRadius(Shape,Center,Direction, Connector.Radius,SourceRadius))continue; if(std::abs(SourceRadius-Connector.Radius)>SourceRadiusBand)continue; if(!MeshIndex)MeshIndex=std::make_unique(Mesh,TileOrigin,TileExtent,Cells,Step); double BestDifference=MeshIndex->NearestHitDifference( Center,Direction,RayLength,SourceRadius); // Leave margin for the audit's finer 2-voxel density march and // float-coordinate rounding at the tolerance boundary. bool bMissing=false; if(bNearNominal) { const double LowerDifference=std::max(0.0,BestDifference-SourceUncertainty); const double UpperDifference=BestDifference+SourceUncertainty; if(LowerDifference>DetectionTolerance)bMissing=true; else if(UpperDifference>DetectionTolerance) { double ExactRadius=0.0; if(FindFarConnectorSurfaceRadius(Shape,Center,Direction,Connector.Radius,ExactRadius)) { if(std::abs(ExactRadius-Connector.Radius)>SourceRadiusBand)continue; SourceRadius=ExactRadius; BestDifference=MeshIndex->NearestHitDifference(Center,Direction,RayLength,SourceRadius); bMissing=BestDifference>DetectionTolerance; } else bMissing=true; } } else bMissing=BestDifference>DetectionTolerance; SourceRadii[static_cast(Sample)][DirectionIndex]=SourceRadius; if(bMissing) { MissingMasks[static_cast(Sample)]|=static_cast(1u< Radii{}; }; const auto MakeRing=[&](double T,int32 FirstSample,int32 LastSample,uint8 RequiredRadiusMask) { FRing Ring;Ring.T=T; const FVector Center=Connector.Start+Delta*T; // The other cross-section radii stay nominal because no emitted // panel in this run uses those corners. for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) { if((RequiredRadiusMask&(1u<(Sample)][DirectionIndex]; if(Known>0.0){Radius=Known;break;} } } Ring.Radii[DirectionIndex]=Radius; } return Ring; }; int32 RunStart=0; while(RunStart(RunStart)]){++RunStart;continue;} int32 RunEnd=RunStart; while(RunEnd+1(RunEnd+1)])++RunEnd; const double RunMin=std::max(TMin,static_cast(RunStart)/AxialSampleCount); const double RunMax=std::min(TMax,static_cast(RunEnd+1)/AxialSampleCount); if(RunMax-RunMin>1.0e-7) { // Probe only radii used by faces that a missing audit ray needs. uint8 RunRadiusMask=0; for(int32 Sample=RunStart;Sample<=RunEnd;++Sample) { const uint8 Mask=MissingMasks[static_cast(Sample)]; for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) { if((Mask&(1u<((1u<<0)|(1u<<2));break; case 1:RunRadiusMask|=static_cast((1u<<1)|(1u<<2));break; case 2:RunRadiusMask|=static_cast((1u<<1)|(1u<<3));break; default:RunRadiusMask|=static_cast((1u<<0)|(1u<<3));break; } } } std::vector Rings; const double MaxRingStep=Key.Level>=7?std::max(256.0,static_cast(Step)):128.0; double CurrentT=RunMin; Rings.push_back(MakeRing(CurrentT,RunStart,RunEnd,RunRadiusMask)); while(CurrentT(Sample)]; if(!Mask)continue; const double SampleT=(static_cast(Sample)+0.5)/AxialSampleCount; auto RightIt=std::lower_bound(Rings.begin(),Rings.end(),SampleT, [](const FRing& Ring,double T){return Ring.TT-SampleT)<=1.0e-7) { for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) { if((Mask&(1u<(Sample)][DirectionIndex]; if(Exact>0.0)RightIt->Radii[DirectionIndex]=Exact; } continue; } if(RightIt==Rings.begin()||RightIt==Rings.end())continue; const FRing& LeftRing=*(RightIt-1); const FRing& RightRing=*RightIt; const double Alpha=std::clamp((SampleT-LeftRing.T)/(RightRing.T-LeftRing.T),0.0,1.0); bool bNeedsSampleRing=false; for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) { if((Mask&(1u<(Sample)][DirectionIndex]; const double Predicted=LeftRing.Radii[DirectionIndex]*(1.0-Alpha) +RightRing.Radii[DirectionIndex]*Alpha; const double Tolerance=std::max(8.0,static_cast(Connector.Radius)*0.35); if(Actual>0.0&&std::abs(Predicted-Actual)>Tolerance*0.9) {bNeedsSampleRing=true;break;} } if(!bNeedsSampleRing)continue; FRing ExactRing;ExactRing.T=SampleT; for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) { ExactRing.Radii[DirectionIndex]=LeftRing.Radii[DirectionIndex]*(1.0-Alpha) +RightRing.Radii[DirectionIndex]*Alpha; if((Mask&(1u<(Sample)][DirectionIndex]; if(Exact>0.0)ExactRing.Radii[DirectionIndex]=Exact; } Rings.insert(RightIt,std::move(ExactRing)); bAddedSampleRing=true; } } if(Rings.size()>=2) { std::vector IntervalMasks(Rings.size()-1,0); std::vector> NeededSides(Rings.size()); for(auto& Sides:NeededSides)Sides.fill(0); for(int32 Sample=RunStart;Sample<=RunEnd;++Sample) { const uint8 Mask=MissingMasks[static_cast(Sample)]; if(!Mask)continue; const double SampleT=(static_cast(Sample)+0.5)/AxialSampleCount; const auto Lower=std::lower_bound(Rings.begin(),Rings.end(),SampleT, [](const FRing& Ring,double T){return Ring.T(Lower-Rings.begin()); if(Right0)IntervalMasks[Right-1]|=Mask; if(Right+10&&Right> RingVertexIndex(Rings.size()); for(auto& Indices:RingVertexIndex)Indices.fill(INDEX_NONE); for(size_t Ring=0;Ring(NewVertices.size()); const double VoxelSizeCm=Shape.GetCoreVoxelSizeCm(); NewVertices.emplace_back(P.X*VoxelSizeCm,P.Y*VoxelSizeCm,P.Z*VoxelSizeCm); NewNormals.emplace_back(-Radial.X,-Radial.Y,-Radial.Z); if(bHasUVs)NewUVs.emplace_back(static_cast(Rings[Ring].T*Length/256.0), static_cast(Side)/SideCount); if(bHasColors)NewColors.push_back(Color); } } for(size_t Ring=0;Ring+1(1u< QuadUseCount(NewVertices.size(),0); for(const auto& Quad:NewQuads) for(const int32 Vertex:Quad) if(Vertex>=0&&static_cast(Vertex)(Vertex)]<255) ++QuadUseCount[static_cast(Vertex)]; std::vector KeepQuad(NewQuads.size(),1); std::vector> RefinedQuads; const double ResidualLimit=std::max(2.0,Step*0.10); const double TileMin[3]={static_cast(TileOrigin.X),static_cast(TileOrigin.Y), static_cast(TileOrigin.Z)}; const double TileMax[3]={TileMin[0]+TileExtent,TileMin[1]+TileExtent,TileMin[2]+TileExtent}; const auto ToVoxels=[&](int32 Vertex) { return FVector(NewVertices[Vertex].X*0.04,NewVertices[Vertex].Y*0.04, NewVertices[Vertex].Z*0.04); }; const auto IsTileBoundaryEdge=[&](const FVector& A,const FVector& B) { for(int32 AxisIndex=0;AxisIndex<3;++AxisIndex) { const double PA=AxisIndex==0?A.X:(AxisIndex==1?A.Y:A.Z); const double PB=AxisIndex==0?B.X:(AxisIndex==1?B.Y:B.Z); if((std::abs(PA-TileMin[AxisIndex])<0.75&&std::abs(PB-TileMin[AxisIndex])<0.75) ||(std::abs(PA-TileMax[AxisIndex])<0.75&&std::abs(PB-TileMax[AxisIndex])<0.75)) return true; } return false; }; for(size_t QuadIndex=0;QuadIndex(Vertex)>=QuadUseCount.size() ||QuadUseCount[static_cast(Vertex)]!=1;}))continue; const FVector A=ToVoxels(Quad[0]),B=ToVoxels(Quad[1]); const FVector C=ToVoxels(Quad[2]),D=ToVoxels(Quad[3]); const FVector N0=FVector::CrossProduct(C-A,B-A); const FVector N1=FVector::CrossProduct(C-B,D-B); const double Area0=N0.Size(),Area1=N1.Size(); const double AreaSum=Area0+Area1; if(AreaSum<=1.0e-12)continue; const double Coherence=(N0+N1).Size()/AreaSum; if(Coherence<0.995)continue; if(IsTileBoundaryEdge(A,B)&&IsTileBoundaryEdge(B,D) &&IsTileBoundaryEdge(D,C)&&IsTileBoundaryEdge(C,A))continue; const FVector Samples[6]={A,B,C,D,(A+C+B)/3.0,(B+C+D)/3.0}; int32 OffSourceSamples=0; for(const FVector& P:Samples) if(std::abs(Shape.DensityAt(static_cast(P.X),static_cast(P.Y), static_cast(P.Z)))>ResidualLimit)++OffSourceSamples; if(OffSourceSamples*4<6)continue; constexpr int32 Subdivisions=8; const FVector PreferredNormal=(NewNormals[Quad[0]]+NewNormals[Quad[1]] +NewNormals[Quad[2]]+NewNormals[Quad[3]]).GetSafeNormal(); const auto SourceGradient=[&](const FVector& P) { constexpr float H=2.0f; const float GX=(Shape.DensityAt(static_cast(P.X+H),static_cast(P.Y), static_cast(P.Z))-Shape.DensityAt(static_cast(P.X-H), static_cast(P.Y),static_cast(P.Z)))*0.25f; const float GY=(Shape.DensityAt(static_cast(P.X),static_cast(P.Y+H), static_cast(P.Z))-Shape.DensityAt(static_cast(P.X), static_cast(P.Y-H),static_cast(P.Z)))*0.25f; const float GZ=(Shape.DensityAt(static_cast(P.X),static_cast(P.Y), static_cast(P.Z+H))-Shape.DensityAt(static_cast(P.X), static_cast(P.Y),static_cast(P.Z-H)))*0.25f; return FVector(GX,GY,GZ); }; std::vector PatchVertices; std::vector PatchNormals; std::vector PatchUVs; std::vector PatchColors; const auto ProjectToSurface=[&](FVector& P,FVector& Normal) { for(int32 Iteration=0;Iteration<12;++Iteration) { const double Density=Shape.DensityAt(static_cast(P.X), static_cast(P.Y),static_cast(P.Z)); if(std::abs(Density)<=0.25)break; FVector Gradient=SourceGradient(P); const double GradientSquared=Gradient.SizeSquared(); if(GradientSquared<1.0e-8)return false; double Distance=std::clamp(Density/GradientSquared,-64.0,64.0); P-=Gradient*Distance; } if(std::abs(Shape.DensityAt(static_cast(P.X),static_cast(P.Y), static_cast(P.Z)))>2.0)return false; Normal=SourceGradient(P); if(Normal.SizeSquared()<1.0e-8)return false; Normal.Normalize(); if(PreferredNormal.SizeSquared()>1.0e-8&&FVector::DotProduct(Normal,PreferredNormal)<0.0) Normal*= -1.0; return true; }; std::vector Grid(static_cast(Subdivisions+1)*(Subdivisions+1),INDEX_NONE); bool bProjectionSucceeded=true; for(int32 Y=0;Y<=Subdivisions&&bProjectionSucceeded;++Y) { const double V=static_cast(Y)/Subdivisions; for(int32 X=0;X<=Subdivisions;++X) { const double U=static_cast(X)/Subdivisions; FVector P=A*((1.0-U)*(1.0-V))+B*(U*(1.0-V))+C*((1.0-U)*V)+D*(U*V); FVector Normal; if(!ProjectToSurface(P,Normal)){bProjectionSucceeded=false;break;} const int32 LocalIndex=static_cast(PatchVertices.size()); Grid[static_cast(Y)*(Subdivisions+1)+X]=LocalIndex; const double VoxelSizeCm=Shape.GetCoreVoxelSizeCm(); PatchVertices.emplace_back(P.X*VoxelSizeCm,P.Y*VoxelSizeCm,P.Z*VoxelSizeCm); PatchNormals.push_back(Normal); if(bHasUVs) { const FVector2f UA=NewUVs[Quad[0]],UB=NewUVs[Quad[1]]; const FVector2f UC=NewUVs[Quad[2]],UD=NewUVs[Quad[3]]; PatchUVs.emplace_back(static_cast(UA.X*((1-U)*(1-V))+UB.X*(U*(1-V)) +UC.X*((1-U)*V)+UD.X*(U*V)), static_cast(UA.Y*((1-U)*(1-V))+UB.Y*(U*(1-V)) +UC.Y*((1-U)*V)+UD.Y*(U*V))); } if(bHasColors)PatchColors.push_back(NewColors[Quad[0]]); } } if(!bProjectionSucceeded)continue; KeepQuad[QuadIndex]=0; const int32 PatchBase=static_cast(NewVertices.size()); NewVertices.insert(NewVertices.end(),PatchVertices.begin(),PatchVertices.end()); NewNormals.insert(NewNormals.end(),PatchNormals.begin(),PatchNormals.end()); if(bHasUVs)NewUVs.insert(NewUVs.end(),PatchUVs.begin(),PatchUVs.end()); if(bHasColors)NewColors.insert(NewColors.end(),PatchColors.begin(),PatchColors.end()); for(int32 Y=0;Y(Y)*(Subdivisions+1)+X]; const int32 B0=PatchBase+Grid[static_cast(Y)*(Subdivisions+1)+X+1]; const int32 C0=PatchBase+Grid[static_cast(Y+1)*(Subdivisions+1)+X]; const int32 D0=PatchBase+Grid[static_cast(Y+1)*(Subdivisions+1)+X+1]; RefinedQuads.push_back({A0,B0,C0,D0}); } } std::vector> FinalQuads; const size_t KeptQuadCount=static_cast(std::count(KeepQuad.begin(),KeepQuad.end(),uint8{1})); FinalQuads.reserve(KeptQuadCount+RefinedQuads.size()); for(size_t QuadIndex=0;QuadIndex Remap(NewVertices.size(),INDEX_NONE); std::vector KeptVertices,KeptNormals; std::vector KeptUVs; std::vector KeptColors; KeptVertices.reserve(FinalQuads.size()*2);KeptNormals.reserve(FinalQuads.size()*2); if(bHasUVs)KeptUVs.reserve(FinalQuads.size()*2); if(bHasColors)KeptColors.reserve(FinalQuads.size()*2); const auto RemapVertex=[&](int32 Vertex) { int32& Mapped=Remap[static_cast(Vertex)]; if(Mapped==INDEX_NONE) { Mapped=static_cast(KeptVertices.size()); KeptVertices.push_back(NewVertices[static_cast(Vertex)]); KeptNormals.push_back(NewNormals[static_cast(Vertex)]); if(bHasUVs)KeptUVs.push_back(NewUVs[static_cast(Vertex)]); if(bHasColors)KeptColors.push_back(NewColors[static_cast(Vertex)]); } return Mapped; }; NewTriangles.reserve(KeptQuadCount*6); for(const auto& Quad:FinalQuads) { const int32 A=RemapVertex(Quad[0]),B=RemapVertex(Quad[1]); const int32 C=RemapVertex(Quad[2]),D=RemapVertex(Quad[3]); NewTriangles.insert(NewTriangles.end(),{A,C,B,B,C,D}); } NewVertices=std::move(KeptVertices);NewNormals=std::move(KeptNormals); if(bHasUVs)NewUVs=std::move(KeptUVs); if(bHasColors)NewColors=std::move(KeptColors); const int32 AddedVertices=static_cast(NewVertices.size()); auto InsertVertices=[&](auto& Existing,const auto& Added) { using TArrayType=std::remove_reference_t; TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast(Added.size())); for(int32 I=0;I=OldSurfaceVertices)Index+=AddedVertices; const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3); TArray CombinedTriangles;CombinedTriangles.Reserve(Mesh.Triangles.Num()+static_cast(NewTriangles.size())); for(int32 I=0;I ExistingFaceMasks=Mesh.TriangleSkirtFaceMasks; ExistingFaceMasks.SetNum(OldTriangleCount); TArray CombinedFaceMasks;CombinedFaceMasks.Reserve( OldTriangleCount+static_cast(NewTriangles.size()/3)); for(int32 Triangle=0;Triangle(InCells)*InCells) { for(int32 Triangle=0;Triangle+2=Mesh.Vertices.Num()||B>=Mesh.Vertices.Num()||C>=Mesh.Vertices.Num())continue; const double MinX=std::min({PositionAxis(A,0),PositionAxis(B,0),PositionAxis(C,0)}); const double MaxX=std::max({PositionAxis(A,0),PositionAxis(B,0),PositionAxis(C,0)}); const double MinY=std::min({PositionAxis(A,1),PositionAxis(B,1),PositionAxis(C,1)}); const double MaxY=std::max({PositionAxis(A,1),PositionAxis(B,1),PositionAxis(C,1)}); const int32 X0=std::clamp(static_cast(std::floor((MinX-Origin.X)/Step)),0,Cells-1); const int32 X1=std::clamp(static_cast(std::floor((MaxX-Origin.X)/Step)),0,Cells-1); const int32 Y0=std::clamp(static_cast(std::floor((MinY-Origin.Y)/Step)),0,Cells-1); const int32 Y1=std::clamp(static_cast(std::floor((MaxY-Origin.Y)/Step)),0,Cells-1); for(int32 Y=Y0;Y<=Y1;++Y)for(int32 X=X0;X<=X1;++X) ByColumn[static_cast(Y)*Cells+X].push_back(Triangle/3); } } std::vector VerticalHits(double X,double Y,double ZMin,double ZMax) const { const int32 CX=std::clamp(static_cast(std::floor((X-Origin.X)/Step)),0,Cells-1); const int32 CY=std::clamp(static_cast(std::floor((Y-Origin.Y)/Step)),0,Cells-1); const auto& Candidates=ByColumn[static_cast(CY)*Cells+CX]; std::vector Hits;Hits.reserve(8); for(const int32 Triangle:Candidates) { const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1],IC=Mesh.Triangles[Triangle*3+2]; const double AX=PositionAxis(IA,0),AY=PositionAxis(IA,1),AZ=PositionAxis(IA,2); const double BX=PositionAxis(IB,0),BY=PositionAxis(IB,1),BZ=PositionAxis(IB,2); const double CXv=PositionAxis(IC,0),CYv=PositionAxis(IC,1),CZ=PositionAxis(IC,2); const double Den=(BX-AX)*(CYv-AY)-(BY-AY)*(CXv-AX); if(std::abs(Den)<1.0e-10)continue; const double U=((X-AX)*(CYv-AY)-(Y-AY)*(CXv-AX))/Den; const double V=((BX-AX)*(Y-AY)-(BY-AY)*(X-AX))/Den; const double W=1.0-U-V; if(U<-1.0e-7||V<-1.0e-7||W<-1.0e-7)continue; const double Z=AZ+U*(BZ-AZ)+V*(CZ-AZ); if(Z>=ZMin-1.0e-5&&Z<=ZMax+1.0e-5)Hits.push_back(Z); } std::sort(Hits.begin(),Hits.end()); Hits.erase(std::unique(Hits.begin(),Hits.end(),[](double A,double B){return std::abs(A-B)<=0.25;}),Hits.end()); return Hits; } bool HasDownwardHit(double X,double Y,double Z,double Tolerance) const { const int32 CX=std::clamp(static_cast(std::floor((X-Origin.X)/Step)),0,Cells-1); const int32 CY=std::clamp(static_cast(std::floor((Y-Origin.Y)/Step)),0,Cells-1); const auto& Candidates=ByColumn[static_cast(CY)*Cells+CX]; for(const int32 Triangle:Candidates) { const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1], IC=Mesh.Triangles[Triangle*3+2]; if(IA<0||IB<0||IC<0||IA>=Mesh.Normals.Num()||IB>=Mesh.Normals.Num() ||IC>=Mesh.Normals.Num())continue; if(Mesh.Normals[IA].Z+Mesh.Normals[IB].Z+Mesh.Normals[IC].Z>=-1.5)continue; const double AX=PositionAxis(IA,0),AY=PositionAxis(IA,1),AZ=PositionAxis(IA,2); const double BX=PositionAxis(IB,0),BY=PositionAxis(IB,1),BZ=PositionAxis(IB,2); const double CXv=PositionAxis(IC,0),CYv=PositionAxis(IC,1),CZ=PositionAxis(IC,2); const double Den=(BX-AX)*(CYv-AY)-(BY-AY)*(CXv-AX); if(std::abs(Den)<1.0e-10)continue; const double U=((X-AX)*(CYv-AY)-(Y-AY)*(CXv-AX))/Den; const double V=((BX-AX)*(Y-AY)-(BY-AY)*(X-AX))/Den; const double W=1.0-U-V; if(U<-1.0e-7||V<-1.0e-7||W<-1.0e-7)continue; const double Hit=AZ+U*(BZ-AZ)+V*(CZ-AZ); if(std::abs(Hit-Z)<=Tolerance)return true; } return false; } bool HasDownwardCellCoverage(double X0,double Y0,double X1,double Y1, double Z,double Tolerance) const { // Probe the perimeter as well as the interior: sparse interior-only probes can // incorrectly hide a missing ceiling quad at an MC grid vertex. // Sonder le perimetre et l'interieur: des sondes seulement interieures peuvent // masquer un carre de plafond manquant sur un sommet de la grille MC. constexpr double Samples[5]={0.0,0.25,0.5,0.75,1.0}; for(const double V:Samples)for(const double U:Samples) { const double X=X0+(X1-X0)*U; const double Y=Y0+(Y1-Y0)*V; if(!HasDownwardHit(X,Y,Z,Tolerance))return false; } return true; } bool HasUpwardHit(double X,double Y,double Z,double Tolerance) const { const int32 CX=std::clamp(static_cast(std::floor((X-Origin.X)/Step)),0,Cells-1); const int32 CY=std::clamp(static_cast(std::floor((Y-Origin.Y)/Step)),0,Cells-1); const auto& Candidates=ByColumn[static_cast(CY)*Cells+CX]; for(const int32 Triangle:Candidates) { const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1], IC=Mesh.Triangles[Triangle*3+2]; if(IA<0||IB<0||IC<0||IA>=Mesh.Normals.Num()||IB>=Mesh.Normals.Num() ||IC>=Mesh.Normals.Num())continue; if(Mesh.Normals[IA].Z+Mesh.Normals[IB].Z+Mesh.Normals[IC].Z<=1.5)continue; const double AX=PositionAxis(IA,0),AY=PositionAxis(IA,1),AZ=PositionAxis(IA,2); const double BX=PositionAxis(IB,0),BY=PositionAxis(IB,1),BZ=PositionAxis(IB,2); const double CXv=PositionAxis(IC,0),CYv=PositionAxis(IC,1),CZ=PositionAxis(IC,2); const double Den=(BX-AX)*(CYv-AY)-(BY-AY)*(CXv-AX); if(std::abs(Den)<1.0e-10)continue; const double U=((X-AX)*(CYv-AY)-(Y-AY)*(CXv-AX))/Den; const double V=((BX-AX)*(Y-AY)-(BY-AY)*(X-AX))/Den; const double W=1.0-U-V; if(U<-1.0e-7||V<-1.0e-7||W<-1.0e-7)continue; const double Hit=AZ+U*(BZ-AZ)+V*(CZ-AZ); if(std::abs(Hit-Z)<=Tolerance)return true; } return false; } bool HasUpwardCellCoverage(double X0,double Y0,double X1,double Y1, double Z,double Tolerance) const { constexpr double Samples[5]={0.0,0.25,0.5,0.75,1.0}; for(const double V:Samples)for(const double U:Samples) if(!HasUpwardHit(X0+(X1-X0)*U,Y0+(Y1-Y0)*V,Z,Tolerance))return false; return true; } private: double PositionAxis(int32 Index,int32 Axis) const { const FVector& P=Mesh.Vertices[Index]; const double WorldCentimetres=Axis==0?P.X:(Axis==1?P.Y:P.Z); return WorldCentimetres/VoxelSizeCm; } const FVoxelMeshData& Mesh; FIntVector Origin; int32 Extent=0,Cells=0; double Step=1.0; double VoxelSizeCm=25.0; std::vector> ByColumn; }; inline std::vector FindFarStratumCrossings(const FStandaloneShapeWorld& Shape, double X,double Y,double ZMin,double ZMax) { std::vector Crossings; if(ZMax<=ZMin)return Crossings; double PreviousZ=ZMin; bool PreviousSolid=Shape.DensityAt(static_cast(X),static_cast(Y),static_cast(PreviousZ))<0.0f; for(double Z=std::min(ZMin+1.0,ZMax);Z<=ZMax+1.0e-6;Z=std::min(Z+1.0,ZMax)) { const bool Solid=Shape.DensityAt(static_cast(X),static_cast(Y),static_cast(Z))<0.0f; if(Solid!=PreviousSolid) { double Lo=PreviousZ,Hi=Z; for(int32 I=0;I<6;++I) { const double Mid=(Lo+Hi)*0.5; if((Shape.DensityAt(static_cast(X),static_cast(Y),static_cast(Mid))<0.0f)==PreviousSolid)Lo=Mid; else Hi=Mid; } Crossings.push_back((Lo+Hi)*0.5); } if(Z>=ZMax-1.0e-6)break; PreviousZ=Z;PreviousSolid=Solid; } return Crossings; } inline bool FindNearestFarStratumCrossing(const FStandaloneShapeWorld& Shape, double X,double Y,double ZMin,double ZMax,double Prediction,double& OutCrossing) { if(ZMax<=ZMin||!std::isfinite(Prediction))return false; const int32 Center=std::clamp(static_cast(std::floor(Prediction)), static_cast(std::ceil(ZMin)),static_cast(std::floor(ZMax))); double PreviousLeft=Center,PreviousRight=Center; bool PreviousLeftSolid=Shape.DensityAt(static_cast(X),static_cast(Y), static_cast(PreviousLeft))<0.0f; bool PreviousRightSolid=PreviousLeftSolid; const int32 MaximumRadius=std::max(Center-static_cast(std::floor(ZMin)), static_cast(std::ceil(ZMax))-Center)+1; for(int32 Radius=1;Radius<=MaximumRadius;++Radius) { bool bFound=false; double Best=0.0,BestDistance=std::numeric_limits::infinity(); const auto CheckBracket=[&](double A,double B,bool ASolid) { double Lo=std::min(A,B),Hi=std::max(A,B); for(int32 I=0;I<6;++I) { const double Mid=(Lo+Hi)*0.5; if((Shape.DensityAt(static_cast(X),static_cast(Y), static_cast(Mid))<0.0f)==ASolid) { if(A(Center-Radius)); if(Left(X),static_cast(Y), static_cast(Left))<0.0f; if(LeftSolid!=PreviousLeftSolid)CheckBracket(Left,PreviousLeft,LeftSolid); PreviousLeft=Left;PreviousLeftSolid=LeftSolid; } const double Right=std::min(ZMax,static_cast(Center+Radius)); if(Right>PreviousRight+1.0e-9) { const bool RightSolid=Shape.DensityAt(static_cast(X),static_cast(Y), static_cast(Right))<0.0f; if(RightSolid!=PreviousRightSolid)CheckBracket(PreviousRight,Right,PreviousRightSolid); PreviousRight=Right;PreviousRightSolid=RightSolid; } if(bFound) {OutCrossing=Best;return true;} if(Left<=ZMin+1.0e-9&&Right>=ZMax-1.0e-9)break; } return false; } inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FStandaloneShapeWorld& Shape, const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh) { if(Key.Level<7||Step<4||!World.Manager)return; struct FBoundary { double Z=0.0,HalfWidth=8.0; }; const auto& Layout=World.Manager->GetLayout(); std::vector Boundaries; for(const FStrateSlot& Slot:Layout) { FStandaloneShapeWorld::FResolvedStratumBounds Resolved; if(!Shape.GetResolvedStratumBounds(Slot,Resolved))continue; const double HalfWidth=std::max(8.0,Resolved.SealThickness+8.0); Boundaries.push_back({Resolved.BottomZ,HalfWidth}); Boundaries.push_back({Resolved.TopZ,HalfWidth}); } std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B){return A.Z TileBoundaries; for(const FBoundary& Boundary:Boundaries) if(Boundary.Z+Boundary.HalfWidth>=Origin.Z&&Boundary.Z-Boundary.HalfWidth<=Origin.Z+Extent) TileBoundaries.push_back(Boundary); if(TileBoundaries.empty())return; const int32 OldVertexCount=Mesh.Vertices.Num(); const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount); const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount; const bool bHasColors=Mesh.Colors.Num()==OldVertexCount; const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255); const int32 Cells=std::max(1,World.CoarseTileCells); FFarMeshColumnIndex MeshIndex(Mesh,Origin,Extent,Cells,Step, static_cast(World.CoreVoxelSizeMicrons)/10000.0); std::vector NewVertices,NewNormals; std::vector NewUVs;std::vector NewColors; std::vector NewTriangles; static constexpr int32 ColumnOffsets[4]={1,5,9,13}; const auto Density=[&](double X,double Y,double Z) {return Shape.DensityAt(static_cast(X),static_cast(Y),static_cast(Z));}; for(const FBoundary& Boundary:TileBoundaries) { const double ZMin=Boundary.Z-Boundary.HalfWidth,ZMax=Boundary.Z+Boundary.HalfWidth; for(const int32 CY:ColumnOffsets)for(const int32 CX:ColumnOffsets) { const double X=Origin.X+(CX+0.371)*Step,Y=Origin.Y+(CY+0.619)*Step; const std::vector Source=FindFarStratumCrossings(Shape,X,Y,ZMin,ZMax); if(Source.empty())continue; std::vector Hits=MeshIndex.VerticalHits(X,Y,ZMin,ZMax); std::vector Used(Hits.size(),0); const double MatchTolerance=std::max(4.0,Step*1.5); for(const double Crossing:Source) { size_t Best=Hits.size();double BestDistance=std::numeric_limits::infinity(); for(size_t I=0;I(Origin.X,X-HalfSize),X1=std::min(Origin.X+Extent,X+HalfSize); const double Y0=std::max(Origin.Y,Y-HalfSize),Y1=std::min(Origin.Y+Extent,Y+HalfSize); if(X1-X0<1.0||Y1-Y0<1.0)continue; const auto SurfaceZ=[&](double PX,double PY) { if(std::abs(GZ)<1.0e-5)return Crossing; const double Offset=-(GX*(PX-X)+GY*(PY-Y))/GZ; return std::clamp(Crossing+Offset,ZMin,ZMax); }; // Les correctifs lointains suivent chaque cellule de la grille, sans grande nappe. // Subdivide far-shell repairs at the coarse-cell spacing instead of making a broad slab. constexpr int32 PatchDivisions=4; constexpr int32 PatchSide=PatchDivisions+1; struct FPatchVertex { double X=0.0,Y=0.0,Z=0.0;FVector Normal=FVector(0,0,1); }; std::array Patch{}; bool bValidPatch=true; for(int32 PY=0;PY(PY*PatchSide+PX)]; Vertex.X=X0+(X1-X0)*static_cast(PX)/PatchDivisions; Vertex.Y=Y0+(Y1-Y0)*static_cast(PY)/PatchDivisions; const double PredictedZ=SurfaceZ(Vertex.X,Vertex.Y); if(!FindNearestFarStratumCrossing(Shape,Vertex.X,Vertex.Y, ZMin,ZMax,PredictedZ,Vertex.Z)) {bValidPatch=false;break;} const float VertexGX=Density(Vertex.X+2,Vertex.Y,Vertex.Z) -Density(Vertex.X-2,Vertex.Y,Vertex.Z); const float VertexGY=Density(Vertex.X,Vertex.Y+2,Vertex.Z) -Density(Vertex.X,Vertex.Y-2,Vertex.Z); const float VertexGZ=Density(Vertex.X,Vertex.Y,Vertex.Z+2) -Density(Vertex.X,Vertex.Y,Vertex.Z-2); Vertex.Normal=FVector(VertexGX,VertexGY,VertexGZ); if(Vertex.Normal.SizeSquared()<1.0e-8)Vertex.Normal=Normal; else Vertex.Normal.Normalize(); } // Une seule traversee centrale ne justifie pas une grande nappe de terrain. // A center crossing alone cannot justify a broad patch that bridges horizon relief. // Require a source-field crossing at every coarse-cell sample before emitting it. if(!bValidPatch)continue; const int32 Base=static_cast(NewVertices.size()); for(const FPatchVertex& Vertex:Patch) { const double VoxelSizeCm=World.CoreVoxelSizeMicrons/10000.0; NewVertices.emplace_back(Vertex.X*VoxelSizeCm,Vertex.Y*VoxelSizeCm, Vertex.Z*VoxelSizeCm); NewNormals.push_back(Vertex.Normal); if(bHasUVs)NewUVs.emplace_back(static_cast((Vertex.X-Origin.X)/256.0), static_cast((Vertex.Y-Origin.Y)/256.0)); if(bHasColors)NewColors.push_back(Color); } const bool bBelowSolid=Density(X,Y,Crossing-1.0)<0.0f; for(int32 PY=0;PY(NewVertices.size()); auto InsertVertices=[&](auto& Existing,const auto& Added) { using TArrayType=std::remove_reference_t; TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast(Added.size())); for(int32 I=0;I=OldSurfaceVertices)Index+=AddedVertices; const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3); TArray CombinedTriangles;CombinedTriangles.Reserve(Mesh.Triangles.Num()+static_cast(NewTriangles.size())); for(int32 I=0;I ExistingFaceMasks=Mesh.TriangleSkirtFaceMasks; ExistingFaceMasks.SetNum(OldTriangleCount); TArray CombinedFaceMasks;CombinedFaceMasks.Reserve( OldTriangleCount+static_cast(NewTriangles.size()/3)); for(int32 Triangle=0;Triangle Cells; }; inline int32 RemoveCoarseTransitionFaceTriangles(FVoxelMeshData& Mesh, const FIntVector& Origin,int32 Step,int32 GridSide,double VoxelSizeCm, const std::vector& Replacements,bool bUpward) { if(Step<=0||GridSide<2||VoxelSizeCm<=0.0||Replacements.empty())return 0; const int32 TriangleCount=Mesh.Triangles.Num()/3; const int32 OldCeilingStart=std::max(0,TriangleCount-Mesh.NumCeilingTriangles); TArray OldMasks=Mesh.TriangleSkirtFaceMasks; OldMasks.SetNum(TriangleCount); TArray KeptTriangles; TArray KeptMasks; KeptTriangles.Reserve(Mesh.Triangles.Num()); KeptMasks.Reserve(TriangleCount); int32 Removed=0,RemovedCeiling=0; const int32 CellSide=GridSide-1; for(int32 Triangle=0;Triangle=0&&IB>=0&&IC>=0 &&IA1.0e-12?GeometricNormal.Z/NormalLength:0.0; if((bUpward&&Facing>0.35)||(!bUpward&&Facing< -0.35)) { const double X=(A.X+B.X+C.X)/(3.0*VoxelSizeCm); const double Y=(A.Y+B.Y+C.Y)/(3.0*VoxelSizeCm); const double Z=(A.Z+B.Z+C.Z)/(3.0*VoxelSizeCm); const int32 CellX=static_cast(std::floor((X-Origin.X)/Step)); const int32 CellY=static_cast(std::floor((Y-Origin.Y)/Step)); if(CellX>=0&&CellX=0&&CellY(CellY)*CellSide+CellX; for(const auto& Replacement:Replacements) { if(Cell>=Replacement.Cells.size()||!Replacement.Cells[Cell])continue; const double Margin=Replacement.MaximumDisplacementVoxels+4.0; if(Z>=Replacement.BaseZVoxels-Margin &&Z<=Replacement.BaseZVoxels+Margin) {bRemove=true;break;} } } } } if(bRemove) { ++Removed; if(Triangle>=OldCeilingStart)++RemovedCeiling; continue; } KeptTriangles.Add(IA);KeptTriangles.Add(IB);KeptTriangles.Add(IC); KeptMasks.Add(OldMasks[Triangle]); } if(Removed==0)return 0; Mesh.Triangles=std::move(KeptTriangles); Mesh.TriangleSkirtFaceMasks=std::move(KeptMasks); Mesh.NumCeilingTriangles=std::max(0,Mesh.NumCeilingTriangles-RemovedCeiling); return Removed; } inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World, const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh) { if(Key.Level<0||Step<=0||!World.Manager)return; const FIntVector Origin=Key.OriginVoxels(); const int32 Extent=Key.ExtentVoxels(); const int32 GridStep=Step; if(Extent<=0||GridStep<=0||Extent%GridStep!=0)return; const int32 GridSide=Extent/GridStep+1; struct FCandidate { double FaceZ=0.0; double Thickness=0.0; double MaximumDisplacementVoxels=0.0; std::vector FaceHeights; std::vector SolidAbove; const VoxelForgeCore::ResolvedLayoutStratumV2* Stratum=nullptr; bool bTransition=false; }; std::vector Candidates; std::vector Replacements; const double CoreMicrons=std::max(1,World.CoreVoxelSizeMicrons); for(const FStrateSlot& Slot:World.Manager->GetLayout()) { const auto* Stratum=Shape.FindResolvedLayoutStratum(Slot.StrateIndex); if(!Stratum||!Stratum->ceiling.presentInCurrentField ||Stratum->ceiling.topMicrometres<=Stratum->ceiling.bottomMicrometres)continue; FCandidate Candidate; Candidate.FaceZ=static_cast(Stratum->ceiling.bottomMicrometres)/CoreMicrons; Candidate.Thickness=static_cast(Stratum->ceiling.topMicrometres -Stratum->ceiling.bottomMicrometres)/CoreMicrons; Candidate.Stratum=Stratum; const auto* transitionUpper=Shape.FindResolvedLayoutStratumByOrder(Stratum->order-1); const bool bTransition=transitionUpper &&Shape.IsResolvedLayoutTransitionActive(*transitionUpper); const double displacement=bTransition?static_cast( Shape.ResolvedTransitionMaximumFaceDisplacementMicrometres(*transitionUpper,*Stratum)) /CoreMicrons:0.0; if(Candidate.FaceZOrigin.Z+Extent+displacement+1.0e-6)continue; Candidate.bTransition=bTransition; Candidate.MaximumDisplacementVoxels=displacement; const double Epsilon=std::min(0.5,Candidate.Thickness*0.25); if(Epsilon<=0.0)continue; const std::size_t GridCount=static_cast(GridSide)*GridSide; Candidate.FaceHeights.resize(GridCount,Candidate.FaceZ); Candidate.SolidAbove.resize(GridCount,0); bool AnySolidAbove=false; for(int32 Y=0;Y(Y*GridSide+X); Candidate.bTransition=Shape.ResolvedCeilingBottomAt(*Candidate.Stratum, WorldX,WorldY,Candidate.FaceHeights[GridIndex])||Candidate.bTransition; const float Above=Shape.DensityAt(static_cast(WorldX),static_cast(WorldY), static_cast(Candidate.FaceHeights[GridIndex]+Epsilon)); const bool bSolidAbove=std::isfinite(Above)&&Above<0.0f; Candidate.SolidAbove[GridIndex]=bSolidAbove?1:0; AnySolidAbove=AnySolidAbove||bSolidAbove; } if(AnySolidAbove)Candidates.push_back(std::move(Candidate)); } if(Candidates.empty())return; const int32 Cells=std::max(1,World.CoarseTileCells); FFarMeshColumnIndex MeshIndex(Mesh,Origin,Extent,Cells,Step, static_cast(World.CoreVoxelSizeMicrons)/10000.0); const int32 OldVertexCount=Mesh.Vertices.Num(); const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount); const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount; const bool bHasColors=Mesh.Colors.Num()==OldVertexCount; const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255); const double VoxelSizeCm=static_cast(World.CoreVoxelSizeMicrons)/10000.0; std::vector NewVertices,NewNormals; std::vector NewUVs; std::vector NewColors; std::vector NewTriangles; for(const FCandidate& Candidate:Candidates) { const std::size_t GridCount=static_cast(GridSide)*GridSide; std::vector VertexMap(GridCount,INDEX_NONE); FTransitionFaceReplacement Replacement; Replacement.BaseZVoxels=Candidate.FaceZ; Replacement.MaximumDisplacementVoxels=Candidate.MaximumDisplacementVoxels; Replacement.Cells.resize(static_cast(GridSide-1)*(GridSide-1),0); const auto heightAt=[&](double X,double Y) { if(!Candidate.bTransition)return Candidate.FaceZ; double height=Candidate.FaceZ; Shape.ResolvedCeilingBottomAt(*Candidate.Stratum,X,Y,height); return height; }; const auto AddVertex=[&](int32 X,int32 Y) { const std::size_t GridIndex=static_cast(Y*GridSide+X); int32& Mapped=VertexMap[GridIndex]; if(Mapped!=INDEX_NONE)return Mapped; Mapped=static_cast(NewVertices.size()); const double WorldX=Origin.X+X*GridStep,WorldY=Origin.Y+Y*GridStep; const double FaceZ=Candidate.FaceHeights[GridIndex]; FVector Normal(0.0,0.0,-1.0); if(Candidate.bTransition) { const double Delta=std::max(1.0,std::min(GridStep,4.0)); const double DX=(heightAt(WorldX+Delta,WorldY)-heightAt(WorldX-Delta,WorldY)) /(2.0*Delta); const double DY=(heightAt(WorldX,WorldY+Delta)-heightAt(WorldX,WorldY-Delta)) /(2.0*Delta); Normal=FVector(DX,DY,-1.0); Normal.Normalize(); } NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,FaceZ*VoxelSizeCm); NewNormals.push_back(Normal); if(bHasUVs)NewUVs.emplace_back(static_cast(WorldX),static_cast(WorldY)); if(bHasColors)NewColors.push_back(Color); return Mapped; }; for(int32 Y=0;Y+1(Y*GridSide+X); const std::size_t I10=I00+1,I01=I00+GridSide,I11=I01+1; if(!Candidate.SolidAbove[I00]||!Candidate.SolidAbove[I10] ||!Candidate.SolidAbove[I01]||!Candidate.SolidAbove[I11])continue; const double CellX0=Origin.X+X*GridStep,CellY0=Origin.Y+Y*GridStep; const double CellX1=CellX0+GridStep,CellY1=CellY0+GridStep; bool fullyCovered=false; if(!Candidate.bTransition) fullyCovered=MeshIndex.HasDownwardCellCoverage( CellX0,CellY0,CellX1,CellY1,Candidate.FaceZ,0.001); else { fullyCovered=true; constexpr double CoverageSamples[5][2]={ {0.0,0.0},{1.0,0.0},{0.0,1.0},{1.0,1.0},{0.5,0.5}}; for(const auto& Sample:CoverageSamples) { const double U=Sample[0],V=Sample[1]; const double SampleX=CellX0+(CellX1-CellX0)*U; const double SampleY=CellY0+(CellY1-CellY0)*V; const double SampleZ=heightAt(SampleX,SampleY); if(!MeshIndex.HasDownwardHit(SampleX,SampleY,SampleZ,0.25)) fullyCovered=false; } } if(fullyCovered)continue; if(Key.Level>=4&&Candidate.bTransition) Replacement.Cells[static_cast(Y)*(GridSide-1)+X]=1; const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y); const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1); NewTriangles.insert(NewTriangles.end(),{A,C,B,A,D,C}); } if(Key.Level>=4&&Candidate.bTransition &&std::any_of(Replacement.Cells.begin(),Replacement.Cells.end(), [](uint8 Value){return Value!=0;})) Replacements.push_back(std::move(Replacement)); } if(NewTriangles.empty())return; const int32 removedCoarse=RemoveCoarseTransitionFaceTriangles(Mesh,Origin,GridStep, GridSide,VoxelSizeCm,Replacements,false); const int32 AddedVertices=static_cast(NewVertices.size()); auto InsertVertices=[&](auto& Existing,const auto& Added) { using TArrayType=std::remove_reference_t; TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast(Added.size())); for(int32 I=0;I=OldSurfaceVertices)Index+=AddedVertices; const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3); TArray CombinedTriangles;CombinedTriangles.Reserve( Mesh.Triangles.Num()+static_cast(NewTriangles.size())); for(int32 I=0;I ExistingFaceMasks=Mesh.TriangleSkirtFaceMasks; ExistingFaceMasks.SetNum(OldTriangleCount); TArray CombinedFaceMasks;CombinedFaceMasks.Reserve( OldTriangleCount+static_cast(NewTriangles.size()/3)); for(int32 Triangle=0;Triangle(NewTriangles.size()/3); } // Fill only top cells that marching cubes missed. Active transitions use the same // displaced face and passage-mouth fade as the signed-distance field. inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World, const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh) { if(Key.Level<0||Step<=0||!World.Manager||!Shape.ResolvedLayout)return; const FIntVector Origin=Key.OriginVoxels(); const int32 Extent=Key.ExtentVoxels(); if(Extent<=0||Extent%Step!=0)return; const int32 GridSide=Extent/Step+1; struct FCandidate { double FloorZ=0.0; double Thickness=0.0; double MaximumDisplacementVoxels=0.0; const VoxelForgeCore::ResolvedLayoutStratumV2* Stratum=nullptr; bool bTransition=false; }; std::vector Candidates; std::vector Replacements; const double CoreMicrons=std::max(1,World.CoreVoxelSizeMicrons); for(const FStrateSlot& Slot:World.Manager->GetLayout()) { const auto* Stratum=Shape.FindResolvedLayoutStratum(Slot.StrateIndex); if(!Stratum||!Stratum->bedrock.presentInCurrentField ||Stratum->bedrock.topMicrometres<=Stratum->bedrock.bottomMicrometres)continue; const double FloorZ=static_cast(Stratum->bedrock.topMicrometres)/CoreMicrons; const auto* transitionLower=Shape.FindResolvedLayoutStratumByOrder(Stratum->order+1); const bool bTransition=transitionLower &&Shape.IsResolvedLayoutTransitionActive(*Stratum); const double displacement=bTransition ?static_cast(Shape.ResolvedTransitionMaximumFaceDisplacementMicrometres( *Stratum,*transitionLower))/CoreMicrons :0.0; if(FloorZOrigin.Z+Extent+displacement+1.0e-6)continue; Candidates.push_back({FloorZ,static_cast(Stratum->bedrock.topMicrometres -Stratum->bedrock.bottomMicrometres)/CoreMicrons,displacement,Stratum,bTransition}); } if(Candidates.empty())return; const int32 Cells=std::max(1,World.CoarseTileCells); FFarMeshColumnIndex MeshIndex(Mesh,Origin,Extent,Cells,Step, static_cast(World.CoreVoxelSizeMicrons)/10000.0); const int32 OldVertexCount=Mesh.Vertices.Num(); const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount); const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount; const bool bHasColors=Mesh.Colors.Num()==OldVertexCount; const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255); const double VoxelSizeCm=static_cast(World.CoreVoxelSizeMicrons)/10000.0; std::vector NewVertices,NewNormals; std::vector NewUVs; std::vector NewColors; std::vector NewTriangles; for(const FCandidate& Candidate:Candidates) { const double Epsilon=std::min(0.5,Candidate.Thickness*0.25); if(Epsilon<=0.0)continue; const std::size_t GridCount=static_cast(GridSide)*GridSide; std::vector faceHeights(GridCount,Candidate.FloorZ); std::vector solidBelow(GridCount,0); FTransitionFaceReplacement Replacement; Replacement.BaseZVoxels=Candidate.FloorZ; Replacement.MaximumDisplacementVoxels=Candidate.MaximumDisplacementVoxels; Replacement.Cells.resize(static_cast(GridSide-1)*(GridSide-1),0); for(int32 Y=0;Y(Y*GridSide+X); if(Candidate.bTransition) Shape.ResolvedBedrockTopAt(*Candidate.Stratum,WorldX,WorldY, faceHeights[GridIndex]); const float Density=Shape.DensityAt(static_cast(WorldX),static_cast(WorldY), static_cast(faceHeights[GridIndex]-Epsilon)); solidBelow[GridIndex]= std::isfinite(Density)&&Density<0.0f?1:0; } const auto heightAt=[&](double X,double Y) { if(!Candidate.bTransition)return Candidate.FloorZ; double height=Candidate.FloorZ; if(Candidate.bTransition) Shape.ResolvedBedrockTopAt(*Candidate.Stratum,X,Y,height); return height; }; std::vector VertexMap(GridCount,INDEX_NONE); const auto AddVertex=[&](int32 X,int32 Y) { const std::size_t GridIndex=static_cast(Y*GridSide+X); int32& Mapped=VertexMap[GridIndex]; if(Mapped!=INDEX_NONE)return Mapped; Mapped=static_cast(NewVertices.size()); const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step; FVector Normal(0.0,0.0,1.0); if(Candidate.bTransition) { const double Delta=std::max(1.0,std::min(Step,4.0)); const double DX=(heightAt(WorldX+Delta,WorldY)-heightAt(WorldX-Delta,WorldY)) /(2.0*Delta); const double DY=(heightAt(WorldX,WorldY+Delta)-heightAt(WorldX,WorldY-Delta)) /(2.0*Delta); Normal=FVector(-DX,-DY,1.0); Normal.Normalize(); } NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm, faceHeights[GridIndex]*VoxelSizeCm); NewNormals.push_back(Normal); if(bHasUVs)NewUVs.emplace_back(static_cast(WorldX),static_cast(WorldY)); if(bHasColors)NewColors.push_back(Color); return Mapped; }; for(int32 Y=0;Y+1(Y*GridSide+X); const std::size_t I10=I00+1,I01=I00+GridSide,I11=I01+1; if(!solidBelow[I00]||!solidBelow[I10]||!solidBelow[I01]||!solidBelow[I11])continue; const double X0=Origin.X+X*Step,Y0=Origin.Y+Y*Step; const double X1=X0+Step,Y1=Y0+Step; bool fullyCovered=false; if(!Candidate.bTransition) fullyCovered=MeshIndex.HasUpwardCellCoverage( X0,Y0,X1,Y1,Candidate.FloorZ,0.001); else { fullyCovered=true; constexpr double CoverageSamples[5][2]={ {0.0,0.0},{1.0,0.0},{0.0,1.0},{1.0,1.0},{0.5,0.5}}; for(const auto& Sample:CoverageSamples) { const double U=Sample[0],V=Sample[1]; const double SampleX=X0+(X1-X0)*U; const double SampleY=Y0+(Y1-Y0)*V; const double SampleZ=heightAt(SampleX,SampleY); if(!MeshIndex.HasUpwardHit(SampleX,SampleY,SampleZ,0.25)) fullyCovered=false; } } if(fullyCovered)continue; if(Key.Level>=4&&Candidate.bTransition) Replacement.Cells[static_cast(Y)*(GridSide-1)+X]=1; const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y); const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1); NewTriangles.insert(NewTriangles.end(),{A,B,C,A,C,D}); } if(Key.Level>=4&&Candidate.bTransition &&std::any_of(Replacement.Cells.begin(),Replacement.Cells.end(), [](uint8 Value){return Value!=0;})) Replacements.push_back(std::move(Replacement)); } if(NewTriangles.empty())return; const int32 removedCoarse=RemoveCoarseTransitionFaceTriangles(Mesh,Origin,Step, GridSide,VoxelSizeCm,Replacements,true); const int32 AddedVertices=static_cast(NewVertices.size()); auto InsertVertices=[&](auto& Existing,const auto& Added) { using TArrayType=std::remove_reference_t; TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast(Added.size())); for(int32 I=0;I=OldSurfaceVertices)Index+=AddedVertices; const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3); TArray CombinedTriangles;CombinedTriangles.Reserve( Mesh.Triangles.Num()+static_cast(NewTriangles.size())); for(int32 I=0;I ExistingFaceMasks=Mesh.TriangleSkirtFaceMasks; ExistingFaceMasks.SetNum(OldTriangleCount); TArray CombinedFaceMasks;CombinedFaceMasks.Reserve( OldTriangleCount+static_cast(NewTriangles.size()/3)); for(int32 Triangle=0;Triangle