- Standalone/Core: public header VoxelForgeCore.h (no Unreal types), static library target, ResolvedWorldSnapshot (from the schema-v1 JSON), World (compile, generateTile, density/sign, point/segment solid, classifyBox), host-supplied JobScheduler (the core starts no threads), sequenced, idempotent EditTransaction returning affected tiles per LOD. - The viewer, benchmarks, edits and audits run on Core; StreamingWorld stays host code. - Same as round 11: 11 hash rows at W1 and W8, routes 5642/5642, look/passage/far audits pass, bad-shell control fails, six fixed captures byte-identical; cold W8 1.025 s vs 1.041 s reference. - SHARED_CPP_LIBRARY_PLAN.md rewritten for a UBT module compiling the same sources via shims. Open issue before Unreal: Core's implementation still compiles the Compat stand-ins named like Unreal types (TArray, FVector...); inside a UE binary that is an ODR clash. Must be namespaced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
527 lines
26 KiB
C++
527 lines
26 KiB
C++
#pragma once
|
|
|
|
#include "VoxelForgeCore/VoxelForgeCore.h"
|
|
|
|
// Standalone-only proof that far meshes retain the owner's strata-boundary shells and
|
|
// generated far-parent connectors. This deliberately compares the dense source field with
|
|
// vertical/radial intersections against the triangles that were actually emitted.
|
|
|
|
struct FFarSurvivalAudit
|
|
{
|
|
struct FBoundary
|
|
{
|
|
double Z=0.0;
|
|
double HalfWidth=16.0;
|
|
};
|
|
|
|
struct FConnectorSample
|
|
{
|
|
size_t ConnectorIndex=0;
|
|
V3 Center{},U{},V{};
|
|
float Radius=0.0f;
|
|
uint8 DirectionMask=0;
|
|
};
|
|
|
|
struct FCounts
|
|
{
|
|
uint64 BoundaryColumns=0,BoundaryReferenceSurfaces=0;
|
|
uint64 BoundaryMissing=0,BoundaryExtra=0,BoundaryHoles=0;
|
|
uint64 ConnectorsInView=0,ConnectorSamples=0,ConnectorRays=0;
|
|
uint64 ConnectorReferenceSurfaces=0,ConnectorRayMissing=0,ConnectorGoodSamples=0,ConnectorMissing=0;
|
|
uint64 ConnectorVisibleSamples=0,ConnectorNoSurfaceSamples=0,ConnectorSampleMissing=0;
|
|
uint64 ConnectorExtra=0,ConnectorHoles=0,ConnectorUnsampled=0;
|
|
uint64 CorruptedTriangles=0;
|
|
};
|
|
|
|
FFarSurvivalAudit(FStandaloneOwnerWorld& InWorld,const VoxelForgeCore::World& InShape,
|
|
const std::vector<FVoxelTileKey>& DesiredTiles,bool bInDropOneShell)
|
|
:World(InWorld),Shape(InShape),bDropOneShell(bInDropOneShell)
|
|
{
|
|
const auto& Layout=World.Manager->GetLayout();
|
|
for(const FStrateSlot& Slot:Layout)
|
|
{
|
|
const FIntVector Mid(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2);
|
|
const FStrateGenerationParams Params=World.Manager->GetGenerationParams(Mid);
|
|
const double HalfWidth=std::max(8.0,static_cast<double>(Params.BoundarySealThickness)+8.0);
|
|
AddBoundary(static_cast<double>(Slot.BottomChunkZ)*CHUNK_SIZE,HalfWidth);
|
|
AddBoundary(static_cast<double>(Slot.TopChunkZ+1)*CHUNK_SIZE,HalfWidth);
|
|
}
|
|
std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B)
|
|
{return A.Z<B.Z;});
|
|
BuildConnectorSamples(DesiredTiles);
|
|
}
|
|
|
|
bool AuditTile(FStandaloneCpuTile& Tile)
|
|
{
|
|
if(Tile.Key.Level<AuditMinLevel)return true;
|
|
const FIntVector Origin=Tile.Key.OriginVoxels();
|
|
const int32 Extent=Tile.Key.ExtentVoxels();
|
|
const int32 Cells=std::max(1,World.CoarseTileCells);
|
|
const double Step=static_cast<double>(std::max(1,Extent/Cells));
|
|
|
|
if(bDropOneShell&&!bCorruptionApplied)
|
|
TryDropShellInTile(Tile,Origin,Extent,Step);
|
|
FMeshColumnIndex MeshIndex(Tile,Origin,Extent,Cells,Step);
|
|
|
|
static constexpr int32 ColumnOffsets[4]={1,5,9,13};
|
|
for(const FBoundary& Boundary:Boundaries)
|
|
{
|
|
if(Boundary.Z+Boundary.HalfWidth<Origin.Z
|
|
||Boundary.Z-Boundary.HalfWidth>Origin.Z+Extent)continue;
|
|
for(const int32 CY:ColumnOffsets)for(const int32 CX:ColumnOffsets)
|
|
{
|
|
const double X=Origin.X+(CX+0.371)*Step;
|
|
const double Y=Origin.Y+(CY+0.619)*Step;
|
|
const std::vector<double> Source=FindVerticalCrossings(X,Y,
|
|
Boundary.Z-Boundary.HalfWidth,Boundary.Z+Boundary.HalfWidth,1.0);
|
|
if(Source.empty())continue;
|
|
++Counts.BoundaryColumns;
|
|
Counts.BoundaryReferenceSurfaces+=Source.size();
|
|
CompareVerticalColumn(MeshIndex,X,Y,Boundary.Z-Boundary.HalfWidth,
|
|
Boundary.Z+Boundary.HalfWidth,Source,Step,
|
|
Counts.BoundaryMissing,Counts.BoundaryExtra,Counts.BoundaryHoles);
|
|
}
|
|
}
|
|
|
|
const auto Samples=ConnectorSamples.find(Tile.Key);
|
|
if(Samples!=ConnectorSamples.end())
|
|
{
|
|
for(const FConnectorSample& Sample:Samples->second)
|
|
AuditConnectorSample(Tile,Sample,Step,MeshIndex);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Passing() const
|
|
{
|
|
return bCorruptionApplied==bDropOneShell
|
|
&&Counts.BoundaryReferenceSurfaces>0
|
|
&&Counts.ConnectorsInView>0&&Counts.ConnectorSamples>0
|
|
&&Counts.BoundaryMissing==0&&Counts.BoundaryHoles==0
|
|
&&Counts.ConnectorMissing==0&&Counts.ConnectorSampleMissing==0
|
|
&&Counts.ConnectorHoles==0
|
|
&&Counts.ConnectorUnsampled==0;
|
|
}
|
|
|
|
void Print() const
|
|
{
|
|
std::printf("[FarSurvivalAudit] boundaries=%zu boundary_columns=%llu boundary_surfaces=%llu boundary_missing=%llu boundary_extra=%llu boundary_holes=%llu far_connectors=%llu connector_samples=%llu connector_visible_samples=%llu connector_no_surface_samples=%llu connector_sample_missing=%llu connector_good_connectors=%llu connector_rays=%llu connector_surfaces=%llu connector_ray_missing=%llu connector_missing=%llu connector_extra=%llu connector_holes=%llu connector_unsampled=%llu bad_shell_removed_triangles=%llu status=%s\n",
|
|
Boundaries.size(),static_cast<unsigned long long>(Counts.BoundaryColumns),
|
|
static_cast<unsigned long long>(Counts.BoundaryReferenceSurfaces),
|
|
static_cast<unsigned long long>(Counts.BoundaryMissing),
|
|
static_cast<unsigned long long>(Counts.BoundaryExtra),
|
|
static_cast<unsigned long long>(Counts.BoundaryHoles),
|
|
static_cast<unsigned long long>(Counts.ConnectorsInView),
|
|
static_cast<unsigned long long>(Counts.ConnectorSamples),
|
|
static_cast<unsigned long long>(Counts.ConnectorVisibleSamples),
|
|
static_cast<unsigned long long>(Counts.ConnectorNoSurfaceSamples),
|
|
static_cast<unsigned long long>(Counts.ConnectorSampleMissing),
|
|
static_cast<unsigned long long>(Counts.ConnectorGoodSamples),
|
|
static_cast<unsigned long long>(Counts.ConnectorRays),
|
|
static_cast<unsigned long long>(Counts.ConnectorReferenceSurfaces),
|
|
static_cast<unsigned long long>(Counts.ConnectorRayMissing),
|
|
static_cast<unsigned long long>(Counts.ConnectorMissing),
|
|
static_cast<unsigned long long>(Counts.ConnectorExtra),
|
|
static_cast<unsigned long long>(Counts.ConnectorHoles),
|
|
static_cast<unsigned long long>(Counts.ConnectorUnsampled),
|
|
static_cast<unsigned long long>(Counts.CorruptedTriangles),Passing()?"PASS":"FAIL");
|
|
std::fflush(stdout);
|
|
}
|
|
|
|
private:
|
|
struct FMeshColumnIndex
|
|
{
|
|
FMeshColumnIndex(const FStandaloneCpuTile& InTile,const FIntVector& InOrigin,
|
|
int32 InExtent,int32 InCells,double InStep)
|
|
:Tile(InTile),Origin(InOrigin),Extent(InExtent),Cells(InCells),Step(InStep),
|
|
ByColumn(static_cast<size_t>(InCells)*InCells)
|
|
{
|
|
const size_t TriangleCount=Tile.Indices.size()/3;
|
|
for(size_t Triangle=0;Triangle<TriangleCount;++Triangle)
|
|
{
|
|
uint32 A=Tile.Indices[Triangle*3],B=Tile.Indices[Triangle*3+1],C=Tile.Indices[Triangle*3+2];
|
|
if(A>=Tile.Vertices.size()||B>=Tile.Vertices.size()||C>=Tile.Vertices.size())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(static_cast<uint32>(Triangle));
|
|
}
|
|
}
|
|
|
|
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 uint32 Triangle:Candidates)
|
|
{
|
|
const uint32 IA=Tile.Indices[static_cast<size_t>(Triangle)*3];
|
|
const uint32 IB=Tile.Indices[static_cast<size_t>(Triangle)*3+1];
|
|
const uint32 IC=Tile.Indices[static_cast<size_t>(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);
|
|
}
|
|
SortUnique(Hits,0.25);
|
|
return Hits;
|
|
}
|
|
|
|
std::vector<double> SegmentHits(V3 Start,V3 Direction,double Length) const
|
|
{
|
|
std::vector<double> Hits;Hits.reserve(8);
|
|
const size_t TriangleCount=Tile.Indices.size()/3;
|
|
for(size_t Triangle=0;Triangle<TriangleCount;++Triangle)
|
|
{
|
|
const uint32 IA=Tile.Indices[Triangle*3],IB=Tile.Indices[Triangle*3+1],IC=Tile.Indices[Triangle*3+2];
|
|
if(IA>=Tile.Vertices.size()||IB>=Tile.Vertices.size()||IC>=Tile.Vertices.size())continue;
|
|
const V3 A=PositionVector(IA),B=PositionVector(IB),C=PositionVector(IC);
|
|
const V3 E1=Sub(B,A),E2=Sub(C,A),P=Cross(Direction,E2);
|
|
const float Determinant=Dot(E1,P);
|
|
if(std::abs(Determinant)<1.0e-8f)continue;
|
|
const float Inv=1.0f/Determinant;
|
|
const V3 T=Sub(Start,A);
|
|
const float U=Dot(T,P)*Inv;
|
|
if(U<-1.0e-5f||U>1.00001f)continue;
|
|
const V3 Q=Cross(T,E1);
|
|
const float V=Dot(Direction,Q)*Inv;
|
|
if(V<-1.0e-5f||U+V>1.00001f)continue;
|
|
const float Distance=Dot(E2,Q)*Inv;
|
|
if(Distance>=-1.0e-4f&&Distance<=Length+1.0e-4f)
|
|
Hits.push_back(std::clamp(static_cast<double>(Distance),0.0,Length));
|
|
}
|
|
SortUnique(Hits,0.25);
|
|
return Hits;
|
|
}
|
|
|
|
private:
|
|
V3 PositionVector(uint32 Index) const
|
|
{return {Tile.Vertices[Index].Position[0]*4.0f,Tile.Vertices[Index].Position[1]*4.0f,
|
|
Tile.Vertices[Index].Position[2]*4.0f};}
|
|
double PositionAxis(uint32 Index,int32 Axis) const
|
|
{return static_cast<double>(Tile.Vertices[Index].Position[Axis])*4.0;}
|
|
static void SortUnique(std::vector<double>& Values,double Epsilon)
|
|
{
|
|
std::sort(Values.begin(),Values.end());
|
|
Values.erase(std::unique(Values.begin(),Values.end(),[&](double A,double B)
|
|
{return std::abs(A-B)<=Epsilon;}),Values.end());
|
|
}
|
|
const FStandaloneCpuTile& Tile;
|
|
FIntVector Origin;
|
|
int32 Extent=0,Cells=0;
|
|
double Step=1.0;
|
|
std::vector<std::vector<uint32>> ByColumn;
|
|
};
|
|
|
|
static V3 AddV(V3 A,V3 B){return {A.X+B.X,A.Y+B.Y,A.Z+B.Z};}
|
|
static V3 MulV(V3 A,float S){return {A.X*S,A.Y*S,A.Z*S};}
|
|
static float LengthV(V3 A){return std::sqrt(std::max(0.0f,Dot(A,A)));}
|
|
static void SortUnique(std::vector<double>& Values,double Epsilon)
|
|
{
|
|
std::sort(Values.begin(),Values.end());
|
|
Values.erase(std::unique(Values.begin(),Values.end(),[&](double A,double B)
|
|
{return std::abs(A-B)<=Epsilon;}),Values.end());
|
|
}
|
|
|
|
void AddBoundary(double Z,double HalfWidth)
|
|
{
|
|
for(FBoundary& Existing:Boundaries)
|
|
if(std::abs(Existing.Z-Z)<0.01)
|
|
{Existing.HalfWidth=std::max(Existing.HalfWidth,HalfWidth);return;}
|
|
Boundaries.push_back({Z,HalfWidth});
|
|
}
|
|
|
|
static V3 ToV3(const FVector& P)
|
|
{return {static_cast<float>(P.X),static_cast<float>(P.Y),static_cast<float>(P.Z)};}
|
|
|
|
static bool Contains(const FVoxelTileKey& Key,V3 P,double Margin=0.0)
|
|
{
|
|
const FIntVector Origin=Key.OriginVoxels();
|
|
const int32 Extent=Key.ExtentVoxels();
|
|
return P.X>=Origin.X+Margin&&P.X<=Origin.X+Extent-Margin
|
|
&&P.Y>=Origin.Y+Margin&&P.Y<=Origin.Y+Extent-Margin
|
|
&&P.Z>=Origin.Z+Margin&&P.Z<=Origin.Z+Extent-Margin;
|
|
}
|
|
|
|
void BuildConnectorSamples(const std::vector<FVoxelTileKey>& DesiredTiles)
|
|
{
|
|
const auto Connectors=Shape.farConnectorSegments();
|
|
ConnectorInView.assign(Connectors.size(),0);
|
|
ConnectorHasSamples.assign(Connectors.size(),0);
|
|
ConnectorHasGoodSample.assign(Connectors.size(),0);
|
|
std::vector<FVoxelTileKey> FarTiles;
|
|
for(const FVoxelTileKey& Key:DesiredTiles)
|
|
if(Key.Level>=AuditMinLevel)FarTiles.push_back(Key);
|
|
for(size_t I=0;I<Connectors.size();++I)
|
|
{
|
|
const auto& Connector=Connectors[I];
|
|
const V3 A{static_cast<float>(Connector.start.x),static_cast<float>(Connector.start.y),static_cast<float>(Connector.start.z)};
|
|
const V3 B{static_cast<float>(Connector.end.x),static_cast<float>(Connector.end.y),static_cast<float>(Connector.end.z)};
|
|
const V3 Delta=Sub(B,A);const float Length=LengthV(Delta);
|
|
if(Length<=2.0f*Connector.radius)continue;
|
|
const V3 Axis=MulV(Delta,1.0f/Length);
|
|
V3 U=Cross(Axis,{0,0,1});
|
|
if(LengthV(U)<0.1f)U=Cross(Axis,{0,1,0});
|
|
U=Normalize(U);const V3 V=Normalize(Cross(Axis,U));
|
|
bool bInsideView=false;
|
|
const int32 SampleCount=std::max(1,static_cast<int32>(std::ceil(Length/128.0f)));
|
|
for(int32 S=0;S<SampleCount;++S)
|
|
{
|
|
const float T=(static_cast<float>(S)+0.5f)/SampleCount;
|
|
const V3 Center=AddV(A,MulV(Delta,T));
|
|
for(const FVoxelTileKey& Key:FarTiles)
|
|
{
|
|
if(!Contains(Key,Center))continue;
|
|
bInsideView=true;
|
|
uint8 Mask=0;
|
|
const V3 Directions[4]={U,MulV(U,-1.0f),V,MulV(V,-1.0f)};
|
|
const double RayLength=Connector.radius*2.0+32.0;
|
|
for(int32 D=0;D<4;++D)
|
|
if(Contains(Key,AddV(Center,MulV(Directions[D],static_cast<float>(RayLength)))))
|
|
Mask|=static_cast<uint8>(1u<<D);
|
|
if(!Mask)continue;
|
|
ConnectorSamples[Key].push_back({I,Center,U,V,Connector.radius,Mask});
|
|
ConnectorHasSamples[I]=1;break;
|
|
}
|
|
}
|
|
ConnectorInView[I]=bInsideView?1:0;
|
|
}
|
|
}
|
|
|
|
public:
|
|
void Finalize()
|
|
{
|
|
Counts.ConnectorsInView=0;
|
|
Counts.ConnectorUnsampled=0;
|
|
Counts.ConnectorMissing=0;
|
|
Counts.ConnectorGoodSamples=0;
|
|
for(size_t I=0;I<ConnectorInView.size();++I)
|
|
{
|
|
if(!ConnectorInView[I])continue;
|
|
++Counts.ConnectorsInView;
|
|
if(!ConnectorHasSamples[I])++Counts.ConnectorUnsampled;
|
|
if(!ConnectorHasGoodSample[I])++Counts.ConnectorMissing;
|
|
else ++Counts.ConnectorGoodSamples;
|
|
}
|
|
}
|
|
|
|
private:
|
|
|
|
std::vector<double> FindVerticalCrossings(double X,double Y,double ZMin,double ZMax,
|
|
double Stride) const
|
|
{
|
|
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+Stride,ZMax);Z<=ZMax+1.0e-6;Z=std::min(Z+Stride,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;const bool LoSolid=PreviousSolid;
|
|
for(int32 I=0;I<6;++I)
|
|
{
|
|
const double Mid=(Lo+Hi)*0.5;
|
|
const bool MidSolid=Shape.densityAt({static_cast<float>(X),static_cast<float>(Y),
|
|
static_cast<float>(Mid)})<0.0f;
|
|
if(MidSolid==LoSolid)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;
|
|
}
|
|
|
|
void CompareVerticalColumn(const FMeshColumnIndex& MeshIndex,double X,double Y,
|
|
double ZMin,double ZMax,const std::vector<double>& Source,double Step,
|
|
uint64& Missing,uint64& Extra,uint64& Holes)
|
|
{
|
|
std::vector<double> Mesh=MeshIndex.VerticalHits(X,Y,ZMin,ZMax);
|
|
const double Tolerance=std::max(4.0,Step*1.5);
|
|
std::vector<uint8> Used(Mesh.size(),0);
|
|
uint64 ColumnMissing=0,ColumnExtra=0;
|
|
for(const double Crossing:Source)
|
|
{
|
|
size_t Best=Mesh.size();double BestDistance=std::numeric_limits<double>::infinity();
|
|
for(size_t I=0;I<Mesh.size();++I)
|
|
if(!Used[I]&&std::abs(Mesh[I]-Crossing)<BestDistance)
|
|
{Best=I;BestDistance=std::abs(Mesh[I]-Crossing);}
|
|
if(Best==Mesh.size()||BestDistance>Tolerance)++ColumnMissing;
|
|
else Used[Best]=1;
|
|
}
|
|
for(const uint8 WasUsed:Used)if(!WasUsed)++ColumnExtra;
|
|
Missing+=ColumnMissing;Extra+=ColumnExtra;
|
|
if(ColumnMissing)
|
|
{
|
|
if((ColumnMissing&1u)!=0)++Holes;
|
|
if(DebugColumns<6)
|
|
{
|
|
std::printf("[FarAuditColumnMismatch] xy=(%.1f,%.1f) z=(%.1f..%.1f) source=%zu mesh=%zu first=(%.1f,%.1f) counts=(%llu,%llu)\n",
|
|
X,Y,ZMin,ZMax,Source.size(),Mesh.size(),Source.empty()?0.0:Source.front(),
|
|
Mesh.empty()?0.0:Mesh.front(),static_cast<unsigned long long>(ColumnMissing),
|
|
static_cast<unsigned long long>(ColumnExtra));
|
|
++DebugColumns;
|
|
}
|
|
}
|
|
}
|
|
|
|
void AuditConnectorSample(const FStandaloneCpuTile& Tile,const FConnectorSample& Sample,
|
|
double Step,const FMeshColumnIndex& MeshIndex)
|
|
{
|
|
++Counts.ConnectorSamples;
|
|
const V3 Directions[4]={Sample.U,MulV(Sample.U,-1.0f),Sample.V,MulV(Sample.V,-1.0f)};
|
|
const float RayLength=Sample.Radius*2.0f+32.0f;
|
|
const double SourceRadiusBand=std::max(16.0,static_cast<double>(Sample.Radius)*0.75);
|
|
uint32 MatchedDirections=0,ReferenceDirections=0;
|
|
uint8 ReferenceMask=0,MatchedMask=0;
|
|
for(int32 D=0;D<4;++D)
|
|
{
|
|
if((Sample.DirectionMask&(1u<<D))==0)continue;
|
|
++Counts.ConnectorRays;
|
|
const V3 Direction=Directions[D];
|
|
const std::vector<double> Source=FindRadialCrossings(Sample.Center,Direction,RayLength);
|
|
if(Source.empty())continue;
|
|
const auto SourceHit=std::min_element(Source.begin(),Source.end(),[&](double A,double B)
|
|
{return std::abs(A-Sample.Radius)<std::abs(B-Sample.Radius);});
|
|
if(SourceHit==Source.end()||std::abs(*SourceHit-Sample.Radius)>SourceRadiusBand)continue;
|
|
++ReferenceDirections;
|
|
ReferenceMask|=static_cast<uint8>(1u<<D);
|
|
++Counts.ConnectorReferenceSurfaces;
|
|
const std::vector<double> Mesh=MeshIndex.SegmentHits(Sample.Center,Direction,RayLength);
|
|
const double Tolerance=std::max(8.0,static_cast<double>(Sample.Radius)*0.35);
|
|
double BestDistance=std::numeric_limits<double>::infinity();
|
|
for(const double Hit:Mesh)
|
|
{
|
|
const double Distance=std::abs(Hit-*SourceHit);
|
|
if(Distance<BestDistance)BestDistance=Distance;
|
|
}
|
|
if(BestDistance<=Tolerance)
|
|
{++MatchedDirections;MatchedMask|=static_cast<uint8>(1u<<D);}
|
|
else
|
|
{
|
|
++Counts.ConnectorRayMissing;
|
|
if(DebugConnectorMismatches<24)
|
|
{
|
|
std::printf("[FarConnectorRayMismatch] connector=%zu tile_lod=%d center=(%.1f,%.1f,%.1f) radius=%.1f direction=%d source=%.2f nearest_mesh=%.2f hits=%zu step=%.1f tolerance=%.1f\n",
|
|
Sample.ConnectorIndex,Tile.Key.Level,Sample.Center.X,Sample.Center.Y,Sample.Center.Z,
|
|
Sample.Radius,D,*SourceHit,BestDistance,Mesh.size(),Step,Tolerance);
|
|
++DebugConnectorMismatches;
|
|
}
|
|
}
|
|
if(Mesh.size()>1)Counts.ConnectorExtra+=Mesh.size()-1;
|
|
}
|
|
// The source field can expose only one or two radial walls where a link
|
|
// joins a parent room. Preserve every source wall at every sampled section;
|
|
// require both sides whenever the source itself exposes an opposing pair.
|
|
const bool bSourcePair=(ReferenceMask&0x03u)==0x03u||(ReferenceMask&0x0Cu)==0x0Cu;
|
|
const bool bMatchedPair=(MatchedMask&0x03u)==0x03u||(MatchedMask&0x0Cu)==0x0Cu;
|
|
if(ReferenceDirections==0)
|
|
{++Counts.ConnectorNoSurfaceSamples;return;}
|
|
++Counts.ConnectorVisibleSamples;
|
|
const bool bSectionPassing=ReferenceDirections>0
|
|
&&MatchedDirections==ReferenceDirections
|
|
&&(!bSourcePair||bMatchedPair);
|
|
if(bSectionPassing&&!ConnectorHasGoodSample[Sample.ConnectorIndex])
|
|
ConnectorHasGoodSample[Sample.ConnectorIndex]=1;
|
|
if(!bSectionPassing)
|
|
{
|
|
++Counts.ConnectorSampleMissing;
|
|
if(MatchedDirections<ReferenceDirections||(bSourcePair&&!bMatchedPair))
|
|
++Counts.ConnectorHoles;
|
|
}
|
|
}
|
|
|
|
std::vector<double> FindRadialCrossings(V3 Start,V3 Direction,double Length) const
|
|
{
|
|
std::vector<double> Crossings;
|
|
double PreviousDistance=0.0;
|
|
bool PreviousSolid=Shape.densityAt({Start.X,Start.Y,Start.Z})<0.0f;
|
|
constexpr double Stride=2.0;
|
|
for(double Distance=Stride;Distance<=Length+1.0e-6;Distance=std::min(Distance+Stride,Length))
|
|
{
|
|
const V3 P=AddV(Start,MulV(Direction,static_cast<float>(Distance)));
|
|
const bool Solid=Shape.densityAt({P.X,P.Y,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 V3 M=AddV(Start,MulV(Direction,static_cast<float>(Mid)));
|
|
const bool MidSolid=Shape.densityAt({M.X,M.Y,M.Z})<0.0f;
|
|
if(MidSolid==PreviousSolid)Lo=Mid;else Hi=Mid;
|
|
}
|
|
Crossings.push_back((Lo+Hi)*0.5);
|
|
}
|
|
if(Distance>=Length-1.0e-6)break;
|
|
PreviousDistance=Distance;PreviousSolid=Solid;
|
|
}
|
|
return Crossings;
|
|
}
|
|
|
|
void TryDropShellInTile(FStandaloneCpuTile& Tile,const FIntVector& Origin,int32 Extent,double Step)
|
|
{
|
|
static constexpr int32 Offsets[4]={1,5,9,13};
|
|
for(const FBoundary& Boundary:Boundaries)
|
|
{
|
|
if(Boundary.Z+Boundary.HalfWidth<Origin.Z
|
|
||Boundary.Z-Boundary.HalfWidth>Origin.Z+Extent)continue;
|
|
for(const int32 CY:Offsets)for(const int32 CX:Offsets)
|
|
{
|
|
const double X=Origin.X+(CX+0.371)*Step;
|
|
const double Y=Origin.Y+(CY+0.619)*Step;
|
|
if(FindVerticalCrossings(X,Y,Boundary.Z-Boundary.HalfWidth,
|
|
Boundary.Z+Boundary.HalfWidth,1.0).empty())continue;
|
|
const double DropHalfWidth=std::max(Boundary.HalfWidth+4.0,Step*0.75);
|
|
std::vector<uint32> Kept;Kept.reserve(Tile.Indices.size());
|
|
for(size_t T=0;T+2<Tile.Indices.size();T+=3)
|
|
{
|
|
const uint32 A=Tile.Indices[T],B=Tile.Indices[T+1],C=Tile.Indices[T+2];
|
|
if(A>=Tile.Vertices.size()||B>=Tile.Vertices.size()||C>=Tile.Vertices.size())continue;
|
|
const double CenterZ=(Tile.Vertices[A].Position[2]+Tile.Vertices[B].Position[2]
|
|
+Tile.Vertices[C].Position[2])*4.0/3.0;
|
|
if(std::abs(CenterZ-Boundary.Z)<=DropHalfWidth)
|
|
{++Counts.CorruptedTriangles;continue;}
|
|
Kept.push_back(A);Kept.push_back(B);Kept.push_back(C);
|
|
}
|
|
Tile.Indices.swap(Kept);
|
|
bCorruptionApplied=Counts.CorruptedTriangles>0;
|
|
std::printf("[FarAuditBadSetting] removed stratum shell triangles at z=%.1f vox tile_lod=%d removed=%llu\n",
|
|
Boundary.Z,Tile.Key.Level,static_cast<unsigned long long>(Counts.CorruptedTriangles));
|
|
std::fflush(stdout);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
FStandaloneOwnerWorld& World;
|
|
const VoxelForgeCore::World& Shape;
|
|
static constexpr int32 AuditMinLevel=6;
|
|
bool bDropOneShell=false,bCorruptionApplied=false;
|
|
std::vector<FBoundary> Boundaries;
|
|
std::unordered_map<FVoxelTileKey,std::vector<FConnectorSample>,FStandaloneTileKeyHash> ConnectorSamples;
|
|
FCounts Counts;
|
|
std::vector<uint8> ConnectorInView,ConnectorHasSamples,ConnectorHasGoodSample;
|
|
uint32 DebugColumns=0;
|
|
uint32 DebugConnectorMismatches=0;
|
|
};
|