Files
VoxelForge/Standalone/Core/src/FarShellsInternal.h
T
Fr0zkaandClaude Opus 5.5 d0158c6019 wip(assembler): round L32 -- smooth Gradient interface at full amplitude (no rings, not flat)
Band faces now use signed distance to the displaced solid intervals, continuous across bands, so
MC interpolates the interface instead of terracing at density levels. The Gradient amplitude is
restored to width/8 (about 4 m peak-to-peak at 16 m). Ring/terrace audit at LOD0-8 on the FI floor and SW
ceiling passes; negative controls: L30 (rings) and L31 (flat) both fail. Hole/interface audits,
16 passage rows and 24/24 determinism pass. Evaluator 20. Cost vs L31 SW +1.9%, FI +1.5%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
2026-10-21 20:27:29 +02:00

1682 lines
87 KiB
C++

#pragma once
#include "OwnerWorldLoader.h"
#include "StandaloneShapeWorld.h"
#include "VoxelTypes.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <limits>
#include <memory>
#include <type_traits>
#include <utility>
#include <vector>
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<size_t>(Cells)*Cells*Cells),
TriangleMarks(static_cast<size_t>(InMesh.Triangles.Num()/3),0)
{
for(int32 Triangle=0;Triangle+2<Mesh.Triangles.Num();Triangle+=3)
{
const int32 A=Mesh.Triangles[Triangle],B=Mesh.Triangles[Triangle+1],C=Mesh.Triangles[Triangle+2];
if(A<0||B<0||C<0||A>=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<double>::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<size_t>(Triangle)]==QueryStamp)continue;
TriangleMarks[static_cast<size_t>(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<double>::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<<Axis;
for(int32 Subset=1;Subset<8;++Subset)
{
if(Subset&~CrossingMask)continue;
int32 X=Cell[0],Y=Cell[1],Z=Cell[2];
if(Subset&1)X+=CellStep[0];
if(Subset&2)Y+=CellStep[1];
if(Subset&4)Z+=CellStep[2];
TestCell(X,Y,Z);
}
for(int32 Axis=0;Axis<3;++Axis)
if(CrossingMask&(1<<Axis))
{Cell[Axis]+=CellStep[Axis];NextBoundary[Axis]+=BoundaryDelta[Axis];}
}
return BestDifference;
}
private:
int32 CellFor(double Value,int32 Axis)const
{return std::clamp(static_cast<int32>(std::floor((Value-(&Origin.X)[Axis])/Step)),0,Cells-1);}
size_t CellOffset(int32 X,int32 Y,int32 Z)const
{return (static_cast<size_t>(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<std::vector<int32>> ByCell;
std::vector<uint32> 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<float>(SearchStart.X),static_cast<float>(SearchStart.Y),
static_cast<float>(SearchStart.Z))<0.0f;
double BestDifference=std::numeric_limits<double>::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<float>(P.X),static_cast<float>(P.Y),
static_cast<float>(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<float>(M.X),static_cast<float>(M.Y),
static_cast<float>(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<BestDifference)
{OutRadius=Crossing;BestDifference=Difference;bFound=true;}
}
if(Distance>=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<float>(Center.X+Direction.X*LocalPrevious),
static_cast<float>(Center.Y+Direction.Y*LocalPrevious),
static_cast<float>(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<float>(P.X),static_cast<float>(P.Y),
static_cast<float>(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<float>(M.X),static_cast<float>(M.Y),
static_cast<float>(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<BestDifference)
{OutRadius=Crossing;BestDifference=Difference;bFound=true;}
}
if(Distance>=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<float>(LoPoint.X),static_cast<float>(LoPoint.Y),
static_cast<float>(LoPoint.Z))<0.0f;
const bool HiSolid=Shape.DensityAt(static_cast<float>(HiPoint.X),static_cast<float>(HiPoint.Y),
static_cast<float>(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<FFarMeshSegmentIndex> MeshIndex;
std::vector<FVector> NewVertices;
std::vector<FVector> NewNormals;
std::vector<FVector2f> NewUVs;
std::vector<FColor> NewColors;
std::vector<int32> NewTriangles;
std::vector<std::array<int32,4>> 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<uint32> 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)<Low[AxisIndex]
||std::min(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]<Low[AxisIndex]||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<double>(Step)):128.0;
const int32 AxialSampleCount=std::max(1,static_cast<int32>(std::ceil(Length/ConnectorStationStep)));
std::vector<uint8> MissingMasks(static_cast<size_t>(AxialSampleCount),0);
std::vector<std::array<double,4>> SourceRadii(static_cast<size_t>(AxialSampleCount));
for(auto& Radii:SourceRadii)Radii.fill(0.0);
bool bAnyMissing=false;
bool bHasAuditSection=false;
for(int32 Sample=0;Sample<AxialSampleCount;++Sample)
{
const double T=(static_cast<double>(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<double>(Connector.Radius)*0.75);
const double MatchTolerance=std::max(8.0,static_cast<double>(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<FFarMeshSegmentIndex>(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<size_t>(Sample)][DirectionIndex]=SourceRadius;
if(bMissing)
{
MissingMasks[static_cast<size_t>(Sample)]|=static_cast<uint8>(1u<<DirectionIndex);
bAnyMissing=true;
}
}
}
if(!bHasAuditSection||!bAnyMissing)continue;
struct FRing { double T=0.0;std::array<double,4> 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<<DirectionIndex))==0)
{Ring.Radii[DirectionIndex]=Connector.Radius;continue;}
double Radius=Connector.Radius;
double RadiusUncertainty=0.0;
if(!FindFarConnectorRadiusNearNominal(Shape,Center,
RadialDirections[DirectionIndex],Connector.Radius,Radius,RadiusUncertainty)
&&!FindFarConnectorSurfaceRadius(Shape,Center,RadialDirections[DirectionIndex],
Connector.Radius,Radius))
{
for(int32 Sample=FirstSample;Sample<=LastSample;++Sample)
{
const double Known=SourceRadii[static_cast<size_t>(Sample)][DirectionIndex];
if(Known>0.0){Radius=Known;break;}
}
}
Ring.Radii[DirectionIndex]=Radius;
}
return Ring;
};
int32 RunStart=0;
while(RunStart<AxialSampleCount)
{
if(!MissingMasks[static_cast<size_t>(RunStart)]){++RunStart;continue;}
int32 RunEnd=RunStart;
while(RunEnd+1<AxialSampleCount&&MissingMasks[static_cast<size_t>(RunEnd+1)])++RunEnd;
const double RunMin=std::max(TMin,static_cast<double>(RunStart)/AxialSampleCount);
const double RunMax=std::min(TMax,static_cast<double>(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<size_t>(Sample)];
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
{
if((Mask&(1u<<DirectionIndex))==0)continue;
switch(SideForDirection[DirectionIndex])
{
case 0:RunRadiusMask|=static_cast<uint8>((1u<<0)|(1u<<2));break;
case 1:RunRadiusMask|=static_cast<uint8>((1u<<1)|(1u<<2));break;
case 2:RunRadiusMask|=static_cast<uint8>((1u<<1)|(1u<<3));break;
default:RunRadiusMask|=static_cast<uint8>((1u<<0)|(1u<<3));break;
}
}
}
std::vector<FRing> Rings;
const double MaxRingStep=Key.Level>=7?std::max(256.0,static_cast<double>(Step)):128.0;
double CurrentT=RunMin;
Rings.push_back(MakeRing(CurrentT,RunStart,RunEnd,RunRadiusMask));
while(CurrentT<RunMax-1.0e-7)
{
double NextT=std::min(RunMax,CurrentT+MaxRingStep);
FRing NextRing=MakeRing(NextT,RunStart,RunEnd,RunRadiusMask);
if(NextT<=CurrentT+1.0e-8)break;
Rings.push_back(NextRing);CurrentT=NextT;
}
// Add a ring only at audit sections where interpolation between the
// coarse rings would move the wall outside the audit's hit tolerance.
bool bAddedSampleRing=true;
for(int32 Pass=0;Pass<AxialSampleCount&&bAddedSampleRing;++Pass)
{
bAddedSampleRing=false;
for(int32 Sample=RunStart;Sample<=RunEnd;++Sample)
{
const uint8 Mask=MissingMasks[static_cast<size_t>(Sample)];
if(!Mask)continue;
const double SampleT=(static_cast<double>(Sample)+0.5)/AxialSampleCount;
auto RightIt=std::lower_bound(Rings.begin(),Rings.end(),SampleT,
[](const FRing& Ring,double T){return Ring.T<T;});
if(RightIt!=Rings.end()&&std::abs(RightIt->T-SampleT)<=1.0e-7)
{
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
{
if((Mask&(1u<<DirectionIndex))==0)continue;
const double Exact=SourceRadii[static_cast<size_t>(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<<DirectionIndex))==0)continue;
const double Actual=SourceRadii[static_cast<size_t>(Sample)][DirectionIndex];
const double Predicted=LeftRing.Radii[DirectionIndex]*(1.0-Alpha)
+RightRing.Radii[DirectionIndex]*Alpha;
const double Tolerance=std::max(8.0,static_cast<double>(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<<DirectionIndex))==0)continue;
const double Exact=SourceRadii[static_cast<size_t>(Sample)][DirectionIndex];
if(Exact>0.0)ExactRing.Radii[DirectionIndex]=Exact;
}
Rings.insert(RightIt,std::move(ExactRing));
bAddedSampleRing=true;
}
}
if(Rings.size()>=2)
{
std::vector<uint8> IntervalMasks(Rings.size()-1,0);
std::vector<std::array<uint8,SideCount>> NeededSides(Rings.size());
for(auto& Sides:NeededSides)Sides.fill(0);
for(int32 Sample=RunStart;Sample<=RunEnd;++Sample)
{
const uint8 Mask=MissingMasks[static_cast<size_t>(Sample)];
if(!Mask)continue;
const double SampleT=(static_cast<double>(Sample)+0.5)/AxialSampleCount;
const auto Lower=std::lower_bound(Rings.begin(),Rings.end(),SampleT,
[](const FRing& Ring,double T){return Ring.T<T;});
const size_t Right=static_cast<size_t>(Lower-Rings.begin());
if(Right<Rings.size()&&std::abs(Rings[Right].T-SampleT)<=1.0e-7)
{
if(Right>0)IntervalMasks[Right-1]|=Mask;
if(Right+1<Rings.size())IntervalMasks[Right]|=Mask;
}
else if(Right>0&&Right<Rings.size())IntervalMasks[Right-1]|=Mask;
}
for(size_t Ring=0;Ring<IntervalMasks.size();++Ring)
{
const uint8 Mask=IntervalMasks[Ring];
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
{
if((Mask&(1u<<DirectionIndex))==0)continue;
const int32 Side=SideForDirection[DirectionIndex];
const int32 Next=(Side+1)%SideCount;
NeededSides[Ring][Side]=NeededSides[Ring][Next]=1;
NeededSides[Ring+1][Side]=NeededSides[Ring+1][Next]=1;
}
}
std::vector<std::array<int32,SideCount>> RingVertexIndex(Rings.size());
for(auto& Indices:RingVertexIndex)Indices.fill(INDEX_NONE);
for(size_t Ring=0;Ring<Rings.size();++Ring)
{
if(std::none_of(NeededSides[Ring].begin(),NeededSides[Ring].end(),
[](uint8 Needed){return Needed!=0;}))continue;
const FVector Center=Connector.Start+Delta*Rings[Ring].T;
for(int32 Side=0;Side<SideCount;++Side)
{
if(!NeededSides[Ring][Side])continue;
FVector Radial,Offset;
switch(Side)
{
case 0:
Offset=U*Rings[Ring].Radii[0]+V*Rings[Ring].Radii[2];
Radial=U+V;break;
case 1:
Offset=U*(-Rings[Ring].Radii[1])+V*Rings[Ring].Radii[2];
Radial=U*-1.0+V;break;
case 2:
Offset=U*(-Rings[Ring].Radii[1])+V*(-Rings[Ring].Radii[3]);
Radial=U*-1.0+V*-1.0;break;
default:
Offset=U*Rings[Ring].Radii[0]+V*(-Rings[Ring].Radii[3]);
Radial=U+V*-1.0;break;
}
Radial.Normalize();
const FVector P=Center+Offset;
RingVertexIndex[Ring][Side]=static_cast<int32>(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<float>(Rings[Ring].T*Length/256.0),
static_cast<float>(Side)/SideCount);
if(bHasColors)NewColors.push_back(Color);
}
}
for(size_t Ring=0;Ring+1<Rings.size();++Ring)
{
const uint8 Mask=IntervalMasks[Ring];
if(!Mask)continue;
uint8 FaceMask=0;
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
{
if((Mask&(1u<<DirectionIndex))==0)continue;
const int32 Side=SideForDirection[DirectionIndex];
FaceMask|=static_cast<uint8>(1u<<Side);
}
for(int32 FaceSide=0;FaceSide<SideCount;++FaceSide)
{
if((FaceMask&(1u<<FaceSide))==0)continue;
const int32 Next=(FaceSide+1)%SideCount;
const int32 A=RingVertexIndex[Ring][FaceSide];
const int32 B=RingVertexIndex[Ring][Next];
const int32 C=RingVertexIndex[Ring+1][FaceSide];
const int32 D=RingVertexIndex[Ring+1][Next];
// Shared panels are emitted once even when both adjoining radial rays are missing.
NewQuads.push_back({A,B,C,D});
}
}
}
}
RunStart=RunEnd+1;
}
}
if(NewVertices.empty()||NewQuads.empty())return;
// The survival repair normally emits long connected strips. A few isolated
// two-triangle panels can be assembled from a radius borrowed at another axial
// sample, though, leaving a free-edged fin away from the sampled density surface.
// Match the visible coarse-panel audit's topology and six density samples here.
std::vector<uint8> QuadUseCount(NewVertices.size(),0);
for(const auto& Quad:NewQuads)
for(const int32 Vertex:Quad)
if(Vertex>=0&&static_cast<size_t>(Vertex)<QuadUseCount.size()
&&QuadUseCount[static_cast<size_t>(Vertex)]<255)
++QuadUseCount[static_cast<size_t>(Vertex)];
std::vector<uint8> KeepQuad(NewQuads.size(),1);
std::vector<std::array<int32,4>> RefinedQuads;
const double ResidualLimit=std::max(2.0,Step*0.10);
const double TileMin[3]={static_cast<double>(TileOrigin.X),static_cast<double>(TileOrigin.Y),
static_cast<double>(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<NewQuads.size();++QuadIndex)
{
const auto& Quad=NewQuads[QuadIndex];
if(std::any_of(Quad.begin(),Quad.end(),[&](int32 Vertex)
{return Vertex<0||static_cast<size_t>(Vertex)>=QuadUseCount.size()
||QuadUseCount[static_cast<size_t>(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<float>(P.X),static_cast<float>(P.Y),
static_cast<float>(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<float>(P.X+H),static_cast<float>(P.Y),
static_cast<float>(P.Z))-Shape.DensityAt(static_cast<float>(P.X-H),
static_cast<float>(P.Y),static_cast<float>(P.Z)))*0.25f;
const float GY=(Shape.DensityAt(static_cast<float>(P.X),static_cast<float>(P.Y+H),
static_cast<float>(P.Z))-Shape.DensityAt(static_cast<float>(P.X),
static_cast<float>(P.Y-H),static_cast<float>(P.Z)))*0.25f;
const float GZ=(Shape.DensityAt(static_cast<float>(P.X),static_cast<float>(P.Y),
static_cast<float>(P.Z+H))-Shape.DensityAt(static_cast<float>(P.X),
static_cast<float>(P.Y),static_cast<float>(P.Z-H)))*0.25f;
return FVector(GX,GY,GZ);
};
std::vector<FVector> PatchVertices;
std::vector<FVector> PatchNormals;
std::vector<FVector2f> PatchUVs;
std::vector<FColor> PatchColors;
const auto ProjectToSurface=[&](FVector& P,FVector& Normal)
{
for(int32 Iteration=0;Iteration<12;++Iteration)
{
const double Density=Shape.DensityAt(static_cast<float>(P.X),
static_cast<float>(P.Y),static_cast<float>(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<float>(P.X),static_cast<float>(P.Y),
static_cast<float>(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<int32> Grid(static_cast<size_t>(Subdivisions+1)*(Subdivisions+1),INDEX_NONE);
bool bProjectionSucceeded=true;
for(int32 Y=0;Y<=Subdivisions&&bProjectionSucceeded;++Y)
{
const double V=static_cast<double>(Y)/Subdivisions;
for(int32 X=0;X<=Subdivisions;++X)
{
const double U=static_cast<double>(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<int32>(PatchVertices.size());
Grid[static_cast<size_t>(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<float>(UA.X*((1-U)*(1-V))+UB.X*(U*(1-V))
+UC.X*((1-U)*V)+UD.X*(U*V)),
static_cast<float>(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<int32>(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<Subdivisions;++Y)
for(int32 X=0;X<Subdivisions;++X)
{
const int32 A0=PatchBase+Grid[static_cast<size_t>(Y)*(Subdivisions+1)+X];
const int32 B0=PatchBase+Grid[static_cast<size_t>(Y)*(Subdivisions+1)+X+1];
const int32 C0=PatchBase+Grid[static_cast<size_t>(Y+1)*(Subdivisions+1)+X];
const int32 D0=PatchBase+Grid[static_cast<size_t>(Y+1)*(Subdivisions+1)+X+1];
RefinedQuads.push_back({A0,B0,C0,D0});
}
}
std::vector<std::array<int32,4>> FinalQuads;
const size_t KeptQuadCount=static_cast<size_t>(std::count(KeepQuad.begin(),KeepQuad.end(),uint8{1}));
FinalQuads.reserve(KeptQuadCount+RefinedQuads.size());
for(size_t QuadIndex=0;QuadIndex<NewQuads.size();++QuadIndex)
if(KeepQuad[QuadIndex])FinalQuads.push_back(NewQuads[QuadIndex]);
FinalQuads.insert(FinalQuads.end(),RefinedQuads.begin(),RefinedQuads.end());
if(FinalQuads.empty())return;
std::vector<int32> Remap(NewVertices.size(),INDEX_NONE);
std::vector<FVector> KeptVertices,KeptNormals;
std::vector<FVector2f> KeptUVs;
std::vector<FColor> 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<size_t>(Vertex)];
if(Mapped==INDEX_NONE)
{
Mapped=static_cast<int32>(KeptVertices.size());
KeptVertices.push_back(NewVertices[static_cast<size_t>(Vertex)]);
KeptNormals.push_back(NewNormals[static_cast<size_t>(Vertex)]);
if(bHasUVs)KeptUVs.push_back(NewUVs[static_cast<size_t>(Vertex)]);
if(bHasColors)KeptColors.push_back(NewColors[static_cast<size_t>(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<int32>(NewVertices.size());
auto InsertVertices=[&](auto& Existing,const auto& Added)
{
using TArrayType=std::remove_reference_t<decltype(Existing)>;
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
for(const auto& Value:Added)Combined.Add(Value);
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
Existing=std::move(Combined);
};
InsertVertices(Mesh.Vertices,NewVertices);
InsertVertices(Mesh.Normals,NewNormals);
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
const int32 OldTriangleCount=Mesh.Triangles.Num()/3;
TArray<uint8> ExistingFaceMasks=Mesh.TriangleSkirtFaceMasks;
ExistingFaceMasks.SetNum(OldTriangleCount);
TArray<uint8> CombinedFaceMasks;CombinedFaceMasks.Reserve(
OldTriangleCount+static_cast<int32>(NewTriangles.size()/3));
for(int32 Triangle=0;Triangle<CeilingStart/3;++Triangle)
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
for(size_t Triangle=0;Triangle<NewTriangles.size()/3;++Triangle)CombinedFaceMasks.Add(0);
for(int32 Triangle=CeilingStart/3;Triangle<OldTriangleCount;++Triangle)
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
Mesh.Triangles=std::move(CombinedTriangles);
Mesh.TriangleSkirtFaceMasks=std::move(CombinedFaceMasks);
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
}
struct FFarMeshColumnIndex
{
FFarMeshColumnIndex(const FVoxelMeshData& InMesh,const FIntVector& InOrigin,int32 InExtent,
int32 InCells,double InStep,double InVoxelSizeCm)
:Mesh(InMesh),Origin(InOrigin),Extent(InExtent),Cells(InCells),Step(InStep),
VoxelSizeCm(InVoxelSizeCm),
ByColumn(static_cast<size_t>(InCells)*InCells)
{
for(int32 Triangle=0;Triangle+2<Mesh.Triangles.Num();Triangle+=3)
{
const int32 A=Mesh.Triangles[Triangle],B=Mesh.Triangles[Triangle+1],C=Mesh.Triangles[Triangle+2];
if(A<0||B<0||C<0||A>=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<int32>(std::floor((MinX-Origin.X)/Step)),0,Cells-1);
const int32 X1=std::clamp(static_cast<int32>(std::floor((MaxX-Origin.X)/Step)),0,Cells-1);
const int32 Y0=std::clamp(static_cast<int32>(std::floor((MinY-Origin.Y)/Step)),0,Cells-1);
const int32 Y1=std::clamp(static_cast<int32>(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<size_t>(Y)*Cells+X].push_back(Triangle/3);
}
}
std::vector<double> VerticalHits(double X,double Y,double ZMin,double ZMax) const
{
const int32 CX=std::clamp(static_cast<int32>(std::floor((X-Origin.X)/Step)),0,Cells-1);
const int32 CY=std::clamp(static_cast<int32>(std::floor((Y-Origin.Y)/Step)),0,Cells-1);
const auto& Candidates=ByColumn[static_cast<size_t>(CY)*Cells+CX];
std::vector<double> 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<int32>(std::floor((X-Origin.X)/Step)),0,Cells-1);
const int32 CY=std::clamp(static_cast<int32>(std::floor((Y-Origin.Y)/Step)),0,Cells-1);
const auto& Candidates=ByColumn[static_cast<size_t>(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<int32>(std::floor((X-Origin.X)/Step)),0,Cells-1);
const int32 CY=std::clamp(static_cast<int32>(std::floor((Y-Origin.Y)/Step)),0,Cells-1);
const auto& Candidates=ByColumn[static_cast<size_t>(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<std::vector<int32>> ByColumn;
};
inline std::vector<double> FindFarStratumCrossings(const FStandaloneShapeWorld& Shape,
double X,double Y,double ZMin,double ZMax)
{
std::vector<double> Crossings;
if(ZMax<=ZMin)return Crossings;
double PreviousZ=ZMin;
bool PreviousSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(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<float>(X),static_cast<float>(Y),static_cast<float>(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<float>(X),static_cast<float>(Y),static_cast<float>(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<int32>(std::floor(Prediction)),
static_cast<int32>(std::ceil(ZMin)),static_cast<int32>(std::floor(ZMax)));
double PreviousLeft=Center,PreviousRight=Center;
bool PreviousLeftSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(PreviousLeft))<0.0f;
bool PreviousRightSolid=PreviousLeftSolid;
const int32 MaximumRadius=std::max(Center-static_cast<int32>(std::floor(ZMin)),
static_cast<int32>(std::ceil(ZMax))-Center)+1;
for(int32 Radius=1;Radius<=MaximumRadius;++Radius)
{
bool bFound=false;
double Best=0.0,BestDistance=std::numeric_limits<double>::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<float>(X),static_cast<float>(Y),
static_cast<float>(Mid))<0.0f)==ASolid)
{
if(A<B)Lo=Mid;else Hi=Mid;
}
else
{
if(A<B)Hi=Mid;else Lo=Mid;
}
}
const double Crossing=(Lo+Hi)*0.5;
const double Difference=std::abs(Crossing-Prediction);
if(Difference<BestDistance)
{Best=Crossing;BestDistance=Difference;bFound=true;}
};
const double Left=std::max(ZMin,static_cast<double>(Center-Radius));
if(Left<PreviousLeft-1.0e-9)
{
const bool LeftSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Left))<0.0f;
if(LeftSolid!=PreviousLeftSolid)CheckBracket(Left,PreviousLeft,LeftSolid);
PreviousLeft=Left;PreviousLeftSolid=LeftSolid;
}
const double Right=std::min(ZMax,static_cast<double>(Center+Radius));
if(Right>PreviousRight+1.0e-9)
{
const bool RightSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(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<FBoundary> 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<B.Z;});
Boundaries.erase(std::unique(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B)
{return std::abs(A.Z-B.Z)<0.01;}),Boundaries.end());
const FIntVector Origin=Key.OriginVoxels();const int32 Extent=Key.ExtentVoxels();
std::vector<FBoundary> 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<double>(World.CoreVoxelSizeMicrons)/10000.0);
std::vector<FVector> NewVertices,NewNormals;
std::vector<FVector2f> NewUVs;std::vector<FColor> NewColors;
std::vector<int32> NewTriangles;
static constexpr int32 ColumnOffsets[4]={1,5,9,13};
const auto Density=[&](double X,double Y,double Z)
{return Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(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<double> Source=FindFarStratumCrossings(Shape,X,Y,ZMin,ZMax);
if(Source.empty())continue;
std::vector<double> Hits=MeshIndex.VerticalHits(X,Y,ZMin,ZMax);
std::vector<uint8> 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<double>::infinity();
for(size_t I=0;I<Hits.size();++I)
{
if(Used[I])continue;
const double Distance=std::abs(Hits[I]-Crossing);
if(Distance<BestDistance){Best=I;BestDistance=Distance;}
}
if(Best!=Hits.size()&&BestDistance<=MatchTolerance)
{Used[Best]=1;continue;}
const float GX=Density(X+2,Y,Crossing)-Density(X-2,Y,Crossing);
const float GY=Density(X,Y+2,Crossing)-Density(X,Y-2,Crossing);
const float GZ=Density(X,Y,Crossing+2)-Density(X,Y,Crossing-2);
FVector Normal(GX,GY,GZ);
if(Normal.SizeSquared()<1.0e-8)Normal=FVector(0,0,1);
Normal.Normalize();
const double HalfSize=Step*2.0;
const double X0=std::max<double>(Origin.X,X-HalfSize),X1=std::min<double>(Origin.X+Extent,X+HalfSize);
const double Y0=std::max<double>(Origin.Y,Y-HalfSize),Y1=std::min<double>(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<FPatchVertex,PatchSide*PatchSide> Patch{};
bool bValidPatch=true;
for(int32 PY=0;PY<PatchSide&&bValidPatch;++PY)
for(int32 PX=0;PX<PatchSide;++PX)
{
FPatchVertex& Vertex=Patch[static_cast<std::size_t>(PY*PatchSide+PX)];
Vertex.X=X0+(X1-X0)*static_cast<double>(PX)/PatchDivisions;
Vertex.Y=Y0+(Y1-Y0)*static_cast<double>(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<int32>(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<float>((Vertex.X-Origin.X)/256.0),
static_cast<float>((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<PatchDivisions;++PY)
for(int32 PX=0;PX<PatchDivisions;++PX)
{
const int32 I0=Base+PY*PatchSide+PX;
const int32 I1=I0+1;
const int32 I2=I1+PatchSide;
const int32 I3=I0+PatchSide;
if(bBelowSolid)NewTriangles.insert(NewTriangles.end(),{I0,I1,I3,I1,I2,I3});
else NewTriangles.insert(NewTriangles.end(),{I0,I3,I1,I1,I3,I2});
}
}
}
}
if(NewVertices.empty()||NewTriangles.empty())return;
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
auto InsertVertices=[&](auto& Existing,const auto& Added)
{
using TArrayType=std::remove_reference_t<decltype(Existing)>;
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
for(const auto& Value:Added)Combined.Add(Value);
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
Existing=std::move(Combined);
};
InsertVertices(Mesh.Vertices,NewVertices);InsertVertices(Mesh.Normals,NewNormals);
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
const int32 OldTriangleCount=Mesh.Triangles.Num()/3;
TArray<uint8> ExistingFaceMasks=Mesh.TriangleSkirtFaceMasks;
ExistingFaceMasks.SetNum(OldTriangleCount);
TArray<uint8> CombinedFaceMasks;CombinedFaceMasks.Reserve(
OldTriangleCount+static_cast<int32>(NewTriangles.size()/3));
for(int32 Triangle=0;Triangle<CeilingStart/3;++Triangle)
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
for(size_t Triangle=0;Triangle<NewTriangles.size()/3;++Triangle)CombinedFaceMasks.Add(0);
for(int32 Triangle=CeilingStart/3;Triangle<OldTriangleCount;++Triangle)
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
Mesh.Triangles=std::move(CombinedTriangles);
Mesh.TriangleSkirtFaceMasks=std::move(CombinedFaceMasks);
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
}
// Fill only underside cells that marching cubes missed. Active transitions use the
// same displaced face and passage-mouth fade as the signed-distance field.
struct FTransitionFaceReplacement
{
double BaseZVoxels=0.0;
double MaximumDisplacementVoxels=0.0;
std::vector<uint8> Cells;
};
inline int32 RemoveCoarseTransitionFaceTriangles(FVoxelMeshData& Mesh,
const FIntVector& Origin,int32 Step,int32 GridSide,double VoxelSizeCm,
const std::vector<FTransitionFaceReplacement>& 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<uint8> OldMasks=Mesh.TriangleSkirtFaceMasks;
OldMasks.SetNum(TriangleCount);
TArray<int32> KeptTriangles;
TArray<uint8> KeptMasks;
KeptTriangles.Reserve(Mesh.Triangles.Num());
KeptMasks.Reserve(TriangleCount);
int32 Removed=0,RemovedCeiling=0;
const int32 CellSide=GridSide-1;
for(int32 Triangle=0;Triangle<TriangleCount;++Triangle)
{
const int32 IA=Mesh.Triangles[Triangle*3];
const int32 IB=Mesh.Triangles[Triangle*3+1];
const int32 IC=Mesh.Triangles[Triangle*3+2];
bool bRemove=false;
if(OldMasks[Triangle]==0&&IA>=0&&IB>=0&&IC>=0
&&IA<Mesh.Vertices.Num()&&IB<Mesh.Vertices.Num()&&IC<Mesh.Vertices.Num())
{
const FVector& A=Mesh.Vertices[IA];
const FVector& B=Mesh.Vertices[IB];
const FVector& C=Mesh.Vertices[IC];
const FVector GeometricNormal=FVector::CrossProduct(B-A,C-A);
const double NormalLength=GeometricNormal.Size();
const double Facing=NormalLength>1.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<int32>(std::floor((X-Origin.X)/Step));
const int32 CellY=static_cast<int32>(std::floor((Y-Origin.Y)/Step));
if(CellX>=0&&CellX<CellSide&&CellY>=0&&CellY<CellSide)
{
const std::size_t Cell=static_cast<std::size_t>(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<double> FaceHeights;
std::vector<uint8> SolidAbove;
const VoxelForgeCore::ResolvedLayoutStratumV2* Stratum=nullptr;
bool bTransition=false;
};
std::vector<FCandidate> Candidates;
std::vector<FTransitionFaceReplacement> Replacements;
const double CoreMicrons=std::max<std::uint32_t>(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<double>(Stratum->ceiling.bottomMicrometres)/CoreMicrons;
Candidate.Thickness=static_cast<double>(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<double>(
Shape.ResolvedTransitionMaximumFaceDisplacementMicrometres(*transitionUpper,*Stratum))
/CoreMicrons:0.0;
if(Candidate.FaceZ<Origin.Z-displacement-1.0e-6
||Candidate.FaceZ>Origin.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<std::size_t>(GridSide)*GridSide;
Candidate.FaceHeights.resize(GridCount,Candidate.FaceZ);
Candidate.SolidAbove.resize(GridCount,0);
bool AnySolidAbove=false;
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
{
const double WorldX=Origin.X+X*GridStep;
const double WorldY=Origin.Y+Y*GridStep;
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
Candidate.bTransition=Shape.ResolvedCeilingBottomAt(*Candidate.Stratum,
WorldX,WorldY,Candidate.FaceHeights[GridIndex])||Candidate.bTransition;
const float Above=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
static_cast<float>(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<double>(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<double>(World.CoreVoxelSizeMicrons)/10000.0;
std::vector<FVector> NewVertices,NewNormals;
std::vector<FVector2f> NewUVs;
std::vector<FColor> NewColors;
std::vector<int32> NewTriangles;
for(const FCandidate& Candidate:Candidates)
{
const std::size_t GridCount=static_cast<std::size_t>(GridSide)*GridSide;
std::vector<int32> VertexMap(GridCount,INDEX_NONE);
FTransitionFaceReplacement Replacement;
Replacement.BaseZVoxels=Candidate.FaceZ;
Replacement.MaximumDisplacementVoxels=Candidate.MaximumDisplacementVoxels;
Replacement.Cells.resize(static_cast<std::size_t>(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<std::size_t>(Y*GridSide+X);
int32& Mapped=VertexMap[GridIndex];
if(Mapped!=INDEX_NONE)return Mapped;
Mapped=static_cast<int32>(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<double>(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<float>(WorldX),static_cast<float>(WorldY));
if(bHasColors)NewColors.push_back(Color);
return Mapped;
};
for(int32 Y=0;Y+1<GridSide;++Y)for(int32 X=0;X+1<GridSide;++X)
{
const std::size_t I00=static_cast<std::size_t>(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<std::size_t>(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<int32>(NewVertices.size());
auto InsertVertices=[&](auto& Existing,const auto& Added)
{
using TArrayType=std::remove_reference_t<decltype(Existing)>;
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
for(const auto& Value:Added)Combined.Add(Value);
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
Existing=std::move(Combined);
};
InsertVertices(Mesh.Vertices,NewVertices);InsertVertices(Mesh.Normals,NewNormals);
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(
Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
const int32 OldTriangleCount=Mesh.Triangles.Num()/3;
TArray<uint8> ExistingFaceMasks=Mesh.TriangleSkirtFaceMasks;
ExistingFaceMasks.SetNum(OldTriangleCount);
TArray<uint8> CombinedFaceMasks;CombinedFaceMasks.Reserve(
OldTriangleCount+static_cast<int32>(NewTriangles.size()/3));
for(int32 Triangle=0;Triangle<CeilingStart/3;++Triangle)
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
for(std::size_t Triangle=0;Triangle<NewTriangles.size()/3;++Triangle)
CombinedFaceMasks.Add(0);
for(int32 Triangle=CeilingStart/3;Triangle<OldTriangleCount;++Triangle)
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
Mesh.Triangles=std::move(CombinedTriangles);
Mesh.TriangleSkirtFaceMasks=std::move(CombinedFaceMasks);
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
Mesh.NumCeilingTriangles+=static_cast<int32>(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<FCandidate> Candidates;
std::vector<FTransitionFaceReplacement> Replacements;
const double CoreMicrons=std::max<std::uint32_t>(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<double>(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<double>(Shape.ResolvedTransitionMaximumFaceDisplacementMicrometres(
*Stratum,*transitionLower))/CoreMicrons
:0.0;
if(FloorZ<Origin.Z-displacement-1.0e-6
||FloorZ>Origin.Z+Extent+displacement+1.0e-6)continue;
Candidates.push_back({FloorZ,static_cast<double>(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<double>(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<double>(World.CoreVoxelSizeMicrons)/10000.0;
std::vector<FVector> NewVertices,NewNormals;
std::vector<FVector2f> NewUVs;
std::vector<FColor> NewColors;
std::vector<int32> 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<std::size_t>(GridSide)*GridSide;
std::vector<double> faceHeights(GridCount,Candidate.FloorZ);
std::vector<uint8> solidBelow(GridCount,0);
FTransitionFaceReplacement Replacement;
Replacement.BaseZVoxels=Candidate.FloorZ;
Replacement.MaximumDisplacementVoxels=Candidate.MaximumDisplacementVoxels;
Replacement.Cells.resize(static_cast<std::size_t>(GridSide-1)*(GridSide-1),0);
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
{
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
if(Candidate.bTransition)
Shape.ResolvedBedrockTopAt(*Candidate.Stratum,WorldX,WorldY,
faceHeights[GridIndex]);
const float Density=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
static_cast<float>(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<int32> VertexMap(GridCount,INDEX_NONE);
const auto AddVertex=[&](int32 X,int32 Y)
{
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
int32& Mapped=VertexMap[GridIndex];
if(Mapped!=INDEX_NONE)return Mapped;
Mapped=static_cast<int32>(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<double>(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<float>(WorldX),static_cast<float>(WorldY));
if(bHasColors)NewColors.push_back(Color);
return Mapped;
};
for(int32 Y=0;Y+1<GridSide;++Y)for(int32 X=0;X+1<GridSide;++X)
{
const std::size_t I00=static_cast<std::size_t>(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<std::size_t>(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<int32>(NewVertices.size());
auto InsertVertices=[&](auto& Existing,const auto& Added)
{
using TArrayType=std::remove_reference_t<decltype(Existing)>;
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
for(const auto& Value:Added)Combined.Add(Value);
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
Existing=std::move(Combined);
};
InsertVertices(Mesh.Vertices,NewVertices);InsertVertices(Mesh.Normals,NewNormals);
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(
Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
const int32 OldTriangleCount=Mesh.Triangles.Num()/3;
TArray<uint8> ExistingFaceMasks=Mesh.TriangleSkirtFaceMasks;
ExistingFaceMasks.SetNum(OldTriangleCount);
TArray<uint8> CombinedFaceMasks;CombinedFaceMasks.Reserve(
OldTriangleCount+static_cast<int32>(NewTriangles.size()/3));
for(int32 Triangle=0;Triangle<CeilingStart/3;++Triangle)
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
for(std::size_t Triangle=0;Triangle<NewTriangles.size()/3;++Triangle)
CombinedFaceMasks.Add(0);
for(int32 Triangle=CeilingStart/3;Triangle<OldTriangleCount;++Triangle)
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
Mesh.Triangles=std::move(CombinedTriangles);
Mesh.TriangleSkirtFaceMasks=std::move(CombinedFaceMasks);
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
}