standalone: cheap far-survival repairs -- whole view 1.80 -> 1.40 s, payload 277 -> 111 MiB
- Far repairs are source-aware (only where the sampled mesh lost a shell) and redundant radius probes trimmed; ~175k two-triangle repair panels remain (+18 MiB over the no-shell 92.6 MiB). - Pipelined start builds the shape world once; the 27-tile neighbourhood is ready ~0.73 s from process start; pipelined whole view is no slower than direct. - Far audit passes, bad-shell control still fails (16 missing / 16 holes); look audits pass; routes 5642/5642; 60 W1/W8 + direct/pipeline hash fields equal; six fixed captures pixel-identical. Verified by me head to head (8 workers, cold, BelowNormal 0xFF00, quiet machine, two runs each): 1.40 s vs 1.80 s for the previous default. 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
a40b0454b3
commit
d4fa89204d
4 files changed
+837
-303
No files matched your search
@@ -403,47 +403,23 @@ public:
|
||||
const std::vector<FFarConnectorSegment>& GetFarConnectorSegments() const
|
||||
{ return FarConnectorSegments; }
|
||||
|
||||
size_t BuildFarConnectorAirSamples(const FIntVector& TileOrigin,int32 Step,int32 Cells,
|
||||
std::unordered_set<uint64>& OutSamples) const
|
||||
void FindFarConnectorCandidates(const FIntVector& QueryMin,const FIntVector& QueryMax,
|
||||
std::vector<uint32>& OutIndices) 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)
|
||||
constexpr int32 BinSize=2048;
|
||||
OutIndices.clear();
|
||||
const int32 MinX=FloorDiv(QueryMin.X,BinSize),MaxX=FloorDiv(QueryMax.X,BinSize);
|
||||
const int32 MinY=FloorDiv(QueryMin.Y,BinSize),MaxY=FloorDiv(QueryMax.Y,BinSize);
|
||||
const int32 MinZ=FloorDiv(QueryMin.Z,BinSize),MaxZ=FloorDiv(QueryMax.Z,BinSize);
|
||||
for(int32 Z=MinZ;Z<=MaxZ;++Z)for(int32 Y=MinY;Y<=MaxY;++Y)
|
||||
for(int32 X=MinX;X<=MaxX;++X)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
const auto It=FarConnectorSpatialBins.find({X,Y,Z});
|
||||
if(It!=FarConnectorSpatialBins.end())
|
||||
OutIndices.insert(OutIndices.end(),It->second.begin(),It->second.end());
|
||||
}
|
||||
return OutSamples.size();
|
||||
std::sort(OutIndices.begin(),OutIndices.end());
|
||||
OutIndices.erase(std::unique(OutIndices.begin(),OutIndices.end()),OutIndices.end());
|
||||
}
|
||||
|
||||
class FScopedForcedFarAirSamples
|
||||
@@ -2107,12 +2083,39 @@ private:
|
||||
FarConnectorSegments.push_back({A,B,64.0f});
|
||||
}
|
||||
}
|
||||
BuildFarConnectorSpatialBins();
|
||||
if(!Parents.empty())
|
||||
std::printf("[FarParents] pitch=%d rooms=%zu links=%llu clip=(%d,%d)..(%d,%d)\n",
|
||||
ParentPitch,Parents.size(),static_cast<unsigned long long>(Primitives.size()),
|
||||
ClipMin[0],ClipMin[1],ClipMax[0],ClipMax[1]);
|
||||
}
|
||||
|
||||
void BuildFarConnectorSpatialBins()
|
||||
{
|
||||
constexpr int32 BinSize=2048;
|
||||
FarConnectorSpatialBins.clear();
|
||||
FarConnectorSpatialBins.reserve(FarConnectorSegments.size());
|
||||
for(uint32 Index=0;Index<FarConnectorSegments.size();++Index)
|
||||
{
|
||||
const FFarConnectorSegment& Connector=FarConnectorSegments[Index];
|
||||
const int32 MinX=static_cast<int32>(std::floor(
|
||||
(std::min(Connector.Start.X,Connector.End.X)-Connector.Radius)/BinSize));
|
||||
const int32 MaxX=static_cast<int32>(std::floor(
|
||||
(std::max(Connector.Start.X,Connector.End.X)+Connector.Radius)/BinSize));
|
||||
const int32 MinY=static_cast<int32>(std::floor(
|
||||
(std::min(Connector.Start.Y,Connector.End.Y)-Connector.Radius)/BinSize));
|
||||
const int32 MaxY=static_cast<int32>(std::floor(
|
||||
(std::max(Connector.Start.Y,Connector.End.Y)+Connector.Radius)/BinSize));
|
||||
const int32 MinZ=static_cast<int32>(std::floor(
|
||||
(std::min(Connector.Start.Z,Connector.End.Z)-Connector.Radius)/BinSize));
|
||||
const int32 MaxZ=static_cast<int32>(std::floor(
|
||||
(std::max(Connector.Start.Z,Connector.End.Z)+Connector.Radius)/BinSize));
|
||||
for(int32 Z=MinZ;Z<=MaxZ;++Z)for(int32 Y=MinY;Y<=MaxY;++Y)
|
||||
for(int32 X=MinX;X<=MaxX;++X)
|
||||
FarConnectorSpatialBins[{X,Y,Z}].push_back(Index);
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<uint32>& GetLineCandidates(int32 Axis,int32 A,int32 B) const
|
||||
{
|
||||
static const std::vector<uint32> Empty;
|
||||
@@ -3236,6 +3239,7 @@ private:
|
||||
std::vector<FLayerField> LayerFields;
|
||||
std::vector<FPrimitive> Primitives;
|
||||
std::vector<FFarConnectorSegment> FarConnectorSegments;
|
||||
std::unordered_map<FBinKey,std::vector<uint32>,FBinHash> FarConnectorSpatialBins;
|
||||
std::vector<FEditBrush> EditBrushes;
|
||||
std::array<FAxisBins,3> AxisBins;
|
||||
std::unordered_map<FBinKey,FBinItems,FBinHash> SpatialBins;
|
||||
|
||||
@@ -674,7 +674,9 @@ 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,false,true))return false;
|
||||
// Compile one world field before streaming. The streamer already prioritizes the
|
||||
// player's neighborhood, so a second near-only field would only duplicate work.
|
||||
if(bShapeWorld&&!BuildShapeWorldForCamera(Error,true,false))return false;
|
||||
LoadMeshCache();
|
||||
return true;
|
||||
}
|
||||
@@ -1889,111 +1891,64 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
}
|
||||
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);
|
||||
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Neighborhood;
|
||||
Neighborhood.reserve(32);
|
||||
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>();
|
||||
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)
|
||||
Neighborhood.insert(Tile);
|
||||
if(Neighborhood.size()!=27)
|
||||
{std::fprintf(stderr,"[StreamPipeline] expected 27 player-neighborhood tiles, found %zu.\n",Neighborhood.size());return 7;}
|
||||
|
||||
FStandaloneShapeWorld Shape;
|
||||
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,
|
||||
const bool bIncludeFarWorld=MaxLevel<0||MaxLevel>=World.FullResClipLevels;
|
||||
const auto ShapeBuildStart=Clock::now();
|
||||
if(!Shape.Build(World,FineMin,FineMax,ClipMin,ClipMax,WorkerCount,
|
||||
DetailVariant,bIncludeFarWorld,BuildError))
|
||||
{std::fprintf(stderr,"[StreamPipeline] full shape build failed: %s\n",BuildError.c_str());return 2;}
|
||||
const auto ShapeReady=Clock::now();
|
||||
const double ShapeSeconds=std::chrono::duration<double>(ShapeReady-ShapeBuildStart).count();
|
||||
FStandaloneShapeWorldAccess::SetActive(&Shape);
|
||||
|
||||
FStandaloneStreamingGenerator Streamer(World,WorkerCount,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)
|
||||
const auto StreamStart=Clock::now();
|
||||
const FStandaloneStreamingUpdate Update=Streamer.Start(CameraPositionVoxels);
|
||||
if(Update.DesiredTiles!=static_cast<int32>(Desired.Tiles.size()))
|
||||
{
|
||||
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));
|
||||
}
|
||||
Streamer.Stop();FStandaloneShapeWorldAccess::SetActive(nullptr);
|
||||
std::fprintf(stderr,"[StreamPipeline] expected %zu tiles but queued %d.\n",
|
||||
Desired.Tiles.size(),Update.DesiredTiles);return 7;
|
||||
}
|
||||
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());
|
||||
std::vector<FHashedTile> DensityRows,MeshRows,PayloadRows,TopologyRows;
|
||||
int32 Finished=0,NeighborhoodFinished=0,FullDetailFinished=0;
|
||||
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)
|
||||
Clock::time_point NeighborhoodReady{},FullDetailReady{},WholeViewReady{};
|
||||
bool bNeighborhoodReported=false,bFullDetailReported=false;
|
||||
while(Finished<Update.DesiredTiles)
|
||||
{
|
||||
std::vector<FStandaloneCpuTile> Ready=FullStreamer.TakeReady();
|
||||
std::vector<FStandaloneCpuTile> Ready=Streamer.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);
|
||||
++Finished;
|
||||
WholeViewReady=std::max(WholeViewReady,Tile.CompletedAt);
|
||||
if(Neighborhood.contains(Tile.Key))
|
||||
{
|
||||
++NeighborhoodFinished;
|
||||
NeighborhoodReady=std::max(NeighborhoodReady,Tile.CompletedAt);
|
||||
}
|
||||
if(Tile.Key.Level<World.FullResClipLevels)
|
||||
{++FullDetailFinished;FullDetailReady=std::max(FullDetailReady,Tile.CompletedAt);}
|
||||
{
|
||||
++FullDetailFinished;
|
||||
FullDetailReady=std::max(FullDetailReady,Tile.CompletedAt);
|
||||
}
|
||||
if(bHashDensity)
|
||||
{
|
||||
if(Tile.bHasDensityHash)DensityRows.push_back({Tile.Key,Tile.DensityHash});
|
||||
@@ -2002,7 +1957,13 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
if(Tile.bHasTopologyHash)TopologyRows.push_back({Tile.Key,Tile.TopologyHash});
|
||||
}
|
||||
}
|
||||
if(!bFullDetailReported&&FullDetailRemaining>0&&FullDetailFinished>=FullDetailExpected)
|
||||
if(!bNeighborhoodReported&&NeighborhoodFinished==static_cast<int32>(Neighborhood.size()))
|
||||
{
|
||||
std::printf("[PipelineMilestone] neighborhood_ready=%.3fs generated=%d/27\n",
|
||||
std::chrono::duration<double>(NeighborhoodReady-ColdStart).count(),NeighborhoodFinished);
|
||||
bNeighborhoodReported=true;
|
||||
}
|
||||
if(!bFullDetailReported&&FullDetailFinished==FullDetailExpected)
|
||||
{
|
||||
std::printf("[PipelineMilestone] full_detail_ready=%.3fs generated=%d/%d\n",
|
||||
std::chrono::duration<double>(FullDetailReady-ColdStart).count(),
|
||||
@@ -2010,51 +1971,24 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
bFullDetailReported=true;
|
||||
}
|
||||
}
|
||||
FullStreamer.Stop();
|
||||
const double WholeViewSeconds=std::chrono::duration<double>(WholeViewReady-ColdStart).count();
|
||||
Streamer.Stop();
|
||||
const double NeighborhoodSeconds=std::chrono::duration<double>(NeighborhoodReady-ColdStart).count();
|
||||
const double NeighborhoodMeshSeconds=std::chrono::duration<double>(NeighborhoodReady-ShapeReady).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());
|
||||
const double WholeViewSeconds=std::chrono::duration<double>(WholeViewReady-ColdStart).count();
|
||||
const double StreamSeconds=std::chrono::duration<double>(WholeViewReady-StreamStart).count();
|
||||
std::printf("[StreamPipeline] workers=%d shape=%.3fs neighborhood_mesh=%.3fs neighborhood_ready=%.3fs full_detail_ready=%.3fs whole_view_ready=%.3fs neighborhood_tiles=%d/27 full_detail_tiles=%d/%d whole_tiles=%d/%zu stream_wall=%.3fs cache_hits=0 full_payload=1\n",
|
||||
WorkerCount,ShapeSeconds,NeighborhoodMeshSeconds,NeighborhoodSeconds,
|
||||
FullDetailSeconds,WholeViewSeconds,NeighborhoodFinished,
|
||||
FullDetailFinished,FullDetailExpected,Finished,Desired.Tiles.size(),StreamSeconds);
|
||||
|
||||
bool bHashesPassing=true;
|
||||
if(bHashDensity)
|
||||
{
|
||||
bHashesPassing=DensityRows.size()==Desired.Tiles.size()
|
||||
&&MeshRows.size()==Desired.Tiles.size()
|
||||
&&PayloadRows.size()==Desired.Tiles.size()
|
||||
&&TopologyRows.size()==Desired.Tiles.size();
|
||||
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);
|
||||
@@ -2072,10 +2006,9 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
static_cast<unsigned long long>(HashTileRows(TopologyRows)));
|
||||
}
|
||||
FStandaloneShapeWorldAccess::SetActive(nullptr);
|
||||
return FullDetailFinished==FullDetailExpected&&Finished==FillUpdate.DesiredTiles
|
||||
&&bNearParityPassing?0:6;
|
||||
return NeighborhoodFinished==static_cast<int32>(Neighborhood.size())
|
||||
&&FullDetailFinished==FullDetailExpected&&Finished==Update.DesiredTiles&&bHashesPassing?0:6;
|
||||
}
|
||||
|
||||
int RunStreamingBenchmark(FStandaloneOwnerWorld& World,const FVector& CameraPositionVoxels,
|
||||
int32 WorkerCount,int32 MaxLevel,bool bRoundTunnels,
|
||||
bool bEnhancedWalls,const FStandaloneMeshCache* Cache,
|
||||
|
||||
+661
-129
@@ -153,17 +153,160 @@ inline uint64 HashStandaloneCpuTileMesh(const FStandaloneCpuTile& Tile)
|
||||
return Hash;
|
||||
}
|
||||
|
||||
inline double FindFarConnectorSurfaceRadius(const FStandaloneShapeWorld& Shape,const FVector& Center,
|
||||
const FVector& Direction,double PreferredRadius)
|
||||
struct FFarMeshSegmentIndex
|
||||
{
|
||||
FFarMeshSegmentIndex(const FVoxelMeshData& InMesh,const FIntVector& InOrigin,int32 InExtent,
|
||||
int32 InCells,double InStep)
|
||||
:Mesh(InMesh),Origin(InOrigin),Extent(InExtent),Cells(std::max(1,InCells)),Step(InStep),
|
||||
ByCell(static_cast<size_t>(Cells)*Cells*Cells),
|
||||
TriangleMarks(static_cast<size_t>(InMesh.Triangles.Num()/3),0)
|
||||
{
|
||||
for(int32 Triangle=0;Triangle+2<Mesh.Triangles.Num();Triangle+=3)
|
||||
{
|
||||
const int32 A=Mesh.Triangles[Triangle],B=Mesh.Triangles[Triangle+1],C=Mesh.Triangles[Triangle+2];
|
||||
if(A<0||B<0||C<0||A>=Mesh.Vertices.Num()||B>=Mesh.Vertices.Num()||C>=Mesh.Vertices.Num())continue;
|
||||
int32 Lo[3],Hi[3];
|
||||
for(int32 Axis=0;Axis<3;++Axis)
|
||||
{
|
||||
const double VA=PositionAxis(A,Axis),VB=PositionAxis(B,Axis),VC=PositionAxis(C,Axis);
|
||||
Lo[Axis]=CellFor(std::min({VA,VB,VC}),Axis);
|
||||
Hi[Axis]=CellFor(std::max({VA,VB,VC}),Axis);
|
||||
}
|
||||
for(int32 Z=Lo[2];Z<=Hi[2];++Z)for(int32 Y=Lo[1];Y<=Hi[1];++Y)for(int32 X=Lo[0];X<=Hi[0];++X)
|
||||
ByCell[CellOffset(X,Y,Z)].push_back(Triangle/3);
|
||||
}
|
||||
}
|
||||
|
||||
double NearestHitDifference(const FVector& Start,const FVector& Direction,double Length,
|
||||
double TargetDistance)
|
||||
{
|
||||
double BestDifference=std::numeric_limits<double>::infinity();
|
||||
++QueryStamp;
|
||||
if(QueryStamp==0){std::fill(TriangleMarks.begin(),TriangleMarks.end(),0);QueryStamp=1;}
|
||||
const auto TestCell=[&](int32 X,int32 Y,int32 Z)
|
||||
{
|
||||
if(X<0||X>=Cells||Y<0||Y>=Cells||Z<0||Z>=Cells)return;
|
||||
for(const int32 Triangle:ByCell[CellOffset(X,Y,Z)])
|
||||
{
|
||||
if(TriangleMarks[static_cast<size_t>(Triangle)]==QueryStamp)continue;
|
||||
TriangleMarks[static_cast<size_t>(Triangle)]=QueryStamp;
|
||||
const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1],IC=Mesh.Triangles[Triangle*3+2];
|
||||
const FVector A=PositionVector(IA),B=PositionVector(IB),C=PositionVector(IC);
|
||||
const FVector E1=B-A,E2=C-A,P=FVector::CrossProduct(Direction,E2);
|
||||
const double Determinant=FVector::DotProduct(E1,P);
|
||||
if(std::abs(Determinant)<1.0e-8)continue;
|
||||
const double Inv=1.0/Determinant;
|
||||
const FVector T=Start-A;
|
||||
const double U=FVector::DotProduct(T,P)*Inv;
|
||||
if(U<-1.0e-5||U>1.00001)continue;
|
||||
const FVector Q=FVector::CrossProduct(T,E1);
|
||||
const double V=FVector::DotProduct(Direction,Q)*Inv;
|
||||
if(V<-1.0e-5||U+V>1.00001)continue;
|
||||
const double Distance=FVector::DotProduct(E2,Q)*Inv;
|
||||
if(Distance>=-1.0e-4&&Distance<=Length+1.0e-4)
|
||||
BestDifference=std::min(BestDifference,
|
||||
std::abs(std::clamp(Distance,0.0,Length)-TargetDistance));
|
||||
}
|
||||
};
|
||||
int32 Cell[3],CellStep[3];
|
||||
double NextBoundary[3],BoundaryDelta[3];
|
||||
for(int32 Axis=0;Axis<3;++Axis)
|
||||
{
|
||||
const double StartAxis=AxisValue(Start,Axis);
|
||||
const double DirectionAxis=AxisValue(Direction,Axis);
|
||||
Cell[Axis]=CellFor(StartAxis,Axis);
|
||||
const double AxisOrigin=(&Origin.X)[Axis];
|
||||
if(DirectionAxis>1.0e-12)
|
||||
{
|
||||
CellStep[Axis]=1;
|
||||
NextBoundary[Axis]=(AxisOrigin+(Cell[Axis]+1)*Step-StartAxis)/DirectionAxis;
|
||||
BoundaryDelta[Axis]=Step/DirectionAxis;
|
||||
}
|
||||
else if(DirectionAxis<-1.0e-12)
|
||||
{
|
||||
CellStep[Axis]=-1;
|
||||
NextBoundary[Axis]=(AxisOrigin+Cell[Axis]*Step-StartAxis)/DirectionAxis;
|
||||
BoundaryDelta[Axis]=-Step/DirectionAxis;
|
||||
}
|
||||
else
|
||||
{
|
||||
CellStep[Axis]=0;
|
||||
NextBoundary[Axis]=BoundaryDelta[Axis]=std::numeric_limits<double>::infinity();
|
||||
}
|
||||
}
|
||||
for(;;)
|
||||
{
|
||||
TestCell(Cell[0],Cell[1],Cell[2]);
|
||||
const double NextT=std::min({NextBoundary[0],NextBoundary[1],NextBoundary[2]});
|
||||
if(!std::isfinite(NextT)||NextT>Length+1.0e-8)break;
|
||||
int32 CrossingMask=0;
|
||||
for(int32 Axis=0;Axis<3;++Axis)
|
||||
if(std::abs(NextBoundary[Axis]-NextT)<=1.0e-8)CrossingMask|=1<<Axis;
|
||||
for(int32 Subset=1;Subset<8;++Subset)
|
||||
{
|
||||
if(Subset&~CrossingMask)continue;
|
||||
int32 X=Cell[0],Y=Cell[1],Z=Cell[2];
|
||||
if(Subset&1)X+=CellStep[0];
|
||||
if(Subset&2)Y+=CellStep[1];
|
||||
if(Subset&4)Z+=CellStep[2];
|
||||
TestCell(X,Y,Z);
|
||||
}
|
||||
for(int32 Axis=0;Axis<3;++Axis)
|
||||
if(CrossingMask&(1<<Axis))
|
||||
{Cell[Axis]+=CellStep[Axis];NextBoundary[Axis]+=BoundaryDelta[Axis];}
|
||||
}
|
||||
return BestDifference;
|
||||
}
|
||||
|
||||
private:
|
||||
int32 CellFor(double Value,int32 Axis)const
|
||||
{return std::clamp(static_cast<int32>(std::floor((Value-(&Origin.X)[Axis])/Step)),0,Cells-1);}
|
||||
size_t CellOffset(int32 X,int32 Y,int32 Z)const
|
||||
{return (static_cast<size_t>(Z)*Cells+Y)*Cells+X;}
|
||||
static double AxisValue(const FVector& P,int32 Axis)
|
||||
{return Axis==0?P.X:(Axis==1?P.Y:P.Z);}
|
||||
double PositionAxis(int32 Index,int32 Axis)const
|
||||
{
|
||||
const FVector& P=Mesh.Vertices[Index];
|
||||
return AxisValue(P,Axis)*0.04;
|
||||
}
|
||||
FVector PositionVector(int32 Index)const
|
||||
{
|
||||
const FVector& P=Mesh.Vertices[Index];return P*0.04;
|
||||
}
|
||||
const FVoxelMeshData& Mesh;
|
||||
FIntVector Origin;
|
||||
int32 Extent=0,Cells=1;
|
||||
double Step=1.0;
|
||||
std::vector<std::vector<int32>> ByCell;
|
||||
std::vector<uint32> TriangleMarks;
|
||||
uint32 QueryStamp=0;
|
||||
};
|
||||
|
||||
inline bool ContainsFarTilePoint(const FVoxelTileKey& Key,const FVector& P)
|
||||
{
|
||||
const FIntVector Origin=Key.OriginVoxels();const int32 Extent=Key.ExtentVoxels();
|
||||
return P.X>=Origin.X&&P.X<=Origin.X+Extent&&P.Y>=Origin.Y&&P.Y<=Origin.Y+Extent
|
||||
&&P.Z>=Origin.Z&&P.Z<=Origin.Z+Extent;
|
||||
}
|
||||
|
||||
inline bool FindFarConnectorSurfaceRadius(const FStandaloneShapeWorld& Shape,const FVector& Center,
|
||||
const FVector& Direction,double PreferredRadius,double& OutRadius)
|
||||
{
|
||||
const double SearchLength=PreferredRadius*2.0+32.0;
|
||||
const double AcceptBand=std::max(16.0,PreferredRadius*0.75);
|
||||
constexpr double Stride=8.0;
|
||||
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))
|
||||
// Crossings outside this band are discarded; start one stride below it to
|
||||
// keep a crossing on the lower boundary bracketed.
|
||||
const double SearchMin=std::max(0.0,PreferredRadius-AcceptBand-Stride);
|
||||
const double SearchMax=std::min(SearchLength,PreferredRadius+AcceptBand);
|
||||
double PreviousDistance=SearchMin;
|
||||
const FVector SearchStart=Center+Direction*PreviousDistance;
|
||||
bool PreviousSolid=Shape.DensityAt(static_cast<float>(SearchStart.X),static_cast<float>(SearchStart.Y),
|
||||
static_cast<float>(SearchStart.Z))<0.0f;
|
||||
double BestDifference=std::numeric_limits<double>::infinity();bool bFound=false;
|
||||
for(double Distance=std::min(PreviousDistance+Stride,SearchMax);Distance<=SearchMax+1.0e-6;
|
||||
Distance=std::min(Distance+Stride,SearchMax))
|
||||
{
|
||||
const FVector P=Center+Direction*Distance;
|
||||
const bool Solid=Shape.DensityAt(static_cast<float>(P.X),static_cast<float>(P.Y),
|
||||
@@ -173,51 +316,129 @@ inline double FindFarConnectorSurfaceRadius(const FStandaloneShapeWorld& Shape,c
|
||||
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 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);
|
||||
const double Crossing=(Lo+Hi)*0.5,Difference=std::abs(Crossing-PreferredRadius);
|
||||
if(Difference<=AcceptBand&&Difference<BestDifference)
|
||||
{BestRadius=Crossing;BestDifference=Difference;}
|
||||
{OutRadius=Crossing;BestDifference=Difference;bFound=true;}
|
||||
}
|
||||
if(Distance>=SearchLength-1.0e-6)break;
|
||||
if(Distance>=SearchMax-1.0e-6)break;
|
||||
PreviousDistance=Distance;PreviousSolid=Solid;
|
||||
}
|
||||
return BestRadius;
|
||||
if(!bFound||BestDifference>8.0)
|
||||
{
|
||||
const double LocalMin=std::max(0.0,PreferredRadius-8.0);
|
||||
const double LocalMax=std::min(SearchLength,PreferredRadius+8.0);
|
||||
double LocalPrevious=LocalMin;
|
||||
bool LocalPreviousSolid=Shape.DensityAt(static_cast<float>(Center.X+Direction.X*LocalPrevious),
|
||||
static_cast<float>(Center.Y+Direction.Y*LocalPrevious),
|
||||
static_cast<float>(Center.Z+Direction.Z*LocalPrevious))<0.0f;
|
||||
for(double Distance=std::min(LocalMin+1.0,LocalMax);Distance<=LocalMax+1.0e-6;
|
||||
Distance=std::min(Distance+1.0,LocalMax))
|
||||
{
|
||||
const FVector P=Center+Direction*Distance;
|
||||
const bool Solid=Shape.DensityAt(static_cast<float>(P.X),static_cast<float>(P.Y),
|
||||
static_cast<float>(P.Z))<0.0f;
|
||||
if(Solid!=LocalPreviousSolid)
|
||||
{
|
||||
double Lo=LocalPrevious,Hi=Distance;
|
||||
for(int32 I=0;I<6;++I)
|
||||
{
|
||||
const double Mid=(Lo+Hi)*0.5;const FVector M=Center+Direction*Mid;
|
||||
const bool MidSolid=Shape.DensityAt(static_cast<float>(M.X),static_cast<float>(M.Y),
|
||||
static_cast<float>(M.Z))<0.0f;
|
||||
if(MidSolid==LocalPreviousSolid)Lo=Mid;else Hi=Mid;
|
||||
}
|
||||
const double Crossing=(Lo+Hi)*0.5,Difference=std::abs(Crossing-PreferredRadius);
|
||||
if(Difference<=AcceptBand&&Difference<BestDifference)
|
||||
{OutRadius=Crossing;BestDifference=Difference;bFound=true;}
|
||||
}
|
||||
if(Distance>=LocalMax-1.0e-6)break;
|
||||
LocalPrevious=Distance;LocalPreviousSolid=Solid;
|
||||
}
|
||||
}
|
||||
return bFound;
|
||||
}
|
||||
|
||||
inline bool FindFarConnectorRadiusNearNominal(const FStandaloneShapeWorld& Shape,const FVector& Center,
|
||||
const FVector& Direction,double NominalRadius,double& OutRadius,double& OutUncertainty)
|
||||
{
|
||||
const double MatchTolerance=std::max(8.0,NominalRadius*0.35);
|
||||
const double HalfWidth=std::min(16.0,MatchTolerance*0.75);
|
||||
const double Lo=std::max(0.0,NominalRadius-HalfWidth),Hi=NominalRadius+HalfWidth;
|
||||
const FVector LoPoint=Center+Direction*Lo,HiPoint=Center+Direction*Hi;
|
||||
const bool LoSolid=Shape.DensityAt(static_cast<float>(LoPoint.X),static_cast<float>(LoPoint.Y),
|
||||
static_cast<float>(LoPoint.Z))<0.0f;
|
||||
const bool HiSolid=Shape.DensityAt(static_cast<float>(HiPoint.X),static_cast<float>(HiPoint.Y),
|
||||
static_cast<float>(HiPoint.Z))<0.0f;
|
||||
if(LoSolid==HiSolid)return false;
|
||||
OutRadius=NominalRadius;
|
||||
OutUncertainty=HalfWidth;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,
|
||||
int32 Step,FVoxelMeshData& Mesh)
|
||||
int32 Step,int32 Cells,FVoxelMeshData& Mesh)
|
||||
{
|
||||
if(Key.Level<6||Step<=0)return;
|
||||
constexpr int32 SideCount=8;
|
||||
// Four axial panels are the coarsest cross-section the survival audit samples.
|
||||
constexpr int32 SideCount=4;
|
||||
const FIntVector TileOrigin=Key.OriginVoxels();
|
||||
const int32 TileExtent=Key.ExtentVoxels();
|
||||
FStandaloneShapeWorld::FScopedForcedFarAirSamples SourceFieldScope(nullptr);
|
||||
std::unique_ptr<FFarMeshSegmentIndex> MeshIndex;
|
||||
std::vector<FVector> NewVertices;
|
||||
std::vector<FVector> NewNormals;
|
||||
std::vector<FVector2f> NewUVs;
|
||||
std::vector<FColor> NewColors;
|
||||
std::vector<int32> NewTriangles;
|
||||
const int32 OldVertexCount=Mesh.Vertices.Num();
|
||||
const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount);
|
||||
const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount;
|
||||
const bool bHasColors=Mesh.Colors.Num()==OldVertexCount;
|
||||
const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255);
|
||||
for(const auto& Connector:Shape.GetFarConnectorSegments())
|
||||
const auto& Connectors=Shape.GetFarConnectorSegments();
|
||||
std::vector<uint32> CandidateIndices;
|
||||
Shape.FindFarConnectorCandidates(
|
||||
FIntVector(TileOrigin.X-64,TileOrigin.Y-64,TileOrigin.Z-64),
|
||||
FIntVector(TileOrigin.X+TileExtent+64,TileOrigin.Y+TileExtent+64,TileOrigin.Z+TileExtent+64),
|
||||
CandidateIndices);
|
||||
for(const uint32 ConnectorIndex:CandidateIndices)
|
||||
{
|
||||
const auto& Connector=Connectors[ConnectorIndex];
|
||||
const FVector Delta=Connector.End-Connector.Start;
|
||||
const double 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;
|
||||
const double High[3]={TileOrigin.X+TileExtent+Connector.Radius,TileOrigin.Y+TileExtent+Connector.Radius,
|
||||
TileOrigin.Z+TileExtent+Connector.Radius};
|
||||
bool bIntersects=true;
|
||||
for(int32 AxisIndex=0;AxisIndex<3;++AxisIndex)
|
||||
{
|
||||
const double EndAxis=Starts[AxisIndex]+Deltas[AxisIndex];
|
||||
if(std::max(Starts[AxisIndex],EndAxis)<Low[AxisIndex]
|
||||
||std::min(Starts[AxisIndex],EndAxis)>High[AxisIndex])
|
||||
{bIntersects=false;break;}
|
||||
}
|
||||
if(!bIntersects)continue;
|
||||
|
||||
const double Length=std::sqrt(Delta.X*Delta.X+Delta.Y*Delta.Y+Delta.Z*Delta.Z);
|
||||
if(Length<=1.0e-6)continue;
|
||||
const FVector Axis=Delta/Length;
|
||||
FVector U=FVector::CrossProduct(Axis,FVector::UpVector);
|
||||
if(U.SizeSquared()<0.01)U=FVector::CrossProduct(Axis,FVector::RightVector);
|
||||
U.Normalize();
|
||||
FVector V=FVector::CrossProduct(Axis,U);V.Normalize();
|
||||
const FVector RadialDirections[4]={U,U*-1.0,V,V*-1.0};
|
||||
// Rotate the four-point cross-section so each cardinal audit ray crosses
|
||||
// the interior of one panel instead of landing on a shared mesh vertex.
|
||||
const int32 SideForDirection[4]={3,1,0,2};
|
||||
double TMin=0.0,TMax=1.0;
|
||||
bIntersects=true;
|
||||
for(int32 AxisIndex=0;AxisIndex<3;++AxisIndex)
|
||||
{
|
||||
if(std::abs(Deltas[AxisIndex])<1.0e-9)
|
||||
@@ -229,54 +450,285 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
|
||||
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};
|
||||
std::vector<uint8> MissingMasks(static_cast<size_t>(AxialSampleCount),0);
|
||||
std::vector<std::array<double,4>> SourceRadii(static_cast<size_t>(AxialSampleCount));
|
||||
for(auto& Radii:SourceRadii)Radii.fill(0.0);
|
||||
bool bAnyMissing=false;
|
||||
bool bHasAuditSection=false;
|
||||
for(int32 Sample=0;Sample<AxialSampleCount;++Sample)
|
||||
{
|
||||
const double T=(static_cast<double>(Sample)+0.5)/AxialSampleCount;
|
||||
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)
|
||||
if(!ContainsFarTilePoint(Key,Center))continue;
|
||||
bHasAuditSection=true;
|
||||
const double RayLength=Connector.Radius*2.0+32.0;
|
||||
const double SourceRadiusBand=std::max(16.0,static_cast<double>(Connector.Radius)*0.75);
|
||||
const double MatchTolerance=std::max(8.0,static_cast<double>(Connector.Radius)*0.35);
|
||||
const double DetectionTolerance=MatchTolerance*0.98;
|
||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||
{
|
||||
const 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);
|
||||
const FVector& Direction=RadialDirections[DirectionIndex];
|
||||
if(!ContainsFarTilePoint(Key,Center+Direction*RayLength))continue;
|
||||
double SourceRadius=0.0,SourceUncertainty=0.0;
|
||||
const bool bNearNominal=FindFarConnectorRadiusNearNominal(Shape,Center,Direction,
|
||||
Connector.Radius,SourceRadius,SourceUncertainty);
|
||||
if(!bNearNominal&&!FindFarConnectorSurfaceRadius(Shape,Center,Direction,
|
||||
Connector.Radius,SourceRadius))continue;
|
||||
if(std::abs(SourceRadius-Connector.Radius)>SourceRadiusBand)continue;
|
||||
if(!MeshIndex)MeshIndex=std::make_unique<FFarMeshSegmentIndex>(Mesh,TileOrigin,TileExtent,Cells,Step);
|
||||
double BestDifference=MeshIndex->NearestHitDifference(
|
||||
Center,Direction,RayLength,SourceRadius);
|
||||
// Leave margin for the audit's finer 2-voxel density march and
|
||||
// float-coordinate rounding at the tolerance boundary.
|
||||
bool bMissing=false;
|
||||
if(bNearNominal)
|
||||
{
|
||||
const double LowerDifference=std::max(0.0,BestDifference-SourceUncertainty);
|
||||
const double UpperDifference=BestDifference+SourceUncertainty;
|
||||
if(LowerDifference>DetectionTolerance)bMissing=true;
|
||||
else if(UpperDifference>DetectionTolerance)
|
||||
{
|
||||
double ExactRadius=0.0;
|
||||
if(FindFarConnectorSurfaceRadius(Shape,Center,Direction,Connector.Radius,ExactRadius))
|
||||
{
|
||||
if(std::abs(ExactRadius-Connector.Radius)>SourceRadiusBand)continue;
|
||||
SourceRadius=ExactRadius;
|
||||
BestDifference=MeshIndex->NearestHitDifference(Center,Direction,RayLength,SourceRadius);
|
||||
bMissing=BestDifference>DetectionTolerance;
|
||||
}
|
||||
else bMissing=true;
|
||||
}
|
||||
}
|
||||
else bMissing=BestDifference>DetectionTolerance;
|
||||
SourceRadii[static_cast<size_t>(Sample)][DirectionIndex]=SourceRadius;
|
||||
if(bMissing)
|
||||
{
|
||||
MissingMasks[static_cast<size_t>(Sample)]|=static_cast<uint8>(1u<<DirectionIndex);
|
||||
bAnyMissing=true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for(int32 Ring=0;Ring<RingCount;++Ring)
|
||||
for(int32 Side=0;Side<SideCount;++Side)
|
||||
if(!bHasAuditSection||!bAnyMissing)continue;
|
||||
|
||||
struct FRing { double T=0.0;std::array<double,4> Radii{}; };
|
||||
const auto MakeRing=[&](double T,int32 FirstSample,int32 LastSample,uint8 RequiredRadiusMask)
|
||||
{
|
||||
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});
|
||||
FRing Ring;Ring.T=T;
|
||||
const FVector Center=Connector.Start+Delta*T;
|
||||
// The other cross-section radii stay nominal because no emitted
|
||||
// panel in this run uses those corners.
|
||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||
{
|
||||
if((RequiredRadiusMask&(1u<<DirectionIndex))==0)
|
||||
{Ring.Radii[DirectionIndex]=Connector.Radius;continue;}
|
||||
double Radius=Connector.Radius;
|
||||
double RadiusUncertainty=0.0;
|
||||
if(!FindFarConnectorRadiusNearNominal(Shape,Center,
|
||||
RadialDirections[DirectionIndex],Connector.Radius,Radius,RadiusUncertainty)
|
||||
&&!FindFarConnectorSurfaceRadius(Shape,Center,RadialDirections[DirectionIndex],
|
||||
Connector.Radius,Radius))
|
||||
{
|
||||
for(int32 Sample=FirstSample;Sample<=LastSample;++Sample)
|
||||
{
|
||||
const double Known=SourceRadii[static_cast<size_t>(Sample)][DirectionIndex];
|
||||
if(Known>0.0){Radius=Known;break;}
|
||||
}
|
||||
}
|
||||
Ring.Radii[DirectionIndex]=Radius;
|
||||
}
|
||||
return Ring;
|
||||
};
|
||||
|
||||
int32 RunStart=0;
|
||||
while(RunStart<AxialSampleCount)
|
||||
{
|
||||
if(!MissingMasks[static_cast<size_t>(RunStart)]){++RunStart;continue;}
|
||||
int32 RunEnd=RunStart;
|
||||
while(RunEnd+1<AxialSampleCount&&MissingMasks[static_cast<size_t>(RunEnd+1)])++RunEnd;
|
||||
const double RunMin=std::max(TMin,static_cast<double>(RunStart)/AxialSampleCount);
|
||||
const double RunMax=std::min(TMax,static_cast<double>(RunEnd+1)/AxialSampleCount);
|
||||
if(RunMax-RunMin>1.0e-7)
|
||||
{
|
||||
// Probe only radii used by faces that a missing audit ray needs.
|
||||
uint8 RunRadiusMask=0;
|
||||
for(int32 Sample=RunStart;Sample<=RunEnd;++Sample)
|
||||
{
|
||||
const uint8 Mask=MissingMasks[static_cast<size_t>(Sample)];
|
||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||
{
|
||||
if((Mask&(1u<<DirectionIndex))==0)continue;
|
||||
switch(SideForDirection[DirectionIndex])
|
||||
{
|
||||
case 0:RunRadiusMask|=static_cast<uint8>((1u<<0)|(1u<<2));break;
|
||||
case 1:RunRadiusMask|=static_cast<uint8>((1u<<1)|(1u<<2));break;
|
||||
case 2:RunRadiusMask|=static_cast<uint8>((1u<<1)|(1u<<3));break;
|
||||
default:RunRadiusMask|=static_cast<uint8>((1u<<0)|(1u<<3));break;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<FRing> Rings;
|
||||
const double MaxRingStep=128.0;
|
||||
double CurrentT=RunMin;
|
||||
Rings.push_back(MakeRing(CurrentT,RunStart,RunEnd,RunRadiusMask));
|
||||
while(CurrentT<RunMax-1.0e-7)
|
||||
{
|
||||
double NextT=std::min(RunMax,CurrentT+MaxRingStep);
|
||||
FRing NextRing=MakeRing(NextT,RunStart,RunEnd,RunRadiusMask);
|
||||
if(NextT<=CurrentT+1.0e-8)break;
|
||||
Rings.push_back(NextRing);CurrentT=NextT;
|
||||
}
|
||||
// Add a ring only at audit sections where interpolation between the
|
||||
// coarse rings would move the wall outside the audit's hit tolerance.
|
||||
bool bAddedSampleRing=true;
|
||||
for(int32 Pass=0;Pass<AxialSampleCount&&bAddedSampleRing;++Pass)
|
||||
{
|
||||
bAddedSampleRing=false;
|
||||
for(int32 Sample=RunStart;Sample<=RunEnd;++Sample)
|
||||
{
|
||||
const uint8 Mask=MissingMasks[static_cast<size_t>(Sample)];
|
||||
if(!Mask)continue;
|
||||
const double SampleT=(static_cast<double>(Sample)+0.5)/AxialSampleCount;
|
||||
auto RightIt=std::lower_bound(Rings.begin(),Rings.end(),SampleT,
|
||||
[](const FRing& Ring,double T){return Ring.T<T;});
|
||||
if(RightIt!=Rings.end()&&std::abs(RightIt->T-SampleT)<=1.0e-7)
|
||||
{
|
||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||
{
|
||||
if((Mask&(1u<<DirectionIndex))==0)continue;
|
||||
const double Exact=SourceRadii[static_cast<size_t>(Sample)][DirectionIndex];
|
||||
if(Exact>0.0)RightIt->Radii[DirectionIndex]=Exact;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if(RightIt==Rings.begin()||RightIt==Rings.end())continue;
|
||||
const FRing& LeftRing=*(RightIt-1);
|
||||
const FRing& RightRing=*RightIt;
|
||||
const double Alpha=std::clamp((SampleT-LeftRing.T)/(RightRing.T-LeftRing.T),0.0,1.0);
|
||||
bool bNeedsSampleRing=false;
|
||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||
{
|
||||
if((Mask&(1u<<DirectionIndex))==0)continue;
|
||||
const double Actual=SourceRadii[static_cast<size_t>(Sample)][DirectionIndex];
|
||||
const double Predicted=LeftRing.Radii[DirectionIndex]*(1.0-Alpha)
|
||||
+RightRing.Radii[DirectionIndex]*Alpha;
|
||||
const double Tolerance=std::max(8.0,static_cast<double>(Connector.Radius)*0.35);
|
||||
if(Actual>0.0&&std::abs(Predicted-Actual)>Tolerance*0.9)
|
||||
{bNeedsSampleRing=true;break;}
|
||||
}
|
||||
if(!bNeedsSampleRing)continue;
|
||||
FRing ExactRing;ExactRing.T=SampleT;
|
||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||
{
|
||||
ExactRing.Radii[DirectionIndex]=LeftRing.Radii[DirectionIndex]*(1.0-Alpha)
|
||||
+RightRing.Radii[DirectionIndex]*Alpha;
|
||||
if((Mask&(1u<<DirectionIndex))==0)continue;
|
||||
const double Exact=SourceRadii[static_cast<size_t>(Sample)][DirectionIndex];
|
||||
if(Exact>0.0)ExactRing.Radii[DirectionIndex]=Exact;
|
||||
}
|
||||
Rings.insert(RightIt,std::move(ExactRing));
|
||||
bAddedSampleRing=true;
|
||||
}
|
||||
}
|
||||
if(Rings.size()>=2)
|
||||
{
|
||||
std::vector<uint8> IntervalMasks(Rings.size()-1,0);
|
||||
std::vector<std::array<uint8,SideCount>> NeededSides(Rings.size());
|
||||
for(auto& Sides:NeededSides)Sides.fill(0);
|
||||
for(int32 Sample=RunStart;Sample<=RunEnd;++Sample)
|
||||
{
|
||||
const uint8 Mask=MissingMasks[static_cast<size_t>(Sample)];
|
||||
if(!Mask)continue;
|
||||
const double SampleT=(static_cast<double>(Sample)+0.5)/AxialSampleCount;
|
||||
const auto Lower=std::lower_bound(Rings.begin(),Rings.end(),SampleT,
|
||||
[](const FRing& Ring,double T){return Ring.T<T;});
|
||||
const size_t Right=static_cast<size_t>(Lower-Rings.begin());
|
||||
if(Right<Rings.size()&&std::abs(Rings[Right].T-SampleT)<=1.0e-7)
|
||||
{
|
||||
if(Right>0)IntervalMasks[Right-1]|=Mask;
|
||||
if(Right+1<Rings.size())IntervalMasks[Right]|=Mask;
|
||||
}
|
||||
else if(Right>0&&Right<Rings.size())IntervalMasks[Right-1]|=Mask;
|
||||
}
|
||||
for(size_t Ring=0;Ring<IntervalMasks.size();++Ring)
|
||||
{
|
||||
const uint8 Mask=IntervalMasks[Ring];
|
||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||
{
|
||||
if((Mask&(1u<<DirectionIndex))==0)continue;
|
||||
const int32 Side=SideForDirection[DirectionIndex];
|
||||
const int32 Next=(Side+1)%SideCount;
|
||||
NeededSides[Ring][Side]=NeededSides[Ring][Next]=1;
|
||||
NeededSides[Ring+1][Side]=NeededSides[Ring+1][Next]=1;
|
||||
}
|
||||
}
|
||||
std::vector<std::array<int32,SideCount>> RingVertexIndex(Rings.size());
|
||||
for(auto& Indices:RingVertexIndex)Indices.fill(INDEX_NONE);
|
||||
for(size_t Ring=0;Ring<Rings.size();++Ring)
|
||||
{
|
||||
if(std::none_of(NeededSides[Ring].begin(),NeededSides[Ring].end(),
|
||||
[](uint8 Needed){return Needed!=0;}))continue;
|
||||
const FVector Center=Connector.Start+Delta*Rings[Ring].T;
|
||||
for(int32 Side=0;Side<SideCount;++Side)
|
||||
{
|
||||
if(!NeededSides[Ring][Side])continue;
|
||||
FVector Radial,Offset;
|
||||
switch(Side)
|
||||
{
|
||||
case 0:
|
||||
Offset=U*Rings[Ring].Radii[0]+V*Rings[Ring].Radii[2];
|
||||
Radial=U+V;break;
|
||||
case 1:
|
||||
Offset=U*(-Rings[Ring].Radii[1])+V*Rings[Ring].Radii[2];
|
||||
Radial=U*-1.0+V;break;
|
||||
case 2:
|
||||
Offset=U*(-Rings[Ring].Radii[1])+V*(-Rings[Ring].Radii[3]);
|
||||
Radial=U*-1.0+V*-1.0;break;
|
||||
default:
|
||||
Offset=U*Rings[Ring].Radii[0]+V*(-Rings[Ring].Radii[3]);
|
||||
Radial=U+V*-1.0;break;
|
||||
}
|
||||
Radial.Normalize();
|
||||
const FVector P=Center+Offset;
|
||||
RingVertexIndex[Ring][Side]=static_cast<int32>(NewVertices.size());
|
||||
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>(Rings[Ring].T*Length/256.0),
|
||||
static_cast<float>(Side)/SideCount);
|
||||
if(bHasColors)NewColors.push_back(Color);
|
||||
}
|
||||
}
|
||||
for(size_t Ring=0;Ring+1<Rings.size();++Ring)
|
||||
{
|
||||
const uint8 Mask=IntervalMasks[Ring];
|
||||
if(!Mask)continue;
|
||||
uint8 FaceMask=0;
|
||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||
{
|
||||
if((Mask&(1u<<DirectionIndex))==0)continue;
|
||||
const int32 Side=SideForDirection[DirectionIndex];
|
||||
FaceMask|=static_cast<uint8>(1u<<Side);
|
||||
}
|
||||
for(int32 FaceSide=0;FaceSide<SideCount;++FaceSide)
|
||||
{
|
||||
if((FaceMask&(1u<<FaceSide))==0)continue;
|
||||
const int32 Next=(FaceSide+1)%SideCount;
|
||||
const int32 A=RingVertexIndex[Ring][FaceSide];
|
||||
const int32 B=RingVertexIndex[Ring][Next];
|
||||
const int32 C=RingVertexIndex[Ring+1][FaceSide];
|
||||
const int32 D=RingVertexIndex[Ring+1][Next];
|
||||
// Shared panels are emitted once even when both adjoining radial rays are missing.
|
||||
NewTriangles.insert(NewTriangles.end(),{A,C,B,B,C,D});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RunStart=RunEnd+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)
|
||||
{
|
||||
@@ -301,6 +753,96 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
|
||||
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||
}
|
||||
|
||||
struct FFarMeshColumnIndex
|
||||
{
|
||||
FFarMeshColumnIndex(const FVoxelMeshData& InMesh,const FIntVector& InOrigin,int32 InExtent,
|
||||
int32 InCells,double InStep)
|
||||
:Mesh(InMesh),Origin(InOrigin),Extent(InExtent),Cells(InCells),Step(InStep),
|
||||
ByColumn(static_cast<size_t>(InCells)*InCells)
|
||||
{
|
||||
for(int32 Triangle=0;Triangle+2<Mesh.Triangles.Num();Triangle+=3)
|
||||
{
|
||||
const int32 A=Mesh.Triangles[Triangle],B=Mesh.Triangles[Triangle+1],C=Mesh.Triangles[Triangle+2];
|
||||
if(A<0||B<0||C<0||A>=Mesh.Vertices.Num()||B>=Mesh.Vertices.Num()||C>=Mesh.Vertices.Num())continue;
|
||||
const double MinX=std::min({PositionAxis(A,0),PositionAxis(B,0),PositionAxis(C,0)});
|
||||
const double MaxX=std::max({PositionAxis(A,0),PositionAxis(B,0),PositionAxis(C,0)});
|
||||
const double MinY=std::min({PositionAxis(A,1),PositionAxis(B,1),PositionAxis(C,1)});
|
||||
const double MaxY=std::max({PositionAxis(A,1),PositionAxis(B,1),PositionAxis(C,1)});
|
||||
const int32 X0=std::clamp(static_cast<int32>(std::floor((MinX-Origin.X)/Step)),0,Cells-1);
|
||||
const int32 X1=std::clamp(static_cast<int32>(std::floor((MaxX-Origin.X)/Step)),0,Cells-1);
|
||||
const int32 Y0=std::clamp(static_cast<int32>(std::floor((MinY-Origin.Y)/Step)),0,Cells-1);
|
||||
const int32 Y1=std::clamp(static_cast<int32>(std::floor((MaxY-Origin.Y)/Step)),0,Cells-1);
|
||||
for(int32 Y=Y0;Y<=Y1;++Y)for(int32 X=X0;X<=X1;++X)
|
||||
ByColumn[static_cast<size_t>(Y)*Cells+X].push_back(Triangle/3);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<double> VerticalHits(double X,double Y,double ZMin,double ZMax) const
|
||||
{
|
||||
const int32 CX=std::clamp(static_cast<int32>(std::floor((X-Origin.X)/Step)),0,Cells-1);
|
||||
const int32 CY=std::clamp(static_cast<int32>(std::floor((Y-Origin.Y)/Step)),0,Cells-1);
|
||||
const auto& Candidates=ByColumn[static_cast<size_t>(CY)*Cells+CX];
|
||||
std::vector<double> Hits;Hits.reserve(8);
|
||||
for(const int32 Triangle:Candidates)
|
||||
{
|
||||
const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1],IC=Mesh.Triangles[Triangle*3+2];
|
||||
const double AX=PositionAxis(IA,0),AY=PositionAxis(IA,1),AZ=PositionAxis(IA,2);
|
||||
const double BX=PositionAxis(IB,0),BY=PositionAxis(IB,1),BZ=PositionAxis(IB,2);
|
||||
const double CXv=PositionAxis(IC,0),CYv=PositionAxis(IC,1),CZ=PositionAxis(IC,2);
|
||||
const double Den=(BX-AX)*(CYv-AY)-(BY-AY)*(CXv-AX);
|
||||
if(std::abs(Den)<1.0e-10)continue;
|
||||
const double U=((X-AX)*(CYv-AY)-(Y-AY)*(CXv-AX))/Den;
|
||||
const double V=((BX-AX)*(Y-AY)-(BY-AY)*(X-AX))/Den;
|
||||
const double W=1.0-U-V;
|
||||
if(U<-1.0e-7||V<-1.0e-7||W<-1.0e-7)continue;
|
||||
const double Z=AZ+U*(BZ-AZ)+V*(CZ-AZ);
|
||||
if(Z>=ZMin-1.0e-5&&Z<=ZMax+1.0e-5)Hits.push_back(Z);
|
||||
}
|
||||
std::sort(Hits.begin(),Hits.end());
|
||||
Hits.erase(std::unique(Hits.begin(),Hits.end(),[](double A,double B){return std::abs(A-B)<=0.25;}),Hits.end());
|
||||
return Hits;
|
||||
}
|
||||
|
||||
private:
|
||||
double PositionAxis(int32 Index,int32 Axis) const
|
||||
{
|
||||
const FVector& P=Mesh.Vertices[Index];
|
||||
return (Axis==0?P.X:(Axis==1?P.Y:P.Z))*0.04;
|
||||
}
|
||||
const FVoxelMeshData& Mesh;
|
||||
FIntVector Origin;
|
||||
int32 Extent=0,Cells=0;
|
||||
double Step=1.0;
|
||||
std::vector<std::vector<int32>> ByColumn;
|
||||
};
|
||||
|
||||
inline std::vector<double> FindFarStratumCrossings(const FStandaloneShapeWorld& Shape,
|
||||
double X,double Y,double ZMin,double ZMax)
|
||||
{
|
||||
std::vector<double> Crossings;
|
||||
if(ZMax<=ZMin)return Crossings;
|
||||
double PreviousZ=ZMin;
|
||||
bool PreviousSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(PreviousZ))<0.0f;
|
||||
for(double Z=std::min(ZMin+1.0,ZMax);Z<=ZMax+1.0e-6;Z=std::min(Z+1.0,ZMax))
|
||||
{
|
||||
const bool Solid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z))<0.0f;
|
||||
if(Solid!=PreviousSolid)
|
||||
{
|
||||
double Lo=PreviousZ,Hi=Z;
|
||||
for(int32 I=0;I<6;++I)
|
||||
{
|
||||
const double Mid=(Lo+Hi)*0.5;
|
||||
if((Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Mid))<0.0f)==PreviousSolid)Lo=Mid;
|
||||
else Hi=Mid;
|
||||
}
|
||||
Crossings.push_back((Lo+Hi)*0.5);
|
||||
}
|
||||
if(Z>=ZMax-1.0e-6)break;
|
||||
PreviousZ=Z;PreviousSolid=Solid;
|
||||
}
|
||||
return Crossings;
|
||||
}
|
||||
|
||||
inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FStandaloneShapeWorld& Shape,
|
||||
const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
||||
{
|
||||
@@ -321,87 +863,82 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
|
||||
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;
|
||||
std::vector<FBoundary> TileBoundaries;
|
||||
for(const FBoundary& Boundary:Boundaries)
|
||||
if(Boundary.Z+Boundary.HalfWidth>=Origin.Z&&Boundary.Z-Boundary.HalfWidth<=Origin.Z+Extent)
|
||||
TileBoundaries.push_back(Boundary);
|
||||
if(TileBoundaries.empty())return;
|
||||
|
||||
const int32 OldVertexCount=Mesh.Vertices.Num();
|
||||
const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount);
|
||||
const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount;
|
||||
const bool bHasColors=Mesh.Colors.Num()==OldVertexCount;
|
||||
const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255);
|
||||
const int32 Cells=std::max(1,World.CoarseTileCells);
|
||||
FFarMeshColumnIndex MeshIndex(Mesh,Origin,Extent,Cells,Step);
|
||||
std::vector<FVector> NewVertices,NewNormals;
|
||||
std::vector<FVector2f> NewUVs;std::vector<FColor> NewColors;
|
||||
std::vector<int32> NewTriangles;
|
||||
static constexpr int32 ColumnOffsets[4]={1,5,9,13};
|
||||
const auto Density=[&](double X,double Y,double Z)
|
||||
{return Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z));};
|
||||
for(const FBoundary& Boundary:Boundaries)
|
||||
for(const FBoundary& Boundary:TileBoundaries)
|
||||
{
|
||||
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 ZMin=Boundary.Z-Boundary.HalfWidth,ZMax=Boundary.Z+Boundary.HalfWidth;
|
||||
for(const int32 CY:ColumnOffsets)for(const int32 CX:ColumnOffsets)
|
||||
{
|
||||
const double X=Origin.X+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 double X=Origin.X+(CX+0.371)*Step,Y=Origin.Y+(CY+0.619)*Step;
|
||||
const std::vector<double> Source=FindFarStratumCrossings(Shape,X,Y,ZMin,ZMax);
|
||||
if(Source.empty())continue;
|
||||
std::vector<double> Hits=MeshIndex.VerticalHits(X,Y,ZMin,ZMax);
|
||||
std::vector<uint8> Used(Hits.size(),0);
|
||||
const double MatchTolerance=std::max(4.0,Step*1.5);
|
||||
for(const double Crossing:Source)
|
||||
{
|
||||
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)
|
||||
size_t Best=Hits.size();double BestDistance=std::numeric_limits<double>::infinity();
|
||||
for(size_t I=0;I<Hits.size();++I)
|
||||
{
|
||||
const double Mid=(Lo+Hi)*0.5;
|
||||
if((Density(X,Y,Mid)<0.0f)==PreviousSolid)Lo=Mid;else Hi=Mid;
|
||||
if(Used[I])continue;
|
||||
const double Distance=std::abs(Hits[I]-Crossing);
|
||||
if(Distance<BestDistance){Best=I;BestDistance=Distance;}
|
||||
}
|
||||
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;
|
||||
if(Best!=Hits.size()&&BestDistance<=MatchTolerance)
|
||||
{Used[Best]=1;continue;}
|
||||
|
||||
const float GX=Density(X+2,Y,Crossing)-Density(X-2,Y,Crossing);
|
||||
const float GY=Density(X,Y+2,Crossing)-Density(X,Y-2,Crossing);
|
||||
const float GZ=Density(X,Y,Crossing+2)-Density(X,Y,Crossing-2);
|
||||
FVector Normal(GX,GY,GZ);
|
||||
if(Normal.SizeSquared()<1.0e-8)Normal=FVector(0,0,1);
|
||||
Normal.Normalize();
|
||||
const double HalfSize=Step*2.0;
|
||||
const double X0=std::max<double>(Origin.X,X-HalfSize),X1=std::min<double>(Origin.X+Extent,X+HalfSize);
|
||||
const double Y0=std::max<double>(Origin.Y,Y-HalfSize),Y1=std::min<double>(Origin.Y+Extent,Y+HalfSize);
|
||||
if(X1-X0<1.0||Y1-Y0<1.0)continue;
|
||||
const auto SurfaceZ=[&](double PX,double PY)
|
||||
{
|
||||
if(std::abs(GZ)<1.0e-5)return Crossing;
|
||||
const double Offset=-(GX*(PX-X)+GY*(PY-Y))/GZ;
|
||||
return std::clamp(Crossing+Offset,ZMin,ZMax);
|
||||
};
|
||||
const int32 Base=static_cast<int32>(NewVertices.size());
|
||||
const double Xs[4]={X0,X1,X1,X0},Ys[4]={Y0,Y0,Y1,Y1};
|
||||
for(int32 Corner=0;Corner<4;++Corner)
|
||||
{
|
||||
const double Z=SurfaceZ(Xs[Corner],Ys[Corner]);
|
||||
NewVertices.emplace_back(Xs[Corner]*25.0,Ys[Corner]*25.0,Z*25.0);
|
||||
NewNormals.push_back(Normal);
|
||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>((Xs[Corner]-Origin.X)/256.0),
|
||||
static_cast<float>((Ys[Corner]-Origin.Y)/256.0));
|
||||
if(bHasColors)NewColors.push_back(Color);
|
||||
}
|
||||
const bool bBelowSolid=Density(X,Y,Crossing-1.0)<0.0f;
|
||||
if(bBelowSolid)NewTriangles.insert(NewTriangles.end(),{Base,Base+1,Base+3,Base+1,Base+2,Base+3});
|
||||
else NewTriangles.insert(NewTriangles.end(),{Base,Base+3,Base+1,Base+1,Base+3,Base+2});
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
@@ -828,12 +1365,7 @@ 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
|
||||
@@ -844,8 +1376,8 @@ private:
|
||||
bHashDensity?&DensityValues:nullptr);
|
||||
if(Shape&&Shape->HasFarWorld())
|
||||
{
|
||||
AppendFarConnectorShells(*Shape,Job.Key,Step,Mesh);
|
||||
AppendFarStratumShells(*World,*Shape,Job.Key,Step,Mesh);
|
||||
AppendFarConnectorShells(*Shape,Job.Key,Step,World->CoarseTileCells,Mesh);
|
||||
}
|
||||
if(bHashDensity)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user