- 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
942 lines
44 KiB
C++
942 lines
44 KiB
C++
#pragma once
|
|
|
|
#include "WorldMesher.h"
|
|
#include "StandaloneShapeWorld.h"
|
|
|
|
#include <condition_variable>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <bit>
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <unordered_set>
|
|
|
|
struct FStandaloneTileKeyHash
|
|
{
|
|
std::size_t operator()(const FVoxelTileKey& Key) const noexcept
|
|
{ return static_cast<std::size_t>(GetTypeHash(Key)); }
|
|
};
|
|
|
|
struct FStandaloneDesiredTileSet
|
|
{
|
|
std::vector<FVoxelTileKey> Tiles;
|
|
FIntVector CenterChunk=FIntVector::ZeroValue;
|
|
int32 MinimumChunkZ=MIN_int32;
|
|
int32 MaximumChunkZ=MAX_int32;
|
|
int32 StrateTopChunkZ=0;
|
|
int32 StrateBottomChunkZ=0;
|
|
bool bHasStrate=false;
|
|
};
|
|
|
|
struct FStandaloneCpuTile
|
|
{
|
|
FVoxelTileKey Key;
|
|
uint64 RequestedCrossing=0;
|
|
std::vector<FStandaloneCpuVertex> Vertices;
|
|
std::vector<uint32> Indices;
|
|
uint64 DensitySamples=0;
|
|
double WorkerSeconds=0.0;
|
|
std::chrono::steady_clock::time_point CompletedAt{};
|
|
uint64 DensityHash=14695981039346656037ull;
|
|
bool bHasDensityHash=false;
|
|
uint64 MeshHash=14695981039346656037ull;
|
|
bool bHasMeshHash=false;
|
|
uint64 PayloadHash=14695981039346656037ull;
|
|
bool bHasPayloadHash=false;
|
|
uint64 TopologyHash=14695981039346656037ull;
|
|
bool bHasTopologyHash=false;
|
|
uint64 MeshPayloadAllocatedBytes=0;
|
|
std::array<uint64,VoxelDensityProfile::BucketCount> ProfileCycles{};
|
|
};
|
|
|
|
inline uint64 HashStandaloneMeshPayload(const FVoxelMeshData& Mesh)
|
|
{
|
|
uint64 Hash=14695981039346656037ull;
|
|
const auto AddWord=[&](uint64 Word,int32 Bytes)
|
|
{
|
|
for(int32 I=0;I<Bytes;++I)
|
|
{
|
|
Hash^=static_cast<uint8>(Word>>(I*8));
|
|
Hash*=1099511628211ull;
|
|
}
|
|
};
|
|
const auto AddDouble=[&](double Value)
|
|
{ AddWord(std::bit_cast<uint64>(Value),8); };
|
|
AddWord(static_cast<uint64>(Mesh.Vertices.Num()),8);
|
|
for(const FVector& Position:Mesh.Vertices)
|
|
{ AddDouble(Position.X);AddDouble(Position.Y);AddDouble(Position.Z); }
|
|
AddWord(static_cast<uint64>(Mesh.Normals.Num()),8);
|
|
for(const FVector& Normal:Mesh.Normals)
|
|
{ AddDouble(Normal.X);AddDouble(Normal.Y);AddDouble(Normal.Z); }
|
|
AddWord(static_cast<uint64>(Mesh.UVs.Num()),8);
|
|
for(const auto& UV:Mesh.UVs)
|
|
{ AddDouble(static_cast<double>(UV.X));AddDouble(static_cast<double>(UV.Y)); }
|
|
AddWord(static_cast<uint64>(Mesh.Colors.Num()),8);
|
|
for(const FColor& Color:Mesh.Colors)
|
|
{ AddWord(Color.R,1);AddWord(Color.G,1);AddWord(Color.B,1);AddWord(Color.A,1); }
|
|
AddWord(static_cast<uint64>(Mesh.Triangles.Num()),8);
|
|
for(const int32 Index:Mesh.Triangles)AddWord(static_cast<uint32>(Index),4);
|
|
AddWord(static_cast<uint32>(Mesh.NumSurfaceVertices),4);
|
|
AddWord(static_cast<uint32>(Mesh.NumSkirtTriangles),4);
|
|
AddWord(static_cast<uint32>(Mesh.NumCeilingTriangles),4);
|
|
return Hash;
|
|
}
|
|
|
|
inline uint64 HashStandaloneMeshTopology(const FVoxelMeshData& Mesh)
|
|
{
|
|
uint64 Hash=14695981039346656037ull;
|
|
const auto AddWord=[&](uint64 Word,int32 Bytes)
|
|
{
|
|
for(int32 I=0;I<Bytes;++I)
|
|
{
|
|
Hash^=static_cast<uint8>(Word>>(I*8));
|
|
Hash*=1099511628211ull;
|
|
}
|
|
};
|
|
using FTriangleKey=std::array<uint64,3>;
|
|
const auto VertexKey=[&](int32 Index)
|
|
{
|
|
uint64 VertexHash=14695981039346656037ull;
|
|
const auto AddVertexWord=[&](uint64 Word)
|
|
{
|
|
for(int32 Byte=0;Byte<8;++Byte)
|
|
{
|
|
VertexHash^=static_cast<uint8>(Word>>(Byte*8));
|
|
VertexHash*=1099511628211ull;
|
|
}
|
|
};
|
|
if(Index<0||Index>=Mesh.Vertices.Num()||Index>=Mesh.Normals.Num())
|
|
{ AddVertexWord(UINT64_MAX);return VertexHash; }
|
|
const FVector& Position=Mesh.Vertices[Index];
|
|
const FVector& Normal=Mesh.Normals[Index];
|
|
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Position.X)));
|
|
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Position.Y)));
|
|
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Position.Z)));
|
|
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Normal.X)));
|
|
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Normal.Y)));
|
|
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Normal.Z)));
|
|
return VertexHash;
|
|
};
|
|
std::vector<FTriangleKey> Triangles;
|
|
Triangles.reserve(static_cast<size_t>(Mesh.Triangles.Num()/3));
|
|
for(int32 I=0;I+2<Mesh.Triangles.Num();I+=3)
|
|
Triangles.push_back({VertexKey(Mesh.Triangles[I]),
|
|
VertexKey(Mesh.Triangles[I+1]),VertexKey(Mesh.Triangles[I+2])});
|
|
std::sort(Triangles.begin(),Triangles.end());
|
|
AddWord(static_cast<uint64>(Triangles.size()),8);
|
|
for(const FTriangleKey& Triangle:Triangles)
|
|
for(const uint64 VertexHash:Triangle)AddWord(VertexHash,8);
|
|
return Hash;
|
|
}
|
|
|
|
inline uint64 HashStandaloneCpuTileMesh(const FStandaloneCpuTile& Tile)
|
|
{
|
|
uint64 Hash=14695981039346656037ull;
|
|
const auto AddWord=[&](uint64 Word,int32 Bytes)
|
|
{
|
|
for(int32 I=0;I<Bytes;++I)
|
|
{
|
|
Hash^=static_cast<uint8>(Word>>(I*8));
|
|
Hash*=1099511628211ull;
|
|
}
|
|
};
|
|
AddWord(Tile.Vertices.size(),8);
|
|
for(const FStandaloneCpuVertex& Vertex:Tile.Vertices)
|
|
{
|
|
for(const float Value:Vertex.Position)AddWord(std::bit_cast<uint32>(Value),4);
|
|
for(const float Value:Vertex.Normal)AddWord(std::bit_cast<uint32>(Value),4);
|
|
}
|
|
AddWord(Tile.Indices.size(),8);
|
|
for(const uint32 Index:Tile.Indices)AddWord(Index,4);
|
|
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;
|
|
int32 Seed=0;
|
|
int32 ClipRadius=3;
|
|
int32 FullResClipLevels=2;
|
|
int32 CoarseTileCells=16;
|
|
int32 StrateContentCutMinLevel=9;
|
|
int32 SkirtCells=0;
|
|
int32 LODOctaveDrop=0;
|
|
int32 bGenerateSkirts=0;
|
|
uint32 AlgorithmVersion=1;
|
|
uint32 TunnelMode=0;
|
|
uint32 VoxelSizeCm=25;
|
|
};
|
|
|
|
class FStandaloneMeshCache
|
|
{
|
|
public:
|
|
static constexpr uint32 CurrentVersion=1;
|
|
|
|
void Clear(){Tiles.clear();bLoaded=false;BytesLoaded=0;}
|
|
bool IsLoaded()const{return bLoaded;}
|
|
size_t Num()const{return Tiles.size();}
|
|
uint64 GetBytesLoaded()const{return BytesLoaded;}
|
|
const FStandaloneCpuTile* Find(const FVoxelTileKey& Key)const
|
|
{
|
|
const auto It=Tiles.find(Key);
|
|
return It==Tiles.end()?nullptr:&It->second;
|
|
}
|
|
|
|
static bool Save(const std::string& Path,const FStandaloneMeshCacheMetadata& Metadata,
|
|
std::vector<FStandaloneCpuTile>& InTiles,std::string& Error)
|
|
{
|
|
std::vector<FStandaloneCpuTile*> Ordered;
|
|
Ordered.reserve(InTiles.size());
|
|
for(FStandaloneCpuTile& Tile:InTiles)Ordered.push_back(&Tile);
|
|
std::sort(Ordered.begin(),Ordered.end(),[](const FStandaloneCpuTile* A,const FStandaloneCpuTile* B)
|
|
{
|
|
if(A->Key.Level!=B->Key.Level)return A->Key.Level<B->Key.Level;
|
|
if(A->Key.Coord.Z!=B->Key.Coord.Z)return A->Key.Coord.Z<B->Key.Coord.Z;
|
|
if(A->Key.Coord.Y!=B->Key.Coord.Y)return A->Key.Coord.Y<B->Key.Coord.Y;
|
|
return A->Key.Coord.X<B->Key.Coord.X;
|
|
});
|
|
std::ofstream File(Path,std::ios::binary|std::ios::trunc);
|
|
if(!File){Error="could not open mesh cache for writing: "+Path;return false;}
|
|
struct FHeader
|
|
{
|
|
char Magic[8];uint32 Version;uint32 Endian;uint64 SourceDataHash;
|
|
int32 Seed,ClipRadius,FullResClipLevels,CoarseTileCells,StrateContentCutMinLevel;
|
|
int32 SkirtCells,LODOctaveDrop,bGenerateSkirts;
|
|
uint32 AlgorithmVersion,TunnelMode,VoxelSizeCm;
|
|
uint64 TileCount;
|
|
};
|
|
static_assert(sizeof(FHeader)==80);
|
|
const FHeader Header{{'V','F','M','E','S','H','1','\0'},1,0x01020304u,
|
|
Metadata.SourceDataHash,Metadata.Seed,Metadata.ClipRadius,Metadata.FullResClipLevels,
|
|
Metadata.CoarseTileCells,Metadata.StrateContentCutMinLevel,Metadata.SkirtCells,
|
|
Metadata.LODOctaveDrop,Metadata.bGenerateSkirts,Metadata.AlgorithmVersion,
|
|
Metadata.TunnelMode,Metadata.VoxelSizeCm,static_cast<uint64>(Ordered.size())};
|
|
File.write(reinterpret_cast<const char*>(&Header),sizeof(Header));
|
|
struct FRecord
|
|
{
|
|
int32 Level,X,Y,Z;uint64 Samples,DensityHash;uint32 VertexCount,IndexCount;
|
|
};
|
|
static_assert(sizeof(FRecord)==40);
|
|
for(const FStandaloneCpuTile* Tile:Ordered)
|
|
{
|
|
if(Tile->Vertices.size()>UINT32_MAX||Tile->Indices.size()>UINT32_MAX
|
|
||Tile->Indices.size()%3!=0)
|
|
{Error="mesh cache tile exceeds the 32-bit record limits";return false;}
|
|
const FRecord Record{Tile->Key.Level,Tile->Key.Coord.X,Tile->Key.Coord.Y,
|
|
Tile->Key.Coord.Z,Tile->DensitySamples,Tile->DensityHash,
|
|
static_cast<uint32>(Tile->Vertices.size()),static_cast<uint32>(Tile->Indices.size())};
|
|
File.write(reinterpret_cast<const char*>(&Record),sizeof(Record));
|
|
if(!Tile->Vertices.empty())File.write(reinterpret_cast<const char*>(Tile->Vertices.data()),
|
|
static_cast<std::streamsize>(Tile->Vertices.size()*sizeof(FStandaloneCpuVertex)));
|
|
if(!Tile->Indices.empty())File.write(reinterpret_cast<const char*>(Tile->Indices.data()),
|
|
static_cast<std::streamsize>(Tile->Indices.size()*sizeof(uint32)));
|
|
if(!File){Error="mesh cache write failed: "+Path;return false;}
|
|
}
|
|
File.flush();
|
|
if(!File){Error="mesh cache flush failed: "+Path;return false;}
|
|
return true;
|
|
}
|
|
|
|
bool Load(const std::string& Path,const FStandaloneMeshCacheMetadata& Expected,std::string& Error)
|
|
{
|
|
Clear();
|
|
std::ifstream File(Path,std::ios::binary);
|
|
if(!File){Error="cache file is missing or unreadable";return false;}
|
|
struct FHeader
|
|
{
|
|
char Magic[8];uint32 Version;uint32 Endian;uint64 SourceDataHash;
|
|
int32 Seed,ClipRadius,FullResClipLevels,CoarseTileCells,StrateContentCutMinLevel;
|
|
int32 SkirtCells,LODOctaveDrop,bGenerateSkirts;
|
|
uint32 AlgorithmVersion,TunnelMode,VoxelSizeCm;
|
|
uint64 TileCount;
|
|
};
|
|
static_assert(sizeof(FHeader)==80);
|
|
FHeader H{};File.read(reinterpret_cast<char*>(&H),sizeof(H));
|
|
const char ExpectedMagic[8]={'V','F','M','E','S','H','1','\0'};
|
|
if(!File||std::memcmp(H.Magic,ExpectedMagic,sizeof(ExpectedMagic))!=0
|
|
||H.Version!=1||H.Endian!=0x01020304u||H.TileCount>1000000)
|
|
{Error="mesh cache header is invalid";return false;}
|
|
if(H.SourceDataHash!=Expected.SourceDataHash||H.Seed!=Expected.Seed
|
|
||H.ClipRadius!=Expected.ClipRadius||H.FullResClipLevels!=Expected.FullResClipLevels
|
|
||H.CoarseTileCells!=Expected.CoarseTileCells
|
|
||H.StrateContentCutMinLevel!=Expected.StrateContentCutMinLevel
|
|
||H.SkirtCells!=Expected.SkirtCells||H.LODOctaveDrop!=Expected.LODOctaveDrop
|
|
||H.bGenerateSkirts!=Expected.bGenerateSkirts
|
|
||H.AlgorithmVersion!=Expected.AlgorithmVersion||H.TunnelMode!=Expected.TunnelMode
|
|
||H.VoxelSizeCm!=Expected.VoxelSizeCm)
|
|
{Error="mesh cache metadata does not match this world configuration";return false;}
|
|
struct FRecord
|
|
{
|
|
int32 Level,X,Y,Z;uint64 Samples,DensityHash;uint32 VertexCount,IndexCount;
|
|
};
|
|
static_assert(sizeof(FRecord)==40);
|
|
std::unordered_map<FVoxelTileKey,FStandaloneCpuTile,FStandaloneTileKeyHash> Loaded;
|
|
Loaded.reserve(static_cast<size_t>(H.TileCount)*2+1);
|
|
for(uint64 I=0;I<H.TileCount;++I)
|
|
{
|
|
FRecord Record{};File.read(reinterpret_cast<char*>(&Record),sizeof(Record));
|
|
if(!File||Record.Level<0||Record.Level>8||Record.VertexCount>10000000
|
|
||Record.IndexCount>60000000)
|
|
{Error="mesh cache contains an invalid tile record";return false;}
|
|
FStandaloneCpuTile Tile;Tile.Key=FVoxelTileKey(FIntVector(Record.X,Record.Y,Record.Z),Record.Level);
|
|
Tile.DensitySamples=Record.Samples;Tile.DensityHash=Record.DensityHash;Tile.bHasDensityHash=true;
|
|
Tile.Vertices.resize(Record.VertexCount);Tile.Indices.resize(Record.IndexCount);
|
|
if(!Tile.Vertices.empty())File.read(reinterpret_cast<char*>(Tile.Vertices.data()),
|
|
static_cast<std::streamsize>(Tile.Vertices.size()*sizeof(FStandaloneCpuVertex)));
|
|
if(!Tile.Indices.empty())File.read(reinterpret_cast<char*>(Tile.Indices.data()),
|
|
static_cast<std::streamsize>(Tile.Indices.size()*sizeof(uint32)));
|
|
if(!File){Error="mesh cache ended inside tile geometry";return false;}
|
|
for(const uint32 Index:Tile.Indices)
|
|
if(Index>=Tile.Vertices.size())
|
|
{Error="mesh cache contains an out-of-range mesh index";return false;}
|
|
for(const FStandaloneCpuVertex& Vertex:Tile.Vertices)
|
|
for(const float Value:Vertex.Position)
|
|
if(!std::isfinite(Value))
|
|
{Error="mesh cache contains a non-finite vertex";return false;}
|
|
for(const FStandaloneCpuVertex& Vertex:Tile.Vertices)
|
|
for(const float Value:Vertex.Normal)
|
|
if(!std::isfinite(Value))
|
|
{Error="mesh cache contains a non-finite normal";return false;}
|
|
if(!Loaded.emplace(Tile.Key,std::move(Tile)).second)
|
|
{Error="mesh cache contains duplicate tile keys";return false;}
|
|
}
|
|
const std::streamoff End=File.tellg();
|
|
if(End<0){Error="could not determine mesh cache size";return false;}
|
|
BytesLoaded=static_cast<uint64>(End);Tiles.swap(Loaded);bLoaded=true;
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
std::unordered_map<FVoxelTileKey,FStandaloneCpuTile,FStandaloneTileKeyHash> Tiles;
|
|
bool bLoaded=false;
|
|
uint64 BytesLoaded=0;
|
|
};
|
|
|
|
struct FStandaloneStreamingUpdate
|
|
{
|
|
FIntVector CenterChunk=FIntVector::ZeroValue;
|
|
int32 DesiredTiles=0;
|
|
int32 RetainedResidentTiles=0;
|
|
int32 NewJobs=0;
|
|
int32 CancelledQueuedJobs=0;
|
|
int32 FullResTiles=0;
|
|
std::vector<std::pair<uint64,uint64>> CancelledByCrossing;
|
|
};
|
|
|
|
struct FStandaloneTileJob
|
|
{
|
|
FVoxelTileKey Key;
|
|
int32 BandZMin=MIN_int32;
|
|
int32 BandZMax=MAX_int32;
|
|
uint64 RequestedCrossing=0;
|
|
double PriorityDistanceSquared=0.0;
|
|
};
|
|
|
|
struct FStandaloneTileJobHeapOrder
|
|
{
|
|
bool operator()(const FStandaloneTileJob& A,const FStandaloneTileJob& B)const
|
|
{
|
|
if(A.PriorityDistanceSquared!=B.PriorityDistanceSquared)
|
|
return A.PriorityDistanceSquared>B.PriorityDistanceSquared;
|
|
if(A.Key.Level!=B.Key.Level)return A.Key.Level>B.Key.Level;
|
|
if(A.Key.Coord.Z!=B.Key.Coord.Z)return A.Key.Coord.Z>B.Key.Coord.Z;
|
|
if(A.Key.Coord.Y!=B.Key.Coord.Y)return A.Key.Coord.Y>B.Key.Coord.Y;
|
|
return A.Key.Coord.X>B.Key.Coord.X;
|
|
}
|
|
};
|
|
|
|
namespace StandaloneWorldMesher
|
|
{
|
|
inline double TileDistanceSquaredChunks(const FVoxelTileKey& Tile,const FIntVector& Center)
|
|
{
|
|
const double Scale=static_cast<double>(1<<Tile.Level);
|
|
const double DX=(static_cast<double>(Tile.Coord.X)+0.5)*Scale-Center.X;
|
|
const double DY=(static_cast<double>(Tile.Coord.Y)+0.5)*Scale-Center.Y;
|
|
const double DZ=(static_cast<double>(Tile.Coord.Z)+0.5)*Scale-Center.Z;
|
|
return DX*DX+DY*DY+DZ*DZ;
|
|
}
|
|
|
|
inline FStandaloneDesiredTileSet BuildDesiredSet(
|
|
const FStandaloneOwnerWorld& World,const FVector& CameraPositionVoxels)
|
|
{
|
|
FStandaloneDesiredTileSet Desired;
|
|
Desired.CenterChunk=CameraChunk(CameraPositionVoxels);
|
|
if(World.Settings->bClampViewToStrate)
|
|
Desired.bHasStrate=World.Manager->GetStrateChunkZBounds(
|
|
Desired.CenterChunk.Z,Desired.StrateTopChunkZ,Desired.StrateBottomChunkZ);
|
|
VoxelClipmapDesiredTiles::ResolveVerticalBounds(
|
|
Desired.CenterChunk.Z,World.Settings->ViewDistanceUp,World.Settings->ViewDistanceDown,
|
|
World.Settings->StrateViewMarginChunks,World.Settings->bClampViewToStrate,
|
|
Desired.bHasStrate,Desired.StrateTopChunkZ,Desired.StrateBottomChunkZ,
|
|
Desired.MinimumChunkZ,Desired.MaximumChunkZ);
|
|
VoxelClipmapDesiredTiles::FParameters Parameters;
|
|
Parameters.ClipRadius=World.ClipRadius;
|
|
Parameters.MaxClipLevel=FMath::Clamp(World.MaxClipLevel,0,8);
|
|
Parameters.RenderDistanceChunks=World.RenderDistanceChunks;
|
|
TArray<FVoxelTileKey> Tiles;
|
|
VoxelClipmapDesiredTiles::Build(Desired.CenterChunk,Desired.MinimumChunkZ,
|
|
Desired.MaximumChunkZ,Parameters,Tiles);
|
|
Desired.Tiles.reserve(static_cast<size_t>(Tiles.Num()));
|
|
for(const FVoxelTileKey& Tile:Tiles)Desired.Tiles.push_back(Tile);
|
|
std::sort(Desired.Tiles.begin(),Desired.Tiles.end(),[&](const FVoxelTileKey& A,const FVoxelTileKey& B)
|
|
{
|
|
const double DA=TileDistanceSquaredChunks(A,Desired.CenterChunk);
|
|
const double DB=TileDistanceSquaredChunks(B,Desired.CenterChunk);
|
|
return DA==DB?A.Level<B.Level:DA<DB;
|
|
});
|
|
return Desired;
|
|
}
|
|
}
|
|
|
|
class FStandaloneStreamingGenerator
|
|
{
|
|
public:
|
|
explicit FStandaloneStreamingGenerator(FStandaloneOwnerWorld& InWorld,int32 InWorkerCount=8,
|
|
bool bInHashDensity=false,
|
|
const FStandaloneMeshCache* InMeshCache=nullptr,
|
|
int32 InMaxLevelFilter=-1,
|
|
bool bInSkipUvAndColors=false,
|
|
bool bInSkipMaterialGroups=false,
|
|
bool bInSkipUV=false,
|
|
bool bInSkipColors=false)
|
|
:World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,16)),bHashDensity(bInHashDensity),
|
|
MeshCache(InMeshCache),MaxLevelFilter(InMaxLevelFilter),
|
|
bSkipUvAndColors(bInSkipUvAndColors),bSkipMaterialGroups(bInSkipMaterialGroups),
|
|
bSkipUV(bInSkipUV),bSkipColors(bInSkipColors)
|
|
{
|
|
VoxelGenLOD::InitializeConsoleSwitches();
|
|
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
|
|
}
|
|
FStandaloneStreamingGenerator(const FStandaloneStreamingGenerator&)=delete;
|
|
FStandaloneStreamingGenerator& operator=(const FStandaloneStreamingGenerator&)=delete;
|
|
~FStandaloneStreamingGenerator(){Stop();}
|
|
|
|
FStandaloneStreamingUpdate Start(const FVector& CameraPositionVoxels,
|
|
const std::vector<FVoxelTileKey>& ResidentKeys={},
|
|
uint64 CrossingIndex=0)
|
|
{
|
|
Stop();
|
|
{
|
|
std::lock_guard<std::mutex> Lock(Mutex);
|
|
bStop=false;
|
|
}
|
|
for(int32 I=0;I<WorkerCount;++I)Workers.emplace_back([this](){WorkerLoop();});
|
|
return UpdateDesired(CameraPositionVoxels,ResidentKeys,CrossingIndex);
|
|
}
|
|
|
|
void SetDesiredTileFilter(const std::vector<FVoxelTileKey>& Keys)
|
|
{
|
|
TileFilter.clear();TileFilter.reserve(Keys.size()*2+1);
|
|
for(const FVoxelTileKey& Key:Keys)TileFilter.insert(Key);
|
|
bUseTileFilter=true;
|
|
}
|
|
|
|
void Stop()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> Lock(Mutex);
|
|
bStop=true;
|
|
Queue.clear();
|
|
}
|
|
Wake.notify_all();
|
|
for(std::thread& Worker:Workers)if(Worker.joinable())Worker.join();
|
|
Workers.clear();
|
|
std::lock_guard<std::mutex> Lock(Mutex);
|
|
Ready.clear();Outstanding.clear();Desired.clear();
|
|
}
|
|
|
|
FStandaloneStreamingUpdate UpdateDesired(
|
|
const FVector& CameraPositionVoxels,const std::vector<FVoxelTileKey>& ResidentKeys,
|
|
uint64 CrossingIndex=0)
|
|
{
|
|
FStandaloneDesiredTileSet NewDesired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraPositionVoxels);
|
|
if(MaxLevelFilter>=0)
|
|
NewDesired.Tiles.erase(std::remove_if(NewDesired.Tiles.begin(),NewDesired.Tiles.end(),
|
|
[&](const FVoxelTileKey& Key){return Key.Level>MaxLevelFilter;}),NewDesired.Tiles.end());
|
|
if(bUseTileFilter)
|
|
NewDesired.Tiles.erase(std::remove_if(NewDesired.Tiles.begin(),NewDesired.Tiles.end(),
|
|
[&](const FVoxelTileKey& Key){return !TileFilter.contains(Key);}),NewDesired.Tiles.end());
|
|
FStandaloneStreamingUpdate Stats;
|
|
Stats.CenterChunk=NewDesired.CenterChunk;
|
|
Stats.DesiredTiles=static_cast<int32>(NewDesired.Tiles.size());
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Resident;
|
|
Resident.reserve(ResidentKeys.size()*2+1);
|
|
for(const FVoxelTileKey& Key:ResidentKeys)Resident.insert(Key);
|
|
for(const FVoxelTileKey& Key:NewDesired.Tiles)
|
|
{
|
|
if(Key.Level<World->FullResClipLevels)++Stats.FullResTiles;
|
|
if(Resident.contains(Key))++Stats.RetainedResidentTiles;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> Lock(Mutex);
|
|
Desired.clear();Desired.reserve(NewDesired.Tiles.size()*2+1);
|
|
for(const FVoxelTileKey& Key:NewDesired.Tiles)Desired.insert(Key);
|
|
FullResTileCount=Stats.FullResTiles;
|
|
std::unordered_map<uint64,uint64> CancelledByCrossing;
|
|
auto QueueEnd=std::remove_if(Queue.begin(),Queue.end(),[&](const FStandaloneTileJob& Job)
|
|
{
|
|
if(Desired.contains(Job.Key))return false;
|
|
Outstanding.erase(Job.Key);++Stats.CancelledQueuedJobs;
|
|
++CancelledByCrossing[Job.RequestedCrossing];return true;
|
|
});
|
|
Queue.erase(QueueEnd,Queue.end());
|
|
Stats.CancelledByCrossing.reserve(CancelledByCrossing.size());
|
|
for(const auto& Pair:CancelledByCrossing)Stats.CancelledByCrossing.push_back(Pair);
|
|
|
|
for(FStandaloneTileJob& Job:Queue)
|
|
Job.PriorityDistanceSquared=StandaloneWorldMesher::TileDistanceSquaredChunks(
|
|
Job.Key,NewDesired.CenterChunk);
|
|
|
|
const int32 BandMin=World->StrateContentCutMinLevel<9&&NewDesired.bHasStrate
|
|
? NewDesired.StrateBottomChunkZ*CHUNK_SIZE:MIN_int32;
|
|
const int32 BandMax=World->StrateContentCutMinLevel<9&&NewDesired.bHasStrate
|
|
? (NewDesired.StrateTopChunkZ+1)*CHUNK_SIZE:MAX_int32;
|
|
for(const FVoxelTileKey& Key:NewDesired.Tiles)
|
|
{
|
|
if(Resident.contains(Key)||Outstanding.contains(Key))continue;
|
|
Queue.push_back({Key,BandMin,BandMax,CrossingIndex,
|
|
StandaloneWorldMesher::TileDistanceSquaredChunks(Key,NewDesired.CenterChunk)});
|
|
Outstanding.insert(Key);++Stats.NewJobs;
|
|
}
|
|
CurrentDesired=std::move(NewDesired);
|
|
std::make_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
|
|
Wake.notify_all();
|
|
return Stats;
|
|
}
|
|
|
|
std::vector<FStandaloneCpuTile> TakeReady()
|
|
{
|
|
std::vector<FStandaloneCpuTile> Out;
|
|
std::lock_guard<std::mutex> Lock(Mutex);
|
|
Out.swap(Ready);
|
|
for(const FStandaloneCpuTile& Tile:Out)Outstanding.erase(Tile.Key);
|
|
return Out;
|
|
}
|
|
|
|
bool IsDesired(const FVoxelTileKey& Key)const
|
|
{std::lock_guard<std::mutex> Lock(Mutex);return Desired.contains(Key);}
|
|
int32 DesiredCount()const
|
|
{std::lock_guard<std::mutex> Lock(Mutex);return static_cast<int32>(Desired.size());}
|
|
int32 FullResDesiredCount()const
|
|
{std::lock_guard<std::mutex> Lock(Mutex);return FullResTileCount;}
|
|
FIntVector CenterChunk()const
|
|
{std::lock_guard<std::mutex> Lock(Mutex);return CurrentDesired.CenterChunk;}
|
|
int32 GetWorkerCount()const{return WorkerCount;}
|
|
uint64 GetCacheHits()const{return CacheHitCount.load(std::memory_order_relaxed);}
|
|
|
|
private:
|
|
FStandaloneCpuTile Generate(const FStandaloneTileJob& Job)
|
|
{
|
|
const auto StartTime=FClock::now();
|
|
FStandaloneCpuTile Out;Out.Key=Job.Key;Out.RequestedCrossing=Job.RequestedCrossing;
|
|
if(MeshCache)
|
|
{
|
|
if(const FStandaloneCpuTile* Cached=MeshCache->Find(Job.Key))
|
|
{
|
|
Out=*Cached;Out.RequestedCrossing=Job.RequestedCrossing;
|
|
Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count();
|
|
Out.CompletedAt=FClock::now();
|
|
if(bHashDensity)
|
|
{Out.MeshHash=HashStandaloneCpuTileMesh(Out);Out.bHasMeshHash=true;}
|
|
CacheHitCount.fetch_add(1,std::memory_order_relaxed);
|
|
return Out;
|
|
}
|
|
}
|
|
UVoxelMarchingCubesMesher Mesher;
|
|
Mesher.SetGenerator(World->Generator.get());
|
|
Mesher.bStandaloneSkipUvAndColors=bSkipUvAndColors;
|
|
Mesher.bStandaloneSkipUV=bSkipUV;
|
|
Mesher.bStandaloneSkipColors=bSkipColors;
|
|
Mesher.bStandaloneSkipMaterialGroups=bSkipMaterialGroups;
|
|
Mesher.bGenerateSkirts=World->bGenerateSkirts;
|
|
Mesher.SkirtCells=static_cast<float>(World->SkirtCells);
|
|
Mesher.LODOctaveDrop=World->LODOctaveDrop;
|
|
const int32 Level=Job.Key.Level;
|
|
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
|
|
? VoxelDensityProfile::SnapshotCurrentThread():VoxelDensityProfile::FSnapshot{};
|
|
std::vector<float> DensityValues;
|
|
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;
|
|
for(const float Density:DensityValues)
|
|
{
|
|
Out.DensityHash^=std::bit_cast<uint32>(Density);
|
|
Out.DensityHash*=1099511628211ull;
|
|
}
|
|
}
|
|
if(bProfile)
|
|
{
|
|
const VoxelDensityProfile::FSnapshot After=VoxelDensityProfile::SnapshotCurrentThread();
|
|
for(size_t I=0;I<Out.ProfileCycles.size();++I)
|
|
{
|
|
const size_t Bucket=I;
|
|
Out.ProfileCycles[I]=After.Cycles[Bucket]>=Before.Cycles[Bucket]
|
|
?After.Cycles[Bucket]-Before.Cycles[Bucket]:0;
|
|
}
|
|
}
|
|
Out.DensitySamples=static_cast<uint64>(FMath::Max<int64>(0,SampleCount));
|
|
Out.MeshPayloadAllocatedBytes=static_cast<uint64>(Mesh.Vertices.GetAllocatedSize())
|
|
+static_cast<uint64>(Mesh.Normals.GetAllocatedSize())
|
|
+static_cast<uint64>(Mesh.UVs.GetAllocatedSize())
|
|
+static_cast<uint64>(Mesh.Colors.GetAllocatedSize())
|
|
+static_cast<uint64>(Mesh.Triangles.GetAllocatedSize());
|
|
if(bHashDensity)
|
|
{
|
|
Out.PayloadHash=HashStandaloneMeshPayload(Mesh);Out.bHasPayloadHash=true;
|
|
Out.TopologyHash=HashStandaloneMeshTopology(Mesh);Out.bHasTopologyHash=true;
|
|
}
|
|
Out.Vertices.reserve(static_cast<size_t>(Mesh.Vertices.Num()));
|
|
for(int32 I=0;I<Mesh.Vertices.Num();++I)
|
|
{
|
|
const FVector& P=Mesh.Vertices[I];const FVector& N=Mesh.Normals[I];
|
|
Out.Vertices.push_back({
|
|
{static_cast<float>(P.X*0.01),static_cast<float>(P.Y*0.01),static_cast<float>(P.Z*0.01)},
|
|
{static_cast<float>(N.X),static_cast<float>(N.Y),static_cast<float>(N.Z)}});
|
|
}
|
|
Out.Indices.reserve(static_cast<size_t>(Mesh.Triangles.Num()));
|
|
for(const int32 Index:Mesh.Triangles)Out.Indices.push_back(static_cast<uint32>(Index));
|
|
if(bHashDensity)
|
|
{Out.MeshHash=HashStandaloneCpuTileMesh(Out);Out.bHasMeshHash=true;}
|
|
Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count();
|
|
Out.CompletedAt=FClock::now();
|
|
return Out;
|
|
}
|
|
|
|
void WorkerLoop()
|
|
{
|
|
for(;;)
|
|
{
|
|
FStandaloneTileJob Job;
|
|
{
|
|
std::unique_lock<std::mutex> Lock(Mutex);
|
|
Wake.wait(Lock,[&](){return bStop||!Queue.empty();});
|
|
if(bStop)return;
|
|
std::pop_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
|
|
Job=Queue.back();Queue.pop_back();
|
|
}
|
|
FStandaloneCpuTile Tile=Generate(Job);
|
|
{
|
|
std::lock_guard<std::mutex> Lock(Mutex);
|
|
if(bStop)return;
|
|
Ready.push_back(std::move(Tile));
|
|
}
|
|
}
|
|
}
|
|
|
|
using FClock=std::chrono::steady_clock;
|
|
FStandaloneOwnerWorld* World=nullptr;
|
|
int32 WorkerCount=8;
|
|
bool bHashDensity=false;
|
|
const FStandaloneMeshCache* MeshCache=nullptr;
|
|
int32 MaxLevelFilter=-1;
|
|
bool bSkipUvAndColors=false;
|
|
bool bSkipMaterialGroups=false;
|
|
bool bSkipUV=false;
|
|
bool bSkipColors=false;
|
|
std::atomic<uint64> CacheHitCount{0};
|
|
mutable std::mutex Mutex;
|
|
std::condition_variable Wake;
|
|
bool bStop=true;
|
|
std::vector<std::thread> Workers;
|
|
std::vector<FStandaloneTileJob> Queue;
|
|
std::vector<FStandaloneCpuTile> Ready;
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Desired;
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Outstanding;
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> TileFilter;
|
|
bool bUseTileFilter=false;
|
|
FStandaloneDesiredTileSet CurrentDesired;
|
|
int32 FullResTileCount=0;
|
|
};
|