standalone: far-survival audit + LOD8 default with explicit far shells; neighbourhood-first viewer
- FarSurvivalAudit: checks every stratum top/bottom boundary (4x4 columns per far tile) and every in-view far connector (sections <=128 voxels, 4 radial rays) against the emitted mesh. Falsified: removing one tile's shell triangles -> 16 missing, 16 holes, exit 14. Passes at LOD6/7/8. - Far connectors follow source radial crossings; strata patches emitted at LOD7/8. Default LOD8. - Viewer: separate near ShapeWorld for the 27 player tiles, full world built on its own thread; 27/27 tiles hash-identical to the direct build. W1/W8 60 hash fields equal; routes 5642/5642. Cost, measured by me head to head (8 workers, cold, BelowNormal 0xFF00): LOD8 without the shells 1.17-1.25 s, LOD8 with the shells 1.80 s; payload 92.6 -> 276.7 MiB. The shells are far too expensive, and the pipelined benchmark whole view is 2.92 s (shape compiled twice): both to fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
5602a281b9
commit
a40b0454b3
7 files changed
+1350
-17
No files matched your search
@@ -0,0 +1,523 @@
|
||||
#pragma once
|
||||
|
||||
// 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 FStandaloneShapeWorld& 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.GetFarConnectorSegments();
|
||||
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=ToV3(Connector.Start),B=ToV3(Connector.End);
|
||||
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 FStandaloneShapeWorld& 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;
|
||||
};
|
||||
@@ -30,7 +30,7 @@ struct FStandaloneOwnerWorld
|
||||
bool ViewerStartFound = false;
|
||||
int32 Seed = 0;
|
||||
int32 ClipRadius = 6;
|
||||
int32 MaxClipLevel = 6;
|
||||
int32 MaxClipLevel = 8;
|
||||
int32 FullResClipLevels = 2;
|
||||
int32 CoarseTileCells = 16;
|
||||
int32 RenderDistanceChunks = 1536;
|
||||
@@ -253,7 +253,7 @@ namespace StandaloneOwnerLoader
|
||||
}
|
||||
Out.Seed = static_cast<int32>(Root.Get("seed").Integer());
|
||||
Out.ClipRadius = static_cast<int32>(Root.Get("clip_radius").Integer(6));
|
||||
Out.MaxClipLevel = static_cast<int32>(Root.Get("max_clip_level").Integer(6));
|
||||
Out.MaxClipLevel = static_cast<int32>(Root.Get("max_clip_level").Integer(8));
|
||||
Out.FullResClipLevels = static_cast<int32>(Root.Get("full_res_clip_levels").Integer(2));
|
||||
Out.CoarseTileCells = static_cast<int32>(Root.Get("coarse_tile_cells").Integer(16));
|
||||
Out.RenderDistanceChunks = static_cast<int32>(Root.Get("render_distance_chunks").Integer(1536));
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
// Standalone-only world compiler. It compiles the runtime room/tunnel/passages into
|
||||
@@ -30,6 +31,12 @@ class FStandaloneShapeWorld
|
||||
{
|
||||
public:
|
||||
enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel };
|
||||
struct FFarConnectorSegment
|
||||
{
|
||||
FVector Start=FVector::ZeroVector;
|
||||
FVector End=FVector::ZeroVector;
|
||||
float Radius=0.0f;
|
||||
};
|
||||
struct FEditBrush
|
||||
{
|
||||
enum class EShape : uint8 { Sphere, Capsule, RoundedBox };
|
||||
@@ -393,6 +400,64 @@ public:
|
||||
const FBuildStats& GetStats() const { return Stats; }
|
||||
bool IsBuilt() const { return Built; }
|
||||
bool HasFarWorld() const { return FarBuilt; }
|
||||
const std::vector<FFarConnectorSegment>& GetFarConnectorSegments() const
|
||||
{ return FarConnectorSegments; }
|
||||
|
||||
size_t BuildFarConnectorAirSamples(const FIntVector& TileOrigin,int32 Step,int32 Cells,
|
||||
std::unordered_set<uint64>& OutSamples) const
|
||||
{
|
||||
OutSamples.clear();
|
||||
if(FarConnectorSegments.empty()||Step<=0||Cells<=0)return 0;
|
||||
const double Pad=Step;
|
||||
const double TileMin[3]={TileOrigin.X-Pad,TileOrigin.Y-Pad,TileOrigin.Z-Pad};
|
||||
const double TileMax[3]={TileOrigin.X+(Cells+1)*Step,TileOrigin.Y+(Cells+1)*Step,
|
||||
TileOrigin.Z+(Cells+1)*Step};
|
||||
for(const FFarConnectorSegment& Connector:FarConnectorSegments)
|
||||
{
|
||||
const double Start[3]={Connector.Start.X,Connector.Start.Y,Connector.Start.Z};
|
||||
const double End[3]={Connector.End.X,Connector.End.Y,Connector.End.Z};
|
||||
bool bOverlaps=true;
|
||||
for(int32 Axis=0;Axis<3;++Axis)
|
||||
bOverlaps &= std::min(Start[Axis],End[Axis])-Connector.Radius<=TileMax[Axis]
|
||||
&&std::max(Start[Axis],End[Axis])+Connector.Radius>=TileMin[Axis];
|
||||
if(!bOverlaps)continue;
|
||||
const double Delta[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]};
|
||||
const double Length=std::sqrt(Delta[0]*Delta[0]+Delta[1]*Delta[1]+Delta[2]*Delta[2]);
|
||||
if(Length<=1.0e-6)continue;
|
||||
const int32 Samples=std::max(1,static_cast<int32>(std::ceil(Length/(Step*0.25))));
|
||||
for(int32 I=0;I<=Samples;++I)
|
||||
{
|
||||
const double T=static_cast<double>(I)/Samples;
|
||||
const double P[3]={Start[0]+Delta[0]*T,Start[1]+Delta[1]*T,Start[2]+Delta[2]*T};
|
||||
if(P[0]<TileMin[0]||P[0]>TileMax[0]||P[1]<TileMin[1]||P[1]>TileMax[1]
|
||||
||P[2]<TileMin[2]||P[2]>TileMax[2])continue;
|
||||
int32 Base[3];
|
||||
for(int32 Axis=0;Axis<3;++Axis)
|
||||
Base[Axis]=static_cast<int32>(std::floor((P[Axis]-(&TileOrigin.X)[Axis])/Step));
|
||||
for(int32 DZ=0;DZ<=1;++DZ)for(int32 DY=0;DY<=1;++DY)for(int32 DX=0;DX<=1;++DX)
|
||||
{
|
||||
const int32 IX=Base[0]+DX,IY=Base[1]+DY,IZ=Base[2]+DZ;
|
||||
if(IX<0||IY<0||IZ<0||IX>Cells||IY>Cells||IZ>Cells)continue;
|
||||
OutSamples.insert(PackLatticeKey(TileOrigin.X+IX*Step,
|
||||
TileOrigin.Y+IY*Step,TileOrigin.Z+IZ*Step));
|
||||
}
|
||||
}
|
||||
}
|
||||
return OutSamples.size();
|
||||
}
|
||||
|
||||
class FScopedForcedFarAirSamples
|
||||
{
|
||||
public:
|
||||
explicit FScopedForcedFarAirSamples(const std::unordered_set<uint64>* AirSamples)
|
||||
:PreviousAir(ForcedFarAirSamples)
|
||||
{ ForcedFarAirSamples=AirSamples; }
|
||||
~FScopedForcedFarAirSamples(){ForcedFarAirSamples=PreviousAir;}
|
||||
FScopedForcedFarAirSamples(const FScopedForcedFarAirSamples&)=delete;
|
||||
FScopedForcedFarAirSamples& operator=(const FScopedForcedFarAirSamples&)=delete;
|
||||
private:
|
||||
const std::unordered_set<uint64>* PreviousAir=nullptr;
|
||||
};
|
||||
bool DebugCriticalAirAt(double X,double Y,double Z) const { return IsInsideCriticalAir(X,Y,Z); }
|
||||
bool DebugSolidOverlayAt(double X,double Y,double Z) const { return IsInsideSolidOverlay(X,Y,Z); }
|
||||
|
||||
@@ -451,6 +516,7 @@ public:
|
||||
{
|
||||
int32 IX=0,IY=0,IZ=0;
|
||||
const bool bInteger=NearInteger(X,IX)&&NearInteger(Y,IY)&&NearInteger(Z,IZ);
|
||||
if(bInteger&&IsForcedFarAirSample(IX,IY,IZ))return 0.05f;
|
||||
const bool bPacked=bInteger&&Contains(IX,IY,IZ);
|
||||
if(bPacked)
|
||||
return PackedDensityAt(IX,IY,IZ,IsRock(IX,IY,IZ),X,Y,Z);
|
||||
@@ -459,6 +525,7 @@ public:
|
||||
|
||||
float DensityAtLattice(int32 X,int32 Y,int32 Z) const
|
||||
{
|
||||
if(IsForcedFarAirSample(X,Y,Z))return 0.05f;
|
||||
const bool bPacked=Contains(X,Y,Z);
|
||||
if(bPacked)
|
||||
return PackedDensityAt(X,Y,Z,IsRock(X,Y,Z),static_cast<float>(X),
|
||||
@@ -468,6 +535,18 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
static uint64 PackLatticeKey(int32 X,int32 Y,int32 Z)
|
||||
{
|
||||
constexpr uint64 Mask=0x1FFFFFull;
|
||||
return (static_cast<uint64>(static_cast<uint32>(X)&Mask)<<42)
|
||||
|(static_cast<uint64>(static_cast<uint32>(Y)&Mask)<<21)
|
||||
|(static_cast<uint64>(static_cast<uint32>(Z)&Mask));
|
||||
}
|
||||
static bool IsForcedFarAirSample(int32 X,int32 Y,int32 Z)
|
||||
{
|
||||
const auto* Samples=ForcedFarAirSamples;
|
||||
return Samples&&Samples->contains(PackLatticeKey(X,Y,Z));
|
||||
}
|
||||
float PackedDensityAt(int32 X,int32 Y,int32 Z,bool bRock,float FX,float FY,float FZ) const
|
||||
{
|
||||
const size_t Column=static_cast<size_t>(Y-Min[1])*Size[0]+static_cast<size_t>(X-Min[0]);
|
||||
@@ -1970,6 +2049,7 @@ private:
|
||||
void BuildFarParentShapes()
|
||||
{
|
||||
constexpr int32 ParentPitch=2048;
|
||||
FarConnectorSegments.clear();
|
||||
struct FParent { int32 GX=0,GY=0,Strate=0; FVector Center; double Rx=0,Ry=0,Rz=0; };
|
||||
const int32 GX0=FloorDiv(ClipMin[0],ParentPitch)-1,GY0=FloorDiv(ClipMin[1],ParentPitch)-1;
|
||||
const int32 GX1=FloorDiv(ClipMax[0],ParentPitch)+1,GY1=FloorDiv(ClipMax[1],ParentPitch)+1;
|
||||
@@ -2024,6 +2104,7 @@ private:
|
||||
if(MaxX>=Min[0]&&MinX<=Max[0]&&MaxY>=Min[1]&&MinY<=Max[1])continue;
|
||||
AddTaperedCapsule(A,B,64.0f,64.0f,true);
|
||||
Primitives.back().BlendRadius=0.0;
|
||||
FarConnectorSegments.push_back({A,B,64.0f});
|
||||
}
|
||||
}
|
||||
if(!Parents.empty())
|
||||
@@ -3154,6 +3235,7 @@ private:
|
||||
std::vector<FInterval> LayerRanges;
|
||||
std::vector<FLayerField> LayerFields;
|
||||
std::vector<FPrimitive> Primitives;
|
||||
std::vector<FFarConnectorSegment> FarConnectorSegments;
|
||||
std::vector<FEditBrush> EditBrushes;
|
||||
std::array<FAxisBins,3> AxisBins;
|
||||
std::unordered_map<FBinKey,FBinItems,FBinHash> SpatialBins;
|
||||
@@ -3164,6 +3246,7 @@ private:
|
||||
std::array<std::vector<uint64>,3> CandidateAxisWords;
|
||||
std::vector<uint64> CandidateWords;
|
||||
FBuildStats Stats;
|
||||
inline static thread_local const std::unordered_set<uint64>* ForcedFarAirSamples=nullptr;
|
||||
};
|
||||
|
||||
namespace FStandaloneShapeWorldAccess
|
||||
|
||||
@@ -49,6 +49,8 @@ namespace
|
||||
V3 Cross(V3 A,V3 B){return {A.Y*B.Z-A.Z*B.Y,A.Z*B.X-A.X*B.Z,A.X*B.Y-A.Y*B.X};}
|
||||
V3 Normalize(V3 A){return Mul(A,1.0f/std::sqrt(std::max(1.0e-20f,Dot(A,A))));}
|
||||
|
||||
#include "FarSurvivalAudit.h"
|
||||
|
||||
struct FMeshSheetAudit
|
||||
{
|
||||
uint64 Components=0;
|
||||
@@ -337,7 +339,13 @@ namespace
|
||||
ID3D11RasterizerState* WireRasterizer=nullptr;
|
||||
std::unique_ptr<FStandaloneOwnerWorld> World;
|
||||
std::unique_ptr<FStandaloneShapeWorld> ShapeWorld;
|
||||
std::unique_ptr<FStandaloneShapeWorld> PendingFullShapeWorld;
|
||||
std::unique_ptr<FStandaloneStreamingGenerator> Streamer;
|
||||
std::thread FullShapeBuildThread;
|
||||
std::atomic<bool> bFullShapeBuildFinished{false};
|
||||
bool bFullShapeBuildSucceeded=false;
|
||||
std::string FullShapeBuildError;
|
||||
std::chrono::steady_clock::time_point FullShapeBuildStart{};
|
||||
FStandaloneMeshCache MeshCache;
|
||||
std::string DataPath;
|
||||
bool bHasCameraOverride=false;
|
||||
@@ -349,6 +357,7 @@ namespace
|
||||
std::unordered_set<FStandaloneBatchKey,FStandaloneBatchKeyHash> DirtyBatchSet;
|
||||
std::unordered_map<uint64,FCrossingWork> CrossingWork;
|
||||
std::vector<FVoxelTileKey> DesiredTiles;
|
||||
std::vector<FVoxelTileKey> InitialNeighborhoodTiles;
|
||||
int32 DesiredByLevel[MaxLevels+1]{};
|
||||
uint64 GeneratedByLevel[MaxLevels+1]{};
|
||||
uint64 SamplesByLevel[MaxLevels+1]{};
|
||||
@@ -362,6 +371,8 @@ namespace
|
||||
bool bRoundTunnels=false;
|
||||
bool bRoundTunnelEnhanced=true;
|
||||
bool bShapeWorld=false;
|
||||
bool bShapePipelineActive=false;
|
||||
bool bNeighborhoodReadyReported=false;
|
||||
bool bShapeAuditOnly=false;
|
||||
bool bShapeCompare=false;
|
||||
bool bFarShapeStagePending=false;
|
||||
@@ -386,6 +397,7 @@ namespace
|
||||
bool bForceGenerate=false;
|
||||
std::string CapturePath;
|
||||
std::string MeshCachePath;
|
||||
std::chrono::steady_clock::time_point ColdProcessStart{};
|
||||
std::chrono::steady_clock::time_point GenerationStart{};
|
||||
bool bShowLevel[MaxLevels+1]={true,true,true,true,true,true,true,true,true};
|
||||
bool Keys[256]{};
|
||||
@@ -435,6 +447,15 @@ namespace
|
||||
(void)bCancel;
|
||||
if(Streamer)Streamer->Stop();
|
||||
Streamer.reset();
|
||||
if(FullShapeBuildThread.joinable())FullShapeBuildThread.join();
|
||||
PendingFullShapeWorld.reset();
|
||||
bFullShapeBuildFinished.store(false,std::memory_order_relaxed);
|
||||
bFullShapeBuildSucceeded=false;
|
||||
FullShapeBuildError.clear();
|
||||
InitialNeighborhoodTiles.clear();
|
||||
bShapePipelineActive=false;
|
||||
bNeighborhoodReadyReported=false;
|
||||
bFarShapeStagePending=false;
|
||||
bGenerationRunning=false;
|
||||
}
|
||||
|
||||
@@ -653,42 +674,95 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
std::printf("Standalone max clip level override applied after world reload: max_level=%d.\n",
|
||||
MaxClipLevelOverride);
|
||||
}
|
||||
if(bShapeWorld&&!BuildShapeWorldForCamera(Error))return false;
|
||||
if(bShapeWorld&&!BuildShapeWorldForCamera(Error,false,true))return false;
|
||||
LoadMeshCache();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BuildShapeWorldForCamera(std::string& Error,bool bIncludeFarWorld=false)
|
||||
bool BuildShapeWorldForCamera(std::string& Error,bool bIncludeFarWorld=false,
|
||||
bool bNeighborhoodOnly=false)
|
||||
{
|
||||
const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
|
||||
const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraVoxels);
|
||||
FIntVector Min(INT32_MAX,INT32_MAX,INT32_MAX),Max(INT32_MIN,INT32_MIN,INT32_MIN);
|
||||
FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN);
|
||||
uint64 FineTiles=0;
|
||||
InitialNeighborhoodTiles.clear();
|
||||
const FIntVector CenterChunk=Desired.CenterChunk;
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
{
|
||||
const FIntVector Origin=Tile.OriginVoxels();
|
||||
const int32 Extent=Tile.ExtentVoxels();
|
||||
ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z);
|
||||
ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent);
|
||||
if(Tile.Level>=World->FullResClipLevels)continue;
|
||||
if(bNeighborhoodOnly)
|
||||
{
|
||||
if(Tile.Level!=0||std::abs(Tile.Coord.X-CenterChunk.X)>1
|
||||
||std::abs(Tile.Coord.Y-CenterChunk.Y)>1
|
||||
||std::abs(Tile.Coord.Z-CenterChunk.Z)>1)continue;
|
||||
InitialNeighborhoodTiles.push_back(Tile);
|
||||
}
|
||||
else if(Tile.Level>=World->FullResClipLevels)continue;
|
||||
Min.X=std::min(Min.X,Origin.X);Min.Y=std::min(Min.Y,Origin.Y);Min.Z=std::min(Min.Z,Origin.Z);
|
||||
Max.X=std::max(Max.X,Origin.X+Extent);Max.Y=std::max(Max.Y,Origin.Y+Extent);Max.Z=std::max(Max.Z,Origin.Z+Extent);
|
||||
++FineTiles;
|
||||
}
|
||||
if(!FineTiles){Error="no full-detail tiles are available for shape compilation";return false;}
|
||||
if(bNeighborhoodOnly)
|
||||
{
|
||||
if(InitialNeighborhoodTiles.size()!=27)
|
||||
{Error="the 3x3x3 player neighborhood does not contain 27 LOD0 tiles";return false;}
|
||||
constexpr int32 NearBuildPad=64;
|
||||
Min.X-=NearBuildPad;Min.Y-=NearBuildPad;Min.Z-=NearBuildPad;
|
||||
Max.X+=NearBuildPad;Max.Y+=NearBuildPad;Max.Z+=NearBuildPad;
|
||||
}
|
||||
ShapeWorld=std::make_unique<FStandaloneShapeWorld>();
|
||||
if(!ShapeWorld->Build(*World,Min,Max,ClipMin,ClipMax,WorkerCount,
|
||||
const int32 ShapeWorkers=bNeighborhoodOnly?(WorkerCount>=8?2:1):WorkerCount;
|
||||
if(!ShapeWorld->Build(*World,Min,Max,ClipMin,ClipMax,ShapeWorkers,
|
||||
ShapeDetailVariant,bIncludeFarWorld,Error))
|
||||
{ShapeWorld.reset();return false;}
|
||||
FStandaloneShapeWorldAccess::SetActive(ShapeWorld.get());
|
||||
bFarShapeStagePending=bShapeWorld&&!ShapeWorld->HasFarWorld();
|
||||
bShapePipelineActive=bNeighborhoodOnly;
|
||||
bNeighborhoodReadyReported=false;
|
||||
bFarShapeStagePending=bShapeWorld&&!bNeighborhoodOnly&&!ShapeWorld->HasFarWorld();
|
||||
std::printf("[ShapeRegion] fine_tiles=%llu bounds=(%d,%d,%d)..(%d,%d,%d) clip=(%d,%d,%d)..(%d,%d,%d)\n",
|
||||
static_cast<unsigned long long>(FineTiles),Min.X,Min.Y,Min.Z,Max.X,Max.Y,Max.Z,
|
||||
ClipMin.X,ClipMin.Y,ClipMin.Z,ClipMax.X,ClipMax.Y,ClipMax.Z);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StartFullShapeBuild(const std::vector<FVoxelTileKey>& FullDesired,std::string& Error)
|
||||
{
|
||||
FIntVector FineMin(INT32_MAX,INT32_MAX,INT32_MAX),FineMax(INT32_MIN,INT32_MIN,INT32_MIN);
|
||||
FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN);
|
||||
for(const FVoxelTileKey& Tile:FullDesired)
|
||||
{
|
||||
const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels();
|
||||
ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z);
|
||||
ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent);
|
||||
if(Tile.Level>=World->FullResClipLevels)continue;
|
||||
FineMin.X=std::min(FineMin.X,Origin.X);FineMin.Y=std::min(FineMin.Y,Origin.Y);FineMin.Z=std::min(FineMin.Z,Origin.Z);
|
||||
FineMax.X=std::max(FineMax.X,Origin.X+Extent);FineMax.Y=std::max(FineMax.Y,Origin.Y+Extent);FineMax.Z=std::max(FineMax.Z,Origin.Z+Extent);
|
||||
}
|
||||
if(FineMin.X==INT32_MAX||ClipMin.X==INT32_MAX)
|
||||
{Error="the full shape build has no desired detail or clipmap tiles";return false;}
|
||||
PendingFullShapeWorld=std::make_unique<FStandaloneShapeWorld>();
|
||||
FullShapeBuildError.clear();bFullShapeBuildSucceeded=false;
|
||||
bFullShapeBuildFinished.store(false,std::memory_order_relaxed);
|
||||
const int32 FullWorkers=WorkerCount>=8?6:WorkerCount;
|
||||
FullShapeBuildStart=std::chrono::steady_clock::now();
|
||||
FullShapeBuildThread=std::thread([this,FineMin,FineMax,ClipMin,ClipMax,FullWorkers]()
|
||||
{
|
||||
bFullShapeBuildSucceeded=PendingFullShapeWorld->Build(*World,FineMin,FineMax,
|
||||
ClipMin,ClipMax,FullWorkers,ShapeDetailVariant,true,FullShapeBuildError);
|
||||
bFullShapeBuildFinished.store(true,std::memory_order_release);
|
||||
});
|
||||
std::printf("[ShapePipelineBuildStart] neighborhood_tiles=%zu near_mesh_workers=%d full_shape_workers=%d\n",
|
||||
InitialNeighborhoodTiles.size(),WorkerCount>=8?2:1,FullWorkers);
|
||||
std::fflush(stdout);
|
||||
return true;
|
||||
}
|
||||
|
||||
FStandaloneMeshCacheMetadata MakeMeshCacheMetadata()const
|
||||
{
|
||||
FStandaloneMeshCacheMetadata Metadata;
|
||||
@@ -752,10 +826,19 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
DesiredTiles=Desired.Tiles;
|
||||
for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0;
|
||||
for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level];
|
||||
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,WorkerCount,false,
|
||||
const int32 StreamWorkers=bShapePipelineActive?(WorkerCount>=8?2:1):WorkerCount;
|
||||
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,StreamWorkers,false,
|
||||
(!bShapeWorld&&MeshCache.IsLoaded())?&MeshCache:nullptr,
|
||||
bFarShapeStagePending?World->FullResClipLevels-1:-1);
|
||||
if(bShapePipelineActive)Streamer->SetDesiredTileFilter(InitialNeighborhoodTiles);
|
||||
const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,{},CrossingIndex);
|
||||
if(bShapePipelineActive&&Update.DesiredTiles!=static_cast<int32>(InitialNeighborhoodTiles.size()))
|
||||
{
|
||||
StopGeneration();
|
||||
Error="the initial neighborhood streamer did not accept all 27 tiles";
|
||||
std::fprintf(stderr,"World load failed: %s\n",Error.c_str());
|
||||
return;
|
||||
}
|
||||
FCrossingWork& InitialWork=CrossingWork[CrossingIndex];
|
||||
InitialWork.SubmittedJobs=Update.NewJobs;InitialWork.PendingJobs=Update.NewJobs;
|
||||
InitialWork.StartTime=GenerationStart;
|
||||
@@ -765,6 +848,16 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
std::printf("Streaming seed %d%s%s: %d desired tiles, %d full-detail tiles, %d workers; upload budget %d batches / %.1f ms per frame.\n",
|
||||
Seed,TunnelMode,bShapeWorld?" [shape intervals]":"",Update.DesiredTiles,
|
||||
Update.FullResTiles,Streamer->GetWorkerCount(),MaxBatchUploadsPerFrame,BatchUploadBudgetMilliseconds);
|
||||
if(bShapePipelineActive)
|
||||
{
|
||||
std::string ShapeError;
|
||||
if(!StartFullShapeBuild(DesiredTiles,ShapeError))
|
||||
{
|
||||
StopGeneration();
|
||||
std::fprintf(stderr,"World load failed: %s\n",ShapeError.c_str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
@@ -797,6 +890,67 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
void TryCompleteShapePipeline()
|
||||
{
|
||||
if(!bShapePipelineActive||InitialNeighborhoodTiles.empty())return;
|
||||
bool bNeighborhoodUploaded=true;
|
||||
for(const FVoxelTileKey& Key:InitialNeighborhoodTiles)
|
||||
if(!ResidentTiles.contains(Key)){bNeighborhoodUploaded=false;break;}
|
||||
bNeighborhoodUploaded &= DirtyBatchSet.empty();
|
||||
if(bNeighborhoodUploaded&&!bNeighborhoodReadyReported)
|
||||
{
|
||||
bNeighborhoodReadyReported=true;
|
||||
const double Elapsed=std::chrono::duration<double>(
|
||||
std::chrono::steady_clock::now()-ColdProcessStart).count();
|
||||
std::printf("[NeighborhoodReady] %.3fs after cold process start; tiles=%zu/%zu vertices=%llu indices=%llu\n",
|
||||
Elapsed,InitialNeighborhoodTiles.size(),InitialNeighborhoodTiles.size(),
|
||||
static_cast<unsigned long long>(TotalVertices),
|
||||
static_cast<unsigned long long>(TotalIndices));
|
||||
std::fflush(stdout);
|
||||
}
|
||||
if(!bNeighborhoodReadyReported
|
||||
||!bFullShapeBuildFinished.load(std::memory_order_acquire))return;
|
||||
if(FullShapeBuildThread.joinable())FullShapeBuildThread.join();
|
||||
if(!bFullShapeBuildSucceeded||!PendingFullShapeWorld)
|
||||
{
|
||||
bShapePipelineActive=false;bGenerationRunning=false;
|
||||
std::fprintf(stderr,"[ShapePipeline] full shape build failed: %s\n",
|
||||
FullShapeBuildError.c_str());
|
||||
PostMessageA(Window,WM_CLOSE,0,0);
|
||||
return;
|
||||
}
|
||||
|
||||
const double FullShapeSeconds=std::chrono::duration<double>(
|
||||
std::chrono::steady_clock::now()-FullShapeBuildStart).count();
|
||||
std::vector<FVoxelTileKey> ResidentKeys;ResidentKeys.reserve(ResidentTiles.size());
|
||||
for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first);
|
||||
if(Streamer)Streamer->Stop();
|
||||
Streamer.reset();
|
||||
ShapeWorld=std::move(PendingFullShapeWorld);
|
||||
FStandaloneShapeWorldAccess::SetActive(ShapeWorld.get());
|
||||
bFarShapeStagePending=false;bShapePipelineActive=false;
|
||||
|
||||
const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
|
||||
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,WorkerCount,false,nullptr,-1);
|
||||
++CrossingIndex;
|
||||
const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,ResidentKeys,CrossingIndex);
|
||||
if(Update.DesiredTiles!=static_cast<int32>(DesiredTiles.size()))
|
||||
{
|
||||
Streamer->Stop();Streamer.reset();bGenerationRunning=false;
|
||||
std::fprintf(stderr,"[ShapePipeline] full streamer accepted %d of %zu desired tiles.\n",
|
||||
Update.DesiredTiles,DesiredTiles.size());
|
||||
PostMessageA(Window,WM_CLOSE,0,0);
|
||||
return;
|
||||
}
|
||||
FCrossingWork& Work=CrossingWork[CrossingIndex];
|
||||
Work.SubmittedJobs=Update.NewJobs;Work.PendingJobs=Update.NewJobs;
|
||||
Work.StartTime=std::chrono::steady_clock::now();
|
||||
bGenerationRunning=true;
|
||||
std::printf("[ShapePipelineFullReady] full_shape=%.3fs retained=%d new_jobs=%d workers=%d; filling outward\n",
|
||||
FullShapeSeconds,Update.RetainedResidentTiles,Update.NewJobs,WorkerCount);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
void MarkBatchDirty(const FStandaloneBatchKey& Key)
|
||||
{DirtyBatchSet.insert(Key);}
|
||||
|
||||
@@ -1050,6 +1204,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
}
|
||||
ReportCrossingWork();
|
||||
UploadDirtyBatches();
|
||||
if(bShapePipelineActive)TryCompleteShapePipeline();
|
||||
if(bTestOneChunkCrossing&&bCrossingMoveFinished&&!bCrossingClosePosted
|
||||
&&DirtyBatchSet.empty())
|
||||
{
|
||||
@@ -1062,11 +1217,11 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
PostMessageA(Window,WM_CLOSE,0,0);
|
||||
}
|
||||
}
|
||||
if(!bFullDetailReported&&IsFullDetailUploaded())
|
||||
if(!bShapePipelineActive&&!bFullDetailReported&&IsFullDetailUploaded())
|
||||
{
|
||||
bFullDetailReported=true;
|
||||
const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-GenerationStart).count();
|
||||
std::printf("[FullDetailReady] %.3f s after spawn; resident=%zu/%d tiles through LOD%d.\n",
|
||||
const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-ColdProcessStart).count();
|
||||
std::printf("[FullDetailReady] %.3f s after cold process start; resident=%zu/%d tiles through LOD%d.\n",
|
||||
Elapsed,ResidentTiles.size(),Streamer->FullResDesiredCount(),World->FullResClipLevels-1);
|
||||
ReportTileTimings();std::fflush(stdout);
|
||||
StartFarShapeGeneration();
|
||||
@@ -1074,8 +1229,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
if(!bInitialClipmapReported&&ResidentTiles.size()==DesiredTiles.size()&&DirtyBatchSet.empty())
|
||||
{
|
||||
bInitialClipmapReported=true;
|
||||
const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-GenerationStart).count();
|
||||
std::printf("[InitialClipmapReady] %.3f s after spawn; %zu tiles, %llu vertices, %llu indices, %zu GPU batches.\n",
|
||||
const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-ColdProcessStart).count();
|
||||
std::printf("[InitialClipmapReady] %.3f s after cold process start; %zu tiles, %llu vertices, %llu indices, %zu GPU batches.\n",
|
||||
Elapsed,ResidentTiles.size(),static_cast<unsigned long long>(TotalVertices),
|
||||
static_cast<unsigned long long>(TotalIndices),StreamingBatches.size());
|
||||
ReportTileTimings();std::fflush(stdout);
|
||||
@@ -1709,12 +1864,225 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
return Hash;
|
||||
}
|
||||
|
||||
int RunPipelinedStreamingBenchmark(FStandaloneOwnerWorld& World,const FVector& CameraPositionVoxels,
|
||||
int32 WorkerCount,int32 MaxLevel,FStandaloneShapeWorld::EDetailVariant DetailVariant,
|
||||
bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,bool bSkipUV,bool bSkipColors,
|
||||
std::chrono::steady_clock::time_point ColdStart)
|
||||
{
|
||||
using Clock=std::chrono::steady_clock;
|
||||
FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels);
|
||||
if(MaxLevel>=0)
|
||||
Desired.Tiles.erase(std::remove_if(Desired.Tiles.begin(),Desired.Tiles.end(),
|
||||
[&](const FVoxelTileKey& Tile){return Tile.Level>MaxLevel;}),Desired.Tiles.end());
|
||||
FIntVector FineMin(INT32_MAX,INT32_MAX,INT32_MAX),FineMax(INT32_MIN,INT32_MIN,INT32_MIN);
|
||||
FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN);
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
{
|
||||
const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels();
|
||||
ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z);
|
||||
ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent);
|
||||
if(Tile.Level<World.FullResClipLevels)
|
||||
{
|
||||
FineMin.X=std::min(FineMin.X,Origin.X);FineMin.Y=std::min(FineMin.Y,Origin.Y);FineMin.Z=std::min(FineMin.Z,Origin.Z);
|
||||
FineMax.X=std::max(FineMax.X,Origin.X+Extent);FineMax.Y=std::max(FineMax.Y,Origin.Y+Extent);FineMax.Z=std::max(FineMax.Z,Origin.Z+Extent);
|
||||
}
|
||||
}
|
||||
if(FineMin.X==INT32_MAX||ClipMin.X==INT32_MAX)
|
||||
{std::fprintf(stderr,"[StreamPipeline] no desired detail or clipmap tiles.\n");return 7;}
|
||||
const FIntVector CenterChunk=Desired.CenterChunk;
|
||||
std::vector<FVoxelTileKey> Neighborhood;
|
||||
FIntVector NearMin(INT32_MAX,INT32_MAX,INT32_MAX),NearMax(INT32_MIN,INT32_MIN,INT32_MIN);
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
{
|
||||
if(Tile.Level!=0||std::abs(Tile.Coord.X-CenterChunk.X)>1
|
||||
||std::abs(Tile.Coord.Y-CenterChunk.Y)>1||std::abs(Tile.Coord.Z-CenterChunk.Z)>1)continue;
|
||||
Neighborhood.push_back(Tile);const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels();
|
||||
NearMin.X=std::min(NearMin.X,Origin.X);NearMin.Y=std::min(NearMin.Y,Origin.Y);NearMin.Z=std::min(NearMin.Z,Origin.Z);
|
||||
NearMax.X=std::max(NearMax.X,Origin.X+Extent);NearMax.Y=std::max(NearMax.Y,Origin.Y+Extent);NearMax.Z=std::max(NearMax.Z,Origin.Z+Extent);
|
||||
}
|
||||
if(Neighborhood.empty())
|
||||
{std::fprintf(stderr,"[StreamPipeline] the player neighborhood is absent from the desired set.\n");return 7;}
|
||||
constexpr int32 NearBuildPad=64;
|
||||
NearMin.X-=NearBuildPad;NearMin.Y-=NearBuildPad;NearMin.Z-=NearBuildPad;
|
||||
NearMax.X+=NearBuildPad;NearMax.Y+=NearBuildPad;NearMax.Z+=NearBuildPad;
|
||||
const int32 NearWorkers=WorkerCount>=8?2:1;
|
||||
const int32 FullBuildWorkers=WorkerCount>=8?6:1;
|
||||
auto NearShape=std::make_unique<FStandaloneShapeWorld>();
|
||||
std::string BuildError;
|
||||
const auto NearBuildStart=Clock::now();
|
||||
if(!NearShape->Build(World,NearMin,NearMax,ClipMin,ClipMax,NearWorkers,
|
||||
DetailVariant,false,BuildError))
|
||||
{std::fprintf(stderr,"[StreamPipeline] near shape build failed: %s\n",BuildError.c_str());return 2;}
|
||||
const double NearShapeSeconds=std::chrono::duration<double>(Clock::now()-NearBuildStart).count();
|
||||
FStandaloneShapeWorldAccess::SetActive(NearShape.get());
|
||||
FStandaloneStreamingGenerator NearStreamer(World,NearWorkers,bHashDensity,nullptr,MaxLevel,
|
||||
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors);
|
||||
NearStreamer.SetDesiredTileFilter(Neighborhood);
|
||||
const auto NearStreamStart=Clock::now();
|
||||
const FStandaloneStreamingUpdate NearUpdate=NearStreamer.Start(CameraPositionVoxels);
|
||||
if(NearUpdate.DesiredTiles!=static_cast<int32>(Neighborhood.size()))
|
||||
{NearStreamer.Stop();FStandaloneShapeWorldAccess::SetActive(nullptr);
|
||||
std::fprintf(stderr,"[StreamPipeline] expected %zu player tiles but queued %d.\n",Neighborhood.size(),NearUpdate.DesiredTiles);return 7;}
|
||||
|
||||
FStandaloneShapeWorld FullShape;
|
||||
bool bFullBuildOk=false;std::string FullBuildError;
|
||||
const auto FullBuildStart=Clock::now();
|
||||
std::thread FullBuildThread([&]
|
||||
{bFullBuildOk=FullShape.Build(World,FineMin,FineMax,ClipMin,ClipMax,FullBuildWorkers,
|
||||
DetailVariant,true,FullBuildError);});
|
||||
struct FPipelineTileHashes {uint64 Density=0,Mesh=0,Payload=0,Topology=0;};
|
||||
std::unordered_map<FVoxelTileKey,FPipelineTileHashes,FStandaloneTileKeyHash> NearHashByKey;
|
||||
NearHashByKey.reserve(Neighborhood.size()*2+1);
|
||||
std::vector<FHashedTile> DensityRows,MeshRows,PayloadRows,TopologyRows;
|
||||
std::vector<FStandaloneCpuTile> NearTiles;NearTiles.reserve(Neighborhood.size());
|
||||
std::chrono::steady_clock::time_point NeighborhoodReady{};
|
||||
int32 NearFinished=0;
|
||||
while(NearFinished<NearUpdate.DesiredTiles)
|
||||
{
|
||||
std::vector<FStandaloneCpuTile> Ready=NearStreamer.TakeReady();
|
||||
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
|
||||
for(FStandaloneCpuTile& Tile:Ready)
|
||||
{
|
||||
++NearFinished;
|
||||
if(bHashDensity)
|
||||
{
|
||||
if(Tile.bHasDensityHash)DensityRows.push_back({Tile.Key,Tile.DensityHash});
|
||||
if(Tile.bHasMeshHash)MeshRows.push_back({Tile.Key,Tile.MeshHash});
|
||||
if(Tile.bHasPayloadHash)PayloadRows.push_back({Tile.Key,Tile.PayloadHash});
|
||||
if(Tile.bHasTopologyHash)TopologyRows.push_back({Tile.Key,Tile.TopologyHash});
|
||||
NearHashByKey[Tile.Key]={Tile.DensityHash,Tile.MeshHash,Tile.PayloadHash,Tile.TopologyHash};
|
||||
}
|
||||
NeighborhoodReady=std::max(NeighborhoodReady,Tile.CompletedAt);
|
||||
NearTiles.push_back(std::move(Tile));
|
||||
}
|
||||
}
|
||||
NearStreamer.Stop();
|
||||
const double NearStreamSeconds=std::chrono::duration<double>(NeighborhoodReady-NearStreamStart).count();
|
||||
FStandaloneShapeWorldAccess::SetActive(nullptr);
|
||||
// Release the small shape before the large build finishes allocating its packed field.
|
||||
NearTiles.clear();NearTiles.shrink_to_fit();NearShape.reset();
|
||||
FullBuildThread.join();
|
||||
const double FullShapeSeconds=std::chrono::duration<double>(Clock::now()-FullBuildStart).count();
|
||||
if(!bFullBuildOk)
|
||||
{std::fprintf(stderr,"[StreamPipeline] full shape build failed: %s\n",FullBuildError.c_str());return 2;}
|
||||
FStandaloneShapeWorldAccess::SetActive(&FullShape);
|
||||
|
||||
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> NearSet;
|
||||
NearSet.reserve(Neighborhood.size()*2+1);for(const FVoxelTileKey& Key:Neighborhood)NearSet.insert(Key);
|
||||
std::vector<FVoxelTileKey> Remaining;Remaining.reserve(Desired.Tiles.size()-Neighborhood.size());
|
||||
int32 FullDetailExpected=0;
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
{
|
||||
if(Tile.Level<World.FullResClipLevels)++FullDetailExpected;
|
||||
if(!NearSet.contains(Tile))Remaining.push_back(Tile);
|
||||
}
|
||||
FStandaloneStreamingGenerator FullStreamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel,
|
||||
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors);
|
||||
FullStreamer.SetDesiredTileFilter(Remaining);
|
||||
const auto FillStart=Clock::now();
|
||||
const FStandaloneStreamingUpdate FillUpdate=FullStreamer.Start(CameraPositionVoxels);
|
||||
int32 Finished=0,FullDetailFinished=static_cast<int32>(Neighborhood.size());
|
||||
const int32 FullDetailRemaining=std::max(0,FullDetailExpected-FullDetailFinished);
|
||||
std::chrono::steady_clock::time_point FullDetailReady=NeighborhoodReady,WholeViewReady{};
|
||||
bool bFullDetailReported=false;
|
||||
while(Finished<FillUpdate.DesiredTiles)
|
||||
{
|
||||
std::vector<FStandaloneCpuTile> Ready=FullStreamer.TakeReady();
|
||||
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
|
||||
for(const FStandaloneCpuTile& Tile:Ready)
|
||||
{
|
||||
++Finished;WholeViewReady=std::max(WholeViewReady,Tile.CompletedAt);
|
||||
if(Tile.Key.Level<World.FullResClipLevels)
|
||||
{++FullDetailFinished;FullDetailReady=std::max(FullDetailReady,Tile.CompletedAt);}
|
||||
if(bHashDensity)
|
||||
{
|
||||
if(Tile.bHasDensityHash)DensityRows.push_back({Tile.Key,Tile.DensityHash});
|
||||
if(Tile.bHasMeshHash)MeshRows.push_back({Tile.Key,Tile.MeshHash});
|
||||
if(Tile.bHasPayloadHash)PayloadRows.push_back({Tile.Key,Tile.PayloadHash});
|
||||
if(Tile.bHasTopologyHash)TopologyRows.push_back({Tile.Key,Tile.TopologyHash});
|
||||
}
|
||||
}
|
||||
if(!bFullDetailReported&&FullDetailRemaining>0&&FullDetailFinished>=FullDetailExpected)
|
||||
{
|
||||
std::printf("[PipelineMilestone] full_detail_ready=%.3fs generated=%d/%d\n",
|
||||
std::chrono::duration<double>(FullDetailReady-ColdStart).count(),
|
||||
FullDetailFinished,FullDetailExpected);
|
||||
bFullDetailReported=true;
|
||||
}
|
||||
}
|
||||
FullStreamer.Stop();
|
||||
const double WholeViewSeconds=std::chrono::duration<double>(WholeViewReady-ColdStart).count();
|
||||
const double NeighborhoodSeconds=std::chrono::duration<double>(NeighborhoodReady-ColdStart).count();
|
||||
const double FullDetailSeconds=std::chrono::duration<double>(FullDetailReady-ColdStart).count();
|
||||
bool bNearParityPassing=true;
|
||||
if(bHashDensity)
|
||||
{
|
||||
FStandaloneStreamingGenerator NearVerifier(World,NearWorkers,true,nullptr,MaxLevel,
|
||||
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors);
|
||||
NearVerifier.SetDesiredTileFilter(Neighborhood);
|
||||
const FStandaloneStreamingUpdate VerifyUpdate=NearVerifier.Start(CameraPositionVoxels);
|
||||
int32 VerifiedTiles=0,NearHashMismatches=0;
|
||||
while(VerifiedTiles<VerifyUpdate.DesiredTiles)
|
||||
{
|
||||
std::vector<FStandaloneCpuTile> Ready=NearVerifier.TakeReady();
|
||||
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
|
||||
for(const FStandaloneCpuTile& Tile:Ready)
|
||||
{
|
||||
++VerifiedTiles;
|
||||
const auto It=NearHashByKey.find(Tile.Key);
|
||||
if(It==NearHashByKey.end()||!Tile.bHasDensityHash||!Tile.bHasMeshHash
|
||||
||!Tile.bHasPayloadHash||!Tile.bHasTopologyHash
|
||||
||It->second.Density!=Tile.DensityHash||It->second.Mesh!=Tile.MeshHash
|
||||
||It->second.Payload!=Tile.PayloadHash||It->second.Topology!=Tile.TopologyHash)
|
||||
{
|
||||
++NearHashMismatches;
|
||||
if(NearHashMismatches<=4)
|
||||
std::fprintf(stderr,"[PipelineNeighborhoodHashMismatch] key=(%d,%d,%d,lod%d)\n",
|
||||
Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z,Tile.Key.Level);
|
||||
}
|
||||
}
|
||||
}
|
||||
NearVerifier.Stop();
|
||||
bNearParityPassing=VerifiedTiles==static_cast<int32>(Neighborhood.size())
|
||||
&&NearHashMismatches==0;
|
||||
std::printf("[PipelineNeighborhoodParity] tiles=%d/%zu hash_fields=%d mismatched_tiles=%d status=%s\n",
|
||||
VerifiedTiles,Neighborhood.size(),VerifiedTiles*4,NearHashMismatches,
|
||||
bNearParityPassing?"PASS":"FAIL");
|
||||
}
|
||||
std::printf("[StreamPipeline] workers=%d near_mesh_workers=%d full_shape_workers=%d near_shape=%.3fs near_mesh=%.3fs full_shape=%.3fs neighborhood_ready=%.3fs full_detail_ready=%.3fs whole_view_ready=%.3fs neighborhood_tiles=%zu/%zu full_detail_tiles=%d/%d whole_tiles=%d/%zu cache_hits=0 full_payload=1\n",
|
||||
WorkerCount,NearWorkers,FullBuildWorkers,NearShapeSeconds,NearStreamSeconds,FullShapeSeconds,
|
||||
NeighborhoodSeconds,FullDetailSeconds,WholeViewSeconds,Neighborhood.size(),Neighborhood.size(),
|
||||
FullDetailFinished,FullDetailExpected,static_cast<int32>(Desired.Tiles.size()),Desired.Tiles.size());
|
||||
if(bHashDensity)
|
||||
{
|
||||
std::vector<FHashedTile> FullDensity,FullMesh,FullPayload,FullTopology;
|
||||
for(const FHashedTile& Row:DensityRows)if(Row.Key.Level<World.FullResClipLevels)FullDensity.push_back(Row);
|
||||
for(const FHashedTile& Row:MeshRows)if(Row.Key.Level<World.FullResClipLevels)FullMesh.push_back(Row);
|
||||
for(const FHashedTile& Row:PayloadRows)if(Row.Key.Level<World.FullResClipLevels)FullPayload.push_back(Row);
|
||||
for(const FHashedTile& Row:TopologyRows)if(Row.Key.Level<World.FullResClipLevels)FullTopology.push_back(Row);
|
||||
std::printf("[PipelineFullDetailHash] tiles=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n",
|
||||
FullDetailExpected,static_cast<unsigned long long>(HashTileRows(FullDensity)),
|
||||
static_cast<unsigned long long>(HashTileRows(FullMesh)),
|
||||
static_cast<unsigned long long>(HashTileRows(FullPayload)),
|
||||
static_cast<unsigned long long>(HashTileRows(FullTopology)));
|
||||
std::printf("[PipelineWholeViewHash] tiles=%zu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n",
|
||||
Desired.Tiles.size(),static_cast<unsigned long long>(HashTileRows(DensityRows)),
|
||||
static_cast<unsigned long long>(HashTileRows(MeshRows)),
|
||||
static_cast<unsigned long long>(HashTileRows(PayloadRows)),
|
||||
static_cast<unsigned long long>(HashTileRows(TopologyRows)));
|
||||
}
|
||||
FStandaloneShapeWorldAccess::SetActive(nullptr);
|
||||
return FullDetailFinished==FullDetailExpected&&Finished==FillUpdate.DesiredTiles
|
||||
&&bNearParityPassing?0:6;
|
||||
}
|
||||
|
||||
int RunStreamingBenchmark(FStandaloneOwnerWorld& World,const FVector& CameraPositionVoxels,
|
||||
int32 WorkerCount,int32 MaxLevel,bool bRoundTunnels,
|
||||
bool bEnhancedWalls,const FStandaloneMeshCache* Cache,
|
||||
bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,
|
||||
bool bSkipUV,bool bSkipColors,
|
||||
bool bShardAudit,float CameraYaw,float CameraPitch,
|
||||
bool bShardAudit,bool bFarSurvivalAudit,bool bFarAuditBadShell,
|
||||
float CameraYaw,float CameraPitch,
|
||||
const std::string& BakeCachePath,
|
||||
const FStandaloneMeshCacheMetadata& CacheMetadata)
|
||||
{
|
||||
@@ -1731,6 +2099,14 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
const int32 FullDetailExpected=static_cast<int32>(std::count_if(
|
||||
Desired.Tiles.begin(),Desired.Tiles.end(),[&](const FVoxelTileKey& Tile)
|
||||
{return Tile.Level<World.FullResClipLevels;}));
|
||||
std::unique_ptr<FFarSurvivalAudit> FarAudit;
|
||||
if(bFarSurvivalAudit)
|
||||
{
|
||||
const FStandaloneShapeWorld* Shape=FStandaloneShapeWorldAccess::GetActive();
|
||||
if(!Shape||!Shape->HasFarWorld())
|
||||
{std::fprintf(stderr,"Far survival audit requires an active shape world with far-parent data.\n");return 15;}
|
||||
FarAudit=std::make_unique<FFarSurvivalAudit>(World,*Shape,Desired.Tiles,bFarAuditBadShell);
|
||||
}
|
||||
std::array<std::vector<FHashedTile>,MaxLevels+1> HashRows;
|
||||
std::array<std::vector<FHashedTile>,MaxLevels+1> MeshHashRows;
|
||||
std::array<std::vector<FHashedTile>,MaxLevels+1> PayloadHashRows;
|
||||
@@ -1787,6 +2163,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
}
|
||||
for(FStandaloneCpuTile& Tile:Ready)
|
||||
{
|
||||
if(FarAudit)FarAudit->AuditTile(Tile);
|
||||
++Finished;
|
||||
const int32 Level=Tile.Key.Level;
|
||||
if(PlayerNeighborhoodTiles.contains(Tile.Key))
|
||||
@@ -1960,6 +2337,12 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
}
|
||||
if(Finished!=DesiredTotal||FullDetailFinished!=FullDetailExpected)return 6;
|
||||
if(bShardAudit&&(MeshSheetStats.ThinSheets!=0||MeshSheetStats.SmallComponentsInView!=0))return 13;
|
||||
if(FarAudit)
|
||||
{
|
||||
FarAudit->Finalize();
|
||||
FarAudit->Print();
|
||||
if(!FarAudit->Passing())return 14;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -2038,7 +2421,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
|
||||
int main(int Argc,char** Argv)
|
||||
{
|
||||
FApp App;App.DataPath=DefaultDataPath().string();
|
||||
const auto ColdProcessStart=std::chrono::steady_clock::now();
|
||||
FApp App;App.DataPath=DefaultDataPath().string();App.ColdProcessStart=ColdProcessStart;
|
||||
std::string JoinedCommandLine;
|
||||
for(int32 I=1;I<Argc;++I)
|
||||
{if(!JoinedCommandLine.empty())JoinedCommandLine.push_back(' ');JoinedCommandLine+=Argv[I];}
|
||||
@@ -2047,8 +2431,11 @@ int main(int Argc,char** Argv)
|
||||
int32 MaxClipLevelOverride=-1;
|
||||
int32 ShapeCompareLevel=-1,ShapeCompareTileLimit=0;
|
||||
bool bStreamBenchmark=false;
|
||||
bool bStreamPipeline=false;
|
||||
bool bProfileDetailBand=false;
|
||||
bool bShardAudit=false;
|
||||
bool bFarSurvivalAudit=false;
|
||||
bool bFarAuditBadShell=false;
|
||||
bool bStreamHash=false;
|
||||
bool bSkipUvAndColors=false;
|
||||
bool bSkipUV=false;
|
||||
@@ -2085,6 +2472,8 @@ int main(int Argc,char** Argv)
|
||||
else if(Arg=="--round-tunnels-legacy")
|
||||
{App.bRoundTunnels=true;App.bRoundTunnelEnhanced=false;}
|
||||
else if(Arg=="--stream-benchmark")bStreamBenchmark=true;
|
||||
else if(Arg=="--stream-pipeline")
|
||||
{bStreamPipeline=true;bStreamBenchmark=true;App.bShapeWorld=true;App.bForceGenerate=true;}
|
||||
else if(Arg=="--edit-benchmark")
|
||||
{bEditBenchmark=true;bStreamBenchmark=true;App.bShapeWorld=true;}
|
||||
else if(Arg=="--stream-hash")bStreamHash=true;
|
||||
@@ -2093,6 +2482,9 @@ int main(int Argc,char** Argv)
|
||||
else if(Arg=="--skip-colors")bSkipColors=true;
|
||||
else if(Arg=="--skip-material-groups")bSkipMaterialGroups=true;
|
||||
else if(Arg=="--shard-audit")bShardAudit=true;
|
||||
else if(Arg=="--far-survival-audit")bFarSurvivalAudit=true,App.bShapeWorld=true;
|
||||
else if(Arg=="--far-audit-bad-shell")
|
||||
{bFarAuditBadShell=true;bFarSurvivalAudit=true;App.bShapeWorld=true;}
|
||||
else if(Arg=="--compare-round-walls")
|
||||
{bRoundWallCompare=true;App.bRoundTunnels=true;}
|
||||
else if(Arg.starts_with("--round-compare-tiles="))
|
||||
@@ -2166,6 +2558,7 @@ int main(int Argc,char** Argv)
|
||||
:(App.bShapeCompare?ShapeCompareLevel>=App.World->FullResClipLevels
|
||||
:(BenchmarkLevel>=App.World->FullResClipLevels));
|
||||
if(App.bShapeWorld&&(App.bShapeAuditOnly||App.bShapeCompare||bStreamBenchmark||BenchmarkLevel>=0)
|
||||
&&!bStreamPipeline
|
||||
&&!App.BuildShapeWorldForCamera(Error,bNeedFarShapeWorld))
|
||||
{std::fprintf(stderr,"Shape interval build failed: %s\n",Error.c_str());return 2;}
|
||||
if(App.bShapeAuditOnly)
|
||||
@@ -2181,6 +2574,12 @@ int main(int Argc,char** Argv)
|
||||
return RunRoundWallComparison(*App.World,App.Position,RoundCompareTileCount);
|
||||
if(bStreamBenchmark)
|
||||
{
|
||||
if(bStreamPipeline)
|
||||
return RunPipelinedStreamingBenchmark(*App.World,
|
||||
FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0),
|
||||
App.WorkerCount,StreamBenchmarkMaxLevel,App.ShapeDetailVariant,
|
||||
bStreamHash||!BakeMeshCachePath.empty(),bSkipUvAndColors,bSkipMaterialGroups,
|
||||
bSkipUV,bSkipColors,ColdProcessStart);
|
||||
if(bEditBenchmark)
|
||||
return RunEditTransactionBenchmark(*App.World,*App.ShapeWorld,
|
||||
FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0),App.WorkerCount);
|
||||
@@ -2194,7 +2593,8 @@ int main(int Argc,char** Argv)
|
||||
App.WorkerCount,StreamBenchmarkMaxLevel,App.bRoundTunnels,
|
||||
App.bRoundTunnelEnhanced,(!App.bShapeWorld&&App.MeshCache.IsLoaded())?&App.MeshCache:nullptr,
|
||||
bStreamHash,bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,bShardAudit,
|
||||
App.Yaw,App.Pitch,BakeMeshCachePath,App.MakeMeshCacheMetadata());
|
||||
bFarSurvivalAudit,bFarAuditBadShell,App.Yaw,App.Pitch,
|
||||
BakeMeshCachePath,App.MakeMeshCacheMetadata());
|
||||
}
|
||||
if(BenchmarkLevel>=0)
|
||||
return RunTileBenchmark(*App.World,App.Position,BenchmarkLevel,BenchmarkCount,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "WorldMesher.h"
|
||||
#include "StandaloneShapeWorld.h"
|
||||
|
||||
#include <condition_variable>
|
||||
#include <algorithm>
|
||||
@@ -152,6 +153,277 @@ inline uint64 HashStandaloneCpuTileMesh(const FStandaloneCpuTile& Tile)
|
||||
return Hash;
|
||||
}
|
||||
|
||||
inline double FindFarConnectorSurfaceRadius(const FStandaloneShapeWorld& Shape,const FVector& Center,
|
||||
const FVector& Direction,double PreferredRadius)
|
||||
{
|
||||
const double SearchLength=PreferredRadius*2.0+32.0;
|
||||
const double AcceptBand=std::max(16.0,PreferredRadius*0.75);
|
||||
constexpr double Stride=8.0;
|
||||
double PreviousDistance=0.0;
|
||||
bool PreviousSolid=Shape.DensityAt(static_cast<float>(Center.X),static_cast<float>(Center.Y),
|
||||
static_cast<float>(Center.Z))<0.0f;
|
||||
double BestRadius=PreferredRadius,BestDifference=std::numeric_limits<double>::infinity();
|
||||
for(double Distance=Stride;Distance<=SearchLength+1.0e-6;Distance=std::min(Distance+Stride,SearchLength))
|
||||
{
|
||||
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;
|
||||
const double Difference=std::abs(Crossing-PreferredRadius);
|
||||
if(Difference<=AcceptBand&&Difference<BestDifference)
|
||||
{BestRadius=Crossing;BestDifference=Difference;}
|
||||
}
|
||||
if(Distance>=SearchLength-1.0e-6)break;
|
||||
PreviousDistance=Distance;PreviousSolid=Solid;
|
||||
}
|
||||
return BestRadius;
|
||||
}
|
||||
|
||||
inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,
|
||||
int32 Step,FVoxelMeshData& Mesh)
|
||||
{
|
||||
if(Key.Level<6||Step<=0)return;
|
||||
constexpr int32 SideCount=8;
|
||||
const FIntVector TileOrigin=Key.OriginVoxels();
|
||||
const int32 TileExtent=Key.ExtentVoxels();
|
||||
std::vector<FVector> NewVertices;
|
||||
std::vector<FVector> NewNormals;
|
||||
std::vector<FVector2f> NewUVs;
|
||||
std::vector<FColor> NewColors;
|
||||
std::vector<int32> NewTriangles;
|
||||
const int32 OldVertexCount=Mesh.Vertices.Num();
|
||||
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);
|
||||
for(const auto& Connector:Shape.GetFarConnectorSegments())
|
||||
{
|
||||
const FVector Delta=Connector.End-Connector.Start;
|
||||
const double Length=std::sqrt(Delta.X*Delta.X+Delta.Y*Delta.Y+Delta.Z*Delta.Z);
|
||||
if(Length<=1.0e-6)continue;
|
||||
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};
|
||||
double TMin=0.0,TMax=1.0;bool 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;
|
||||
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 int32 AxialSampleCount=std::max(1,static_cast<int32>(std::ceil(Length/128.0)));
|
||||
std::vector<double> RingParameters{TMin};
|
||||
for(int32 Sample=0;Sample<AxialSampleCount;++Sample)
|
||||
{
|
||||
const double T=(static_cast<double>(Sample)+0.5)/AxialSampleCount;
|
||||
if(T>TMin+1.0e-6&&T<TMax-1.0e-6)RingParameters.push_back(T);
|
||||
}
|
||||
RingParameters.push_back(TMax);
|
||||
const int32 RingCount=static_cast<int32>(RingParameters.size())-1;
|
||||
const int32 VertexBase=static_cast<int32>(NewVertices.size());
|
||||
for(int32 Ring=0;Ring<=RingCount;++Ring)
|
||||
{
|
||||
const double T=RingParameters[static_cast<size_t>(Ring)];
|
||||
const FVector Center=Connector.Start+Delta*T;
|
||||
for(int32 Side=0;Side<SideCount;++Side)
|
||||
{
|
||||
const double Angle=2.0*PI*Side/SideCount;
|
||||
FVector Radial=U*std::cos(Angle)+V*std::sin(Angle);Radial.Normalize();
|
||||
const double SourceRadius=FindFarConnectorSurfaceRadius(Shape,Center,Radial,
|
||||
Connector.Radius);
|
||||
const FVector P=Center+Radial*SourceRadius;
|
||||
NewVertices.emplace_back(P.X*25.0,P.Y*25.0,P.Z*25.0);
|
||||
NewNormals.emplace_back(-Radial.X,-Radial.Y,-Radial.Z);
|
||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(T*Length/256.0),
|
||||
static_cast<float>(Side)/SideCount);
|
||||
if(bHasColors)NewColors.push_back(Color);
|
||||
}
|
||||
}
|
||||
for(int32 Ring=0;Ring<RingCount;++Ring)
|
||||
for(int32 Side=0;Side<SideCount;++Side)
|
||||
{
|
||||
const int32 Next=(Side+1)%SideCount;
|
||||
const int32 A=VertexBase+Ring*SideCount+Side;
|
||||
const int32 B=VertexBase+Ring*SideCount+Next;
|
||||
const int32 C=VertexBase+(Ring+1)*SideCount+Side;
|
||||
const int32 D=VertexBase+(Ring+1)*SideCount+Next;
|
||||
// The far links are carved passages, so their visible normals face inward.
|
||||
NewTriangles.insert(NewTriangles.end(),{A,C,B,B,C,D});
|
||||
}
|
||||
}
|
||||
if(NewVertices.empty()||NewTriangles.empty())return;
|
||||
|
||||
const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount);
|
||||
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]);
|
||||
Mesh.Triangles=std::move(CombinedTriangles);
|
||||
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||
}
|
||||
|
||||
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;
|
||||
FStandaloneShapeWorld::FScopedForcedFarAirSamples SourceFieldScope(nullptr);
|
||||
struct FBoundary { double Z=0.0,HalfWidth=8.0; };
|
||||
const auto& Layout=World.Manager->GetLayout();
|
||||
std::vector<FBoundary> Boundaries;
|
||||
for(const FStrateSlot& Slot:Layout)
|
||||
{
|
||||
const FStrateGenerationParams Params=World.Manager->GetGenerationParams(
|
||||
FIntVector(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2));
|
||||
const double HalfWidth=std::max(8.0,static_cast<double>(Params.BoundarySealThickness)+8.0);
|
||||
Boundaries.push_back({static_cast<double>(Slot.BottomChunkZ)*CHUNK_SIZE,HalfWidth});
|
||||
Boundaries.push_back({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;});
|
||||
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();
|
||||
const int32 FineStep=std::max(1,Step/2);const int32 GridCells=Extent/FineStep;
|
||||
if(GridCells<=0||GridCells>256)return;
|
||||
struct FNode { double Z=0.0;FVector Normal{};int32 Index=INDEX_NONE;bool bValid=false,bBelowSolid=false; };
|
||||
std::vector<FVector> NewVertices,NewNormals;
|
||||
std::vector<FVector2f> NewUVs;std::vector<FColor> NewColors;
|
||||
std::vector<int32> NewTriangles;
|
||||
const int32 OldVertexCount=Mesh.Vertices.Num();
|
||||
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 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:Boundaries)
|
||||
{
|
||||
if(Boundary.Z+Boundary.HalfWidth<Origin.Z||Boundary.Z-Boundary.HalfWidth>Origin.Z+Extent)continue;
|
||||
const int32 ZMin=std::max(Origin.Z,static_cast<int32>(std::floor(Boundary.Z-Boundary.HalfWidth)));
|
||||
const int32 ZMax=std::min(Origin.Z+Extent,static_cast<int32>(std::ceil(Boundary.Z+Boundary.HalfWidth)));
|
||||
std::vector<FNode> Nodes(static_cast<size_t>(GridCells+1)*(GridCells+1));
|
||||
for(int32 YI=0;YI<=GridCells;++YI)for(int32 XI=0;XI<=GridCells;++XI)
|
||||
{
|
||||
const double X=Origin.X+XI*FineStep,Y=Origin.Y+YI*FineStep;
|
||||
int32 PreviousZ=ZMin;bool PreviousSolid=Density(X,Y,PreviousZ)<0.0f;
|
||||
FNode& Node=Nodes[static_cast<size_t>(YI)*(GridCells+1)+XI];
|
||||
for(int32 Z=ZMin+1;Z<=ZMax;++Z)
|
||||
{
|
||||
const bool Solid=Density(X,Y,Z)<0.0f;
|
||||
if(Solid==PreviousSolid){PreviousZ=Z;PreviousSolid=Solid;continue;}
|
||||
double Lo=PreviousZ,Hi=Z;
|
||||
for(int32 I=0;I<8;++I)
|
||||
{
|
||||
const double Mid=(Lo+Hi)*0.5;
|
||||
if((Density(X,Y,Mid)<0.0f)==PreviousSolid)Lo=Mid;else Hi=Mid;
|
||||
}
|
||||
Node.Z=(Lo+Hi)*0.5;Node.bBelowSolid=PreviousSolid;Node.bValid=true;
|
||||
const float GX=Density(X+2,Y,Node.Z)-Density(X-2,Y,Node.Z);
|
||||
const float GY=Density(X,Y+2,Node.Z)-Density(X,Y-2,Node.Z);
|
||||
const float GZ=Density(X,Y,Node.Z+2)-Density(X,Y,Node.Z-2);
|
||||
Node.Normal=FVector(GX,GY,GZ);
|
||||
if(Node.Normal.SizeSquared()<1.0e-8)Node.Normal=FVector(0,0,PreviousSolid?1:-1);
|
||||
Node.Normal.Normalize();break;
|
||||
}
|
||||
}
|
||||
const auto EnsureVertex=[&](FNode& Node,int32 XI,int32 YI)
|
||||
{
|
||||
if(Node.Index!=INDEX_NONE)return Node.Index;
|
||||
Node.Index=static_cast<int32>(NewVertices.size());
|
||||
NewVertices.emplace_back((Origin.X+XI*FineStep)*25.0,(Origin.Y+YI*FineStep)*25.0,Node.Z*25.0);
|
||||
NewNormals.push_back(Node.Normal);
|
||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(XI)*FineStep/256.0f,
|
||||
static_cast<float>(YI)*FineStep/256.0f);
|
||||
if(bHasColors)NewColors.push_back(Color);
|
||||
return Node.Index;
|
||||
};
|
||||
for(int32 YI=0;YI<GridCells;++YI)for(int32 XI=0;XI<GridCells;++XI)
|
||||
{
|
||||
FNode* Corners[4]={&Nodes[static_cast<size_t>(YI)*(GridCells+1)+XI],
|
||||
&Nodes[static_cast<size_t>(YI)*(GridCells+1)+XI+1],
|
||||
&Nodes[static_cast<size_t>(YI+1)*(GridCells+1)+XI+1],
|
||||
&Nodes[static_cast<size_t>(YI+1)*(GridCells+1)+XI]};
|
||||
int32 Valid[4],Count=0;
|
||||
for(int32 I=0;I<4;++I)if(Corners[I]->bValid)Valid[Count++]=I;
|
||||
if(Count<3)continue;
|
||||
int32 Indices[4];
|
||||
const int32 CornerXI[4]={XI,XI+1,XI+1,XI};
|
||||
const int32 CornerYI[4]={YI,YI,YI+1,YI+1};
|
||||
for(int32 I=0;I<Count;++I)
|
||||
{
|
||||
const int32 C=Valid[I];Indices[I]=EnsureVertex(*Corners[C],CornerXI[C],CornerYI[C]);
|
||||
}
|
||||
const bool bUp=Corners[Valid[0]]->bBelowSolid;
|
||||
if(Count==4)
|
||||
{
|
||||
if(bUp)NewTriangles.insert(NewTriangles.end(),{Indices[0],Indices[1],Indices[3],Indices[1],Indices[2],Indices[3]});
|
||||
else NewTriangles.insert(NewTriangles.end(),{Indices[0],Indices[3],Indices[1],Indices[1],Indices[3],Indices[2]});
|
||||
}
|
||||
else if(bUp)NewTriangles.insert(NewTriangles.end(),{Indices[0],Indices[1],Indices[2]});
|
||||
else NewTriangles.insert(NewTriangles.end(),{Indices[0],Indices[2],Indices[1]});
|
||||
}
|
||||
}
|
||||
if(NewVertices.empty()||NewTriangles.empty())return;
|
||||
const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount);
|
||||
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]);
|
||||
Mesh.Triangles=std::move(CombinedTriangles);Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||
}
|
||||
|
||||
struct FStandaloneMeshCacheMetadata
|
||||
{
|
||||
uint64 SourceDataHash=0;
|
||||
@@ -556,6 +828,12 @@ private:
|
||||
const int32 Extent=Job.Key.ExtentVoxels();
|
||||
const int32 Cells=Level<World->FullResClipLevels?CHUNK_SIZE:FMath::Max(1,World->CoarseTileCells);
|
||||
const int32 Step=FMath::Max(1,Extent/Cells);
|
||||
std::unordered_set<uint64> ForcedFarAirSamples;
|
||||
const FStandaloneShapeWorld* Shape=FStandaloneShapeWorldAccess::GetActive();
|
||||
if(Level>=6&&Shape&&Shape->HasFarWorld())
|
||||
Shape->BuildFarConnectorAirSamples(Job.Key.OriginVoxels(),Step,Cells,ForcedFarAirSamples);
|
||||
FStandaloneShapeWorld::FScopedForcedFarAirSamples ForcedSamplesScope(
|
||||
ForcedFarAirSamples.empty()?nullptr:&ForcedFarAirSamples);
|
||||
int64 SampleCount=0;
|
||||
const bool bProfile=VoxelDensityProfile::GetMode()!=VoxelDensityProfile::EMode::Disabled;
|
||||
const VoxelDensityProfile::FSnapshot Before=bProfile
|
||||
@@ -564,6 +842,11 @@ private:
|
||||
FVoxelMeshData Mesh=Mesher.GenerateMesh(Job.Key.OriginVoxels(),Step,Cells,nullptr,
|
||||
Job.BandZMin,Job.BandZMax,&SampleCount,nullptr,nullptr,
|
||||
bHashDensity?&DensityValues:nullptr);
|
||||
if(Shape&&Shape->HasFarWorld())
|
||||
{
|
||||
AppendFarConnectorShells(*Shape,Job.Key,Step,Mesh);
|
||||
AppendFarStratumShells(*World,*Shape,Job.Key,Step,Mesh);
|
||||
}
|
||||
if(bHashDensity)
|
||||
{
|
||||
Out.bHasDensityHash=true;
|
||||
|
||||
Reference in new issue
Block a user