wip(standalone): speed round -- interrupted by a PC hard reset at 10:16 (Kernel-Power 41)

Verified before the reset (density + full mesh hashes equal at 1/8/16 workers, routes 5642/5642,
look audits green):
- merged field lookups, parallel mask merge, compact inline spatial bins, line-local relief hash
  cache, lattice-specific mesher entry, fused sign+magnitude evaluation, far connectors as one
  capsule each (181,836 -> 16,491 far primitives).
- crater edit (sphere+box+capsule, one transaction across LOD0/1): 15-27 ms; player 3x3x3
  neighbourhood meshes ready ~18 ms after streaming starts.
NOT yet verified (in the tree, unhashed): standalone-only skips of UV/colour arrays and of the
ceiling/ground material grouping (best cold whole view 0.958 s @16 workers) and a far-bin reserve
tweak. Those skips drop data the plugin's materials may need: not a game-path win as-is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-04 12:18:48 +02:00
1 parent 97c0455939
commit ee5b05b357
16 files changed
+1011 -156

No files matched your search

+540 -69
View File
@@ -17,6 +17,7 @@
#include <limits>
#include <memory>
#include <mutex>
#include <immintrin.h>
#include <thread>
#include <unordered_map>
#include <vector>
@@ -28,6 +29,24 @@ class FStandaloneShapeWorld
{
public:
enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel };
struct FEditBrush
{
enum class EShape : uint8 { Sphere, Capsule, RoundedBox };
enum class EOperation : uint8 { Carve, AddRock };
EShape Shape=EShape::Sphere;
EOperation Operation=EOperation::Carve;
std::array<double,3> A{};
std::array<double,3> B{};
std::array<double,3> HalfExtent{};
double Radius=0.0;
double Rounding=0.0;
};
struct FParallelTiming
{
double WallSeconds = 0.0;
double WorkerSeconds = 0.0;
};
struct FBuildStats
{
@@ -55,6 +74,15 @@ public:
double MaxUnmarkedFloorDirectionSlopeDelta = 0.0;
double MaxMarkedFloorSlopeDelta = 0.0;
double MaxFloorCurveDeviation = 0.0;
double LocalPrimitiveSeconds = 0.0;
double LocalLineBinSeconds = 0.0;
double CandidateMergeSeconds = 0.0;
double FarPrimitiveSeconds = 0.0;
double FarBinSeconds = 0.0;
FParallelTiming RockIntervalLines;
FParallelTiming CandidateAxisLines;
FParallelTiming CandidateMergeLines;
FParallelTiming DetailBandLines;
uint64 CurvedFloorSegments = 0;
uint64 MarkedFloorSlopeJoins = 0;
double LegacySegmentRadiusStep = 0.0;
@@ -70,6 +98,10 @@ public:
int32 CenterSub[3]{};
int32 CapsuleStartSub[3]{};
int32 CapsuleEndSub[3]{};
double CapsuleStartWorld[3]{};
double CapsuleAxisDelta[3]{};
double CapsuleAxisLengthSquared = 0.0;
double CapsuleAxisLength = 0.0;
int64 Q[3][3]{};
int32 Min[3]{};
int32 Max[3]{};
@@ -123,6 +155,81 @@ public:
return static_cast<size_t>(H);
}
};
struct FBinItems
{
static constexpr size_t InlineCapacity=4;
struct FHeapStorage {uint32* Data=nullptr;uint32 Capacity=0;uint32 Reserved=0;};
union FStorage
{
std::array<uint32,InlineCapacity> Inline;
FHeapStorage Heap;
FStorage():Inline{}{}
~FStorage(){}
} Storage;
uint32 Count=0;
uint32 bHeap=0;
FBinItems()=default;
FBinItems(const FBinItems&)=delete;
FBinItems& operator=(const FBinItems&)=delete;
FBinItems(FBinItems&& Other) noexcept {MoveFrom(Other);}
FBinItems& operator=(FBinItems&& Other) noexcept
{
if(this!=&Other){Release();MoveFrom(Other);}
return *this;
}
~FBinItems(){Release();}
void push_back(uint32 Id)
{
if(!bHeap&&Count<InlineCapacity)
{
Storage.Inline[Count++]=Id;
return;
}
if(!bHeap)
{
const std::array<uint32,InlineCapacity> Previous=Storage.Inline;
Storage.Heap={new uint32[8],8,0};
std::copy(Previous.begin(),Previous.end(),Storage.Heap.Data);
bHeap=1;
}
else if(Count==Storage.Heap.Capacity)
{
const uint32 NewCapacity=Storage.Heap.Capacity*2;
uint32* NewData=new uint32[NewCapacity];
std::copy_n(Storage.Heap.Data,Count,NewData);
delete[] Storage.Heap.Data;
Storage.Heap={NewData,NewCapacity,0};
}
Storage.Heap.Data[Count++]=Id;
}
size_t size() const {return Count;}
size_t capacity() const {return bHeap?Storage.Heap.Capacity:InlineCapacity;}
const uint32* begin() const {return bHeap?Storage.Heap.Data:Storage.Inline.data();}
const uint32* end() const {return begin()+Count;}
private:
void Release()
{
if(bHeap)delete[] Storage.Heap.Data;
bHeap=0;Count=0;
}
void MoveFrom(FBinItems& Other)
{
Count=Other.Count;bHeap=Other.bHeap;
if(bHeap)
{
Storage.Heap=Other.Storage.Heap;
Other.bHeap=0;Other.Count=0;
}
else
{
Storage.Inline=Other.Storage.Inline;
Other.Count=0;
}
}
};
static constexpr int32 QScale = 1 << 28;
static constexpr int32 SubPerVoxel = 16;
@@ -161,31 +268,62 @@ public:
auto Stage=Clock::now();
BuildLayerBounds();
auto Substage=Clock::now();
BuildOwnerPrimitives();
Stats.LocalPrimitiveSeconds=std::chrono::duration<double>(Clock::now()-Substage).count();
Substage=Clock::now();
if(FStandaloneResearchTunnels::Enabled.load(std::memory_order_acquire))
BuildRoundTunnelPrimitives();
BuildSpatialBins(LineBinSize);
Stats.LocalLineBinSeconds=std::chrono::duration<double>(Clock::now()-Substage).count();
Stats.ShapeSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
Stats.PrimitiveCount=Primitives.size();
if(Primitives.empty())
{Error="owner shape extraction produced no rooms, tunnels, or passages";return false;}
Stage=Clock::now();
const size_t NZ=static_cast<size_t>(Size[2]);
WordsPerZ=(NZ+63u)/64u;
RockWords.assign(static_cast<size_t>(Size[0])*Size[1]*WordsPerZ,0);
SurfaceDistanceQ=std::make_unique_for_overwrite<int8[]>(static_cast<size_t>(NodeCount));
BuildPackedRockIntervals();
BuildCandidateBands();
BuildPackedRockIntervals(Stats.RockIntervalLines);
BuildCandidateBands(Stats.CandidateAxisLines,Stats.CandidateMergeLines,
Stats.CandidateMergeSeconds);
Stats.IntervalSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
Stage=Clock::now();
ApplyDetailBand();
ApplyDetailBand(Stats.DetailBandLines);
Stats.SurfaceSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
ValidatePassageRoutes();
Built=true;
if(bIncludeFarWorld)BuildFarWorld();
const uint64 WordBytes=static_cast<uint64>(RockWords.size()+CandidateWords.size())*sizeof(uint64);
uint64 CandidateAxisBytes=0;
for(const auto& AxisWords:CandidateAxisWords)
CandidateAxisBytes+=static_cast<uint64>(AxisWords.capacity())*sizeof(uint64);
uint64 BinReferences=0,BinCapacity=0,InlineBins=0,OverflowBins=0;
for(const auto& Bin:SpatialBins)
{
BinReferences+=Bin.second.size();BinCapacity+=Bin.second.capacity();
if(Bin.second.size()<=FBinItems::InlineCapacity)++InlineBins;
else ++OverflowBins;
}
std::printf("[ShapeProfile] local_primitives=%.3fms line_bins=%.3fms rock_lines_wall=%.3fms rock_lines_worker=%.3fms candidate_axes_wall=%.3fms candidate_axes_worker=%.3fms candidate_merge_wall=%.3fms candidate_merge_worker=%.3fms candidate_merge_serial=%.3fms detail_lines_wall=%.3fms detail_lines_worker=%.3fms\n",
Stats.LocalPrimitiveSeconds*1000.0,Stats.LocalLineBinSeconds*1000.0,
Stats.RockIntervalLines.WallSeconds*1000.0,Stats.RockIntervalLines.WorkerSeconds*1000.0,
Stats.CandidateAxisLines.WallSeconds*1000.0,Stats.CandidateAxisLines.WorkerSeconds*1000.0,
Stats.CandidateMergeLines.WallSeconds*1000.0,Stats.CandidateMergeLines.WorkerSeconds*1000.0,
Stats.CandidateMergeSeconds*1000.0,Stats.DetailBandLines.WallSeconds*1000.0,
Stats.DetailBandLines.WorkerSeconds*1000.0);
std::printf("[ShapeAllocation] words=%.2fMiB candidate_axis_words=%.2fMiB proxy=%.2fMiB primitives=%.2fMiB spatial_bins=%llu spatial_bin_refs=%llu spatial_bin_capacity=%llu inline_bins=%llu overflow_bins=%llu far_primitive_build=%.3fms far_bin_rebuild=%.3fms\n",
static_cast<double>(WordBytes)/1048576.0,static_cast<double>(CandidateAxisBytes)/1048576.0,
static_cast<double>(Stats.SignNodes*sizeof(int8))/1048576.0,
static_cast<double>(Primitives.capacity()*sizeof(FPrimitive))/1048576.0,
static_cast<unsigned long long>(SpatialBins.size()),
static_cast<unsigned long long>(BinReferences),
static_cast<unsigned long long>(BinCapacity),
static_cast<unsigned long long>(InlineBins),
static_cast<unsigned long long>(OverflowBins),Stats.FarPrimitiveSeconds*1000.0,
Stats.FarBinSeconds*1000.0);
std::printf("[ShapeCompile] primitives=%llu rooms=%llu tunnels=%llu passages=%llu nodes=%llu packed=%.2f MiB surface_proxy_nodes=%llu proxy_address_capacity=%.2f MiB workers=%d\n",
static_cast<unsigned long long>(Stats.PrimitiveCount),
static_cast<unsigned long long>(Stats.RoomCount),
@@ -243,15 +381,51 @@ public:
bool DebugCriticalAirAt(double X,double Y,double Z) const { return IsInsideCriticalAir(X,Y,Z); }
bool DebugSolidOverlayAt(double X,double Y,double Z) const { return IsInsideSolidOverlay(X,Y,Z); }
void SetEditTransaction(std::vector<FEditBrush> Brushes)
{
EditBrushes=std::move(Brushes);
EditBoundsMin={MAX_int32,MAX_int32,MAX_int32};
EditBoundsMax={MIN_int32,MIN_int32,MIN_int32};
for(const FEditBrush& Brush:EditBrushes)
{
double Extent[3]{};
if(Brush.Shape==FEditBrush::EShape::Sphere)
for(int32 Axis=0;Axis<3;++Axis)Extent[Axis]=Brush.Radius;
else if(Brush.Shape==FEditBrush::EShape::Capsule)
for(int32 Axis=0;Axis<3;++Axis)Extent[Axis]=Brush.Radius;
else
for(int32 Axis=0;Axis<3;++Axis)Extent[Axis]=Brush.HalfExtent[Axis]+Brush.Rounding;
for(int32 Axis=0;Axis<3;++Axis)
{
const double Lo=Brush.Shape==FEditBrush::EShape::Capsule
?std::min(Brush.A[Axis],Brush.B[Axis])-Extent[Axis]
:Brush.A[Axis]-Extent[Axis];
const double Hi=Brush.Shape==FEditBrush::EShape::Capsule
?std::max(Brush.A[Axis],Brush.B[Axis])+Extent[Axis]
:Brush.A[Axis]+Extent[Axis];
(&EditBoundsMin.X)[Axis]=std::min((&EditBoundsMin.X)[Axis],static_cast<int32>(std::floor(Lo)));
(&EditBoundsMax.X)[Axis]=std::max((&EditBoundsMax.X)[Axis],static_cast<int32>(std::ceil(Hi)));
}
}
}
bool HasEditTransaction() const { return !EditBrushes.empty(); }
FIntVector GetEditBoundsMin() const { return EditBoundsMin; }
FIntVector GetEditBoundsMax() const { return EditBoundsMax; }
void BuildFarWorld()
{
if(FarBuilt)return;
const auto Start=std::chrono::steady_clock::now();
auto Stage=std::chrono::steady_clock::now();
BuildFarParentShapes();
BuildSpatialBins(SpatialBinSize);
Stats.FarShapeSeconds=std::chrono::duration<double>(std::chrono::steady_clock::now()-Start).count();
Stats.FarPrimitiveSeconds=std::chrono::duration<double>(std::chrono::steady_clock::now()-Stage).count();
Stage=std::chrono::steady_clock::now();
BuildSpatialBins(SpatialBinSize,false);
Stats.FarBinSeconds=std::chrono::duration<double>(std::chrono::steady_clock::now()-Stage).count();
Stats.FarShapeSeconds=Stats.FarPrimitiveSeconds+Stats.FarBinSeconds;
Stats.PrimitiveCount=Primitives.size();FarBuilt=true;
std::printf("[FarShapeStage] %.3f ms primitives=%llu\n",Stats.FarShapeSeconds*1000.0,
std::printf("[FarShapeStage] parent_shapes=%.3f ms spatial_bins=%.3f ms total=%.3f ms primitives=%llu\n",
Stats.FarPrimitiveSeconds*1000.0,Stats.FarBinSeconds*1000.0,Stats.FarShapeSeconds*1000.0,
static_cast<unsigned long long>(Stats.PrimitiveCount));
std::fflush(stdout);
}
@@ -263,51 +437,85 @@ public:
int32 IX=0,IY=0,IZ=0;
const bool bInteger=NearInteger(X,IX)&&NearInteger(Y,IY)&&NearInteger(Z,IZ);
const bool bPacked=bInteger&&Contains(IX,IY,IZ);
const bool bRock=bPacked?IsRock(IX,IY,IZ):EvaluateFast(X,Y,Z);
if(bPacked)
return PackedDensityAt(IX,IY,IZ,IsRock(IX,IY,IZ),X,Y,Z);
return AnalyticDensityAt(X,Y,Z);
}
float DensityAtLattice(int32 X,int32 Y,int32 Z) const
{
const bool bPacked=Contains(X,Y,Z);
if(bPacked)
return PackedDensityAt(X,Y,Z,IsRock(X,Y,Z),static_cast<float>(X),
static_cast<float>(Y),static_cast<float>(Z));
return AnalyticDensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z));
}
private:
float PackedDensityAt(int32 X,int32 Y,int32 Z,bool bRock,float FX,float FY,float FZ) const
{
const size_t Column=static_cast<size_t>(Y-Min[1])*Size[0]+static_cast<size_t>(X-Min[0]);
const size_t Zi=static_cast<size_t>(Z-Min[2]);
const size_t Word=Column*WordsPerZ+(Zi>>6);
const uint64 Mask=uint64{1}<<(Zi&63u);
if(CandidateWords[Word]&Mask)
{
const size_t Column=static_cast<size_t>(IY-Min[1])*Size[0]+static_cast<size_t>(IX-Min[0]);
const size_t Zi=static_cast<size_t>(IZ-Min[2]);
const size_t Word=Column*WordsPerZ+(Zi>>6);
const uint64 Mask=uint64{1}<<(Zi&63u);
if(CandidateWords[Word]&Mask)
{
const size_t Node=Zi*static_cast<size_t>(Size[1])*Size[0]+Column;
const int8 Proxy=SurfaceDistanceQ[Node];
float Density=static_cast<float>(Proxy)*(1.0f/16.0f);
if((Density<0.0f)!=bRock)
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),1.0f/16.0f);
return Density;
}
// Match the fixed fallback to the edge of the stored proxy band. The
// displacement is tapered to zero there, so this avoids a magnitude
// jump along the candidate bit-mask boundary.
constexpr float ProxyBandEdge=DetailSupremum+0.125f;
return bRock?-ProxyBandEdge:ProxyBandEdge;
const size_t Node=Zi*static_cast<size_t>(Size[1])*Size[0]+Column;
const int8 Proxy=SurfaceDistanceQ[Node];
float Density=static_cast<float>(Proxy)*(1.0f/16.0f);
if((Density<0.0f)!=bRock)
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),1.0f/16.0f);
return ApplyEditBrushes(Density,FX,FY,FZ);
}
ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
bool bFloorSurface=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,
&bFloorSurface,&CaveInfluence,&FloorInfluence);
// Match the fixed fallback to the edge of the stored proxy band. The
// displacement is tapered to zero there, so this avoids a magnitude
// jump along the candidate bit-mask boundary.
constexpr float ProxyBandEdge=DetailSupremum+0.125f;
return ApplyEditBrushes(bRock?-ProxyBandEdge:ProxyBandEdge,FX,FY,FZ);
}
float AnalyticDensityAt(float X,float Y,float Z) const
{
const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]
&&Z>=Min[2]&&Z<=Max[2];
const bool bCriticalAir=IsInsideCriticalAir(X,Y,Z);
if(!bInsideFine)
{
bool bCriticalAir=false;
float RockDistance=CoarseRockDistance(X,Y,Z,&bCriticalAir);
if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f);
const bool bRock=RockDistance>=0.0f;
float Density=-RockDistance;
if((Density<0.0f)!=bRock||std::abs(Density)<0.05f)
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),0.05f);
return ApplyEditBrushes(Density,X,Y,Z);
}
ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,
&bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir);
if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth
&&std::abs(RockDistance)<=DetailSupremum+0.125f)
RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence)
*DetailBandFade(RockDistance);
if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f);
const bool bRock=RockDistance>=0.0f;
float Density=-RockDistance;
if((Density<0.0f)!=bRock||std::abs(Density)<0.05f)
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),0.05f);
return Density;
return ApplyEditBrushes(Density,X,Y,Z);
}
public:
EVoxelTileClass ClassifyBox(const FIntVector& Origin,int32 Step,int32 Cells) const
{
const int32 Pad=std::max(Step,1);
int32 BMin[3]={Origin.X-Pad,Origin.Y-Pad,Origin.Z-Pad};
int32 BMax[3]={Origin.X+(Cells+1)*Step,Origin.Y+(Cells+1)*Step,Origin.Z+(Cells+1)*Step};
if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0]
&&EditBoundsMin.Y<=BMax[1]&&EditBoundsMax.Y>=BMin[1]
&&EditBoundsMin.Z<=BMax[2]&&EditBoundsMax.Z>=BMin[2])
return EVoxelTileClass::Mixed;
for(int32 BZ=FloorDiv(BMin[2],ActiveSpatialBinSize);BZ<=FloorDiv(BMax[2],ActiveSpatialBinSize);++BZ)
for(int32 BY=FloorDiv(BMin[1],ActiveSpatialBinSize);BY<=FloorDiv(BMax[1],ActiveSpatialBinSize);++BY)
for(int32 BX=FloorDiv(BMin[0],ActiveSpatialBinSize);BX<=FloorDiv(BMax[0],ActiveSpatialBinSize);++BX)
@@ -655,6 +863,50 @@ public:
}
private:
float ApplyEditBrushes(float Density,double X,double Y,double Z) const
{
if(EditBrushes.empty()||X<EditBoundsMin.X||X>EditBoundsMax.X
||Y<EditBoundsMin.Y||Y>EditBoundsMax.Y||Z<EditBoundsMin.Z||Z>EditBoundsMax.Z)
return Density;
const double P[3]={X,Y,Z};
for(const FEditBrush& Brush:EditBrushes)
{
double Distance=0.0;
if(Brush.Shape==FEditBrush::EShape::Sphere)
{
const double DX=X-Brush.A[0],DY=Y-Brush.A[1],DZ=Z-Brush.A[2];
Distance=DeterministicSqrt(DX*DX+DY*DY+DZ*DZ)-Brush.Radius;
}
else if(Brush.Shape==FEditBrush::EShape::Capsule)
{
double AB[3]{},AP[3]{},LengthSq=0.0,Dot=0.0;
for(int32 Axis=0;Axis<3;++Axis)
{AB[Axis]=Brush.B[Axis]-Brush.A[Axis];AP[Axis]=P[Axis]-Brush.A[Axis];LengthSq+=AB[Axis]*AB[Axis];Dot+=AB[Axis]*AP[Axis];}
const double T=LengthSq>1.0e-12?std::clamp(Dot/LengthSq,0.0,1.0):0.0;
double D2=0.0;
for(int32 Axis=0;Axis<3;++Axis)
{const double D=AP[Axis]-T*AB[Axis];D2+=D*D;}
Distance=DeterministicSqrt(D2)-Brush.Radius;
}
else
{
double OutsideSq=0.0,MaxQ=-std::numeric_limits<double>::infinity();
for(int32 Axis=0;Axis<3;++Axis)
{
const double Q=std::abs(P[Axis]-Brush.A[Axis])-Brush.HalfExtent[Axis];
const double Outside=std::max(Q,0.0);
OutsideSq+=Outside*Outside;MaxQ=std::max(MaxQ,Q);
}
Distance=DeterministicSqrt(OutsideSq)+std::min(MaxQ,0.0)-Brush.Rounding;
}
if(Brush.Operation==FEditBrush::EOperation::Carve)
Density=std::max(Density,static_cast<float>(-Distance));
else
Density=std::min(Density,static_cast<float>(Distance));
}
return Density;
}
struct FAxisBins { int32 MinA=0,MinB=0,CountA=0,CountB=0; std::vector<std::vector<uint32>> Items; };
struct FLayerField
{
@@ -671,6 +923,12 @@ private:
Out=static_cast<int32>(R);return true;
}
static double DeterministicSqrt(double Value)
{
if(!(Value>0.0))return 0.0;
return _mm_cvtsd_f64(_mm_sqrt_sd(_mm_setzero_pd(),_mm_set_sd(Value)));
}
bool Contains(int32 X,int32 Y,int32 Z) const
{return X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]&&Z>=Min[2]&&Z<=Max[2];}
@@ -883,14 +1141,21 @@ private:
P.CapsuleStartRadius=ExtendedRadiusA;P.CapsuleEndRadius=ExtendedRadiusB;
P.MinRadius=std::min(ExtendedRadiusA,ExtendedRadiusB);P.BlendRadius=ActiveBlendRadius;
const double R=std::max(ExtendedRadiusA,ExtendedRadiusB);
double AxisLengthSquared=0.0;
for(int32 Axis=0;Axis<3;++Axis)
{
P.CapsuleStartSub[Axis]=static_cast<int32>(std::llround(Start[Axis]*SubPerVoxel));
P.CapsuleEndSub[Axis]=static_cast<int32>(std::llround(End[Axis]*SubPerVoxel));
P.CapsuleStartWorld[Axis]=P.CapsuleStartSub[Axis]/static_cast<double>(SubPerVoxel);
const double CapsuleEndWorld=P.CapsuleEndSub[Axis]/static_cast<double>(SubPerVoxel);
P.CapsuleAxisDelta[Axis]=CapsuleEndWorld-P.CapsuleStartWorld[Axis];
AxisLengthSquared+=P.CapsuleAxisDelta[Axis]*P.CapsuleAxisDelta[Axis];
P.CenterSub[Axis]=static_cast<int32>(std::llround((Start[Axis]+End[Axis])*0.5*SubPerVoxel));
P.Min[Axis]=static_cast<int32>(std::floor(std::min(Start[Axis],End[Axis])-R))-1;
P.Max[Axis]=static_cast<int32>(std::ceil(std::max(Start[Axis],End[Axis])+R))+1;
}
P.CapsuleAxisLengthSquared=AxisLengthSquared;
P.CapsuleAxisLength=std::sqrt(std::max(0.0,AxisLengthSquared));
if(bFloorClip)
{
const double FloorSlope=(FloorB-FloorA)/Length;
@@ -1584,12 +1849,20 @@ private:
ActiveBlendRadius=PreviousBlendRadius;
}
void BuildSpatialBins(int32 BinResolution)
void BuildSpatialBins(int32 BinResolution,bool bBuildAxisBins=true)
{
SpatialBins.clear();
CriticalAirBins.clear();
ActiveSpatialBinSize=BinResolution;
for(int32 Axis=0;Axis<3;++Axis)
// Primitive bounds overlap multiple cells. Reserve from the measured bins-per-primitive
// density for each grid instead of repeatedly rehashing from a 2x primitive guess.
const size_t BinsPerPrimitive=bBuildAxisBins?96u:24u;
SpatialBins.reserve(std::max<size_t>(Primitives.size()*BinsPerPrimitive,1u));
if(!bBuildAxisBins)
{
CriticalAirBins.reserve(Primitives.size()/16+1);
}
if(bBuildAxisBins)for(int32 Axis=0;Axis<3;++Axis)
{
FAxisBins& B=AxisBins[Axis];
const int32 A=(Axis+1)%3,C=(Axis+2)%3;
@@ -1681,7 +1954,8 @@ private:
const int32 MinY=static_cast<int32>(std::floor(std::min(A.Y,B.Y)-64.0));
const int32 MaxY=static_cast<int32>(std::ceil(std::max(A.Y,B.Y)+64.0));
if(MaxX>=Min[0]&&MinX<=Max[0]&&MaxY>=Min[1]&&MinY<=Max[1])continue;
AddCapsule(A,B,64.0f,true);
AddTaperedCapsule(A,B,64.0f,64.0f,true);
Primitives.back().BlendRadius=0.0;
}
}
if(!Parents.empty())
@@ -1787,16 +2061,16 @@ private:
double V[3],D[3],D2=0.0,VD=0.0;
for(int32 A=0;A<3;++A)
{
const double Start=P.CapsuleStartSub[A]/static_cast<double>(SubPerVoxel);
const double End=P.CapsuleEndSub[A]/static_cast<double>(SubPerVoxel);
V[A]=Position[A]-Start;D[A]=End-Start;D2+=D[A]*D[A];VD+=V[A]*D[A];
V[A]=Position[A]-P.CapsuleStartWorld[A];
D[A]=P.CapsuleAxisDelta[A];
VD+=V[A]*D[A];
}
const double AxisLength=std::sqrt(std::max(0.0,D2));
D2=P.CapsuleAxisLengthSquared;
const double T=D2>1.0e-12?std::clamp(VD/D2,0.0,1.0):0.0;
double DistanceSquared=0.0;
for(int32 A=0;A<3;++A)
{const double E=V[A]-T*D[A];DistanceSquared+=E*E;}
const double Radius=CapsuleRadiusAtT(P,T,AxisLength);
const double Radius=CapsuleRadiusAtT(P,T,P.CapsuleAxisLength);
return static_cast<float>(std::sqrt(DistanceSquared)-Radius);
}
if(P.bRoundedBox)
@@ -1949,7 +2223,7 @@ private:
}
}
void BuildPackedRockIntervals()
void BuildPackedRockIntervals(FParallelTiming& Timing)
{
const int32 NX=Size[0],NY=Size[1],NZ=Size[2];
ParallelLines(static_cast<size_t>(NX)*NY,[&](size_t Line)
@@ -1981,10 +2255,11 @@ private:
for(const FInterval& R:Solid)FillBits(RockWords,Base,R.Lo,R.Hi,true);
// Critical passage air is the final authored override, including at landing floors.
for(const FInterval& R:ProtectedAir)FillBits(RockWords,Base,R.Lo,R.Hi,false);
});
},&Timing);
}
void BuildCandidateBands()
void BuildCandidateBands(FParallelTiming& AxisTiming,FParallelTiming& MergeTiming,
double& MergeSeconds)
{
const int32 N[3]={Size[0],Size[1],Size[2]};
for(int32 Axis=0;Axis<3;++Axis)
@@ -2010,14 +2285,15 @@ private:
FillBits(CandidateAxisWords[Axis],Base,Lo-Min[Axis],Hi-Min[Axis],true);
FillBits(CandidateAxisWords[Axis],Base,Lo2-Min[Axis],Hi2-Min[Axis],true);
}
});
},&AxisTiming);
}
CandidateWords.assign(RockWords.size(),0);
for(int32 Axis=0;Axis<3;++Axis)
const auto MergeStart=std::chrono::steady_clock::now();
CandidateWords=CandidateAxisWords[2];
for(int32 Axis=0;Axis<2;++Axis)
{
const int32 A=(Axis+1)%3,B=(Axis+2)%3;
const size_t Words=(static_cast<size_t>(Size[Axis])+63u)/64u;
for(size_t Line=0;Line<static_cast<size_t>(Size[A])*Size[B];++Line)
ParallelLines(static_cast<size_t>(Size[A])*Size[B],[&,Axis,A,B,Words](size_t Line)
{
const int32 CA=Min[A]+static_cast<int32>(Line%Size[A]);
const int32 CB=Min[B]+static_cast<int32>(Line/Size[A]);
@@ -2033,10 +2309,11 @@ private:
const size_t Column=static_cast<size_t>(Coord[1]-Min[1])*Size[0]
+static_cast<size_t>(Coord[0]-Min[0]);
const size_t Zi=static_cast<size_t>(Coord[2]-Min[2]);
CandidateWords[Column*WordsPerZ+(Zi>>6)]|=uint64{1}<<(Zi&63u);
std::atomic_ref<uint64>(CandidateWords[Column*WordsPerZ+(Zi>>6)])
.fetch_or(uint64{1}<<(Zi&63u),std::memory_order_relaxed);
}
}
}
},&MergeTiming);
}
const int32 LayerLo=LayerRanges.empty()?1:LayerRanges[0].Lo;
const int32 LayerHi=LayerRanges.empty()?0:LayerRanges[0].Hi;
@@ -2053,15 +2330,20 @@ private:
FillBits(CandidateWords,Base,Lo1,Hi1,true);
}
}
const double MergeElapsed=std::chrono::duration<double>(
std::chrono::steady_clock::now()-MergeStart).count();
MergeSeconds=std::max(0.0,MergeElapsed-MergeTiming.WallSeconds);
}
void ApplyDetailBand()
void ApplyDetailBand(FParallelTiming& Timing)
{
std::atomic<uint64> CandidateCount{0},ProxyCount{0},FlipCount{0};
ParallelLines(static_cast<size_t>(Size[0])*Size[1],[&](size_t Line)
{
const int32 X=Min[0]+static_cast<int32>(Line%Size[0]);
const int32 Y=Min[1]+static_cast<int32>(Line/Size[0]);
FDetailNoiseLineContext NoiseLine(static_cast<float>(X),static_cast<float>(Y),
static_cast<uint32>(Owner->Seed));
uint64 LocalCandidates=0,LocalProxies=0,LocalFlips=0;
const size_t WordBase=Line*WordsPerZ;
for(size_t W=0;W<WordsPerZ;++W)
@@ -2074,13 +2356,13 @@ private:
const size_t Zi=W*64+Bit;if(Zi>=static_cast<size_t>(Size[2]))continue;
const int32 Z=Min[2]+static_cast<int32>(Zi);
++LocalCandidates;
const bool bCriticalAir=IsInsideCriticalAir(X,Y,Z);
ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
bool bFloorSurface=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
const float SDF=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,
&bFloorSurface,&CaveInfluence,&FloorInfluence);
&bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir);
if(std::abs(SDF)>DetailSupremum+0.125f)continue;
float Displaced=SDF+DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence)
float Displaced=SDF+DetailNoiseOnLine(X,Y,Z,SurfaceSDF,CaveInfluence,
FloorInfluence,NoiseLine)
*DetailBandFade(SDF);
if(bCriticalAir)Displaced=std::min(Displaced,-1.0f/16.0f);
const bool bNewRock=Displaced>=0.0f;
@@ -2101,7 +2383,7 @@ private:
CandidateCount.fetch_add(LocalCandidates,std::memory_order_relaxed);
ProxyCount.fetch_add(LocalProxies,std::memory_order_relaxed);
FlipCount.fetch_add(LocalFlips,std::memory_order_relaxed);
});
},&Timing);
Stats.SurfaceCandidates=CandidateCount.load(std::memory_order_relaxed);
Stats.SurfaceProxyNodes=ProxyCount.load(std::memory_order_relaxed);
Stats.DetailFlips=FlipCount.load(std::memory_order_relaxed);
@@ -2238,13 +2520,53 @@ private:
float* OutSurfaceSDF=nullptr,
bool* OutFloorSurface=nullptr,
float* OutCaveInfluence=nullptr,
float* OutFloorInfluence=nullptr) const
float* OutFloorInfluence=nullptr,
bool* OutCriticalAir=nullptr) const
{
const float Base=StrataDistance(Z);
float Cave=100000.0f;
float Overlay=-100000.0f;
float CaveFloorInfluence=0.0f;
const float Cave=CaveDistance(X,Y,Z,nullptr,
(OutFloorSurface||OutFloorInfluence)?&CaveFloorInfluence:nullptr);
const float Overlay=SolidOverlayDistance(X,Y,Z);
bool bCriticalAir=false;
const auto Bin=SpatialBins.find(PointBin(X,Y,Z));
if(Bin!=SpatialBins.end())
{
for(uint32 Id:Bin->second)
{
const FPrimitive& P=Primitives[Id];
if(P.bCarve)
{
if(P.bCritical&&X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1]
&&Z>=P.Min[2]&&Z<=P.Max[2]&&ShapeContains(P,
static_cast<int32>(X),static_cast<int32>(Y),static_cast<int32>(Z)))
bCriticalAir=true;
if(X<P.Min[0]-DetailBand||X>P.Max[0]+DetailBand
||Y<P.Min[1]-DetailBand||Y>P.Max[1]+DetailBand
||Z<P.Min[2]-DetailBand||Z>P.Max[2]+DetailBand)continue;
float Distance=ShapeDistance(P,X,Y,Z);
float PrimitiveFloorInfluence=0.0f;
if(P.bHasFloorClip)
{
const double Floor=FloorAt(P,X,Y);
const float FloorT=(static_cast<float>(std::abs(Z-Floor))
-static_cast<float>(P.FloorBlendRadius))
/static_cast<float>(std::max(DetailBand*2,1));
PrimitiveFloorInfluence=1.0f-SmoothUnit(FloorT);
Distance=SmoothMaxDistance(Distance,static_cast<float>(Floor-Z),
static_cast<float>(P.FloorBlendRadius));
}
const float MetadataWidth=static_cast<float>(std::max(P.BlendRadius,6.0));
const float PrimitiveWeight=SmoothUnit(0.5f+(Cave-Distance)/(2.0f*MetadataWidth));
const float Combined=SmoothMinDistance(Cave,Distance,static_cast<float>(P.BlendRadius));
CaveFloorInfluence=std::lerp(CaveFloorInfluence,PrimitiveFloorInfluence,PrimitiveWeight);
Cave=Combined;
}
else if(X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1]
&&Z>=P.Min[2]&&Z<=P.Max[2])
Overlay=std::max(Overlay,-ShapeDistance(P,X,Y,Z));
}
}
if(OutCriticalAir)*OutCriticalAir=bCriticalAir;
const float CarveOrOverlay=std::max(Cave,Overlay);
const ESurfaceFamily Family=Base<=CarveOrOverlay?ESurfaceFamily::Strata:
(Cave>=Overlay?ESurfaceFamily::Cave:ESurfaceFamily::SolidOverlay);
@@ -2265,6 +2587,44 @@ private:
return RockDistance;
}
float CoarseRockDistance(double X,double Y,double Z,bool* OutCriticalAir) const
{
const float Base=StrataDistance(Z);
float Cave=100000.0f,Overlay=-100000.0f;
bool bCriticalAir=false;
const auto Bin=SpatialBins.find(PointBin(X,Y,Z));
if(Bin!=SpatialBins.end())
{
for(uint32 Id:Bin->second)
{
const FPrimitive& P=Primitives[Id];
if(P.bCarve)
{
if(P.bCritical&&X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1]
&&Z>=P.Min[2]&&Z<=P.Max[2]&&ShapeContains(P,
static_cast<int32>(X),static_cast<int32>(Y),static_cast<int32>(Z)))
bCriticalAir=true;
if(X<P.Min[0]-DetailBand||X>P.Max[0]+DetailBand
||Y<P.Min[1]-DetailBand||Y>P.Max[1]+DetailBand
||Z<P.Min[2]-DetailBand||Z>P.Max[2]+DetailBand)continue;
float Distance=ShapeDistance(P,X,Y,Z);
if(P.bHasFloorClip)
{
const double Floor=FloorAt(P,X,Y);
Distance=SmoothMaxDistance(Distance,static_cast<float>(Floor-Z),
static_cast<float>(P.FloorBlendRadius));
}
Cave=SmoothMinDistance(Cave,Distance,static_cast<float>(P.BlendRadius));
}
else if(X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1]
&&Z>=P.Min[2]&&Z<=P.Max[2])
Overlay=std::max(Overlay,-ShapeDistance(P,X,Y,Z));
}
}
if(OutCriticalAir)*OutCriticalAir=bCriticalAir;
return SmoothMinDistance(Base,std::max(Cave,Overlay),StrataCaveBlendRadius);
}
float OwnerCaveRoughness(double X,double Y,double Z,float SurfaceSDF,
const FStrateGenerationParams& Params,
float CaveInfluence) const
@@ -2334,6 +2694,77 @@ private:
return 1.00f*Macro+(0.80f+0.25f*Macro)*(0.85f*Mid+0.18f*Fine);
}
struct FValueNoiseLineContext
{
int32 X0=0,Y0=0,Z0=std::numeric_limits<int32>::min();
int32 Wavelength=1;
uint32 Seed=0;
float Inv=1.0f,TX=0.0f,TY=0.0f;
std::array<float,8> Corners{};
FValueNoiseLineContext(float X,float Y,int32 InWavelength,uint32 InSeed)
:Wavelength(InWavelength),Seed(InSeed),Inv(1.0f/static_cast<float>(InWavelength))
{
const float FX=X*Inv,FY=Y*Inv;
X0=FMath::FloorToInt(FX);Y0=FMath::FloorToInt(FY);
TX=FStandaloneResearchTunnels::Fade(FX-static_cast<float>(X0));
TY=FStandaloneResearchTunnels::Fade(FY-static_cast<float>(Y0));
}
void SetPlane(int32 Z,bool bUpper)
{
const size_t I=bUpper?4u:0u;
Corners[I+0]=FStandaloneResearchTunnels::NoiseValue(X0,Y0,Z,Seed);
Corners[I+1]=FStandaloneResearchTunnels::NoiseValue(X0+1,Y0,Z,Seed);
Corners[I+2]=FStandaloneResearchTunnels::NoiseValue(X0,Y0+1,Z,Seed);
Corners[I+3]=FStandaloneResearchTunnels::NoiseValue(X0+1,Y0+1,Z,Seed);
}
float Sample(float Z)
{
const float FZ=Z*Inv;
const int32 NewZ0=FMath::FloorToInt(FZ);
const float TZ=FStandaloneResearchTunnels::Fade(FZ-static_cast<float>(NewZ0));
if(NewZ0!=Z0)
{
if(NewZ0==Z0+1)
{
Corners[0]=Corners[4];Corners[1]=Corners[5];
Corners[2]=Corners[6];Corners[3]=Corners[7];
SetPlane(NewZ0+1,true);
}
else
{
SetPlane(NewZ0,false);SetPlane(NewZ0+1,true);
}
Z0=NewZ0;
}
const float Z00=FMath::Lerp(Corners[0],Corners[1],TX);
const float Z10=FMath::Lerp(Corners[2],Corners[3],TX);
const float Z01=FMath::Lerp(Corners[4],Corners[5],TX);
const float Z11=FMath::Lerp(Corners[6],Corners[7],TX);
return FMath::Lerp(FMath::Lerp(Z00,Z10,TY),FMath::Lerp(Z01,Z11,TY),TZ);
}
};
struct FDetailNoiseLineContext
{
FValueNoiseLineContext Macro,Mid,Fine;
FDetailNoiseLineContext(float X,float Y,uint32 Seed)
:Macro(X,Y,32,Seed^0xB5297A4Du),Mid(X,Y,16,Seed^0x63D83595u),
Fine(X,Y,6,Seed^0xA511E9B3u)
{}
float SurfaceReliefAt(float Z)
{
const float MacroValue=Macro.Sample(Z);
const float MidValue=Mid.Sample(Z);
const float FineValue=Fine.Sample(Z);
return 1.00f*MacroValue+(0.80f+0.25f*MacroValue)
*(0.85f*MidValue+0.18f*FineValue);
}
};
float DetailNoise(double X,double Y,double Z,float SurfaceSDF,
float CaveInfluence,float FloorInfluence) const
{
@@ -2353,13 +2784,27 @@ private:
return std::clamp((Relief+OwnerRelief*0.5f)*Contrast,-3.9f,3.9f);
}
float DetailNoiseOnLine(double X,double Y,double Z,float SurfaceSDF,
float CaveInfluence,float FloorInfluence,
FDetailNoiseLineContext& NoiseLine) const
{
if(DetailVariant!=EDetailVariant::Coherent)
return DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence);
const float Relief=NoiseLine.SurfaceReliefAt(static_cast<float>(Z));
const FLayerField* Field=FindLayerField(Z);
const float OwnerRelief=Field?OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field->Params,
CaveInfluence):0.0f;
const float Contrast=1.35f-0.80f*std::clamp(CaveInfluence,0.0f,1.0f)
+1.85f*std::clamp(FloorInfluence,0.0f,1.0f);
return std::clamp((Relief+OwnerRelief*0.5f)*Contrast,-3.9f,3.9f);
}
bool EvaluateFast(double X,double Y,double Z) const
{
const bool bCriticalAir=IsInsideCriticalAir(X,Y,Z);
ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
bool bFloorSurface=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,
&bFloorSurface,&CaveInfluence,&FloorInfluence);
&bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir);
const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]&&Z>=Min[2]&&Z<=Max[2];
if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth
&&std::abs(RockDistance)<=DetailSupremum+0.125f)
@@ -2421,19 +2866,44 @@ private:
return Count?PairAB/std::sqrt(std::max(1.0e-12,PairAA*PairBB)):0.0;
}
template<class Fn> void ParallelLines(size_t Count,Fn&& Work) const
template<class Fn> void ParallelLines(size_t Count,Fn&& Work,FParallelTiming* Timing=nullptr) const
{
const auto WallStart=std::chrono::steady_clock::now();
if(WorkerCount<=1||Count<512)
{for(size_t I=0;I<Count;++I)Work(I);return;}
std::atomic<size_t> Next{0};std::vector<std::thread> Threads;Threads.reserve(WorkerCount);
{
const auto WorkerStart=std::chrono::steady_clock::now();
for(size_t I=0;I<Count;++I)Work(I);
if(Timing)
{
const double WorkerSeconds=std::chrono::duration<double>(std::chrono::steady_clock::now()-WorkerStart).count();
Timing->WallSeconds+=std::chrono::duration<double>(std::chrono::steady_clock::now()-WallStart).count();
Timing->WorkerSeconds+=WorkerSeconds;
}
return;
}
std::atomic<size_t> Next{0};std::atomic<uint64> WorkerNanoseconds{0};
std::vector<std::thread> Threads;Threads.reserve(WorkerCount);
for(int32 W=0;W<WorkerCount;++W)
Threads.emplace_back([&](){for(;;){const size_t I=Next.fetch_add(1,std::memory_order_relaxed);if(I>=Count)break;Work(I);}});
Threads.emplace_back([&]()
{
const auto WorkerStart=std::chrono::steady_clock::now();
for(;;){const size_t I=Next.fetch_add(1,std::memory_order_relaxed);if(I>=Count)break;Work(I);}
WorkerNanoseconds.fetch_add(static_cast<uint64>(std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now()-WorkerStart).count()),std::memory_order_relaxed);
});
for(std::thread& T:Threads)T.join();
if(Timing)
{
Timing->WallSeconds+=std::chrono::duration<double>(std::chrono::steady_clock::now()-WallStart).count();
Timing->WorkerSeconds+=static_cast<double>(WorkerNanoseconds.load(std::memory_order_relaxed))*1.0e-9;
}
}
FStandaloneOwnerWorld* Owner=nullptr;
int32 WorkerCount=6;
int32 Min[3]{},Max[3]{},ClipMin[3]{},ClipMax[3]{},Size[3]{};
FIntVector EditBoundsMin=FIntVector(MAX_int32,MAX_int32,MAX_int32);
FIntVector EditBoundsMax=FIntVector(MIN_int32,MIN_int32,MIN_int32);
int32 ActiveSpatialBinSize=LineBinSize;
double ActiveBlendRadius=0.0;
double ActiveFloorBlendRadius=0.0;
@@ -2444,9 +2914,10 @@ private:
std::vector<FInterval> LayerRanges;
std::vector<FLayerField> LayerFields;
std::vector<FPrimitive> Primitives;
std::vector<FEditBrush> EditBrushes;
std::array<FAxisBins,3> AxisBins;
std::unordered_map<FBinKey,std::vector<uint32>,FBinHash> SpatialBins;
std::unordered_map<FBinKey,std::vector<uint32>,FBinHash> CriticalAirBins;
std::unordered_map<FBinKey,FBinItems,FBinHash> SpatialBins;
std::unordered_map<FBinKey,FBinItems,FBinHash> CriticalAirBins;
std::vector<uint64> RockWords;
std::unique_ptr<int8[]> SurfaceDistanceQ;
std::array<std::vector<uint64>,3> CandidateAxisWords;
+149 -17
View File
@@ -670,7 +670,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
}
if(!FineTiles){Error="no full-detail tiles are available for shape compilation";return false;}
ShapeWorld=std::make_unique<FStandaloneShapeWorld>();
if(!ShapeWorld->Build(*World,Min,Max,ClipMin,ClipMax,WorkerCount,ShapeDetailVariant,bIncludeFarWorld,Error))
if(!ShapeWorld->Build(*World,Min,Max,ClipMin,ClipMax,WorkerCount,
ShapeDetailVariant,bIncludeFarWorld,Error))
{ShapeWorld.reset();return false;}
FStandaloneShapeWorldAccess::SetActive(ShapeWorld.get());
bFarShapeStagePending=bShapeWorld&&!ShapeWorld->HasFarWorld();
@@ -1721,11 +1722,24 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
Desired.Tiles.begin(),Desired.Tiles.end(),[&](const FVoxelTileKey& Tile)
{return Tile.Level<World.FullResClipLevels;}));
std::array<std::vector<FHashedTile>,MaxLevels+1> HashRows;
std::array<std::vector<FHashedTile>,MaxLevels+1> MeshHashRows;
std::array<uint64,MaxLevels+1> SamplesByLevel{};
std::array<uint64,MaxLevels+1> VerticesByLevel{};
std::array<uint64,MaxLevels+1> IndicesByLevel{};
std::array<uint64,MaxLevels+1> TilesByLevel{};
std::array<double,MaxLevels+1> WorkerSecondsByLevel{};
std::array<double,MaxLevels+1> FirstCompletionByLevel{};
std::array<double,MaxLevels+1> LastCompletionByLevel{};
std::array<uint64,MaxLevels+1> OutputBytesByLevel{};
FirstCompletionByLevel.fill(std::numeric_limits<double>::infinity());
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> PlayerNeighborhoodTiles;
for(const FVoxelTileKey& Tile:Desired.Tiles)
if(Tile.Level==0&&std::abs(Tile.Coord.X-Desired.CenterChunk.X)<=1
&&std::abs(Tile.Coord.Y-Desired.CenterChunk.Y)<=1
&&std::abs(Tile.Coord.Z-Desired.CenterChunk.Z)<=1)
PlayerNeighborhoodTiles.insert(Tile);
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> PlayerNeighborhoodReady;
auto PlayerNeighborhoodCompleted=std::chrono::steady_clock::time_point{};
std::vector<FStandaloneCpuTile> CacheTiles;
if(!BakeCachePath.empty())CacheTiles.reserve(Desired.Tiles.size());
const auto Start=std::chrono::steady_clock::now();
@@ -1758,6 +1772,12 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
{
++Finished;
const int32 Level=Tile.Key.Level;
if(PlayerNeighborhoodTiles.contains(Tile.Key))
{
PlayerNeighborhoodReady.insert(Tile.Key);
if(PlayerNeighborhoodReady.size()==PlayerNeighborhoodTiles.size())
PlayerNeighborhoodCompleted=Tile.CompletedAt;
}
if(Level>=0&&Level<=MaxLevels)
{
SamplesByLevel[Level]+=Tile.DensitySamples;
@@ -1765,7 +1785,13 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
IndicesByLevel[Level]+=Tile.Indices.size();
++TilesByLevel[Level];
WorkerSecondsByLevel[Level]+=Tile.WorkerSeconds;
const double CompletedSeconds=std::chrono::duration<double>(Tile.CompletedAt-Start).count();
FirstCompletionByLevel[Level]=std::min(FirstCompletionByLevel[Level],CompletedSeconds);
LastCompletionByLevel[Level]=std::max(LastCompletionByLevel[Level],CompletedSeconds);
OutputBytesByLevel[Level]+=static_cast<uint64>(Tile.Vertices.capacity())*sizeof(FStandaloneCpuVertex)
+static_cast<uint64>(Tile.Indices.capacity())*sizeof(uint32);
if(Tile.bHasDensityHash)HashRows[Level].push_back({Tile.Key,Tile.DensityHash});
if(Tile.bHasMeshHash)MeshHashRows[Level].push_back({Tile.Key,Tile.MeshHash});
}
if(Level<World.FullResClipLevels)++FullDetailFinished;
if(bShardAudit&&Level==0)
@@ -1793,12 +1819,17 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
FullDetailSeconds=std::chrono::duration<double>(
std::chrono::steady_clock::now()-Start).count();
std::vector<FHashedTile> FullRows;
std::vector<FHashedTile> FullMeshRows;
for(int32 Level=0;Level<World.FullResClipLevels&&Level<=MaxLevels;++Level)
{
FullRows.insert(FullRows.end(),HashRows[Level].begin(),HashRows[Level].end());
FullMeshRows.insert(FullMeshRows.end(),MeshHashRows[Level].begin(),MeshHashRows[Level].end());
}
if(bHashDensity||!BakeCachePath.empty())
std::printf("[StreamFullDetail] wall=%.3fs generated=%d/%d resident_tiles_cpu_only=1 density_hash=0x%016llx\n",
std::printf("[StreamFullDetail] wall=%.3fs generated=%d/%d resident_tiles_cpu_only=1 density_hash=0x%016llx mesh_hash=0x%016llx\n",
FullDetailSeconds,FullDetailFinished,FullDetailExpected,
static_cast<unsigned long long>(HashTileRows(FullRows)));
static_cast<unsigned long long>(HashTileRows(FullRows)),
static_cast<unsigned long long>(HashTileRows(FullMeshRows)));
else
std::printf("[StreamFullDetail] wall=%.3fs generated=%d/%d resident_tiles_cpu_only=1 hash=off\n",
FullDetailSeconds,FullDetailFinished,FullDetailExpected);
@@ -1807,18 +1838,34 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
}
const double FullClipSeconds=std::chrono::duration<double>(
std::chrono::steady_clock::now()-Start).count();
std::vector<FHashedTile> AllRows;
if(bHashDensity||!BakeCachePath.empty())
for(auto& LevelRows:HashRows)AllRows.insert(AllRows.end(),LevelRows.begin(),LevelRows.end());
if(bHashDensity||!BakeCachePath.empty())
std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs tiles=%d density_hash=0x%016llx\n",
FullClipSeconds,Finished,DesiredTotal,
std::accumulate(WorkerSecondsByLevel.begin(),WorkerSecondsByLevel.end(),0.0),
DesiredTotal,static_cast<unsigned long long>(HashTileRows(AllRows)));
if(!PlayerNeighborhoodTiles.empty()&&PlayerNeighborhoodReady.size()==PlayerNeighborhoodTiles.size())
std::printf("[PlayerNeighborhood] lod0_tiles=%zu/%zu mesh_ready=%.3fs collision_cook=unavailable_in_standalone\n",
PlayerNeighborhoodReady.size(),PlayerNeighborhoodTiles.size(),
std::chrono::duration<double>(PlayerNeighborhoodCompleted-Start).count());
else
std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs tiles=%d hash=off\n",
FullClipSeconds,Finished,DesiredTotal,
std::accumulate(WorkerSecondsByLevel.begin(),WorkerSecondsByLevel.end(),0.0),DesiredTotal);
std::printf("[PlayerNeighborhood] lod0_tiles=%zu/%zu mesh_ready=missing collision_cook=unavailable_in_standalone\n",
PlayerNeighborhoodReady.size(),PlayerNeighborhoodTiles.size());
const double TotalWorkerSeconds=std::accumulate(WorkerSecondsByLevel.begin(),WorkerSecondsByLevel.end(),0.0);
const double WorkerBusyPercent=100.0*TotalWorkerSeconds
/(std::max(1,WorkerCount)*std::max(0.001,FullClipSeconds));
std::vector<FHashedTile> AllRows;
std::vector<FHashedTile> AllMeshRows;
if(bHashDensity||!BakeCachePath.empty())
for(int32 Level=0;Level<=MaxLevels;++Level)
{
AllRows.insert(AllRows.end(),HashRows[Level].begin(),HashRows[Level].end());
AllMeshRows.insert(AllMeshRows.end(),MeshHashRows[Level].begin(),MeshHashRows[Level].end());
}
if(bHashDensity||!BakeCachePath.empty())
std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs worker_busy=%.1f%% worker_idle=%.1f%% tiles=%d density_hash=0x%016llx mesh_hash=0x%016llx\n",
FullClipSeconds,Finished,DesiredTotal,TotalWorkerSeconds,WorkerBusyPercent,
std::max(0.0,100.0-WorkerBusyPercent),
DesiredTotal,static_cast<unsigned long long>(HashTileRows(AllRows)),
static_cast<unsigned long long>(HashTileRows(AllMeshRows)));
else
std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs worker_busy=%.1f%% worker_idle=%.1f%% tiles=%d hash=off\n",
FullClipSeconds,Finished,DesiredTotal,TotalWorkerSeconds,WorkerBusyPercent,
std::max(0.0,100.0-WorkerBusyPercent),DesiredTotal);
std::printf("[StreamCacheUse] hits=%llu requested=%d\n",
static_cast<unsigned long long>(Streamer.GetCacheHits()),DesiredTotal);
if(bShardAudit)
@@ -1835,16 +1882,23 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
{
if(TilesByLevel[Level]==0)continue;
if(bHashDensity||!BakeCachePath.empty())
std::printf("[StreamLevel] lod=%d tiles=%llu samples=%.3fM worker_sum=%.3fs vertices=%llu indices=%llu density_hash=0x%016llx\n",
std::printf("[StreamLevel] lod=%d tiles=%llu samples=%.3fM worker_sum=%.3fs first_ready=%.3fs complete=%.3fs worker_busy=%.1f%% output=%.2fMiB vertices=%llu indices=%llu density_hash=0x%016llx mesh_hash=0x%016llx\n",
Level,static_cast<unsigned long long>(TilesByLevel[Level]),
static_cast<double>(SamplesByLevel[Level])/1.0e6,WorkerSecondsByLevel[Level],
FirstCompletionByLevel[Level],LastCompletionByLevel[Level],
100.0*WorkerSecondsByLevel[Level]/(WorkerCount*std::max(0.001,LastCompletionByLevel[Level])),
static_cast<double>(OutputBytesByLevel[Level])/1048576.0,
static_cast<unsigned long long>(VerticesByLevel[Level]),
static_cast<unsigned long long>(IndicesByLevel[Level]),
static_cast<unsigned long long>(HashTileRows(HashRows[Level])));
static_cast<unsigned long long>(HashTileRows(HashRows[Level])),
static_cast<unsigned long long>(HashTileRows(MeshHashRows[Level])));
else
std::printf("[StreamLevel] lod=%d tiles=%llu samples=%.3fM worker_sum=%.3fs vertices=%llu indices=%llu hash=off\n",
std::printf("[StreamLevel] lod=%d tiles=%llu samples=%.3fM worker_sum=%.3fs first_ready=%.3fs complete=%.3fs worker_busy=%.1f%% output=%.2fMiB vertices=%llu indices=%llu hash=off\n",
Level,static_cast<unsigned long long>(TilesByLevel[Level]),
static_cast<double>(SamplesByLevel[Level])/1.0e6,WorkerSecondsByLevel[Level],
FirstCompletionByLevel[Level],LastCompletionByLevel[Level],
100.0*WorkerSecondsByLevel[Level]/(WorkerCount*std::max(0.001,LastCompletionByLevel[Level])),
static_cast<double>(OutputBytesByLevel[Level])/1048576.0,
static_cast<unsigned long long>(VerticesByLevel[Level]),
static_cast<unsigned long long>(IndicesByLevel[Level]));
}
@@ -1864,6 +1918,78 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
if(bShardAudit&&(MeshSheetStats.ThinSheets!=0||MeshSheetStats.SmallComponentsInView!=0))return 13;
return 0;
}
int RunEditTransactionBenchmark(FStandaloneOwnerWorld& World,FStandaloneShapeWorld& Shape,
const FVector& CameraPositionVoxels,int32 WorkerCount)
{
using Clock=std::chrono::steady_clock;
// Place the spell across the LOD0/LOD1 clipmap seam so one edit exercises every visible
// level it intersects, instead of timing only the center LOD0 tiles.
const double X=CameraPositionVoxels.X+3.0*CHUNK_SIZE;
const double Y=CameraPositionVoxels.Y,Z=CameraPositionVoxels.Z;
std::vector<FStandaloneShapeWorld::FEditBrush> Brushes(3);
Brushes[0].Shape=FStandaloneShapeWorld::FEditBrush::EShape::Sphere;
Brushes[0].Operation=FStandaloneShapeWorld::FEditBrush::EOperation::Carve;
Brushes[0].A={X,Y,Z};Brushes[0].Radius=20.0;
Brushes[1].Shape=FStandaloneShapeWorld::FEditBrush::EShape::RoundedBox;
Brushes[1].Operation=FStandaloneShapeWorld::FEditBrush::EOperation::Carve;
Brushes[1].A={X,Y,Z+12.0};Brushes[1].HalfExtent={11.0,11.0,4.0};Brushes[1].Rounding=3.0;
Brushes[2].Shape=FStandaloneShapeWorld::FEditBrush::EShape::Capsule;
Brushes[2].Operation=FStandaloneShapeWorld::FEditBrush::EOperation::AddRock;
Brushes[2].A={X-22.0,Y,Z-8.0};Brushes[2].B={X+22.0,Y,Z-8.0};Brushes[2].Radius=3.0;
const auto EditStart=Clock::now();
Shape.SetEditTransaction(std::move(Brushes));
FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels);
const FIntVector EditMin=Shape.GetEditBoundsMin(),EditMax=Shape.GetEditBoundsMax();
std::vector<FVoxelTileKey> Affected;Affected.reserve(128);
std::array<uint64,MaxLevels+1> AffectedByLevel{},FinishedByLevel{};
for(const FVoxelTileKey& Tile:Desired.Tiles)
{
const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels();
const int32 Pad=std::max(2,Tile.StepVoxels());
if(Origin.X-Pad>EditMax.X||Origin.X+Extent+Pad<EditMin.X
||Origin.Y-Pad>EditMax.Y||Origin.Y+Extent+Pad<EditMin.Y
||Origin.Z-Pad>EditMax.Z||Origin.Z+Extent+Pad<EditMin.Z)continue;
Affected.push_back(Tile);
if(Tile.Level>=0&&Tile.Level<=MaxLevels)++AffectedByLevel[Tile.Level];
}
if(Affected.empty())
{
std::fprintf(stderr,"[EditTransaction] failed: no desired tiles intersect the edit bounds.\n");
return 9;
}
FStandaloneStreamingGenerator Streamer(World,WorkerCount,false,nullptr,-1);
Streamer.SetDesiredTileFilter(Affected);
const auto Start=Clock::now();
const FStandaloneStreamingUpdate Update=Streamer.Start(CameraPositionVoxels);
int32 Finished=0;
double WorkerSeconds=0.0;
while(Finished<Update.DesiredTiles)
{
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;WorkerSeconds+=Tile.WorkerSeconds;
if(Tile.Key.Level>=0&&Tile.Key.Level<=MaxLevels)++FinishedByLevel[Tile.Key.Level];
}
}
const double RemeshWall=std::chrono::duration<double>(Clock::now()-Start).count();
Streamer.Stop();
std::printf("[EditTransaction] operation=crater_batch transaction_count=1 brushes=3 diameter=10.0m offset_from_player=%.2fm affected_tiles=%d/%zu workers=%d cache_hits=%llu remesh_wall=%.3fs worker_sum=%.3fs edit_to_ready=%.3fs status=%s\n",
3.0*CHUNK_SIZE*VOXEL_SIZE/100.0,
Finished,Affected.size(),WorkerCount,static_cast<unsigned long long>(Streamer.GetCacheHits()),
RemeshWall,WorkerSeconds,std::chrono::duration<double>(Clock::now()-EditStart).count(),
Finished==static_cast<int32>(Affected.size())?"PASS":"FAIL");
for(int32 Level=0;Level<=MaxLevels;++Level)
if(AffectedByLevel[Level])
std::printf("[EditTransactionLevel] lod=%d tiles=%llu remeshed=%llu status=%s\n",Level,
static_cast<unsigned long long>(AffectedByLevel[Level]),
static_cast<unsigned long long>(FinishedByLevel[Level]),
AffectedByLevel[Level]==FinishedByLevel[Level]?"PASS":"FAIL");
return Finished==static_cast<int32>(Affected.size())?0:10;
}
}
int main(int Argc,char** Argv)
@@ -1878,6 +2004,7 @@ int main(int Argc,char** Argv)
bool bStreamBenchmark=false;
bool bShardAudit=false;
bool bStreamHash=false;
bool bEditBenchmark=false;
bool bRoundWallCompare=false;
int32 RoundCompareTileCount=16;
std::string BakeMeshCachePath;
@@ -1907,6 +2034,8 @@ int main(int Argc,char** Argv)
else if(Arg=="--round-tunnels-legacy")
{App.bRoundTunnels=true;App.bRoundTunnelEnhanced=false;}
else if(Arg=="--stream-benchmark")bStreamBenchmark=true;
else if(Arg=="--edit-benchmark")
{bEditBenchmark=true;bStreamBenchmark=true;App.bShapeWorld=true;}
else if(Arg=="--stream-hash")bStreamHash=true;
else if(Arg=="--shard-audit")bShardAudit=true;
else if(Arg=="--compare-round-walls")
@@ -1984,6 +2113,9 @@ int main(int Argc,char** Argv)
return RunRoundWallComparison(*App.World,App.Position,RoundCompareTileCount);
if(bStreamBenchmark)
{
if(bEditBenchmark)
return RunEditTransactionBenchmark(*App.World,*App.ShapeWorld,
FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0),App.WorkerCount);
const double FullDetailWidth=static_cast<double>(2*App.ClipRadius+2)
*CHUNK_SIZE*VOXEL_SIZE/100.0;
std::printf("Standalone clip override: exported radius=%d (112 m cube) -> radius=%d (%.0f m LOD0 cube), voxel size=25 cm.\n",
+45
View File
@@ -6,6 +6,7 @@
#include <algorithm>
#include <array>
#include <bit>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <fstream>
@@ -36,11 +37,36 @@ struct FStandaloneCpuTile
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;
std::array<uint64,VoxelDensityProfile::BucketCount> ProfileCycles{};
};
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;
}
struct FStandaloneMeshCacheMetadata
{
uint64 SourceDataHash=0;
@@ -308,6 +334,13 @@ public:
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()
{
{
@@ -330,6 +363,9 @@ public:
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());
@@ -409,12 +445,16 @@ private:
{
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.bStandaloneCompactPayload=true;
Mesher.bGenerateSkirts=World->bGenerateSkirts;
Mesher.SkirtCells=static_cast<float>(World->SkirtCells);
Mesher.LODOctaveDrop=World->LODOctaveDrop;
@@ -460,7 +500,10 @@ private:
}
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;
}
@@ -500,6 +543,8 @@ private:
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;
};
@@ -4080,6 +4080,19 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
return Result;
}
float UVoxelGenerator::GetDensityAtLattice(int32 WorldX, int32 WorldY, int32 WorldZ,
float* OutPreEditDensity) const
{
if (const FStandaloneShapeWorld* ShapeWorld=FStandaloneShapeWorldAccess::GetActive())
{
const float ShapeDensity=ShapeWorld->DensityAtLattice(WorldX,WorldY,WorldZ);
if (OutPreEditDensity) *OutPreEditDensity=ShapeDensity;
return ShapeDensity;
}
return GetDensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
static_cast<float>(WorldZ),OutPreEditDensity);
}
float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
const FStrateGenerationParams& Params,
uint32 ParamsFingerprint, uint32 LayoutVersion,
@@ -144,6 +144,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
// plus de (ré)allocation de hash-map par tuile (chaque worker a son propre exemplaire).
static thread_local TMap<FIntVector, int32> VertexMap;
VertexMap.Reset();
const int32 VertexCapacityHint=FMath::Clamp(InCellsPerAxis,2,CHUNK_SIZE);
VertexMap.Reserve(VertexCapacityHint*VertexCapacityHint*2);
//=========================================================================
// F17 — CLASSE DE SURFACE (sol vs plafond sky-cap), par vertex → par triangle
@@ -158,8 +160,11 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
// ~zéro sur une tuile de sol pur, borné par lattice² sur une tuile de cap.
static thread_local TArray<uint8> VertexClasses; // 0 = sol, 1 = sky-cap
static thread_local TMap<FIntVector, FVector2f> SurfColMemo; // (Xq,Yq,chunkZ) → (TerrainZ, CeilSurf) ; X=FLT_MAX ⇒ pas SurfaceWorld
VertexClasses.Reset();
SurfColMemo.Reset();
if (!bStandaloneCompactPayload)
{
VertexClasses.Reset();
SurfColMemo.Reset();
}
// Normale fournie par l'appelant (gradient lu dans la grille de densité, T1.b) —
// plus d'échantillonnage de densité par vertex. RawNormal pointe solide→air ; on la
@@ -189,65 +194,56 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
}
MeshData.Normals.Add(Normal);
// UVs planaires — le triplanar mapping se fait dans le matériau.
MeshData.UVs.Add(FVector2D(WorldPos.X / VOXEL_SIZE, WorldPos.Y / VOXEL_SIZE));
//---------------------------------------------------------------------
// VERTEX COLOUR — masques pour le matériau triplanar maître (F6).
// R = index de palette du biome DOMINANT (0-255) — re-skin par biome.
// G = pente (0 = sol/plafond plat, 1 = paroi verticale) — roche sur falaises.
// B = poids de fondu de bordure (0 au cœur d'un biome → ~0.5 à la frontière).
// A = index de palette du biome VOISIN — le matériau lerp(R,A) par B → bords sans couture.
// La hauteur/snow-line se déduit de WorldPosition.Z dans le matériau (pas besoin de canal).
// Coût: une résolution biome par vertex UNIQUE (déduplication) ; nul si la strate n'a pas
// de biomes (GetBiomeMaterialAt sort en O(1) → palette 0 partout).
int32 PalD = 0, PalN = 0; float BlendW = 0.0f;
Generator->GetBiomeMaterialAt(WorldPos.X / VOXEL_SIZE, WorldPos.Y / VOXEL_SIZE,
WorldPos.Z / VOXEL_SIZE, PalD, PalN, BlendW);
const uint8 R = (uint8)FMath::Clamp(PalD, 0, 255);
const uint8 A = (uint8)FMath::Clamp(PalN, 0, 255);
const uint8 G = (uint8)FMath::Clamp(FMath::RoundToInt((1.0f - FMath::Abs((float)Normal.Z)) * 255.0f), 0, 255);
const uint8 Bc = (uint8)FMath::Clamp(FMath::RoundToInt(BlendW * 255.0f), 0, 255);
MeshData.Colors.Add(FColor(R, G, Bc, A));
// F17 — classe de surface (voir le bloc en tête de fonction). Down-facing seulement :
// les vertex de sol/falaise (N.Z ≥ -0.1) sont "sol" sans payer la requête colonne.
uint8 SurfClass = 0;
if (Normal.Z < -0.1f)
if (!bStandaloneCompactPayload)
{
const float Zv = WorldPos.Z / VOXEL_SIZE;
const float StepF = (float)Step;
const FIntVector MemoKey(
FMath::RoundToInt(WorldPos.X / (VOXEL_SIZE * StepF)),
FMath::RoundToInt(WorldPos.Y / (VOXEL_SIZE * StepF)),
FMath::FloorToInt(Zv / (float)CHUNK_SIZE));
FVector2f* Col = SurfColMemo.Find(MemoKey);
if (!Col)
// UVs planaires — le triplanar mapping se fait dans le matériau.
MeshData.UVs.Add(FVector2D(WorldPos.X / VOXEL_SIZE, WorldPos.Y / VOXEL_SIZE));
//-----------------------------------------------------------------
// VERTEX COLOUR — masks for biome palette, slope and border blend.
int32 PalD = 0, PalN = 0; float BlendW = 0.0f;
Generator->GetBiomeMaterialAt(WorldPos.X / VOXEL_SIZE, WorldPos.Y / VOXEL_SIZE,
WorldPos.Z / VOXEL_SIZE, PalD, PalN, BlendW);
const uint8 R = (uint8)FMath::Clamp(PalD, 0, 255);
const uint8 A = (uint8)FMath::Clamp(PalN, 0, 255);
const uint8 G = (uint8)FMath::Clamp(FMath::RoundToInt((1.0f - FMath::Abs((float)Normal.Z)) * 255.0f), 0, 255);
const uint8 Bc = (uint8)FMath::Clamp(FMath::RoundToInt(BlendW * 255.0f), 0, 255);
MeshData.Colors.Add(FColor(R, G, Bc, A));
}
if (!bStandaloneCompactPayload)
{
// F17 — classify down-facing vertices for material-only ceiling grouping.
uint8 SurfClass = 0;
if (Normal.Z < -0.1f)
{
float TerrainZ = 0.0f, CeilSurf = 0.0f;
// Un vertex du cap vit à la FRONTIÈRE HAUTE de la strate : arrondi/interpolation
// peuvent le faire flotter dans le chunk gap/seal juste AU-DESSUS (hors
// SurfaceWorld ⇒ sonde ratée ⇒ faux "sol"). On retente un chunk plus bas.
const bool bSurf = Generator->GetSurfaceHeightAt(
(float)(MemoKey.X * Step), (float)(MemoKey.Y * Step), MemoKey.Z,
TerrainZ, CeilSurf)
|| Generator->GetSurfaceHeightAt(
(float)(MemoKey.X * Step), (float)(MemoKey.Y * Step), MemoKey.Z - 1,
TerrainZ, CeilSurf);
Col = &SurfColMemo.Add(MemoKey, bSurf ? FVector2f(TerrainZ, CeilSurf)
: FVector2f(FLT_MAX, -FLT_MAX));
const float Zv = WorldPos.Z / VOXEL_SIZE;
const float StepF = (float)Step;
const FIntVector MemoKey(
FMath::RoundToInt(WorldPos.X / (VOXEL_SIZE * StepF)),
FMath::RoundToInt(WorldPos.Y / (VOXEL_SIZE * StepF)),
FMath::FloorToInt(Zv / (float)CHUNK_SIZE));
FVector2f* Col = SurfColMemo.Find(MemoKey);
if (!Col)
{
float TerrainZ = 0.0f, CeilSurf = 0.0f;
const bool bSurf = Generator->GetSurfaceHeightAt(
(float)(MemoKey.X * Step), (float)(MemoKey.Y * Step), MemoKey.Z,
TerrainZ, CeilSurf)
|| Generator->GetSurfaceHeightAt(
(float)(MemoKey.X * Step), (float)(MemoKey.Y * Step), MemoKey.Z - 1,
TerrainZ, CeilSurf);
Col = &SurfColMemo.Add(MemoKey, bSurf ? FVector2f(TerrainZ, CeilSurf)
: FVector2f(FLT_MAX, -FLT_MAX));
}
if (Col->X != FLT_MAX && FMath::Abs(Zv - Col->Y) < FMath::Abs(Zv - Col->X)
&& Zv <= Col->Y + 2.0f * StepF)
{
SurfClass = 1;
}
}
// Plus proche du plafond que du terrain ⇒ sky-cap. (Hors SurfaceWorld : sol.)
// + garde : un vrai vertex de cap est AU niveau du cap (± slop d'interpolation) ;
// loin AU-DESSUS = plancher/toit de la strate d'à côté atteint via le retry chunkZ-1
// (strates contiguës) → reste sol.
if (Col->X != FLT_MAX && FMath::Abs(Zv - Col->Y) < FMath::Abs(Zv - Col->X)
&& Zv <= Col->Y + 2.0f * StepF)
{
SurfClass = 1;
}
VertexClasses.Add(SurfClass);
}
VertexClasses.Add(SurfClass);
return NewIndex;
};
@@ -919,7 +915,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
((ReachBlockZ * ReachBlocksPerAxis) + ReachBlockY)
* ReachBlocksPerAxis + ReachBlockX;
float PreEditDensity = 0.0f;
DensityGrid[LocalIndex] = Generator->GetDensityAt(
DensityGrid[LocalIndex] = Generator->GetDensityAtLattice(
OriginVoxels.X + gx * Step,
OriginVoxels.Y + gy * Step,
OriginVoxels.Z + gz * Step,
@@ -1244,14 +1240,18 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
const int32 Idx1 = GetOrCreateVertex(EdgeVertices[E1], EdgeNormals[E1]);
const int32 Idx2 = GetOrCreateVertex(EdgeVertices[E2], EdgeNormals[E2]);
// F17 — vote majoritaire (≥ 2 vertex sky-cap ⇒ triangle sky-cap).
const int32 CapVotes = (int32)VertexClasses[Idx0]
+ (int32)VertexClasses[Idx1]
+ (int32)VertexClasses[Idx2];
TArray<int32>& Dst = (CapVotes >= 2) ? CapTris : GroundTris;
Dst.Add(Idx0);
Dst.Add(Idx2);
Dst.Add(Idx1);
TArray<int32>* Dst = &GroundTris;
if (!bStandaloneCompactPayload)
{
// F17 — majority vote (at least two ceiling vertices => ceiling triangle).
const int32 CapVotes = (int32)VertexClasses[Idx0]
+ (int32)VertexClasses[Idx1]
+ (int32)VertexClasses[Idx2];
Dst = (CapVotes >= 2) ? &CapTris : &GroundTris;
}
Dst->Add(Idx0);
Dst->Add(Idx2);
Dst->Add(Idx1);
}
if (bSampleCell)
{
@@ -1314,8 +1314,11 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
const int32 Idx = MeshData.Vertices.Num();
MeshData.Vertices.Add(Pos);
MeshData.Normals.Add(Nrm);
MeshData.UVs.Add(FVector2D(Pos.X / VOXEL_SIZE, Pos.Y / VOXEL_SIZE));
MeshData.Colors.Add(Col); // hérite la couleur du vertex source (parallèle aux autres tableaux)
if (!bStandaloneCompactPayload)
{
MeshData.UVs.Add(FVector2D(Pos.X / VOXEL_SIZE, Pos.Y / VOXEL_SIZE));
MeshData.Colors.Add(Col); // parallel with the other vertex arrays
}
return Idx;
};
@@ -1339,8 +1342,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
const FVector NA = MeshData.Normals[iA];
const FVector NB = MeshData.Normals[iB];
const FColor CA = MeshData.Colors[iA];
const FColor CB = MeshData.Colors[iB];
const FColor CA = bStandaloneCompactPayload ? FColor::White : MeshData.Colors[iA];
const FColor CB = bStandaloneCompactPayload ? FColor::White : MeshData.Colors[iB];
const int32 iA2 = AddSkirtVert(PA - NA * SkirtDepth, NA, CA);
const int32 iB2 = AddSkirtVert(PB - NB * SkirtDepth, NB, CB);
@@ -171,6 +171,11 @@ public:
float GetDensityAt(float WorldX, float WorldY, float WorldZ,
float* OutPreEditDensity = nullptr) const;
// Exact integer-grid dispatch used by the standalone tile mesher. It skips the
// float-to-lattice tolerance check while retaining the active field's same values.
float GetDensityAtLattice(int32 WorldX, int32 WorldY, int32 WorldZ,
float* OutPreEditDensity = nullptr) const;
/**
* Open a worker-local lattice hand-off for the following GetDensityAt calls. The mesher uses
* this around one tile's density-grid fill. The first sample in each resolved chunk lets the
@@ -116,6 +116,11 @@ public:
// SETTINGS
//=========================================================================
// Standalone CPU streaming consumes positions, normals and indices only. When enabled,
// GenerateMesh omits UVs, colors and material-only ceiling grouping. Runtime callers leave
// this false and keep the complete FVoxelMeshData contract.
bool bStandaloneCompactPayload = false;
// Seuil de l'isosurface dans le champ de densité.
// Convention MC: densité < IsoLevel = solide, >= = air.
float IsoLevel = 0.0f;