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:
1 parent
97c0455939
commit
ee5b05b357
16 files changed
+1011
-156
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user