Files
VoxelForge/Standalone/Core/src/ShapeWorldInternal.h
T
Fr0zkaandClaude Opus 5.5 6ddb834b1c wip(assembler): round S2 (Sol) -- shared per-tile feature queries for every Core family; island placement 30x faster
Island-only worker context replaced by a shared worker-local candidate domain
(WorldAssemblyFeatureTileListV1 / ScopedWorldAssemblyFeatureQueriesV1) serving
SurfaceWorld, FloatingIslands and far-shell queries; a per-domain cost hint keeps the
full scan on small plans. Content placement prepares the same queries and skips proven
air: owner island cell p95 7.07 -> 0.24 ms; populated fixture 572 exact placements,
3.27 s -> 29 ms. Full/candidate hashes equal with dropped-feature controls on both
families; S1 default W1/W8/W16 hashes reproduced; Core-off and M2 fixtures pass.
Default contract for future families in ARCHITECTURE 8.23. Run on gpt-6.1-sol medium.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2026-10-18 19:50:45 +02:00

5201 lines
271 KiB
C++

#pragma once
#include "OwnerWorldLoader.h"
#include "StandaloneResearchTunnels.h"
#include "VoxelCaveMorphology.h"
#include "VoxelNoise.h"
#include "VoxelStrateManager.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
#include "VoxelForgeFieldShared.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <bit>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <limits>
#include <memory>
#include <mutex>
#include <immintrin.h>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
// Standalone-only world compiler. It compiles the runtime room/tunnel/passages into
// axis-line intervals, composes rock/cave signs into packed Z-column words, then displaces
// only the conservative surface band. The copied Unreal generator remains the A/B reference.
class FStandaloneShapeWorld
{
public:
double GetCoreVoxelSizeCm() const
{
return Owner == nullptr || Owner->CoreVoxelSizeMicrons == 0
? static_cast<double>(VOXEL_SIZE)
: static_cast<double>(Owner->CoreVoxelSizeMicrons) / 10000.0;
}
double GetCoreVoxelsPerOwnerChunk() const
{
const double OwnerVoxelSizeCm = Owner == nullptr || Owner->OwnerVoxelSizeMicrons == 0
? 0.5 * static_cast<double>(VOXEL_SIZE)
: static_cast<double>(Owner->OwnerVoxelSizeMicrons) / 10000.0;
return static_cast<double>(CHUNK_SIZE) * OwnerVoxelSizeCm / GetCoreVoxelSizeCm();
}
enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel };
struct FBoxClassification
{
EVoxelTileClass Verdict=EVoxelTileClass::Mixed;
bool bNativeSurface=false;
bool bUnknown=false;
};
struct FClassificationThreadStats
{
uint64 SolidBoxes=0,AirBoxes=0,MixedBoxes=0,UnknownBoxes=0;
uint64 NativeFloatFieldCalls=0,NativeLatticeFieldCalls=0,NativeUnclampedFieldCalls=0;
double Milliseconds=0.0;
};
static void ResetThreadClassificationStats() { ThreadClassificationStats()={}; }
static FClassificationThreadStats GetThreadClassificationStats()
{ return ThreadClassificationStats(); }
struct FFarConnectorSegment
{
FVector Start=FVector::ZeroVector;
FVector End=FVector::ZeroVector;
float Radius=0.0f;
};
struct FEditBrush
{
enum class EShape : uint8 { Sphere, Capsule, RoundedBox };
enum class EOperation : uint8 { Carve, AddRock, DensityOffset };
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;
float Strength=0.0f;
float Falloff=0.0f;
};
struct FParallelTiming
{
double WallSeconds = 0.0;
double WorkerSeconds = 0.0;
};
struct FDetailWorkerProfile
{
uint64 Lines=0,Candidates=0,DetailEvaluations=0,Flips=0;
uint64 LocalBandSkips=0;
uint64 GpuVisualCandidates=0,CpuVisualCandidates=0,GpuOnlyCandidates=0;
uint64 GpuVisualDetailEvaluations=0,GpuOnlyDetailEvaluations=0;
uint64 DistanceSamples=0,DistanceSampleNanoseconds=0;
uint64 DetailSamples=0,DetailSampleNanoseconds=0;
uint64 ReliefSampleNanoseconds=0,LayerLookupSampleNanoseconds=0;
uint64 OwnerRoughnessSampleNanoseconds=0,OwnerRoughnessFieldCalls=0;
uint64 OwnerRoughnessActiveCalls=0,OwnerNoiseCacheHits=0,OwnerNoiseCacheMisses=0;
};
struct FBuildStats
{
double ShapeSeconds = 0.0;
double IntervalSeconds = 0.0;
double SurfaceSeconds = 0.0;
double FarShapeSeconds = 0.0;
uint64 PrimitiveCount = 0;
uint64 RoomCount = 0;
uint64 RoomFloorJoinCount = 0;
uint64 TunnelCount = 0;
uint64 PassageCount = 0;
uint64 SignNodes = 0;
uint64 SurfaceCandidates = 0;
uint64 SurfaceProxyNodes = 0;
uint64 DetailFlips = 0;
uint64 WalkSamples = 0;
uint64 WalkFailures = 0;
double MaxCapsuleJoinRadiusDelta = 0.0;
double MaxCapsuleJoinRadiusSlopeDelta = 0.0;
double MaxCapsuleJoinFloorDelta = 0.0;
double MaxCapsuleJoinFloorSlopeDelta = 0.0;
double MaxRawUnmarkedFloorSlopeDelta = 0.0;
double MaxUnmarkedFloorSlopeDelta = 0.0;
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 DetailApplySeconds = 0.0;
double SignEdgeDistanceSeconds = 0.0;
double PassageValidationSeconds = 0.0;
double SharedFieldSeconds = 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;
double SmoothedProfileRadiusStep = 0.0;
double MaxTunnelBendDegrees = 0.0;
FVector MaxTunnelBendVoxels = FVector::ZeroVector;
FVector MaxTunnelBendDirection = FVector::ZeroVector;
uint64 CapsuleJoinCount = 0;
};
struct FPrimitive
{
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]{};
float MinRadius = 1.0f;
float CapsuleStartRadius = 1.0f;
float CapsuleEndRadius = 1.0f;
float CapsuleProfileStartRadius = 1.0f;
float CapsuleProfileEndRadius = 1.0f;
double CapsuleProfileStartT = 0.0;
double CapsuleProfileEndT = 1.0;
double CapsuleRadiusSlopeStart = 0.0;
double CapsuleRadiusSlopeEnd = 0.0;
double BlendRadius = 0.0;
double FloorBlendRadius = 0.0;
bool bCarve = true;
bool bCritical = false;
bool bPassageSurface = false;
bool bCapsule = false;
bool bSmoothCapsuleRadius = false;
bool bRoundedBox = false;
bool bHasFloorClip = false;
bool bCurvedFloorClip = false;
double RoundedHalfExtent[3]{};
double Rounding = 0.0;
double FloorCenterZ = 0.0;
double FloorSlopeX = 0.0;
double FloorSlopeY = 0.0;
double FloorCurveOriginX = 0.0;
double FloorCurveOriginY = 0.0;
double FloorCurveDeltaX = 0.0;
double FloorCurveDeltaY = 0.0;
double FloorCurveLength = 0.0;
double FloorCurveStartZ = 0.0;
double FloorCurveEndZ = 0.0;
double FloorCurveTangentStart = 0.0;
double FloorCurveTangentEnd = 0.0;
double FloorCurveStartBlend = 0.0;
double FloorCurveEndBlend = 0.0;
double FloorCurveMaxDeviation = 0.0;
};
struct FInterval { int32 Lo = 0, Hi = -1; };
struct FBinKey { int32 X=0,Y=0,Z=0; bool operator==(const FBinKey&)const=default; };
struct FBinHash
{
size_t operator()(const FBinKey& K) const noexcept
{
uint64 H=static_cast<uint32>(K.X)*0x9E3779B185EBCA87ull;
H^=static_cast<uint64>(static_cast<uint32>(K.Y))*0xC2B2AE3D27D4EB4Full;
H^=static_cast<uint64>(static_cast<uint32>(K.Z))*0x165667B19E3779F9ull;
H^=H>>29;H*=0xBF58476D1CE4E5B9ull;H^=H>>31;
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;
static constexpr int32 DetailBand = 6;
static constexpr float DetailSupremum = 4.0f;
static constexpr double JoinBlendRadius = 1.5;
static constexpr double RoomUnionBlendRadius = 12.0;
static constexpr double FloorClipBlendRadius = 16.0;
static constexpr float StrataCaveBlendRadius = 12.0f;
static constexpr int32 LineBinSize = 32;
static constexpr int32 SpatialBinSize = 256;
bool Build(FStandaloneOwnerWorld& World, const FIntVector& MinVoxels,
const FIntVector& MaxVoxels, const FIntVector& ClipMinVoxels,
const FIntVector& ClipMaxVoxels, int32 Workers, EDetailVariant Variant,
bool bIncludeFarWorld,
std::string& Error,
const VoxelForgeCore::JobScheduler* InScheduler=nullptr,
bool bInRoundTunnels=false,bool bInProfileDetailBand=false,
bool bValidatePassageRoutes=true,
const std::vector<VoxelForgeCore::DetailBandTileRegion>& InGpuVisualDetailTiles={},
const std::vector<VoxelForgeCore::DetailBandTileRegion>& InCpuVisualDetailTiles={},
const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly=nullptr)
{
using Clock=std::chrono::steady_clock;
const auto TotalStart=Clock::now();
Owner=&World;WorldAssembly=InWorldAssembly;
WorkerCount=std::clamp(Workers,1,16);DetailVariant=Variant;
Scheduler=InScheduler;RoundTunnelsEnabled=bInRoundTunnels;
bAnalyticOnly=false;
bProfileDetailBand=bInProfileDetailBand;
for(int32 A=0;A<3;++A)
{
Min[A]=(&MinVoxels.X)[A]-2;
Max[A]=(&MaxVoxels.X)[A]+2;
ClipMin[A]=(&ClipMinVoxels.X)[A];
ClipMax[A]=(&ClipMaxVoxels.X)[A];
Size[A]=Max[A]-Min[A]+1;
if(Size[A]<=0||Size[A]>4096||ClipMax[A]<ClipMin[A])
{Error="shape interval region is invalid or outside the bounded 4096-node compiler envelope";return false;}
}
const uint64 NodeCount=static_cast<uint64>(Size[0])*Size[1]*Size[2];
if(NodeCount>160000000ull)
{Error="shape interval region exceeds the 160-million-node safety bound";return false;}
Stats={};Stats.SignNodes=NodeCount;
auto Stage=Clock::now();
BuildLayerBounds();
auto Substage=Clock::now();
BuildOwnerPrimitives();
Stats.LocalPrimitiveSeconds=std::chrono::duration<double>(Clock::now()-Substage).count();
Substage=Clock::now();
if(RoundTunnelsEnabled)
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<int16[]>(static_cast<size_t>(NodeCount));
BuildPackedRockIntervals(Stats.RockIntervalLines);
BuildCandidateBands(Stats.CandidateAxisLines,Stats.CandidateMergeLines,
Stats.CandidateMergeSeconds);
if(!BuildGpuVisualDetailNodeSets(InGpuVisualDetailTiles,InCpuVisualDetailTiles,Error))
return false;
Stats.IntervalSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
Stage=Clock::now();
Substage=Clock::now();
ApplyDetailBand(Stats.DetailBandLines);
Stats.DetailApplySeconds=std::chrono::duration<double>(Clock::now()-Substage).count();
Substage=Clock::now();
Stats.SurfaceProxyNodes+=CompleteSignEdgeDistances();
Stats.SignEdgeDistanceSeconds=std::chrono::duration<double>(Clock::now()-Substage).count();
Stats.SurfaceSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
// Route validation is world-wide. Detail regions reuse the same authored route data and
// do not need to repeat the 5,642-point world audit for every entering tile group.
Substage=Clock::now();
if(bValidatePassageRoutes&&!ValidatePassageRoutes())
{
Error="compiled shape world contains a solid passage centreline sample";
return false;
}
Stats.PassageValidationSeconds=std::chrono::duration<double>(Clock::now()-Substage).count();
Substage=Clock::now();
RebuildSharedFieldPrimitives();
Stats.SharedFieldSeconds=std::chrono::duration<double>(Clock::now()-Substage).count();
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("[ShapeSubstage] detail_apply=%.3fms sign_edge_distances=%.3fms passage_validation=%.3fms shared_field_primitives=%.3fms far_parents=%.3fms\n",
Stats.DetailApplySeconds*1000.0,Stats.SignEdgeDistanceSeconds*1000.0,
Stats.PassageValidationSeconds*1000.0,Stats.SharedFieldSeconds*1000.0,
Stats.FarShapeSeconds*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(int16))/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),
static_cast<unsigned long long>(Stats.TunnelCount),
static_cast<unsigned long long>(Stats.PassageCount),
static_cast<unsigned long long>(Stats.SignNodes),
static_cast<double>(WordBytes)/1048576.0,
static_cast<unsigned long long>(Stats.SurfaceProxyNodes),
static_cast<double>(Stats.SignNodes*sizeof(int16))/1048576.0,WorkerCount);
std::printf("[ShapeStage] local-shapes=%.3f ms intervals+signs=%.3f ms surface-band=%.3f ms far-parents=%.3f ms total=%.3f ms candidates=%llu flips=%llu walk=%llu/%llu\n",
Stats.ShapeSeconds*1000.0,Stats.IntervalSeconds*1000.0,Stats.SurfaceSeconds*1000.0,Stats.FarShapeSeconds*1000.0,
std::chrono::duration<double>(Clock::now()-TotalStart).count()*1000.0,
static_cast<unsigned long long>(Stats.SurfaceCandidates),
static_cast<unsigned long long>(Stats.DetailFlips),
static_cast<unsigned long long>(Stats.WalkSamples-Stats.WalkFailures),
static_cast<unsigned long long>(Stats.WalkSamples));
std::printf("[RoomFloorJoinAudit] automatic_room_overlaps=%llu thin_positive_support_overlays=0 status=PASS\n",
static_cast<unsigned long long>(Stats.RoomFloorJoinCount));
const double SmoothUnionGrowthBound=JoinBlendRadius/6.0;
const bool bFloorSlopePass=Stats.MaxUnmarkedFloorSlopeDelta<=0.75;
const bool bFloorBlendPass=Stats.MaxFloorCurveDeviation<=2.0;
std::printf("[FloorSlopeJoinAudit] raw_unmarked_max_delta=%.5f vox generated_tangent_delta=%.5f vox bend_direction_slope_change=%.5f vox marked_max_delta=%.5f vox marked_joins=%llu threshold=0.750 status=%s\n",
Stats.MaxRawUnmarkedFloorSlopeDelta,Stats.MaxUnmarkedFloorSlopeDelta,
Stats.MaxUnmarkedFloorDirectionSlopeDelta,Stats.MaxMarkedFloorSlopeDelta,
static_cast<unsigned long long>(Stats.MarkedFloorSlopeJoins),bFloorSlopePass?"PASS":"FAIL");
std::printf("[FloorProfileBlend] c1_segments=%llu max_curve_deviation=%.3f vox threshold=2.000 status=%s\n",
static_cast<unsigned long long>(Stats.CurvedFloorSegments),Stats.MaxFloorCurveDeviation,
bFloorBlendPass?"PASS":"FAIL");
std::printf("[TunnelRadiusAudit] shared_joins=%llu legacy_fixed_segment_max_step=%.4f vox smoothed_profile_max_step=%.4f vox tapered_endpoint_max_delta=%.6f vox radius_slope_join_delta=%.8f floor_endpoint_max_delta=%.6f vox floor_slope_join_delta=%.5f unmarked_floor_slope_delta=%.5f marked_floor_slope_delta=%.5f marked_slope_joins=%llu smooth_union_growth_bound=%.4f vox thresholds=(0.005,0.000001,0.005,0.25) status=%s\n",
static_cast<unsigned long long>(Stats.CapsuleJoinCount),Stats.LegacySegmentRadiusStep,
Stats.SmoothedProfileRadiusStep,Stats.MaxCapsuleJoinRadiusDelta,
Stats.MaxCapsuleJoinRadiusSlopeDelta,
Stats.MaxCapsuleJoinFloorDelta,Stats.MaxCapsuleJoinFloorSlopeDelta,
Stats.MaxUnmarkedFloorSlopeDelta,Stats.MaxMarkedFloorSlopeDelta,
static_cast<unsigned long long>(Stats.MarkedFloorSlopeJoins),SmoothUnionGrowthBound,
Stats.MaxCapsuleJoinRadiusDelta<=0.005&&Stats.MaxCapsuleJoinRadiusSlopeDelta<=1.0e-6
&&Stats.MaxCapsuleJoinFloorDelta<=0.005
&&SmoothUnionGrowthBound<=0.25?"PASS":"FAIL");
const double BendHorizontal=std::sqrt(Stats.MaxTunnelBendDirection.X*Stats.MaxTunnelBendDirection.X
+Stats.MaxTunnelBendDirection.Y*Stats.MaxTunnelBendDirection.Y);
const double BendYaw=std::atan2(Stats.MaxTunnelBendDirection.X,Stats.MaxTunnelBendDirection.Y)*57.29577951308232;
const double BendPitch=std::atan2(Stats.MaxTunnelBendDirection.Z,BendHorizontal)*57.29577951308232;
std::printf("[TunnelBendAudit] sharpest_bend=%.2f deg centerline=(%.2f,%.2f,%.2f) vox tangent=(%.4f,%.4f,%.4f) camera_yaw=%.2f camera_pitch=%.2f\n",
Stats.MaxTunnelBendDegrees,Stats.MaxTunnelBendVoxels.X,
Stats.MaxTunnelBendVoxels.Y,Stats.MaxTunnelBendVoxels.Z,
Stats.MaxTunnelBendDirection.X,Stats.MaxTunnelBendDirection.Y,
Stats.MaxTunnelBendDirection.Z,BendYaw,BendPitch);
std::fflush(stdout);
return true;
}
bool BuildAnalyticRegion(FStandaloneOwnerWorld& World,const FIntVector& MinVoxels,
const FIntVector& MaxVoxels,const FIntVector& ClipMinVoxels,
const FIntVector& ClipMaxVoxels,EDetailVariant Variant,
bool bInRoundTunnels,std::string& Error)
{
using Clock=std::chrono::steady_clock;
Owner=&World;WorkerCount=1;DetailVariant=Variant;Scheduler=nullptr;
RoundTunnelsEnabled=bInRoundTunnels;bAnalyticOnly=true;bProfileDetailBand=false;
for(int32 A=0;A<3;++A)
{
Min[A]=(&MinVoxels.X)[A]-2;Max[A]=(&MaxVoxels.X)[A]+2;
ClipMin[A]=(&ClipMinVoxels.X)[A];ClipMax[A]=(&ClipMaxVoxels.X)[A];
Size[A]=Max[A]-Min[A]+1;
if(Size[A]<=0||Size[A]>4096||ClipMax[A]<ClipMin[A])
{Error="analytic tile region is invalid or outside the bounded 4096-node compiler envelope";return false;}
}
Stats={};
const auto Stage=Clock::now();
BuildLayerBounds();BuildOwnerPrimitives();
if(RoundTunnelsEnabled)BuildRoundTunnelPrimitives();
BuildSpatialBins(LineBinSize);
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;}
RebuildSharedFieldPrimitives();
Built=true;
return true;
}
const FBuildStats& GetStats() const { return Stats; }
bool IsBuilt() const { return Built; }
bool HasFarWorld() const { return FarBuilt; }
const std::vector<FFarConnectorSegment>& GetFarConnectorSegments() const
{ return FarConnectorSegments; }
void FindFarConnectorCandidates(const FIntVector& QueryMin,const FIntVector& QueryMax,
std::vector<uint32>& OutIndices) const
{
constexpr int32 BinSize=2048;
OutIndices.clear();
const int32 MinX=FloorDiv(QueryMin.X,BinSize),MaxX=FloorDiv(QueryMax.X,BinSize);
const int32 MinY=FloorDiv(QueryMin.Y,BinSize),MaxY=FloorDiv(QueryMax.Y,BinSize);
const int32 MinZ=FloorDiv(QueryMin.Z,BinSize),MaxZ=FloorDiv(QueryMax.Z,BinSize);
for(int32 Z=MinZ;Z<=MaxZ;++Z)for(int32 Y=MinY;Y<=MaxY;++Y)
for(int32 X=MinX;X<=MaxX;++X)
{
const auto It=FarConnectorSpatialBins.find({X,Y,Z});
if(It!=FarConnectorSpatialBins.end())
OutIndices.insert(OutIndices.end(),It->second.begin(),It->second.end());
}
std::sort(OutIndices.begin(),OutIndices.end());
OutIndices.erase(std::unique(OutIndices.begin(),OutIndices.end()),OutIndices.end());
}
static uint32 FarFieldBinHash(int32 X,int32 Y,int32 Z)
{
uint32 Hash=2166136261u;
Hash=(Hash^static_cast<uint32>(X))*16777619u;
Hash=(Hash^static_cast<uint32>(Y))*16777619u;
Hash=(Hash^static_cast<uint32>(Z))*16777619u;
Hash^=Hash>>16;Hash*=0x7FEB352Du;Hash^=Hash>>15;
Hash*=0x846CA68Bu;Hash^=Hash>>16;
return Hash;
}
static VoxelForgeCore::FarFieldPrimitiveGpu ToFarFieldGpuPrimitive(const VF_FieldPrimitive& S)
{
VoxelForgeCore::FarFieldPrimitiveGpu G{};
G.flags=S.flags;
for(int32 A=0;A<3;++A)
{
G.minVoxel[A]=S.minVoxel[A];G.maxVoxel[A]=S.maxVoxel[A];
G.center[A]=static_cast<float>(S.center[A]);
G.capsuleStart[A]=static_cast<float>(S.capsuleStart[A]);
G.capsuleDelta[A]=static_cast<float>(S.capsuleDelta[A]);
G.roundedHalfExtent[A]=static_cast<float>(S.roundedHalfExtent[A]);
}
for(int32 I=0;I<9;++I)G.q[I]=static_cast<float>(S.q[I]);
G.minRadius=static_cast<float>(S.minRadius);
G.capsuleAxisLengthSquared=static_cast<float>(S.capsuleAxisLengthSquared);
G.capsuleAxisLength=static_cast<float>(S.capsuleAxisLength);
G.capsuleStartRadius=static_cast<float>(S.capsuleStartRadius);
G.capsuleEndRadius=static_cast<float>(S.capsuleEndRadius);
G.profileStartT=static_cast<float>(S.profileStartT);G.profileEndT=static_cast<float>(S.profileEndT);
G.profileStartRadius=static_cast<float>(S.profileStartRadius);G.profileEndRadius=static_cast<float>(S.profileEndRadius);
G.radiusSlopeStart=static_cast<float>(S.radiusSlopeStart);G.radiusSlopeEnd=static_cast<float>(S.radiusSlopeEnd);
G.rounding=static_cast<float>(S.rounding);
G.blendRadius=static_cast<float>(S.blendRadius);G.floorBlendRadius=static_cast<float>(S.floorBlendRadius);
G.floorCenterZ=static_cast<float>(S.floorCenterZ);G.floorSlopeX=static_cast<float>(S.floorSlopeX);
G.floorSlopeY=static_cast<float>(S.floorSlopeY);
G.floorCurveOriginX=static_cast<float>(S.floorCurveOriginX);G.floorCurveOriginY=static_cast<float>(S.floorCurveOriginY);
G.floorCurveDeltaX=static_cast<float>(S.floorCurveDeltaX);G.floorCurveDeltaY=static_cast<float>(S.floorCurveDeltaY);
G.floorCurveLength=static_cast<float>(S.floorCurveLength);
G.floorCurveStartZ=static_cast<float>(S.floorCurveStartZ);G.floorCurveEndZ=static_cast<float>(S.floorCurveEndZ);
G.floorCurveTangentStart=static_cast<float>(S.floorCurveTangentStart);
G.floorCurveTangentEnd=static_cast<float>(S.floorCurveTangentEnd);
G.floorCurveStartBlend=static_cast<float>(S.floorCurveStartBlend);
G.floorCurveEndBlend=static_cast<float>(S.floorCurveEndBlend);
return G;
}
std::shared_ptr<const VoxelForgeCore::FarFieldWorldInput> GetFarFieldWorldInput(
std::string& Error) const
{
std::lock_guard<std::mutex> Lock(FarFieldGpuInputMutex);
if(CachedFarFieldGpuInput)return CachedFarFieldGpuInput;
if(!FarBuilt||SpatialBins.empty()||SharedFieldPrimitives.empty())
{Error="far world has no shared primitives or spatial bins";return {};}
if(SpatialBins.size()>std::numeric_limits<uint32>::max()/2u)
{Error="far world spatial-bin table exceeds the GPU 32-bit slot limit";return {};}
const size_t TableCapacity=std::bit_ceil(std::max<size_t>(2,SpatialBins.size()*2));
if(TableCapacity>std::numeric_limits<uint32>::max())
{Error="far world spatial-bin table exceeds the D3D11 element limit";return {};}
auto Data=std::make_shared<VoxelForgeCore::FarFieldWorldInput>();
Data->spatialBinSize=static_cast<uint32>(ActiveSpatialBinSize);
Data->spatialBinMask=static_cast<uint32>(TableCapacity-1);
Data->fieldSeed=static_cast<uint32>(Owner?Owner->Seed:0);
for(int32 Axis=0;Axis<3;++Axis)
{Data->detailMinVoxel[Axis]=Min[Axis];Data->detailMaxVoxel[Axis]=Max[Axis];}
Data->shapes.reserve(SharedFieldPrimitives.size());
for(const VF_FieldPrimitive& Shape:SharedFieldPrimitives)
Data->shapes.push_back(ToFarFieldGpuPrimitive(Shape));
Data->detailLayers.reserve(LayerFields.size());
for(const FLayerField& Field:LayerFields)
{
const FStrateGenerationParams& Params=Field.Params;
Data->detailLayers.push_back({Field.Lo,Field.Hi,Params.SurfaceRoughness,
Params.RoughnessFrequency,Params.VerticalScale,Params.DomainWarpStrength,
Params.DomainWarpFrequency,static_cast<uint32>(Params.RoughnessNoiseType)});
}
Data->spatialBins.resize(TableCapacity);
size_t ReferenceCount=0;
for(const auto& Bin:SpatialBins)ReferenceCount+=Bin.second.size();
Data->spatialBinShapeIds.reserve(ReferenceCount);
for(const auto& [Key,Items]:SpatialBins)
{
if(Items.size()>std::numeric_limits<uint32>::max()
||Data->spatialBinShapeIds.size()+Items.size()>std::numeric_limits<uint32>::max())
{Error="far world spatial-bin shape IDs exceed the GPU 32-bit offset limit";return {};}
uint32 Slot=FarFieldBinHash(Key.X,Key.Y,Key.Z)&Data->spatialBinMask;
while(Data->spatialBins[Slot].count!=0)Slot=(Slot+1)&Data->spatialBinMask;
VoxelForgeCore::FarFieldSpatialBin& GpuBin=Data->spatialBins[Slot];
GpuBin.x=Key.X;GpuBin.y=Key.Y;GpuBin.z=Key.Z;
GpuBin.offset=static_cast<uint32>(Data->spatialBinShapeIds.size());
GpuBin.count=static_cast<uint32>(Items.size());
for(const uint32 Id:Items)Data->spatialBinShapeIds.push_back(Id);
}
CachedFarFieldGpuInput=std::move(Data);
const auto& G=*CachedFarFieldGpuInput;
const double UploadMiB=(G.shapes.size()*sizeof(VoxelForgeCore::FarFieldPrimitiveGpu)
+G.spatialBins.size()*sizeof(VoxelForgeCore::FarFieldSpatialBin)
+G.spatialBinShapeIds.size()*sizeof(uint32))/1048576.0;
std::printf("[FarGpuWorldData] shapes=%zu spatial_bins=%zu hash_slots=%zu bin_shape_ids=%zu bin_size=%d detail_min=(%d,%d,%d) detail_max=(%d,%d,%d) bytes=%.3fMiB upload_scope=once_per_world\n",
G.shapes.size(),SpatialBins.size(),G.spatialBins.size(),G.spatialBinShapeIds.size(),
ActiveSpatialBinSize,G.detailMinVoxel[0],G.detailMinVoxel[1],G.detailMinVoxel[2],
G.detailMaxVoxel[0],G.detailMaxVoxel[1],G.detailMaxVoxel[2],UploadMiB);
return CachedFarFieldGpuInput;
}
bool ExportFarFieldTileInput(const FIntVector& Origin,int32 Cells,int32 Step,
bool bNearDetail,VoxelForgeCore::FarFieldTileInput& Out,std::string& Error) const
{
if(!FarBuilt||Cells<=0||Cells>32||Step<=0)
{Error="far field export requires a built far world and a bounded coarse tile";return false;}
const int32 Dim=Cells+3;
const size_t SampleCount=static_cast<size_t>(Dim)*Dim*Dim;
Out={};Error.clear();Out.origin={Origin.X,Origin.Y,Origin.Z};Out.cells=Cells;Out.step=Step;Out.sampleDim=Dim;
Out.nearDetailEnabled=bNearDetail;
Out.worldInput=GetFarFieldWorldInput(Error);
if(!Out.worldInput)return false;
if(LayerRanges.empty())
{Out.strataLo=-100000.0f;Out.strataHi=100000.0f;}
else
{Out.strataLo=static_cast<float>(LayerRanges[0].Lo);Out.strataHi=static_cast<float>(LayerRanges[0].Hi);}
Out.strataBlend=StrataCaveBlendRadius;
if(Owner&&Owner->Manager)
{
const double TileZ0=Origin.Z,TileZ1=Origin.Z+static_cast<double>(Cells)*Step;
for(const FStrateSlot& Slot:Owner->Manager->GetLayout())
{
const FStrateGenerationParams Params=Owner->Manager->GetGenerationParams(
FIntVector(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2));
const float HalfWidth=static_cast<float>(std::max(8.0,
static_cast<double>(Params.BoundarySealThickness)+8.0));
const float Boundaries[2]={static_cast<float>(Slot.BottomChunkZ)*CHUNK_SIZE,
static_cast<float>(Slot.TopChunkZ+1)*CHUNK_SIZE};
for(const float Z:Boundaries)
if(Z+HalfWidth>=TileZ0&&Z-HalfWidth<=TileZ1)
Out.stratumBoundaries.push_back({Z,HalfWidth});
}
std::sort(Out.stratumBoundaries.begin(),Out.stratumBoundaries.end(),
[](const auto& A,const auto& B){return A.z<B.z;});
Out.stratumBoundaries.erase(std::unique(Out.stratumBoundaries.begin(),
Out.stratumBoundaries.end(),[](const auto& A,const auto& B)
{return std::abs(A.z-B.z)<0.01f;}),Out.stratumBoundaries.end());
}
Out.protectedAir.assign((SampleCount+31u)/32u,0u);
Out.protectedSolid.assign((SampleCount+31u)/32u,0u);
Out.protectedThin.assign((SampleCount+31u)/32u,0u);
if(bNearDetail)
Out.nearPackedProxy.assign(SampleCount,std::numeric_limits<uint32>::max());
const int32 LatticeMin[3]={Origin.X-Step,Origin.Y-Step,Origin.Z-Step};
const int32 LatticeMax[3]={Origin.X+(Cells+1)*Step,Origin.Y+(Cells+1)*Step,
Origin.Z+(Cells+1)*Step};
std::vector<uint32> CriticalCandidates,OverlayCandidates;
for(uint32 Id=0;Id<Primitives.size();++Id)
{
const FPrimitive& Shape=Primitives[Id];
bool CriticalOverlap=Shape.bCarve&&Shape.bCritical;
bool OverlayOverlap=!Shape.bCarve;
for(int32 Axis=0;Axis<3;++Axis)
{
CriticalOverlap&=Shape.Max[Axis]>=LatticeMin[Axis]-4
&&Shape.Min[Axis]<=LatticeMax[Axis]+4;
OverlayOverlap&=Shape.Max[Axis]>=LatticeMin[Axis]
&&Shape.Min[Axis]<=LatticeMax[Axis];
}
if(CriticalOverlap)CriticalCandidates.push_back(Id);
if(OverlayOverlap)OverlayCandidates.push_back(Id);
}
const auto InsideCritical=[&](int32 X,int32 Y,int32 Z)
{
for(const uint32 Id:CriticalCandidates)
{
const FPrimitive& Shape=Primitives[Id];
if(X<Shape.Min[0]||X>Shape.Max[0]||Y<Shape.Min[1]||Y>Shape.Max[1]
||Z<Shape.Min[2]||Z>Shape.Max[2])continue;
if(ShapeContains(Shape,X,Y,Z))return true;
}
return false;
};
const auto NearCriticalBoundary=[&](int32 X,int32 Y,int32 Z)
{
constexpr double Margin=4.0;
for(const uint32 Id:CriticalCandidates)
{
const FPrimitive& Shape=Primitives[Id];
if(X<Shape.Min[0]-Margin||X>Shape.Max[0]+Margin
||Y<Shape.Min[1]-Margin||Y>Shape.Max[1]+Margin
||Z<Shape.Min[2]-Margin||Z>Shape.Max[2]+Margin)continue;
float Distance=ShapeDistance(Shape,X,Y,Z);
if(Shape.bHasFloorClip)
{
const float FloorDistance=static_cast<float>(FloorAt(Shape,X,Y)-Z);
Distance=SmoothMaxDistance(Distance,FloorDistance,
static_cast<float>(Shape.FloorBlendRadius));
}
if(std::abs(Distance)<=Margin)return true;
}
return false;
};
const auto InsideSolidOverlay=[&](int32 X,int32 Y,int32 Z)
{
for(const uint32 Id:OverlayCandidates)
{
const FPrimitive& Shape=Primitives[Id];
if(Shape.bCarve||X<Shape.Min[0]||X>Shape.Max[0]
||Y<Shape.Min[1]||Y>Shape.Max[1]||Z<Shape.Min[2]||Z>Shape.Max[2])continue;
if(ShapeDistance(Shape,X,Y,Z)<=0.0f)return true;
}
return false;
};
const auto& Connectors=GetFarConnectorSegments();
// CPU repair panels are only used by the survival path at LOD6 and above.
// Keep this sparse repair out of LOD2..5, where the base GPU surface has
// sufficient resolution and the CPU path emits no panel geometry.
if(Cells*Step>=(CHUNK_SIZE<<6))
{
std::vector<uint32> ConnectorCandidates;
FindFarConnectorCandidates(
FIntVector(Origin.X-64,Origin.Y-64,Origin.Z-64),
FIntVector(Origin.X+Cells*Step+64,Origin.Y+Cells*Step+64,
Origin.Z+Cells*Step+64),ConnectorCandidates);
const double TileMin[3]={static_cast<double>(Origin.X),static_cast<double>(Origin.Y),
static_cast<double>(Origin.Z)};
const double TileMax[3]={TileMin[0]+Cells*Step,TileMin[1]+Cells*Step,
TileMin[2]+Cells*Step};
for(const uint32 ConnectorIndex:ConnectorCandidates)
{
if(ConnectorIndex>=Connectors.size())continue;
const FFarConnectorSegment& Connector=Connectors[ConnectorIndex];
const double Start[3]={Connector.Start.X,Connector.Start.Y,Connector.Start.Z};
const double Delta[3]={Connector.End.X-Connector.Start.X,
Connector.End.Y-Connector.Start.Y,Connector.End.Z-Connector.Start.Z};
double TMin=0.0,TMax=1.0;bool Intersects=true;
for(int32 Axis=0;Axis<3;++Axis)
{
if(std::abs(Delta[Axis])<1.0e-9)
{if(Start[Axis]<TileMin[Axis]||Start[Axis]>TileMax[Axis])Intersects=false;continue;}
double A=(TileMin[Axis]-Start[Axis])/Delta[Axis];
double B=(TileMax[Axis]-Start[Axis])/Delta[Axis];
if(A>B)std::swap(A,B);
TMin=std::max(TMin,A);TMax=std::min(TMax,B);
if(TMin>TMax){Intersects=false;break;}
}
const double Length=std::sqrt(Delta[0]*Delta[0]+Delta[1]*Delta[1]+Delta[2]*Delta[2]);
if(!Intersects||Length<=1.0e-6||TMax-TMin<=1.0e-7)continue;
const uint32 AxialSampleCount=std::max<uint32>(1,static_cast<uint32>(std::ceil(Length/128.0)));
const double FirstValue=std::ceil(TMin*AxialSampleCount-0.5-1.0e-8);
const double LastValue=std::floor(TMax*AxialSampleCount-0.5+1.0e-8);
const uint32 FirstSample=static_cast<uint32>(std::clamp(FirstValue,0.0,
static_cast<double>(AxialSampleCount)));
const uint32 SampleCount=LastValue>=FirstValue?static_cast<uint32>(std::max(0.0,
std::min(LastValue,static_cast<double>(AxialSampleCount-1))-FirstValue+1.0)):0;
const uint32 RingCount=SampleCount+2;
const uint64 VertexCount=static_cast<uint64>(RingCount-1)*24u;
if(Out.connectorVertexCount+VertexCount>std::numeric_limits<uint32>::max())
{Error="far connector panel output exceeds the 32-bit tile limit";return false;}
VoxelForgeCore::FarFieldConnectorPanel Panel{};
for(int32 Axis=0;Axis<3;++Axis)
{Panel.start[Axis]=static_cast<float>(Start[Axis]);Panel.end[Axis]=static_cast<float>(Start[Axis]+Delta[Axis]);}
Panel.radius=Connector.Radius;Panel.tMin=static_cast<float>(TMin);Panel.tMax=static_cast<float>(TMax);
Panel.ringCount=RingCount;Panel.ringOffset=Out.connectorRingCount;
Panel.vertexOffset=Out.connectorVertexCount;
Panel.axialSampleStart=FirstSample;Panel.axialSampleCount=AxialSampleCount;
Out.connectorRingCount+=Panel.ringCount;Out.connectorVertexCount+=static_cast<uint32>(VertexCount);
Out.connectorPanels.push_back(Panel);
}
}
size_t SampleIndex=0;
for(int32 Z=-1;Z<=Cells+1;++Z)
for(int32 Y=-1;Y<=Cells+1;++Y)
for(int32 X=-1;X<=Cells+1;++X,++SampleIndex)
{
const int32 WX=Origin.X+X*Step,WY=Origin.Y+Y*Step,WZ=Origin.Z+Z*Step;
if(bNearDetail&&!bAnalyticOnly&&Contains(WX,WY,WZ))
{
const size_t Column=static_cast<size_t>(WY-Min[1])*Size[0]
+static_cast<size_t>(WX-Min[0]);
const size_t Zi=static_cast<size_t>(WZ-Min[2]);
const size_t Word=Column*WordsPerZ+(Zi>>6);
const uint64 Mask=uint64{1}<<(Zi&63u);
if((CandidateWords[Word]&Mask)==0)
Out.nearPackedProxy[SampleIndex]=0u;
else
{
++Out.nearPackedCandidateSampleCount;
const size_t Node=Zi*static_cast<size_t>(Size[1])*Size[0]+Column;
const int32 Q=std::abs(static_cast<int32>(SurfaceDistanceQ[Node]));
const uint32 Magnitude=static_cast<uint32>(std::clamp(Q,1,32767));
if(Magnitude==1u)
{
// At the minimum quantization step a one-ULP GPU sign flip
// can change an entire marching-cubes cell. Keep this sparse
// ambiguous boundary decision aligned with the packed cache.
const bool bRock=(RockWords[Word]&Mask)!=0;
Out.nearPackedProxy[SampleIndex]=0x40000000u
|(bRock?0x80000000u:0u)|Magnitude;
}
else Out.nearPackedProxy[SampleIndex]=Magnitude;
}
}
if(!bNearDetail&&WX>=Min[0]&&WX<=Max[0]&&WY>=Min[1]&&WY<=Max[1]
&&WZ>=Min[2]&&WZ<=Max[2])
{Error="tile lattice intersects the near analytic/detail region; this export is far-field only";return false;}
if(InsideCritical(WX,WY,WZ))
{
Out.protectedAir[SampleIndex>>5]|=1u<<(SampleIndex&31u);
++Out.protectedSampleCount;
}
else if(NearCriticalBoundary(WX,WY,WZ))
{
// Preserve exact signs within roughly one far-field lattice step of
// route air. This catches thin walls/floors at the route boundary
// without evaluating the ordinary far field on the CPU.
const float CriticalDensity=AnalyticDensityAt(static_cast<float>(WX),
static_cast<float>(WY),static_cast<float>(WZ));
Out.protectedThin[SampleIndex>>5]|=1u<<(SampleIndex&31u);
if(CriticalDensity>=0.0f)Out.protectedAir[SampleIndex>>5]|=1u<<(SampleIndex&31u);
else
{
Out.protectedSolid[SampleIndex>>5]|=1u<<(SampleIndex&31u);
++Out.protectedSolidSampleCount;
}
++Out.protectedThinSampleCount;
}
else
{
bool bSealSample=false;
for(const VoxelForgeCore::FarFieldStratumBoundary& Boundary:Out.stratumBoundaries)
{
if(std::abs(static_cast<double>(WZ)-Boundary.z)>0.5)continue;
// Only the exact lattice samples that lie on a seal height need a
// CPU sign override. The surrounding far field remains GPU-decided.
const float SealDensity=AnalyticDensityAt(static_cast<float>(WX),
static_cast<float>(WY),static_cast<float>(WZ));
if(SealDensity>=0.0f)
Out.protectedAir[SampleIndex>>5]|=1u<<(SampleIndex&31u);
else
Out.protectedSolid[SampleIndex>>5]|=1u<<(SampleIndex&31u);
++Out.protectedSealSampleCount;
if(SealDensity<0.0f)++Out.protectedSolidSampleCount;
bSealSample=true;
break;
}
if(!bSealSample&&InsideSolidOverlay(WX,WY,WZ))
{
Out.protectedSolid[SampleIndex>>5]|=1u<<(SampleIndex&31u);
++Out.protectedSolidSampleCount;
++Out.protectedSolidOverlaySampleCount;
}
}
}
return true;
}
bool IsFarFieldTileOutsideNearRegion(const FIntVector& Origin,int32 Cells,int32 Step) const
{
if(!FarBuilt||Cells<=0||Cells>32||Step<=0)return false;
const int32 Origins[3]={Origin.X,Origin.Y,Origin.Z};
for(int32 Axis=0;Axis<3;++Axis)
{
const double First=std::ceil((static_cast<double>(Min[Axis])-Origins[Axis])/Step);
const double Last=std::floor((static_cast<double>(Max[Axis])-Origins[Axis])/Step);
if(First>Cells+1||Last< -1)return true;
}
return false;
}
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
+(Brush.Operation==FEditBrush::EOperation::DensityOffset
?std::max(Brush.Falloff,0.01f):0.0f);
else
for(int32 Axis=0;Axis<3;++Axis)Extent[Axis]=Brush.HalfExtent[Axis]+Brush.Rounding
+(Brush.Operation==FEditBrush::EOperation::DensityOffset
?std::max(Brush.Falloff,0.01f):0.0f);
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;
auto Stage=std::chrono::steady_clock::now();
BuildFarParentShapes();
RebuildSharedFieldPrimitives();
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] 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);
}
// Lattice signs come from packed intervals; a bounded analytic distance proxy supplies
// edge interpolation and normals to the existing mesher.
float DensityAt(float X,float Y,float Z) const
{
float IslandDensity=0.0f;
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
{
++ThreadClassificationStats().NativeFloatFieldCalls;
return ApplyEditBrushes(IslandDensity,X,Y,Z);
}
float NativeDensity=0.0f;
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
{
++ThreadClassificationStats().NativeFloatFieldCalls;
return ApplyEditBrushes(NativeDensity,X,Y,Z);
}
if(UsesLegacySurfaceField(X,Y,Z))
return LegacySurfaceDensityAt(X,Y,Z);
int32 IX=0,IY=0,IZ=0;
const bool bInteger=NearInteger(X,IX)&&NearInteger(Y,IY)&&NearInteger(Z,IZ);
const bool bPacked=!bAnalyticOnly&&bInteger&&Contains(IX,IY,IZ);
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
{
float IslandDensity=0.0f;
if(NativeFloatingIslandDensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z),IslandDensity))
{
++ThreadClassificationStats().NativeLatticeFieldCalls;
return ApplyEditBrushes(IslandDensity,X,Y,Z);
}
float NativeDensity=0.0f;
if(NativeSurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z),NativeDensity))
{
++ThreadClassificationStats().NativeLatticeFieldCalls;
return ApplyEditBrushes(NativeDensity,X,Y,Z);
}
if(UsesLegacySurfaceField(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z)))
return LegacySurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z));
const bool bPacked=!bAnalyticOnly&&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));
}
float DensityAtUnclamped(float X,float Y,float Z) const
{
float IslandDensity=0.0f;
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
{
++ThreadClassificationStats().NativeUnclampedFieldCalls;
return ApplyEditBrushes(IslandDensity,X,Y,Z);
}
float NativeDensity=0.0f;
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
{
++ThreadClassificationStats().NativeUnclampedFieldCalls;
return ApplyEditBrushes(NativeDensity,X,Y,Z);
}
if(UsesLegacySurfaceField(X,Y,Z))
return LegacySurfaceDensityAt(X,Y,Z);
return AnalyticDensityAt(X,Y,Z,false);
}
VoxelForgeCore::DetailFieldSample DetailSampleAt(float X,float Y,float Z) const
{
VoxelForgeCore::DetailFieldSample Sample;
AnalyticDensityAt(X,Y,Z,false,&Sample.displacement,
&Sample.caveInfluence,&Sample.floorInfluence);
return Sample;
}
bool IsProxyBandFallback(float Density) const
{
return std::abs(std::abs(Density)-(DetailSupremum+0.125f))<1.0e-4f;
}
bool IsAnalyticOnly() const { return bAnalyticOnly; }
void DensityAtLatticeX8(int32 XStart,int32 XStep,int32 Y,int32 Z,
int32 Count,float OutDensity[8]) const
{
Count=std::clamp(Count,0,8);
if(Count==0)return;
bool bHasLegacySurfaceSample=false;
bool bHasNativeIslandSample=false;
for(int32 Lane=0;Lane<Count;++Lane)
{
const float SampleX=static_cast<float>(XStart+Lane*XStep);
const float SampleY=static_cast<float>(Y);
const float SampleZ=static_cast<float>(Z);
bHasLegacySurfaceSample|=UsesLegacySurfaceField(SampleX,SampleY,SampleZ);
float IslandDensity=0.0f;
bHasNativeIslandSample|=NativeFloatingIslandDensityAt(
SampleX,SampleY,SampleZ,IslandDensity);
}
if(bHasLegacySurfaceSample||bHasNativeIslandSample)
{
for(int32 Lane=0;Lane<Count;++Lane)
OutDensity[Lane]=DensityAtLattice(XStart+Lane*XStep,Y,Z);
return;
}
if(bAnalyticOnly)
{
for(int32 Lane=0;Lane<Count;++Lane)
OutDensity[Lane]=DensityAtLattice(XStart+Lane*XStep,Y,Z);
return;
}
bool bFast=EditBrushes.empty()&&Y>=Min[1]&&Y<=Max[1]
&&Z>=Min[2]&&Z<=Max[2]
&&XStart>=Min[0]
&&XStart+static_cast<int64>(Count-1)*XStep<=Max[0];
if(!bFast)
{
for(int32 Lane=0;Lane<Count;++Lane)
OutDensity[Lane]=DensityAtLattice(XStart+Lane*XStep,Y,Z);
return;
}
const size_t ColumnStart=static_cast<size_t>(Y-Min[1])*Size[0]
+static_cast<size_t>(XStart-Min[0]);
const size_t Zi=static_cast<size_t>(Z-Min[2]);
const size_t WordOffset=Zi>>6;
const uint64 Mask=uint64{1}<<(Zi&63u);
const size_t NodeBase=Zi*static_cast<size_t>(Size[1])*Size[0]+ColumnStart;
constexpr float ProxyBandEdge=DetailSupremum+0.125f;
for(int32 Lane=0;Lane<Count;++Lane)
{
const size_t Column=ColumnStart+static_cast<size_t>(Lane*XStep);
const size_t Word=Column*WordsPerZ+WordOffset;
const bool bRock=(RockWords[Word]&Mask)!=0;
float Density;
if(CandidateWords[Word]&Mask)
{
const int16 Proxy=SurfaceDistanceQ[NodeBase
+static_cast<size_t>(Lane*XStep)];
Density=static_cast<float>(Proxy)*(1.0f/256.0f);
if((Density<0.0f)!=bRock)
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),1.0f/256.0f);
}
else
Density=bRock?-ProxyBandEdge:ProxyBandEdge;
OutDensity[Lane]=Density;
}
}
private:
static uint64 PackLatticeKey(int32 X,int32 Y,int32 Z)
{
constexpr uint64 Mask=0x1FFFFFull;
return (static_cast<uint64>(static_cast<uint32>(X)&Mask)<<42)
|(static_cast<uint64>(static_cast<uint32>(Y)&Mask)<<21)
|(static_cast<uint64>(static_cast<uint32>(Z)&Mask));
}
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 Node=Zi*static_cast<size_t>(Size[1])*Size[0]+Column;
const int16 Proxy=SurfaceDistanceQ[Node];
float Density=static_cast<float>(Proxy)*(1.0f/256.0f);
if((Density<0.0f)!=bRock)
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),1.0f/256.0f);
return ApplyEditBrushes(Density,FX,FY,FZ);
}
// 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,bool bClampNearZero=true,
float* OutDetailDisplacement=nullptr,
float* OutCaveInfluence=nullptr,
float* OutFloorInfluence=nullptr,
bool bApplyEdits=true) const
{
if(OutDetailDisplacement)*OutDetailDisplacement=0.0f;
if(OutCaveInfluence)*OutCaveInfluence=0.0f;
if(OutFloorInfluence)*OutFloorInfluence=0.0f;
float IslandDensity=0.0f;
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
return bApplyEdits?ApplyEditBrushes(IslandDensity,X,Y,Z):IslandDensity;
if(UsesLegacySurfaceField(X,Y,Z))
{
float NativeDensity=0.0f;
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
return bApplyEdits?ApplyEditBrushes(NativeDensity,X,Y,Z):NativeDensity;
const float Density=Owner->Generator->GetDensityAt(X,Y,Z);
return bApplyEdits?ApplyEditBrushes(Density,X,Y,Z):Density;
}
const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]
&&Z>=Min[2]&&Z<=Max[2];
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||(bClampNearZero&&std::abs(Density)<0.05f))
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),
bClampNearZero?0.05f:1.0e-7f);
return bApplyEdits?ApplyEditBrushes(Density,X,Y,Z):Density;
}
ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
float PassageInfluence=0.0f;
float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,
&bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir,nullptr,false,
&PassageInfluence);
if(OutCaveInfluence)*OutCaveInfluence=CaveInfluence;
if(OutFloorInfluence)*OutFloorInfluence=FloorInfluence;
if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth
&&std::abs(RockDistance)<=DetailSupremum+0.125f)
{
const float Fade=DetailBandFadeAt(X,Y,Z,RockDistance,SurfaceSDF,
CaveInfluence,FloorInfluence);
if(Fade>0.0f)
{
const float Detail=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,
FloorInfluence,PassageInfluence)*Fade;
RockDistance+=Detail;
if(OutDetailDisplacement)*OutDetailDisplacement=Detail;
}
}
if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f);
const bool bRock=RockDistance>=0.0f;
float Density=-RockDistance;
if((Density<0.0f)!=bRock||(bClampNearZero&&std::abs(Density)<0.05f))
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),
bClampNearZero?0.05f:1.0e-7f);
return bApplyEdits?ApplyEditBrushes(Density,X,Y,Z):Density;
}
public:
bool GetNativeSurfaceHeightsAt(float X,float Y,int32 ChunkZ,
float& OutGroundZ,float& OutCeilingZ) const
{
if(!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
||!std::isfinite(X)||!std::isfinite(Y))return false;
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
!=ECaveGeneratorType::SurfaceWorld)return false;
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
const auto* Plan=FindWorldAssemblyStratum(StrateIndex,ChunkZ);
if(!Plan)return false;
constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
const double MaxVoxel=static_cast<double>(std::numeric_limits<int64>::max())/Scale;
if(std::abs(static_cast<double>(X))>MaxVoxel||std::abs(static_cast<double>(Y))>MaxVoxel)
return false;
const auto ToFixed=[](float Value)
{return static_cast<int64>(std::llround(static_cast<double>(Value)
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));};
const VoxelForgeCore::Heightfield2D Block(WorldAssembly->recipe,*Plan);
if(!Block.isValid())return false;
const auto Column=Block.point(ToFixed(X),ToFixed(Y),
static_cast<int64>(ChunkZ)*CHUNK_SIZE*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
OutGroundZ=static_cast<float>(Column.groundHeightSubvoxels/Scale);
OutCeilingZ=static_cast<float>(Column.capHeightSubvoxels/Scale);
return std::isfinite(OutGroundZ)&&std::isfinite(OutCeilingZ);
}
EVoxelTileClass ClassifyBox(const FIntVector& Origin,int32 Step,int32 Cells) const
{ return ClassifyBoxDetailed(Origin,Step,Cells).Verdict; }
FBoxClassification ClassifyBoxDetailed(const FIntVector& Origin,int32 Step,int32 Cells) const
{
using Clock=std::chrono::steady_clock;
const auto Start=Clock::now();
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};
const VoxelForgeCore::WorldAssemblyStratumPlanV1* SurfacePlan=nullptr;
const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1* IslandPlan=nullptr;
bool bSurfaceOverlap=false,bIslandOverlap=false,bOtherStrateOverlap=false;
bool bIslandOtherStrateOverlap=false,bBadPlan=false;
if(Owner&&Owner->Manager)
{
// Keep the delivered SurfaceWorld chunk mapping unchanged.
// Conserver le mapping de chunks SurfaceWorld deja livre.
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin[2])
/static_cast<float>(CHUNK_SIZE));
const int32 LastChunk=FMath::FloorToInt(static_cast<float>(BMax[2])
/static_cast<float>(CHUNK_SIZE));
for(int32 ChunkZ=FirstChunk;ChunkZ<=LastChunk;++ChunkZ)
{
const ECaveGeneratorType Type=Owner->Manager->GetGeneratorTypeForChunk(
FIntVector(0,0,ChunkZ));
if(Type==ECaveGeneratorType::SurfaceWorld)
{
bSurfaceOverlap=true;
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
const auto* Candidate=FindWorldAssemblyStratum(StrateIndex,ChunkZ);
if(!Candidate||(SurfacePlan&&SurfacePlan!=Candidate))
bBadPlan=true;
SurfacePlan=Candidate;
}
else if(Type!=ECaveGeneratorType::FloatingIslands)
bOtherStrateOverlap=true;
}
// Island strata preserve owner-authored physical bounds through the resolved scale ratio.
// Les strates d'iles conservent les bornes physiques owner via le rapport de resolution.
const double CoreVoxelsPerIslandChunk=GetCoreVoxelsPerOwnerChunk();
const int32 FirstIslandChunk=FMath::FloorToInt(static_cast<float>(BMin[2])
/static_cast<float>(CoreVoxelsPerIslandChunk));
const int32 LastIslandChunk=FMath::FloorToInt(static_cast<float>(BMax[2])
/static_cast<float>(CoreVoxelsPerIslandChunk));
for(int32 ChunkZ=FirstIslandChunk;ChunkZ<=LastIslandChunk;++ChunkZ)
{
const ECaveGeneratorType Type=Owner->Manager->GetGeneratorTypeForChunk(
FIntVector(0,0,ChunkZ));
if(Type==ECaveGeneratorType::FloatingIslands)
{
bIslandOverlap=true;
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
const auto* Candidate=FindWorldAssemblyFloatingIslandStratum(StrateIndex,ChunkZ);
if(!Candidate||(IslandPlan&&IslandPlan!=Candidate))bBadPlan=true;
IslandPlan=Candidate;
}
else bIslandOtherStrateOverlap=true;
}
}
const auto Finish=[&](EVoxelTileClass Verdict,bool bUnknown=false)
{
const bool bNative=(bSurfaceOverlap&&!bOtherStrateOverlap)
||(bIslandOverlap&&!bIslandOtherStrateOverlap);
FBoxClassification Result{Verdict,bNative,bUnknown};
auto& Counts=ThreadClassificationStats();
Counts.Milliseconds+=std::chrono::duration<double,std::milli>(Clock::now()-Start).count();
if(!Result.bNativeSurface||bUnknown)++Counts.UnknownBoxes;
else if(Verdict==EVoxelTileClass::AllSolid)++Counts.SolidBoxes;
else if(Verdict==EVoxelTileClass::AllAir)++Counts.AirBoxes;
else ++Counts.MixedBoxes;
return Result;
};
// A box crossing recipes cannot use one field interval; unknown composition stays Mixed.
// Une boite qui traverse plusieurs recettes reste Mixed, car l'intervalle d'un seul champ ne suffit pas.
if(bBadPlan||(bSurfaceOverlap&&(bIslandOverlap||bOtherStrateOverlap))
||(bIslandOverlap&&(bIslandOtherStrateOverlap
||(!bSurfaceOverlap&&bOtherStrateOverlap))))
return Finish(EVoxelTileClass::Mixed,true);
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 Finish(EVoxelTileClass::Mixed,true);
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)
{
const auto It=SpatialBins.find({BX,BY,BZ});if(It==SpatialBins.end())continue;
for(uint32 Id:It->second)
{
const FPrimitive& P=Primitives[Id];bool Overlap=true;
for(int32 A=0;A<3;++A)
Overlap &= P.Max[A]+DetailBand>=BMin[A]&&P.Min[A]-DetailBand<=BMax[A];
if(Overlap)return Finish(EVoxelTileClass::Mixed);
}
}
for(const FInterval& R:LayerRanges)
if(R.Lo<=BMax[2]&&R.Hi>=BMin[2]
&&(R.Lo>=BMin[2]||R.Hi<=BMax[2]))
return Finish(EVoxelTileClass::Mixed);
if(bSurfaceOverlap)
{
if(!WorldAssembly||!SurfacePlan)return Finish(EVoxelTileClass::Mixed,true);
const VoxelForgeCore::Heightfield2D Block(WorldAssembly->recipe,*SurfacePlan);
const auto Verdict=Block.classifyLatticeBox(
static_cast<int64>(BMin[0])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
static_cast<int64>(BMin[1])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
static_cast<int64>(BMin[2])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
static_cast<int64>(BMax[0])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
static_cast<int64>(BMax[1])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
static_cast<int64>(BMax[2])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
std::max(Step,1));
switch(Verdict)
{
case VoxelForgeCore::HeightfieldBoxClass::allAir:
return Finish(EVoxelTileClass::AllAir);
case VoxelForgeCore::HeightfieldBoxClass::allSolid:
return Finish(EVoxelTileClass::AllSolid);
case VoxelForgeCore::HeightfieldBoxClass::mixed:
return Finish(EVoxelTileClass::Mixed);
default:return Finish(EVoxelTileClass::Mixed,true);
}
}
if(bIslandOverlap)
{
if(!IslandPlan)return Finish(EVoxelTileClass::Mixed,true);
constexpr std::int64_t Scale=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
const std::int64_t Bottom=IslandPlan->manifest.recipe.layoutMinimumZSubvoxels;
const std::int64_t Top=IslandPlan->manifest.recipe.layoutMaximumZSubvoxels;
const std::int64_t Seal=IslandPlan->manifest.recipe.boundarySealSubvoxels;
const std::int64_t BoxBottom=static_cast<std::int64_t>(BMin[2])*Scale;
const std::int64_t BoxTop=static_cast<std::int64_t>(BMax[2])*Scale;
const bool bBottomSealOverlap=BoxBottom<Bottom+Seal&&BoxTop>=Bottom;
const bool bTopSealOverlap=BoxBottom<=Top&&BoxTop>Top-Seal;
if(bBottomSealOverlap)
return Finish(BoxBottom>=Bottom&&BoxTop<=Bottom+Seal
?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed);
if(bTopSealOverlap)
return Finish(BoxBottom>=Top-Seal&&BoxTop<=Top
?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed);
const VoxelForgeCore::FloatingIslandFieldV1 Block(
IslandPlan->manifest.recipe,IslandPlan->manifest.plan);
if(!Block.isValid())return Finish(EVoxelTileClass::Mixed,true);
const auto Interval=Block.interval(static_cast<int64>(BMin[0])*Scale,
static_cast<int64>(BMin[1])*Scale,static_cast<int64>(BMin[2])*Scale,
static_cast<int64>(BMax[0])*Scale,static_cast<int64>(BMax[1])*Scale,
static_cast<int64>(BMax[2])*Scale);
if(!Interval.known)return Finish(EVoxelTileClass::Mixed,true);
if(Interval.minimumDensitySubvoxels>=0)return Finish(EVoxelTileClass::AllAir);
if(Interval.maximumDensitySubvoxels<0)return Finish(EVoxelTileClass::AllSolid);
return Finish(EVoxelTileClass::Mixed);
}
const bool bRock=EvaluateFast((BMin[0]+BMax[0])*0.5,
(BMin[1]+BMax[1])*0.5,(BMin[2]+BMax[2])*0.5);
return Finish(bRock?EVoxelTileClass::AllSolid:EVoxelTileClass::AllAir);
}
bool IsRock(int32 X,int32 Y,int32 Z) const
{
if(!Contains(X,Y,Z))return EvaluateFast(X,Y,Z);
const size_t Line=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]);
return ((RockWords[Line*WordsPerZ+(Zi>>6)]>>(Zi&63u))&1u)!=0;
}
bool IsDetailAuditPassing() const
{
const double Correlation=DetailAutocorrelation(DetailVariant);
const bool bFlips=Stats.DetailFlips>=1000;
const bool bCoherent=Correlation>0.45;
const bool bFamilies=AuditSurfaceBandFamilies();
std::printf("[DetailAudit] mode=%s supremum=%.2f vox band=%d changed_signs=%llu adjacent_correlation=%.4f surface_detail_check=%s spatial_scale_check=%s\n",
DetailVariant==EDetailVariant::Coherent?"coherent":DetailVariant==EDetailVariant::Smooth?"smooth":"per-voxel",
DetailSupremum,DetailBand,static_cast<unsigned long long>(Stats.DetailFlips),Correlation,
bFlips&&bFamilies?"PASS":"FAIL",bCoherent?"PASS":"FAIL");
return bFlips&&bCoherent&&bFamilies;
}
bool AuditSurfaceBandFamilies(bool bPassageOnly=false) const
{
struct FPatchKey
{
int32 X=0,Y=0,Z=0;uint8 Family=0;
bool operator==(const FPatchKey&)const=default;
};
struct FPatchHash
{
size_t operator()(const FPatchKey& K)const noexcept
{
uint64 H=static_cast<uint32>(K.X)*0x9E3779B185EBCA87ull;
H^=static_cast<uint64>(static_cast<uint32>(K.Y))*0xC2B2AE3D27D4EB4Full;
H^=static_cast<uint64>(static_cast<uint32>(K.Z))*0x165667B19E3779F9ull;
H^=static_cast<uint64>(K.Family)*0x94D049BB133111EBull;
H^=H>>29;H*=0xBF58476D1CE4E5B9ull;H^=H>>31;
return static_cast<size_t>(H);
}
};
struct FPatchStats {uint64 Count=0;double Sum=0.0,SumSq=0.0;};
struct FFamilyStats {uint64 Count=0,NearZero=0;double Sum=0.0,SumSq=0.0;};
std::unordered_map<FPatchKey,FPatchStats,FPatchHash> Patches;
std::array<FFamilyStats,3> Families{};
FFamilyStats PassageWalls,OrdinaryCaveWalls;
for(size_t Line=0;Line<static_cast<size_t>(Size[0])*Size[1];++Line)
{
const int32 X=Min[0]+static_cast<int32>(Line%Size[0]);
const int32 Y=Min[1]+static_cast<int32>(Line/Size[0]);
const size_t WordBase=Line*WordsPerZ;
for(size_t W=0;W<WordsPerZ;++W)
{
uint64 Bits=CandidateWords[WordBase+W];
while(Bits)
{
const uint32 Bit=std::countr_zero(Bits);Bits&=Bits-1;
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);
ESurfaceFamily Family=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
bool bFloorSurface=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
float PassageInfluence=0.0f;
const float Raw=EffectiveRockDistance(X,Y,Z,&Family,&SurfaceSDF,
&bFloorSurface,&CaveInfluence,&FloorInfluence,nullptr,nullptr,false,
&PassageInfluence);
if(std::abs(Raw)>=0.5f)continue;
const float Fade=DetailBandFadeAt(X,Y,Z,Raw,SurfaceSDF,
CaveInfluence,FloorInfluence);
const float Detail=Fade>0.0f
?DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence,
PassageInfluence)*Fade:0.0f;
const size_t FamilyIndex=static_cast<size_t>(Family);
FFamilyStats& F=Families[FamilyIndex];++F.Count;F.Sum+=Detail;F.SumSq+=Detail*Detail;
if(std::abs(Detail)<0.05f)++F.NearZero;
if(PassageInfluence>=0.5f)
{++PassageWalls.Count;PassageWalls.Sum+=Detail;PassageWalls.SumSq+=Detail*Detail;}
else if(Family==ESurfaceFamily::Cave&&PassageInfluence<0.1f)
{++OrdinaryCaveWalls.Count;OrdinaryCaveWalls.Sum+=Detail;OrdinaryCaveWalls.SumSq+=Detail*Detail;}
const FPatchKey Key{FloorDiv(X,16),FloorDiv(Y,16),FloorDiv(Z,16),
static_cast<uint8>(FamilyIndex)};
FPatchStats& P=Patches[Key];++P.Count;P.Sum+=Detail;P.SumSq+=Detail*Detail;
}
}
}
std::array<uint64,3> FlatCounts{};uint64 LargestFlatPatch=0;
for(const auto& Entry:Patches)
{
const FPatchStats& P=Entry.second;if(P.Count<16)continue;
const double Mean=P.Sum/P.Count;
const double StdDev=std::sqrt(std::max(0.0,P.SumSq/P.Count-Mean*Mean));
if(StdDev<0.10)
{
FlatCounts[Entry.first.Family]+=P.Count;
LargestFlatPatch=std::max(LargestFlatPatch,P.Count);
}
}
bool bPass=true;uint64 Tested=0,FlatTotal=0,Total=0;
for(size_t I=0;I<Families.size();++I)
{
const FFamilyStats& F=Families[I];Total+=F.Count;FlatTotal+=FlatCounts[I];
if(F.Count<16)continue;
++Tested;
const double Mean=F.Sum/F.Count;
const double StdDev=std::sqrt(std::max(0.0,F.SumSq/F.Count-Mean*Mean));
const double ZeroShare=static_cast<double>(F.NearZero)/F.Count;
const bool FamilyPass=StdDev>=0.15&&ZeroShare<0.60&&FlatCounts[I]<F.Count/2;
bPass&=FamilyPass;
const char* Name=I==0?"strata":I==1?"cave":"solid_overlay";
std::printf("[SurfaceBandFamilyAudit] family=%s samples=%llu detail_stddev=%.4f near_zero_share=%.1f%% flat_patch_samples=%llu status=%s\n",
Name,static_cast<unsigned long long>(F.Count),StdDev,ZeroShare*100.0,
static_cast<unsigned long long>(FlatCounts[I]),FamilyPass?"PASS":"FAIL");
}
const double FlatShare=Total?100.0*static_cast<double>(FlatTotal)/Total:100.0;
std::printf("[FlatPatchAudit] patch_width=16 vox min_patch_samples=16 flat_stddev_lt=0.10 flat_share=%.1f%% largest_flat_patch=%llu status=%s\n",
FlatShare,static_cast<unsigned long long>(LargestFlatPatch),
Tested>0&&FlatShare<20.0?"PASS":"FAIL");
const double PassageRms=PassageWalls.Count
?std::sqrt(PassageWalls.SumSq/PassageWalls.Count):0.0;
const double OrdinaryCaveRms=OrdinaryCaveWalls.Count
?std::sqrt(OrdinaryCaveWalls.SumSq/OrdinaryCaveWalls.Count):0.0;
const double PassageRmsRatio=OrdinaryCaveRms>1.0e-9
?PassageRms/OrdinaryCaveRms:0.0;
const bool bHasPassageSamples=PassageWalls.Count>0;
const bool bRatioPass=PassageWalls.Count>=32&&OrdinaryCaveWalls.Count>=32
&&PassageRmsRatio>=0.75&&PassageRmsRatio<=1.25;
const bool bPassageDetail=bPassageOnly?bRatioPass:(!bHasPassageSamples||bRatioPass);
const char* PassageStatus=!bHasPassageSamples
?(bPassageOnly?"FAIL":"SKIP"):(bPassageDetail?"PASS":"FAIL");
std::printf("[PassageWallDetailAudit] passage_samples=%llu passage_rms=%.4f cave_samples=%llu cave_rms=%.4f ratio=%.3f range=(0.750,1.250) status=%s\n",
static_cast<unsigned long long>(PassageWalls.Count),PassageRms,
static_cast<unsigned long long>(OrdinaryCaveWalls.Count),OrdinaryCaveRms,
PassageRmsRatio,PassageStatus);
return bPassageOnly?bPassageDetail:(Tested>0&&FlatShare<20.0&&bPass&&bPassageDetail);
}
bool AuditGridStairIntegrity() const
{
uint64 CheckedEdges=0,MissingAny=0,MissingBoth=0;
std::array<std::array<int32,4>,8> Examples{};
size_t ExampleCount=0;
const auto Record=[&](uint64 EdgeMask,uint64 CandidateA,uint64 CandidateB,
int32 X,int32 Y,int32 Z,int32 Axis)
{
CheckedEdges+=std::popcount(EdgeMask);
const uint64 Missing=EdgeMask&~(CandidateA&CandidateB);
uint64 AnyMissing=Missing;
MissingAny+=std::popcount(Missing);
MissingBoth+=std::popcount(EdgeMask&~(CandidateA|CandidateB));
while(AnyMissing&&ExampleCount<Examples.size())
{
const uint32 Bit=std::countr_zero(AnyMissing);AnyMissing&=AnyMissing-1;
Examples[ExampleCount++]={X,Y,Z+static_cast<int32>(Bit),Axis};
}
};
const size_t Columns=static_cast<size_t>(Size[0])*Size[1];
for(size_t Line=0;Line<Columns;++Line)
{
const int32 X=Min[0]+static_cast<int32>(Line%Size[0]);
const int32 Y=Min[1]+static_cast<int32>(Line/Size[0]);
const size_t Base=Line*WordsPerZ;
for(size_t W=0;W<WordsPerZ;++W)
{
const size_t Word=Base+W;
const uint64 Rock=RockWords[Word],Candidates=CandidateWords[Word];
const uint64 PreviousRock=W?RockWords[Word-1]>>63:0;
const uint64 PreviousCandidates=W?CandidateWords[Word-1]>>63:0;
uint64 Valid=(Size[2]-W*64)>=64?~uint64{0}
:((uint64{1}<<(Size[2]-W*64))-1);
if(W==0)Valid&=~uint64{1};
const uint64 ShiftedRock=(Rock<<1)|PreviousRock;
const uint64 ShiftedCandidates=(Candidates<<1)|PreviousCandidates;
Record((Rock^ShiftedRock)&Valid,Candidates,ShiftedCandidates,
X,Y,Min[2]+static_cast<int32>(W*64),2);
if(X+1<Max[0]+1)
{
const size_t Other=Word+WordsPerZ;
Record(Rock^RockWords[Other],Candidates,CandidateWords[Other],
X,Y,Min[2]+static_cast<int32>(W*64),0);
}
if(Y+1<Max[1]+1)
{
const size_t Other=Word+static_cast<size_t>(Size[0])*WordsPerZ;
Record(Rock^RockWords[Other],Candidates,CandidateWords[Other],
X,Y,Min[2]+static_cast<int32>(W*64),1);
}
}
}
std::printf("[GridStairDistanceAudit] region=(%d,%d,%d)..(%d,%d,%d) sign_edges=%llu candidate_fallback_edges=%llu both_fallback_edges=%llu status=%s\n",
Min[0],Min[1],Min[2],Max[0],Max[1],Max[2],
static_cast<unsigned long long>(CheckedEdges),
static_cast<unsigned long long>(MissingAny),
static_cast<unsigned long long>(MissingBoth),MissingAny==0?"PASS":"FAIL");
const auto CandidateAt=[&](int32 X,int32 Y,int32 Z)
{
if(!Contains(X,Y,Z))return false;
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]);
return (CandidateWords[Column*WordsPerZ+(Zi>>6)]&(uint64{1}<<(Zi&63u)))!=0;
};
for(size_t I=0;I<ExampleCount;++I)
{
int32 AX=Examples[I][0],AY=Examples[I][1],AZ=Examples[I][2];
int32 BX=AX,BY=AY,BZ=AZ;
if(Examples[I][3]==0)++BX;else if(Examples[I][3]==1)++BY;else --BZ;
const float AD=DensityAtLattice(AX,AY,AZ),BD=DensityAtLattice(BX,BY,BZ);
const float AExact=AnalyticDensityAt(static_cast<float>(AX),static_cast<float>(AY),static_cast<float>(AZ));
const float BExact=AnalyticDensityAt(static_cast<float>(BX),static_cast<float>(BY),static_cast<float>(BZ));
std::printf("[GridStairDistanceCandidate] a=(%d,%d,%d) b=(%d,%d,%d) axis=%d candidate=(%d,%d) solid=(%d,%d) density=(%.4f,%.4f) exact=(%.4f,%.4f) critical_air=(%d,%d)\n",
AX,AY,AZ,BX,BY,BZ,Examples[I][3],CandidateAt(AX,AY,AZ),CandidateAt(BX,BY,BZ),
IsRock(AX,AY,AZ),IsRock(BX,BY,BZ),
AD,BD,AExact,BExact,
IsInsideCriticalAir(AX,AY,AZ),IsInsideCriticalAir(BX,BY,BZ));
}
return MissingAny==0;
}
bool AuditViewSurface(double CameraX,double CameraY,double CameraZ,
float Yaw,float Pitch) const
{
struct FFamilyStats
{
uint64 Hits=0,NearZero=0,AxisLocked=0;
double Sum=0.0,SumSq=0.0;
};
std::array<FFamilyStats,3> FamilyStats{};
constexpr std::array<float,13> YawOffsets={-36.0f,-30.0f,-24.0f,-18.0f,-12.0f,-6.0f,
0.0f,6.0f,12.0f,18.0f,24.0f,30.0f,36.0f};
constexpr std::array<float,11> PitchOffsets={-80.0f,-70.0f,-60.0f,-50.0f,-40.0f,
-30.0f,-20.0f,-10.0f,0.0f,10.0f,20.0f};
const double OX=CameraX*4.0,OY=CameraY*4.0,OZ=CameraZ*4.0;
auto Distance=[&](double X,double Y,double Z,ESurfaceFamily* OutFamily=nullptr,
float* OutRaw=nullptr,float* OutDetail=nullptr,
bool* OutFloorSurface=nullptr,float* OutPassageInfluence=nullptr,
float* OutCaveInfluence=nullptr,float* OutFloorInfluence=nullptr,
float* OutSurfaceSDF=nullptr)
{
ESurfaceFamily Family=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
bool bFloorSurface=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
float PassageInfluence=0.0f;
float D=EffectiveRockDistance(X,Y,Z,&Family,&SurfaceSDF,
&bFloorSurface,&CaveInfluence,&FloorInfluence,nullptr,nullptr,false,
&PassageInfluence);
float Detail=0.0f;
const bool bInside=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(bInside&&DetailVariant!=EDetailVariant::Smooth&&std::abs(D)<=DetailSupremum+0.125f)
{
const float Fade=DetailBandFadeAt(X,Y,Z,D,SurfaceSDF,CaveInfluence,FloorInfluence);
if(Fade>0.0f)
{Detail=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence,
PassageInfluence)*Fade;D+=Detail;}
}
if(bCriticalAir)D=std::min(D,-1.0f/16.0f);
if(OutFamily)*OutFamily=Family;
if(OutRaw)*OutRaw=D-Detail;
if(OutDetail)*OutDetail=Detail;
if(OutFloorSurface)*OutFloorSurface=bFloorSurface;
if(OutPassageInfluence)*OutPassageInfluence=PassageInfluence;
if(OutCaveInfluence)*OutCaveInfluence=CaveInfluence;
if(OutFloorInfluence)*OutFloorInfluence=FloorInfluence;
if(OutSurfaceSDF)*OutSurfaceSDF=SurfaceSDF;
return D;
};
uint64 TotalHits=0,TotalAxisLocked=0,TotalNearZero=0,FloorTaggedHits=0,FloorAxisLocked=0;
uint64 FamilyTransitionPairs=0,StableFamilyPairs=0;
uint64 ThinDetachedSheets=0;
double ThinnestDetachedSheet=std::numeric_limits<double>::max();
double FamilyTransitionGradientSq=0.0,StableFamilyGradientSq=0.0;
for(float YawOffset:YawOffsets)for(float PitchOffset:PitchOffsets)
{
const double YawRad=Yaw+YawOffset*0.017453292519943295;
const double PitchRad=Pitch+PitchOffset*0.017453292519943295;
const double CP=std::cos(PitchRad),DX=std::sin(YawRad)*CP,
DY=std::cos(YawRad)*CP,DZ=std::sin(PitchRad);
double PreviousT=0.0;
float PreviousD=Distance(OX,OY,OZ+0.001);
bool bFound=false;
double PrimaryHitT=-1.0;
for(double T=0.5;T<=256.0;T+=0.5)
{
const double X=OX+DX*T,Y=OY+DY*T,Z=OZ+DZ*T;
const float D=Distance(X,Y,Z);
if(PreviousD<0.0f&&D>=0.0f)
{
double Lo=PreviousT,Hi=T;
for(int32 It=0;It<10;++It)
{
const double Mid=(Lo+Hi)*0.5;
if(Distance(OX+DX*Mid,OY+DY*Mid,OZ+DZ*Mid)<0.0f)Lo=Mid;else Hi=Mid;
}
const double HitT=(Lo+Hi)*0.5;
PrimaryHitT=HitT;
const double XH=OX+DX*HitT,YH=OY+DY*HitT,ZH=OZ+DZ*HitT;
ESurfaceFamily Family=ESurfaceFamily::Strata;float Raw=0.0f,Detail=0.0f;
bool bFloorSurface=false;float PassageInfluence=0.0f;
float CaveInfluence=0.0f,FloorInfluence=0.0f,SurfaceSDF=100000.0f;
Distance(XH,YH,ZH,&Family,&Raw,&Detail,&bFloorSurface,&PassageInfluence,
&CaveInfluence,&FloorInfluence,&SurfaceSDF);
const double GX=Distance(XH+0.5,YH,ZH)-Distance(XH-0.5,YH,ZH);
const double GY=Distance(XH,YH+0.5,ZH)-Distance(XH,YH-0.5,ZH);
const double GZ=Distance(XH,YH,ZH+0.5)-Distance(XH,YH,ZH-0.5);
const double GL=std::sqrt(GX*GX+GY*GY+GZ*GZ);
const double AxisDot=GL>1.0e-8?std::max({std::abs(GX),std::abs(GY),std::abs(GZ)})/GL:1.0;
for(int32 Axis=0;Axis<3;++Axis)
{
double AP[3]={XH,YH,ZH},AM[3]={XH,YH,ZH};
AP[Axis]+=0.5;AM[Axis]-=0.5;
ESurfaceFamily FamilyP=ESurfaceFamily::Strata,FamilyM=ESurfaceFamily::Strata;
float DetailP=0.0f,DetailM=0.0f;bool bFloorP=false,bFloorM=false;
Distance(AP[0],AP[1],AP[2],&FamilyP,nullptr,&DetailP,&bFloorP);
Distance(AM[0],AM[1],AM[2],&FamilyM,nullptr,&DetailM,&bFloorM);
const double Gradient=static_cast<double>(DetailP-DetailM);
if(FamilyP!=FamilyM||bFloorP!=bFloorM)
{FamilyTransitionGradientSq+=Gradient*Gradient;++FamilyTransitionPairs;}
else
{StableFamilyGradientSq+=Gradient*Gradient;++StableFamilyPairs;}
}
constexpr double ScanRadius=16.0;
constexpr int32 ScanSteps=128;
double ScanT=std::max(0.0,HitT-ScanRadius);
ESurfaceFamily ScanFamily=ESurfaceFamily::Strata;
bool bScanFloor=false;float ScanRaw=0.0f,ScanDetail=0.0f;
Distance(OX+DX*ScanT,OY+DY*ScanT,OZ+DZ*ScanT,
&ScanFamily,&ScanRaw,&ScanDetail,&bScanFloor);
for(int32 Step=1;Step<=ScanSteps;++Step)
{
const double T=std::min(HitT+ScanRadius,ScanT+0.25);
ESurfaceFamily NextFamily=ESurfaceFamily::Strata;
bool bNextFloor=false;float NextRaw=0.0f,NextDetail=0.0f;
Distance(OX+DX*T,OY+DY*T,OZ+DZ*T,
&NextFamily,&NextRaw,&NextDetail,&bNextFloor);
if(std::abs(ScanRaw)<=5.0f&&std::abs(NextRaw)<=5.0f)
{
const double Gradient=static_cast<double>(NextDetail-ScanDetail);
if(ScanFamily!=NextFamily||bScanFloor!=bNextFloor)
{FamilyTransitionGradientSq+=Gradient*Gradient;++FamilyTransitionPairs;}
else
{StableFamilyGradientSq+=Gradient*Gradient;++StableFamilyPairs;}
}
ScanT=T;ScanFamily=NextFamily;bScanFloor=bNextFloor;
ScanRaw=NextRaw;ScanDetail=NextDetail;
}
FFamilyStats& S=FamilyStats[static_cast<size_t>(Family)];
++S.Hits;++TotalHits;S.Sum+=Detail;S.SumSq+=Detail*Detail;
if(std::abs(Detail)<0.05f){++S.NearZero;++TotalNearZero;}
if(AxisDot>0.985){++S.AxisLocked;++TotalAxisLocked;}
if(bFloorSurface){++FloorTaggedHits;if(AxisDot>0.985)++FloorAxisLocked;}
if(YawOffset==0.0f&&PitchOffset==0.0f)
{
const char* Name=Family==ESurfaceFamily::Strata?"strata":
Family==ESurfaceFamily::Cave?"cave":"solid_overlay";
const FLayerField* Field=FindLayerField(ZH);
FOwnerNoiseLineContext OwnerNoise8,OwnerNoise2;
const float Owner8=Field?OwnerCaveRoughness(XH,YH,ZH,SurfaceSDF,
*Field,CaveInfluence,OwnerNoise8,nullptr,8):0.0f;
const float Owner2=Field?OwnerCaveRoughness(XH,YH,ZH,SurfaceSDF,
*Field,CaveInfluence,OwnerNoise2,nullptr,2):0.0f;
const float Relief=SurfaceRelief(static_cast<float>(XH),
static_cast<float>(YH),static_cast<float>(ZH),
static_cast<uint32>(Owner->Seed));
std::printf("[SurfaceRay] t=%.2f vox hit=(%.2f,%.2f,%.2f) family=%s floor_surface=%s cave_influence=%.3f floor_influence=%.3f passage_influence=%.3f surface_sdf=%.4f raw_sdf=%.4f detail=%.4f relief=%.4f owner8=%.4f owner2=%.4f axis_dot=%.4f\n",
HitT,XH,YH,ZH,Name,bFloorSurface?"yes":"no",CaveInfluence,
FloorInfluence,PassageInfluence,SurfaceSDF,Raw,Detail,Relief,
Owner8,Owner2,AxisDot);
}
bFound=true;break;
}
PreviousT=T;PreviousD=D;
}
if(PrimaryHitT>=0.0)
{
constexpr double SheetStep=0.25;
constexpr double MinimumAirGap=3.0;
constexpr double MaximumSheetThickness=1.0;
double T=PrimaryHitT+SheetStep;
float Prev=Distance(OX+DX*T,OY+DY*T,OZ+DZ*T);
bool bInSolid=Prev>=0.0f;
double SolidStart=T,LastSolidExit=PrimaryHitT;
bool bSeparatedFromPrevious=false;
for(T+=SheetStep;T<=256.0;T+=SheetStep)
{
const float D=Distance(OX+DX*T,OY+DY*T,OZ+DZ*T);
if(!bInSolid&&Prev<0.0f&&D>=0.0f)
{
SolidStart=T;
bSeparatedFromPrevious=(T-LastSolidExit)>=MinimumAirGap;
bInSolid=true;
}
else if(bInSolid&&Prev>=0.0f&&D<0.0f)
{
const double Thickness=T-SolidStart;
if(bSeparatedFromPrevious&&Thickness<MaximumSheetThickness)
{
++ThinDetachedSheets;
ThinnestDetachedSheet=std::min(ThinnestDetachedSheet,Thickness);
if(ThinDetachedSheets<=8)
std::printf("[ThinSheetCandidate] yaw_offset=%.0f pitch_offset=%.0f entry=%.2f vox thickness=%.3f vox air_gap=%.2f vox\n",
YawOffset,PitchOffset,SolidStart,Thickness,SolidStart-LastSolidExit);
}
LastSolidExit=T;
bSeparatedFromPrevious=false;
bInSolid=false;
}
Prev=D;
}
}
(void)bFound;
}
const double NearZeroShare=TotalHits?static_cast<double>(TotalNearZero)/TotalHits:1.0;
const double AxisShare=TotalHits?static_cast<double>(TotalAxisLocked)/TotalHits:1.0;
const double FloorAxisShare=FloorTaggedHits?static_cast<double>(FloorAxisLocked)/FloorTaggedHits:1.0;
const double TransitionRMS=FamilyTransitionPairs
?std::sqrt(FamilyTransitionGradientSq/FamilyTransitionPairs):0.0;
const double StableRMS=StableFamilyPairs
?std::sqrt(StableFamilyGradientSq/StableFamilyPairs):0.0;
const double TransitionRatio=StableRMS>1.0e-6?TransitionRMS/StableRMS:1000000.0;
double Sum=0.0,SumSq=0.0;
for(const FFamilyStats& S:FamilyStats){Sum+=S.Sum;SumSq+=S.SumSq;}
const double Mean=TotalHits?Sum/TotalHits:0.0;
const double Variance=TotalHits?std::max(0.0,SumSq/TotalHits-Mean*Mean):0.0;
const double SurfaceDetailRMS=std::sqrt(Variance);
const double RelativeJoinStep=SurfaceDetailRMS>1.0e-6?TransitionRMS/SurfaceDetailRMS:1000000.0;
const bool bDetailPass=TotalHits>=24&&std::sqrt(Variance)>=0.15&&NearZeroShare<0.50;
constexpr double GridStairThreshold=0.40;
const bool bJoinCoverage=FloorTaggedHits>=8
&&(FamilyTransitionPairs==0||(FamilyTransitionPairs>=8&&RelativeJoinStep<GridStairThreshold));
const bool bGridPass=TotalHits>=24&&AxisShare<0.60
&&bJoinCoverage;
const bool bThinSheetPass=ThinDetachedSheets==0;
std::printf("[FlatSurfaceAudit] rays=%llu detail_stddev=%.4f vox near_zero_share=%.1f%% threshold=50.0%% status=%s\n",
static_cast<unsigned long long>(TotalHits),std::sqrt(Variance),NearZeroShare*100.0,
bDetailPass?"PASS":"FAIL");
std::printf("[GridStairAudit] rays=%llu axis_locked_normals=%.1f%% threshold=60.0%% floor_tagged_hits=%llu floor_axis_locked=%.1f%% family_transition_pairs=%llu transition_rms=%.4f vox stable_rms=%.4f vox surface_detail_rms=%.4f vox relative_join_step=%.3f threshold=%.3f legacy_join_ratio=%.3f status=%s\n",
static_cast<unsigned long long>(TotalHits),AxisShare*100.0,
static_cast<unsigned long long>(FloorTaggedHits),FloorAxisShare*100.0,
static_cast<unsigned long long>(FamilyTransitionPairs),TransitionRMS,StableRMS,
SurfaceDetailRMS,RelativeJoinStep,GridStairThreshold,TransitionRatio,bGridPass?"PASS":"FAIL");
std::printf("[ThinSheetAudit] rays=%llu isolated_sheets=%llu thickness_lt=1.000 vox air_gap_ge=3.000 vox thinnest=%.3f vox status=%s\n",
static_cast<unsigned long long>(TotalHits),static_cast<unsigned long long>(ThinDetachedSheets),
ThinDetachedSheets?ThinnestDetachedSheet:0.0,bThinSheetPass?"PASS":"FAIL");
for(size_t I=0;I<FamilyStats.size();++I)
{
const char* Name=I==0?"strata":I==1?"cave":"solid_overlay";
const FFamilyStats& S=FamilyStats[I];
const double M=S.Hits?S.Sum/S.Hits:0.0;
const double V=S.Hits?std::sqrt(std::max(0.0,S.SumSq/S.Hits-M*M)):0.0;
std::printf("[SurfaceFamilyAudit] family=%s rays=%llu detail_mean=%.4f detail_stddev=%.4f near_zero=%llu axis_locked=%llu\n",
Name,static_cast<unsigned long long>(S.Hits),M,V,
static_cast<unsigned long long>(S.NearZero),static_cast<unsigned long long>(S.AxisLocked));
}
const bool bFloorProfilePass=Stats.MaxUnmarkedFloorSlopeDelta<=0.75
&&Stats.MaxFloorCurveDeviation<=2.0;
return bDetailPass&&bGridPass&&bThinSheetPass&&bFloorProfilePass;
}
private:
static FClassificationThreadStats& ThreadClassificationStats()
{
static thread_local FClassificationThreadStats Stats;
return Stats;
}
const VoxelForgeCore::WorldAssemblyStratumPlanV1* FindWorldAssemblyStratum(
int32 StrateIndex,int32 ChunkZ) const
{
if(!WorldAssembly||StrateIndex<0)return nullptr;
const auto It=std::find_if(WorldAssembly->plan.strates.begin(),WorldAssembly->plan.strates.end(),
[StrateIndex,ChunkZ](const auto& Entry)
{return Entry.strateIndex==StrateIndex&&ChunkZ<=Entry.topChunkZ&&ChunkZ>=Entry.bottomChunkZ;});
return It==WorldAssembly->plan.strates.end()?nullptr:&*It;
}
const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1* FindWorldAssemblyFloatingIslandStratum(
int32 StrateIndex,int32 ChunkZ) const
{
if(!WorldAssembly||StrateIndex<0)return nullptr;
const auto It=std::find_if(WorldAssembly->floatingIslands.begin(),
WorldAssembly->floatingIslands.end(),[StrateIndex,ChunkZ](const auto& Entry)
{return Entry.strateIndex==StrateIndex&&ChunkZ<=Entry.topChunkZ&&ChunkZ>=Entry.bottomChunkZ;});
return It==WorldAssembly->floatingIslands.end()?nullptr:&*It;
}
bool NativeSurfaceDensityAt(float X,float Y,float Z,float& OutDensity) const
{
if(!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))return false;
const int32 ChunkZ=FMath::FloorToInt(Z/static_cast<float>(CHUNK_SIZE));
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
!=ECaveGeneratorType::SurfaceWorld)return false;
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
const auto* Plan=FindWorldAssemblyStratum(StrateIndex,ChunkZ);
if(!Plan)return false;
constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
const double MaxVoxel=static_cast<double>(std::numeric_limits<int64>::max())/Scale;
if(std::abs(static_cast<double>(X))>MaxVoxel||std::abs(static_cast<double>(Y))>MaxVoxel
||std::abs(static_cast<double>(Z))>MaxVoxel)return false;
const auto ToFixed=[](float Value)
{return static_cast<int64>(std::llround(static_cast<double>(Value)
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));};
const VoxelForgeCore::Heightfield2D Block(WorldAssembly->recipe,*Plan);
if(!Block.isValid())return false;
OutDensity=static_cast<float>(Block.point(ToFixed(X),ToFixed(Y),ToFixed(Z)).densitySubvoxels
/Scale);
return true;
}
bool NativeFloatingIslandDensityAt(float X,float Y,float Z,float& OutDensity) const
{
if(!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))return false;
// Convert Core query Z back to the owner's legacy chunk coordinate.
// Convertir Z du Core vers la coordonnee de chunk legacy du proprietaire.
const double CoreVoxelsPerLegacyChunk=GetCoreVoxelsPerOwnerChunk();
const int32 ChunkZ=FMath::FloorToInt(Z/static_cast<float>(CoreVoxelsPerLegacyChunk));
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
!=ECaveGeneratorType::FloatingIslands)return false;
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
const auto* Plan=FindWorldAssemblyFloatingIslandStratum(StrateIndex,ChunkZ);
if(!Plan)return false;
constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
constexpr double Maximum=1000000000000.0/Scale;
if(std::abs(static_cast<double>(X))>Maximum||std::abs(static_cast<double>(Y))>Maximum
||std::abs(static_cast<double>(Z))>Maximum)return false;
const auto ToFixed=[](float Value)
{return static_cast<int64>(std::llround(static_cast<double>(Value)
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));};
const VoxelForgeCore::FloatingIslandFieldV1 Field(
Plan->manifest.recipe,Plan->manifest.plan);
if(!Field.isValid())return false;
std::int64_t Density=Field.densitySubvoxels(ToFixed(X),ToFixed(Y),ToFixed(Z));
const std::int64_t Bottom=Plan->manifest.recipe.layoutMinimumZSubvoxels;
const std::int64_t Top=Plan->manifest.recipe.layoutMaximumZSubvoxels;
const std::int64_t Seal=Plan->manifest.recipe.boundarySealSubvoxels;
const std::int64_t Zq=ToFixed(Z);
if(Zq>=Bottom&&Zq<Bottom+Seal)Density=std::min(Density,-std::max<std::int64_t>(1,Zq-Bottom));
if(Zq<=Top&&Zq>Top-Seal)Density=std::min(Density,-std::max<std::int64_t>(1,Top-Zq));
OutDensity=static_cast<float>(static_cast<double>(Density)/Scale);
return std::isfinite(OutDensity);
}
bool UsesLegacySurfaceField(float X,float Y,float Z) const
{
if(!Owner||!Owner->Manager||!Owner->Generator)return false;
const FIntVector ChunkCoord(FMath::FloorToInt(X/static_cast<float>(CHUNK_SIZE)),
FMath::FloorToInt(Y/static_cast<float>(CHUNK_SIZE)),
FMath::FloorToInt(Z/static_cast<float>(CHUNK_SIZE)));
return Owner->Manager->GetGeneratorTypeForChunk(ChunkCoord)
==ECaveGeneratorType::SurfaceWorld;
}
float LegacySurfaceDensityAt(float X,float Y,float Z) const
{
// SurfaceWorld consumes biome overrides, terrain ops, water level and its sky cap in
// the legacy stack. Reuse that immutable evaluator as an exact temporary bridge.
// SurfaceWorld lit biomes, ops de terrain, niveau d'eau et plafond; reutiliser ce champ.
return ApplyEditBrushes(Owner->Generator->GetDensityAt(X,Y,Z),X,Y,Z);
}
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::DensityOffset)
{
double Influence=0.0;
if(Brush.Shape==FEditBrush::EShape::Sphere)
{
const double Radius=std::max(Brush.Radius,1.0e-12);
const double T=std::clamp((Distance+Brush.Radius)/Radius,0.0,1.0);
if(Distance+Brush.Radius>=Brush.Radius)continue;
Influence=1.0-T*T*(3.0-2.0*T);
}
else
{
const double Band=std::max(static_cast<double>(Brush.Falloff),0.01);
if(Distance>=Band)continue;
const double T=std::clamp(std::max(Distance,0.0)/Band,0.0,1.0);
Influence=1.0-T*T*(3.0-2.0*T);
}
Density+=Brush.Strength*static_cast<float>(Influence);
continue;
}
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
{
int32 Lo=0,Hi=-1;
int32 StrateIndex=INDEX_NONE;
FStrateGenerationParams Params;
};
enum class ESurfaceFamily : uint8 { Strata, Cave, SolidOverlay };
static bool NearInteger(float V,int32& Out)
{
const float R=std::round(V);
if(std::abs(V-R)>0.0001f||R<static_cast<float>(MIN_int32)||R>static_cast<float>(MAX_int32))return false;
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];}
static int32 FloorDiv(int32 A,int32 B)
{int32 Q=A/B,R=A%B;return Q-((R!=0&&R<0)?1:0);}
static int64 RoundDiv(int64 N,int64 D)
{return N>=0?(N+D/2)/D:-((-N+D/2)/D);}
static float SmoothMinDistance(float A,float B,float Width)
{
if(Width<=0.0f)return std::min(A,B);
const float H=std::max(Width-std::abs(A-B),0.0f)/Width;
return std::min(A,B)-(H*H*H)*(Width/6.0f);
}
static float SmoothMaxDistance(float A,float B,float Width)
{return -SmoothMinDistance(-A,-B,Width);}
static float SmoothUnit(float V)
{
V=std::clamp(V,0.0f,1.0f);
return V*V*(3.0f-2.0f*V);
}
static float DetailBandFade(float SignedDistance)
{
// Keep the larger candidate mask conservative around smooth family unions;
// visible relief still fades to zero beyond its 4.125-voxel support radius.
constexpr float Edge=DetailSupremum+0.125f;
constexpr float Width=1.5f;
return SmoothUnit((Edge-std::abs(SignedDistance))/Width);
}
static float DetailContrast(float CaveInfluence,float FloorInfluence)
{
const float Cave=std::clamp(CaveInfluence,0.0f,1.0f);
const float Floor=std::min(Cave,std::clamp(FloorInfluence,0.0f,1.0f));
const float StrataWeight=1.0f-Cave;
const float CaveWeight=Cave-Floor;
const float FloorWeight=Floor;
// The three influences are a partition of unity. Cave floors blend at the
// cave family's amplitude instead of multiplying both family contributions.
return 1.35f*StrataWeight+1.00f*CaveWeight+0.55f*FloorWeight;
}
static float BlendDetailFamilies(float Relief,float PassageBandRelief,float OwnerRelief,
float CaveInfluence,float FloorInfluence,float PassageInfluence)
{
const float Cave=std::clamp(CaveInfluence,0.0f,1.0f);
const float Floor=std::min(Cave,std::clamp(FloorInfluence,0.0f,1.0f));
const float Passage=std::clamp(PassageInfluence,0.0f,1.0f);
const float NonPassageWeight=1.0f-Passage;
const float StrataWeight=NonPassageWeight*(1.0f-Cave);
const float CaveWeight=NonPassageWeight*(Cave-Floor);
const float FloorWeight=NonPassageWeight*Floor;
const float StrataRelief=1.35f*(Relief+0.5f*OwnerRelief);
const float CaveRelief=1.00f*Relief;
const float FloorRelief=0.55f*Relief;
// Passage walls keep the broad coherent relief bands but omit the shortest
// octave, which makes repeated rings on the authored shafts.
const float PassageRelief=0.75f*PassageBandRelief;
return StrataWeight*StrataRelief+CaveWeight*CaveRelief
+FloorWeight*FloorRelief+Passage*PassageRelief;
}
static constexpr float DetailBandInnerRadius=1.5f;
static float DetailBandFade(float SignedDistance,float Edge)
{
const float Distance=std::abs(SignedDistance);
if(Distance<=DetailBandInnerRadius)return 1.0f;
const float Width=Edge-DetailBandInnerRadius;
return Width>0.0f?SmoothUnit((Edge-Distance)/Width):0.0f;
}
static float LocalDetailBandEdge(float MacroValue,float SurfaceSDF,
float CaveInfluence,float FloorInfluence,const FLayerField* Field)
{
float OwnerBound=0.0f;
if(Field)
{
const FStrateGenerationParams& Params=Field->Params;
const float Depth=Params.SurfaceRoughness*2.0f;
if(Depth>0.0f&&std::abs(SurfaceSDF)<Depth&&Params.RoughnessFrequency>0.0f)
{
const float Fade=1.0f-std::abs(SurfaceSDF)/Depth;
// Perlin is bounded by 2; the squared ridged fold by 2.25. This
// uses the largest type bound and remains valid under domain warp.
OwnerBound=3.15f*VOXEL_NOISE_SCALE*Params.SurfaceRoughness*Fade*Fade;
}
}
const float ReliefBound=1.20f*std::abs(MacroValue)
+(0.80f+0.25f*MacroValue)*(0.40f+0.60f);
const float Contrast=DetailContrast(CaveInfluence,FloorInfluence);
const float LocalSupremum=std::min(3.9f,(ReliefBound+0.5f*OwnerBound)*Contrast);
return std::clamp(LocalSupremum+0.125f,DetailBandInnerRadius,DetailSupremum+0.125f);
}
float DetailBandFadeAt(double X,double Y,double Z,float SignedDistance,float SurfaceSDF,
float CaveInfluence,float FloorInfluence) const
{
if(DetailVariant!=EDetailVariant::Coherent
||std::abs(SignedDistance)<=DetailBandInnerRadius)
return DetailBandFade(SignedDistance);
const float MacroValue=FStandaloneResearchTunnels::ValueNoise(
static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z),32,
static_cast<uint32>(Owner->Seed)^0xB5297A4Du);
const FLayerField* Field=FindLayerField(Z);
return DetailBandFade(SignedDistance,LocalDetailBandEdge(MacroValue,SurfaceSDF,
CaveInfluence,FloorInfluence,Field));
}
void BuildLayerBounds()
{
LayerRanges.clear();LayerFields.clear();
const auto& Layout=Owner->Manager->GetLayout();
int32 Top=MIN_int32,Bottom=MAX_int32;
for(const FStrateSlot& Slot:Layout)
{
Top=std::max(Top,Slot.TopChunkZ);Bottom=std::min(Bottom,Slot.BottomChunkZ);
if(!Slot.Definition)continue;
const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2;
FLayerField Field;
Field.Lo=Slot.BottomChunkZ*CHUNK_SIZE;
Field.Hi=(Slot.TopChunkZ+1)*CHUNK_SIZE;
Field.StrateIndex=Slot.StrateIndex;
Field.Params=Owner->Manager->GetGenerationParams(FIntVector(0,0,MidChunkZ));
LayerFields.push_back(std::move(Field));
}
if(Top>=Bottom)
{
LayerRanges.push_back({Bottom*CHUNK_SIZE,(Top+1)*CHUNK_SIZE});
}
}
static std::array<double,3> V3(const FVector& V){return {V.X,V.Y,V.Z};}
static double Dot(const std::array<double,3>& A,const std::array<double,3>& B)
{return A[0]*B[0]+A[1]*B[1]+A[2]*B[2];}
static std::array<double,3> Sub(const FVector& A,const FVector& B)
{return {A.X-B.X,A.Y-B.Y,A.Z-B.Z};}
static std::array<double,3> Normalize(std::array<double,3> V)
{
const double L=std::sqrt(std::max(1.0e-24,Dot(V,V)));
for(double& C:V)C/=L;return V;
}
static std::array<double,3> Cross(const std::array<double,3>& A,const std::array<double,3>& B)
{return {A[1]*B[2]-A[2]*B[1],A[2]*B[0]-A[0]*B[2],A[0]*B[1]-A[1]*B[0]};}
static VF_FieldPrimitive MakeSharedFieldPrimitive(const FPrimitive& P)
{
VF_FieldPrimitive Out{};
if(P.bCapsule)Out.flags|=VF_FIELD_PRIMITIVE_CAPSULE;
if(P.bSmoothCapsuleRadius)Out.flags|=VF_FIELD_PRIMITIVE_SMOOTH_CAPSULE;
if(P.bRoundedBox)Out.flags|=VF_FIELD_PRIMITIVE_ROUNDED_BOX;
if(P.bHasFloorClip)Out.flags|=VF_FIELD_PRIMITIVE_HAS_FLOOR;
if(P.bCurvedFloorClip)Out.flags|=VF_FIELD_PRIMITIVE_CURVED_FLOOR;
if(P.bCarve)Out.flags|=VF_FIELD_PRIMITIVE_CARVE;
if(P.bCritical)Out.flags|=VF_FIELD_PRIMITIVE_CRITICAL;
if(P.bPassageSurface)Out.flags|=VF_FIELD_PRIMITIVE_PASSAGE;
for(int32 A=0;A<3;++A)
{
Out.minVoxel[A]=P.Min[A];Out.maxVoxel[A]=P.Max[A];
Out.center[A]=P.CenterSub[A]/16.0;
Out.capsuleStart[A]=P.CapsuleStartWorld[A];
Out.capsuleDelta[A]=P.CapsuleAxisDelta[A];
Out.roundedHalfExtent[A]=P.RoundedHalfExtent[A];
}
for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J)
Out.q[I*3+J]=static_cast<double>(P.Q[I][J])/QScale;
Out.minRadius=P.MinRadius;
Out.capsuleAxisLengthSquared=P.CapsuleAxisLengthSquared;
Out.capsuleAxisLength=P.CapsuleAxisLength;
Out.capsuleStartRadius=P.CapsuleStartRadius;
Out.capsuleEndRadius=P.CapsuleEndRadius;
Out.profileStartT=P.CapsuleProfileStartT;
Out.profileEndT=P.CapsuleProfileEndT;
Out.profileStartRadius=P.CapsuleProfileStartRadius;
Out.profileEndRadius=P.CapsuleProfileEndRadius;
Out.radiusSlopeStart=P.CapsuleRadiusSlopeStart;
Out.radiusSlopeEnd=P.CapsuleRadiusSlopeEnd;
Out.rounding=P.Rounding;
Out.blendRadius=P.BlendRadius;
Out.floorBlendRadius=P.FloorBlendRadius;
Out.floorCenterZ=P.FloorCenterZ;
Out.floorSlopeX=P.FloorSlopeX;
Out.floorSlopeY=P.FloorSlopeY;
Out.floorCurveOriginX=P.FloorCurveOriginX;
Out.floorCurveOriginY=P.FloorCurveOriginY;
Out.floorCurveDeltaX=P.FloorCurveDeltaX;
Out.floorCurveDeltaY=P.FloorCurveDeltaY;
Out.floorCurveLength=P.FloorCurveLength;
Out.floorCurveStartZ=P.FloorCurveStartZ;
Out.floorCurveEndZ=P.FloorCurveEndZ;
Out.floorCurveTangentStart=P.FloorCurveTangentStart;
Out.floorCurveTangentEnd=P.FloorCurveTangentEnd;
Out.floorCurveStartBlend=P.FloorCurveStartBlend;
Out.floorCurveEndBlend=P.FloorCurveEndBlend;
return Out;
}
void RebuildSharedFieldPrimitives()
{
InvalidateFarFieldGpuInput();
SharedFieldPrimitives.clear();SharedFieldPrimitives.reserve(Primitives.size());
for(const FPrimitive& P:Primitives)
SharedFieldPrimitives.push_back(MakeSharedFieldPrimitive(P));
}
void AddEllipsoid(const FVector& Center,const std::array<double,3>& Radii,
const std::array<double,3>& Axis0={1,0,0},
const std::array<double,3>& Axis1={0,1,0},
const std::array<double,3>& Axis2={0,0,1},bool bCarve=true)
{
FPrimitive P;const auto C=V3(Center);
for(int32 A=0;A<3;++A)P.CenterSub[A]=static_cast<int32>(std::llround(C[A]*SubPerVoxel));
const std::array<std::array<double,3>,3> Axes={Axis0,Axis1,Axis2};
int32 RSub[3];
for(int32 K=0;K<3;++K)RSub[K]=std::max(16,static_cast<int32>(std::llround(std::max(1.0,Radii[K])*SubPerVoxel)));
for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J)
{
int64 Sum=0;
for(int32 K=0;K<3;++K)
{
const int64 RI=static_cast<int64>(std::llround(Axes[K][I]*32768.0));
const int64 RJ=static_cast<int64>(std::llround(Axes[K][J]*32768.0));
const int64 Den=static_cast<int64>(32768)*32768*RSub[K]*RSub[K];
Sum+=RoundDiv(RI*RJ*QScale,Den);
}
P.Q[I][J]=Sum;
}
for(int32 A=0;A<3;++A)
{
double E=0.0;for(int32 K=0;K<3;++K)E+=std::abs(Axes[K][A])*Radii[K];
P.Min[A]=static_cast<int32>(std::floor(C[A]-E))-1;
P.Max[A]=static_cast<int32>(std::ceil(C[A]+E))+1;
}
P.MinRadius=static_cast<float>(std::min({Radii[0],Radii[1],Radii[2]}));
P.BlendRadius=ActiveBlendRadius;
P.bCarve=bCarve;Primitives.push_back(P);
}
void AddRoundedBox(const FVector& Center,const std::array<double,3>& HalfExtent,
double Rounding,bool bCarve=true)
{
const std::array<double,3> Outer={HalfExtent[0]+Rounding,HalfExtent[1]+Rounding,HalfExtent[2]+Rounding};
AddEllipsoid(Center,Outer,{1,0,0},{0,1,0},{0,0,1},bCarve);
FPrimitive& P=Primitives.back();P.bRoundedBox=true;P.Rounding=Rounding;
for(int32 A=0;A<3;++A)P.RoundedHalfExtent[A]=HalfExtent[A];
}
static void ApplyFloorClip(FPrimitive& P,double FloorCenterZ,double SlopeX=0.0,double SlopeY=0.0,
double FloorBlendRadius=0.0)
{
P.bHasFloorClip=true;P.FloorCenterZ=FloorCenterZ-0.5;
P.FloorSlopeX=SlopeX;P.FloorSlopeY=SlopeY;P.FloorBlendRadius=FloorBlendRadius;
}
void AddCapsule(const FVector& A,const FVector& B,float Radius,bool bCarve=true,bool bCritical=false,
bool bFloorClip=false,double FloorA=0.0,double FloorB=0.0)
{
AddLegacyEllipsoidCapsule(A,B,Radius,bCarve,bCritical,bFloorClip,FloorA,FloorB);
}
static FVector ApproximateUnwarp(const FVector& Target,const FStrateGenerationParams& Params,
uint32 Seed)
{
FVector WorldPoint=Target;
const float ZScale=Params.VerticalScale>0.0f?Params.VerticalScale:1.0f;
if(ZScale!=1.0f)WorldPoint.Z*=ZScale;
if(Params.CaveWarpStrength<=0.0f)return WorldPoint;
for(int32 Iteration=0;Iteration<8;++Iteration)
{
const FVector Mapped=VoxelCaveMorphology::ApplyCaveWarp(WorldPoint,Params,Seed);
const FVector Error=Target-Mapped;
if(!Error.ContainsNaN()&&VoxelMath::IsFinite(Error.X)
&&VoxelMath::IsFinite(Error.Y)&&VoxelMath::IsFinite(Error.Z))
{
WorldPoint.X+=Error.X;WorldPoint.Y+=Error.Y;WorldPoint.Z+=Error.Z*ZScale;
}
}
return WorldPoint;
}
void AddLegacyEllipsoidCapsule(const FVector& A,const FVector& B,float Radius,bool bCarve,
bool bCritical,bool bFloorClip,double FloorA,double FloorB)
{
const auto D=Sub(B,A);const double Length=std::sqrt(Dot(D,D));
const double PreviousBlendRadius=ActiveBlendRadius;ActiveBlendRadius=0.0;
if(!(Length>1.0e-4))
{
const size_t Before=Primitives.size();
AddEllipsoid(A,{Radius,Radius,Radius},{1,0,0},{0,1,0},{0,0,1},bCarve);
if(bFloorClip)ApplyFloorClip(Primitives.back(),std::min(FloorA,FloorB),0.0,0.0,
0.0);
if(bCritical)for(size_t I=Before;I<Primitives.size();++I)Primitives[I].bCritical=true;
ActiveBlendRadius=PreviousBlendRadius;return;
}
const auto U=Normalize(D);
const std::array<double,3> Ref=std::abs(U[2])<0.90?std::array<double,3>{0,0,1}:std::array<double,3>{0,1,0};
const auto V=Normalize(Cross(U,Ref));const auto W=Normalize(Cross(U,V));
const double SegmentStep=std::max(8.0,static_cast<double>(Radius)*2.0);
const int32 Segments=std::max(1,static_cast<int32>(std::ceil(Length/SegmentStep)));
for(int32 I=0;I<Segments;++I)
{
const double T0=static_cast<double>(I)/Segments,T1=static_cast<double>(I+1)/Segments;
const FVector P0(A.X+D[0]*T0,A.Y+D[1]*T0,A.Z+D[2]*T0);
const FVector P1(A.X+D[0]*T1,A.Y+D[1]*T1,A.Z+D[2]*T1);
const FVector C((P0.X+P1.X)*0.5,(P0.Y+P1.Y)*0.5,(P0.Z+P1.Z)*0.5);
const double Half=Length*(T1-T0)*0.5+Radius;
const size_t Before=Primitives.size();AddEllipsoid(C,{Half,Radius,Radius},U,V,W,bCarve);
if(bFloorClip)
{
const double Mid=(T0+T1)*0.5;
const double Floor=(1.0-Mid)*FloorA+Mid*FloorB;
const double DX=P1.X-P0.X,DY=P1.Y-P0.Y,HorizontalSq=DX*DX+DY*DY;
const double FloorStep=(FloorB-FloorA)*(T1-T0);
if(HorizontalSq>1.0e-6)
ApplyFloorClip(Primitives.back(),Floor,FloorStep*DX/HorizontalSq,
FloorStep*DY/HorizontalSq,0.0);
else ApplyFloorClip(Primitives.back(),std::min(FloorA,FloorB),0.0,0.0,
0.0);
}
if(bCritical)for(size_t J=Before;J<Primitives.size();++J)Primitives[J].bCritical=true;
}
ActiveBlendRadius=PreviousBlendRadius;
}
void AddTaperedCapsule(const FVector& A,const FVector& B,float RadiusA,float RadiusB,
bool bCarve=true,bool bCritical=false,bool bFloorClip=false,
double FloorA=0.0,double FloorB=0.0,
double ExtendStart=0.0,double ExtendEnd=0.0)
{
RadiusA=std::max(1.0f,RadiusA);RadiusB=std::max(1.0f,RadiusB);
const auto D=Sub(B,A);const double Length=std::sqrt(Dot(D,D));
if(!(Length>1.0e-4))
{
AddEllipsoid(A,{RadiusA,RadiusA,RadiusA},{1,0,0},{0,1,0},{0,0,1},bCarve);
Primitives.back().bCritical=bCritical;
if(bFloorClip)ApplyFloorClip(Primitives.back(),std::min(FloorA,FloorB),0.0,0.0,
ActiveFloorBlendRadius);
return;
}
ExtendStart=std::clamp(ExtendStart,0.0,Length*0.4);
ExtendEnd=std::clamp(ExtendEnd,0.0,Length*0.4);
const double RadiusSlope=(static_cast<double>(RadiusB)-RadiusA)/Length;
const float ExtendedRadiusA=static_cast<float>(std::max(1.0,RadiusA-RadiusSlope*ExtendStart));
const float ExtendedRadiusB=static_cast<float>(std::max(1.0,RadiusB+RadiusSlope*ExtendEnd));
const double Start[3]={A.X-D[0]*(ExtendStart/Length),A.Y-D[1]*(ExtendStart/Length),A.Z-D[2]*(ExtendStart/Length)};
const double End[3]={B.X+D[0]*(ExtendEnd/Length),B.Y+D[1]*(ExtendEnd/Length),B.Z+D[2]*(ExtendEnd/Length)};
FPrimitive P;
P.bCapsule=true;P.bCarve=bCarve;P.bCritical=bCritical;
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;
const double ExtendedFloorA=FloorA-FloorSlope*ExtendStart;
const double ExtendedFloorB=FloorB+FloorSlope*ExtendEnd;
const double ExtendedD[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]};
const double HorizontalSq=ExtendedD[0]*ExtendedD[0]+ExtendedD[1]*ExtendedD[1];
if(HorizontalSq>1.0e-6)
ApplyFloorClip(P,(ExtendedFloorA+ExtendedFloorB)*0.5,
(ExtendedFloorB-ExtendedFloorA)*ExtendedD[0]/HorizontalSq,
(ExtendedFloorB-ExtendedFloorA)*ExtendedD[1]/HorizontalSq,ActiveFloorBlendRadius);
else ApplyFloorClip(P,std::min(ExtendedFloorA,ExtendedFloorB),0.0,0.0,ActiveFloorBlendRadius);
}
Primitives.push_back(P);
}
static double ContinuousJoinExtension(float RadiusA,float RadiusB,double SegmentLength)
{
return std::min(std::max(0.0,SegmentLength*0.35),
JoinBlendRadius+0.5*static_cast<double>(std::max(RadiusA,RadiusB)));
}
static float CapsuleRadiusAt(const FPrimitive& P,const FVector& Point)
{
if(!P.bCapsule)return P.MinRadius;
const double Start[3]={P.CapsuleStartSub[0]/static_cast<double>(SubPerVoxel),
P.CapsuleStartSub[1]/static_cast<double>(SubPerVoxel),
P.CapsuleStartSub[2]/static_cast<double>(SubPerVoxel)};
const double End[3]={P.CapsuleEndSub[0]/static_cast<double>(SubPerVoxel),
P.CapsuleEndSub[1]/static_cast<double>(SubPerVoxel),
P.CapsuleEndSub[2]/static_cast<double>(SubPerVoxel)};
const double V[3]={Point.X-Start[0],Point.Y-Start[1],Point.Z-Start[2]};
const double D[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]};
const double D2=D[0]*D[0]+D[1]*D[1]+D[2]*D[2];
const double T=D2>1.0e-12?(V[0]*D[0]+V[1]*D[1]+V[2]*D[2])/D2:0.0;
return CapsuleRadiusAtT(P,T,std::sqrt(std::max(0.0,D2)));
}
static void FitCapsuleRadiiAtEndpoints(FPrimitive& P,const FVector& PointA,float RadiusA,
const FVector& PointB,float RadiusB)
{
const double Start[3]={P.CapsuleStartSub[0]/static_cast<double>(SubPerVoxel),
P.CapsuleStartSub[1]/static_cast<double>(SubPerVoxel),
P.CapsuleStartSub[2]/static_cast<double>(SubPerVoxel)};
const double End[3]={P.CapsuleEndSub[0]/static_cast<double>(SubPerVoxel),
P.CapsuleEndSub[1]/static_cast<double>(SubPerVoxel),
P.CapsuleEndSub[2]/static_cast<double>(SubPerVoxel)};
const double D[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]};
const double D2=D[0]*D[0]+D[1]*D[1]+D[2]*D[2];
if(D2<=1.0e-12)return;
const auto Parameter=[&](const FVector& Point)
{
const double V[3]={Point.X-Start[0],Point.Y-Start[1],Point.Z-Start[2]};
return (V[0]*D[0]+V[1]*D[1]+V[2]*D[2])/D2;
};
const double TA=Parameter(PointA),TB=Parameter(PointB);
if(std::abs(TB-TA)<=1.0e-9)return;
const double Slope=(RadiusB-RadiusA)/(TB-TA);
P.CapsuleStartRadius=static_cast<float>(RadiusA-Slope*TA);
P.CapsuleEndRadius=static_cast<float>(P.CapsuleStartRadius+Slope);
P.MinRadius=std::min(P.CapsuleStartRadius,P.CapsuleEndRadius);
}
static void SetCapsuleRadiusProfile(FPrimitive& P,const FVector& PointA,float RadiusA,
double SlopeA,const FVector& PointB,float RadiusB,double SlopeB)
{
const double Start[3]={P.CapsuleStartSub[0]/static_cast<double>(SubPerVoxel),
P.CapsuleStartSub[1]/static_cast<double>(SubPerVoxel),
P.CapsuleStartSub[2]/static_cast<double>(SubPerVoxel)};
const double End[3]={P.CapsuleEndSub[0]/static_cast<double>(SubPerVoxel),
P.CapsuleEndSub[1]/static_cast<double>(SubPerVoxel),
P.CapsuleEndSub[2]/static_cast<double>(SubPerVoxel)};
const double D[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]};
const double D2=D[0]*D[0]+D[1]*D[1]+D[2]*D[2];
if(D2<=1.0e-12)return;
const auto Parameter=[&](const FVector& Point)
{
const double V[3]={Point.X-Start[0],Point.Y-Start[1],Point.Z-Start[2]};
return std::clamp((V[0]*D[0]+V[1]*D[1]+V[2]*D[2])/D2,0.0,1.0);
};
P.CapsuleProfileStartT=Parameter(PointA);
P.CapsuleProfileEndT=Parameter(PointB);
P.CapsuleProfileStartRadius=RadiusA;
P.CapsuleProfileEndRadius=RadiusB;
P.CapsuleRadiusSlopeStart=SlopeA;
P.CapsuleRadiusSlopeEnd=SlopeB;
P.bSmoothCapsuleRadius=P.CapsuleProfileEndT-P.CapsuleProfileStartT>1.0e-6;
}
static float SmoothedControlRadius(const TArray<float>& Radii,int32 Index,float Fallback,
float Minimum)
{
const int32 Count=Radii.Num();
const float Current=std::max(Minimum,Radii.IsValidIndex(Index)?Radii[Index]:Fallback);
if(Index<=0||Index+1>=Count)return Current;
const auto At=[&](int32 I)
{return std::max(Minimum,Radii[std::clamp(I,0,Count-1)]);};
return (At(Index-4)+8.0f*At(Index-3)+28.0f*At(Index-2)+56.0f*At(Index-1)
+70.0f*Current+56.0f*At(Index+1)+28.0f*At(Index+2)+8.0f*At(Index+3)
+At(Index+4))*(1.0f/256.0f);
}
static double ControlRadiusSlopeAt(const TArray<FVector>& Points,const TArray<float>& Radii,
int32 Index,float Fallback,float Minimum)
{
if(Points.Num()<2||Index<0||Index>=Points.Num())return 0.0;
const auto RadiusAt=[&](int32 I)
{return static_cast<double>(SmoothedControlRadius(Radii,I,Fallback,Minimum));};
const auto LengthAt=[&](int32 I)
{
const FVector& A=Points[I];const FVector& B=Points[I+1];
const double DX=B.X-A.X,DY=B.Y-A.Y,DZ=B.Z-A.Z;
return std::sqrt(DX*DX+DY*DY+DZ*DZ);
};
if(Index==0)
{
const double Length=LengthAt(0);
return Length>1.0e-9?(RadiusAt(1)-RadiusAt(0))/Length:0.0;
}
if(Index+1==Points.Num())
{
const double Length=LengthAt(Index-1);
return Length>1.0e-9?(RadiusAt(Index)-RadiusAt(Index-1))/Length:0.0;
}
const double PrevLength=LengthAt(Index-1),NextLength=LengthAt(Index);
if(PrevLength<=1.0e-9||NextLength<=1.0e-9)return 0.0;
const double PrevSlope=(RadiusAt(Index)-RadiusAt(Index-1))/PrevLength;
const double NextSlope=(RadiusAt(Index+1)-RadiusAt(Index))/NextLength;
if(PrevSlope*NextSlope<=0.0)return 0.0;
const double W1=2.0*NextLength+PrevLength,W2=NextLength+2.0*PrevLength;
const double Harmonic=(W1+W2)/(W1/PrevSlope+W2/NextSlope);
const double Limit=3.0*std::min(std::abs(PrevSlope),std::abs(NextSlope));
return std::copysign(std::min(std::abs(Harmonic),Limit),Harmonic);
}
static double EvaluateFloorProfile(double A,double B,double TangentA,double TangentB,
double Length,double StartBlend,double EndBlend,double S)
{
const double Secant=(B-A)/Length;
if(S<0.0)return A+TangentA*S;
if(S>Length)return B+TangentB*(S-Length);
double Result=std::lerp(A,B,S/Length);
const auto Correction=[](double DeltaSlope,double Span,double T)
{
if(!(Span>0.0)||T<0.0||T>=1.0)return 0.0;
return DeltaSlope*Span*(T-2.0*T*T+T*T*T);
};
if(S<StartBlend)
Result+=Correction(TangentA-Secant,StartBlend,S/StartBlend);
if(Length-S<EndBlend)
Result-=Correction(TangentB-Secant,EndBlend,(Length-S)/EndBlend);
return Result;
}
static double FloorSurfaceRelief(double X,double Y,uint32 Seed)
{
// A shared XY field keeps independently authored floor clips from ending on
// separate flat planes. Its peak displacement stays under 1.2 voxels.
const float Broad=FStandaloneResearchTunnels::ValueNoise(
static_cast<float>(X),static_cast<float>(Y),0.0f,32,Seed^0x7F4A7C15u);
const float Mid=FStandaloneResearchTunnels::ValueNoise(
static_cast<float>(X),static_cast<float>(Y),0.0f,16,Seed^0x94D049BBu);
return 0.80*Broad+0.35*Mid;
}
static std::vector<double> BuildFloorTangents(const TArray<FVector>& Points,
const std::vector<float>& Floor,const std::vector<uint8_t>& MarkedSegments)
{
const size_t Count=Floor.size();std::vector<double> Tangent(Count,0.0);
if(Count<2||Count!=static_cast<size_t>(Points.Num())||MarkedSegments.size()+1!=Count)return Tangent;
std::vector<double> Length(Count-1,0.0),Slope(Count-1,0.0);
for(size_t I=0;I+1<Count;++I)
{
const FVector& A=Points[static_cast<int32>(I)];const FVector& B=Points[static_cast<int32>(I+1)];
Length[I]=std::hypot(B.X-A.X,B.Y-A.Y);
if(Length[I]>1.0e-6)Slope[I]=(Floor[I+1]-Floor[I])/Length[I];
}
for(size_t I=0;I<Count;++I)
{
const bool bHasPrev=I>0&&Length[I-1]>1.0e-6;
const bool bHasNext=I+1<Count&&Length[I]>1.0e-6;
if(I==0){Tangent[I]=MarkedSegments[0]?0.0:Slope[0];continue;}
if(I+1==Count){Tangent[I]=MarkedSegments[I-1]?0.0:Slope[I-1];continue;}
const bool bPrevMarked=MarkedSegments[I-1]!=0,bNextMarked=MarkedSegments[I]!=0;
if(bPrevMarked||bNextMarked)
{
if(!bPrevMarked&&bHasPrev)Tangent[I]=Slope[I-1];
else if(!bNextMarked&&bHasNext)Tangent[I]=Slope[I];
continue;
}
if(!bHasPrev||!bHasNext||Slope[I-1]*Slope[I]<=0.0)continue;
const double W0=2.0*Length[I]+Length[I-1],W1=Length[I]+2.0*Length[I-1];
const double Denominator=W0/Slope[I-1]+W1/Slope[I];
if(std::abs(Denominator)>1.0e-12)
Tangent[I]=(W0+W1)/Denominator;
}
return Tangent;
}
static void SetCurvedFloorClip(FPrimitive& P,const FVector& A,const FVector& B,
double FloorA,double FloorB,double TangentA,double TangentB)
{
if(!P.bHasFloorClip)return;
const double DX=B.X-A.X,DY=B.Y-A.Y,Length=std::hypot(DX,DY);
if(Length<=1.0e-6)return;
P.bCurvedFloorClip=true;P.FloorCurveOriginX=A.X;P.FloorCurveOriginY=A.Y;
P.FloorCurveDeltaX=DX;P.FloorCurveDeltaY=DY;P.FloorCurveLength=Length;
P.FloorCurveStartZ=FloorA;P.FloorCurveEndZ=FloorB;
P.FloorCurveTangentStart=TangentA;P.FloorCurveTangentEnd=TangentB;
P.FloorCurveStartBlend=std::min(8.0,Length*0.35);
P.FloorCurveEndBlend=std::min(8.0,Length*0.35);
for(int32 I=1;I<64;++I)
{
const double T=I/64.0;
const double Curve=EvaluateFloorProfile(FloorA,FloorB,TangentA,TangentB,
Length,P.FloorCurveStartBlend,P.FloorCurveEndBlend,Length*T);
const double Linear=std::lerp(FloorA,FloorB,T);
P.FloorCurveMaxDeviation=std::max(P.FloorCurveMaxDeviation,std::abs(Curve-Linear));
}
}
double FloorAt(const FPrimitive& P,double X,double Y) const
{
double Base=0.0;
if(P.bCurvedFloorClip)
{
const double T=((X-P.FloorCurveOriginX)*P.FloorCurveDeltaX
+(Y-P.FloorCurveOriginY)*P.FloorCurveDeltaY)
/(P.FloorCurveLength*P.FloorCurveLength);
Base=EvaluateFloorProfile(P.FloorCurveStartZ,P.FloorCurveEndZ,
P.FloorCurveTangentStart,P.FloorCurveTangentEnd,P.FloorCurveLength,
P.FloorCurveStartBlend,P.FloorCurveEndBlend,T*P.FloorCurveLength);
}
else
Base=P.FloorCenterZ+P.FloorSlopeX*(X-P.CenterSub[0]/16.0)
+P.FloorSlopeY*(Y-P.CenterSub[1]/16.0);
return Base;
}
double FloorSlopeAtEndpoint(const FPrimitive& P,bool bEnd) const
{
const double DX=P.bCurvedFloorClip?P.FloorCurveDeltaX
:P.CapsuleEndSub[0]-P.CapsuleStartSub[0];
const double DY=P.bCurvedFloorClip?P.FloorCurveDeltaY
:P.CapsuleEndSub[1]-P.CapsuleStartSub[1];
const double Length=P.bCurvedFloorClip?P.FloorCurveLength
:std::hypot(DX,DY)/SubPerVoxel;
if(Length<=1.0e-9)return 0.0;
const double OriginX=P.bCurvedFloorClip?P.FloorCurveOriginX
:P.CapsuleStartSub[0]/static_cast<double>(SubPerVoxel);
const double OriginY=P.bCurvedFloorClip?P.FloorCurveOriginY
:P.CapsuleStartSub[1]/static_cast<double>(SubPerVoxel);
const double X=OriginX+(bEnd?DX/(P.bCurvedFloorClip?1.0:SubPerVoxel):0.0);
const double Y=OriginY+(bEnd?DY/(P.bCurvedFloorClip?1.0:SubPerVoxel):0.0);
constexpr double Epsilon=0.125;
const double UX=DX/(Length*(P.bCurvedFloorClip?1.0:SubPerVoxel));
const double UY=DY/(Length*(P.bCurvedFloorClip?1.0:SubPerVoxel));
return (FloorAt(P,X+UX*Epsilon,Y+UY*Epsilon)
-FloorAt(P,X-UX*Epsilon,Y-UY*Epsilon))/(2.0*Epsilon);
}
void BuildOwnerPrimitives()
{
const auto& Layout=Owner->Manager->GetLayout();
const float MinX=static_cast<float>(Min[0]-256),MinY=static_cast<float>(Min[1]-256);
const float MaxX=static_cast<float>(Max[0]+256),MaxY=static_cast<float>(Max[1]+256);
for(const FStrateSlot& Slot:Layout)
{
if(!Slot.Definition)continue;
const FIntVector MidChunk(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2);
const FStrateGenerationParams Params=Owner->Manager->GetGenerationParams(MidChunk);
ActiveBlendRadius=0.0;
ActiveFloorBlendRadius=0.0;
FChunkSDFCache Cache;
VoxelCaveMorphology::BuildChunkCache(Cache,MinX,MinY,MaxX,MaxY,Params,
static_cast<uint32>(Owner->Seed),
Slot.StrateIndex,&Slot.Definition->TerrainOperations,ERoomGraphBuildSite::Unknown);
for(const FCachedRoom& R:Cache.Rooms)
{
++Stats.RoomCount;
const size_t RoomPrimitiveStart=Primitives.size();
const FVector RoomCenter=ApproximateUnwarp(R.Center,Params,
static_cast<uint32>(Owner->Seed));
if(R.ShapeType==2)
AddCapsule(ApproximateUnwarp(R.ShapeA,Params,static_cast<uint32>(Owner->Seed)),
ApproximateUnwarp(R.ShapeB,Params,static_cast<uint32>(Owner->Seed)),
std::max(1.0f,R.ShapeR),true);
else if(R.ShapeType==1)
{
AddRoundedBox(RoomCenter,{std::abs(R.ShapeA.X),std::abs(R.ShapeA.Y),std::abs(R.ShapeA.Z)},
std::max(0.0f,R.ShapeR),true);
}
else
AddEllipsoid(RoomCenter,{std::abs(R.ShapeA.X),std::abs(R.ShapeA.Y),std::abs(R.ShapeA.Z)},
{1,0,0},{0,1,0},{0,0,1},true);
if(R.FloorCutZ>-FLT_MAX*0.5f)
{
const double Relief=std::max(0.0f,R.FloorReliefStrength);
const double FloorZ=ApproximateUnwarp(
FVector(R.Center.X,R.Center.Y,R.FloorCutZ-Relief),Params,
static_cast<uint32>(Owner->Seed)).Z;
for(size_t I=RoomPrimitiveStart;I<Primitives.size();++I)
if(Primitives[I].bCarve)ApplyFloorClip(Primitives[I],FloorZ,0.0,0.0,
FloorClipBlendRadius);
}
// The packed signs still union room intervals exactly. Blend only
// the analytic cave field where neighboring authored rooms meet;
// this rounds the room-to-room edge without touching tunnel radii.
for(size_t I=RoomPrimitiveStart;I<Primitives.size();++I)
if(Primitives[I].bCarve)Primitives[I].BlendRadius=RoomUnionBlendRadius;
}
// Do not turn automatic room-overlap joins into thin positive ellipsoids.
// Their 2.5-voxel support plates can sit above the lower room floor and render as
// disconnected shards. Each room already carries its own clipped floor; the join
// metadata remains available to the owner generator and is audited here.
Stats.RoomFloorJoinCount+=static_cast<uint64>(Cache.RoomFloorJoins.Num());
const double RoomBlendRadius=ActiveBlendRadius;
ActiveBlendRadius=JoinBlendRadius;
ActiveFloorBlendRadius=FloorClipBlendRadius;
for(const FCachedTunnel& T:Cache.Tunnels)
{
++Stats.TunnelCount;
const bool bHasWorldChain=T.WorldControlPoints.Num()>1
&&T.WorldControlRadii.Num()==T.WorldControlPoints.Num();
const TArray<FVector>& Points=bHasWorldChain?T.WorldControlPoints:T.ControlPoints;
const TArray<float>& Radii=bHasWorldChain?T.WorldControlRadii:T.ControlRadii;
const TArray<FTunnelFloorSegmentProfile>& FloorProfiles=bHasWorldChain
&&T.WorldFloorProfiles.Num()==Points.Num()-1?T.WorldFloorProfiles:T.FloorProfiles;
std::vector<float> RawFloor(static_cast<size_t>(Points.Num()));
std::vector<uint8_t> MarkedFloorSegments(Points.Num()>0?static_cast<size_t>(Points.Num()-1):0,0);
for(int32 I=0;I<Points.Num();++I)
RawFloor[static_cast<size_t>(I)]=static_cast<float>(Points[I].Z
-SmoothedControlRadius(Radii,I,T.RadiusA,2.0f));
if(FloorProfiles.Num()==Points.Num()-1)
{
for(int32 I=0;I+1<Points.Num();++I)
{
const FTunnelFloorSegmentProfile& Profile=FloorProfiles[I];
if(I==0)RawFloor[0]=Profile.StartFloorZ;
else RawFloor[static_cast<size_t>(I)]=0.5f*(FloorProfiles[I-1].EndFloorZ+Profile.StartFloorZ);
RawFloor[static_cast<size_t>(I+1)]=Profile.EndFloorZ;
MarkedFloorSegments[static_cast<size_t>(I)]=Profile.bLedgeTransition?1:0;
}
}
for(int32 I=0;I<Points.Num();++I)
{
const bool bPreviousUnmarked=I==0
||!MarkedFloorSegments[static_cast<size_t>(I-1)];
const bool bNextUnmarked=I+1>=Points.Num()
||!MarkedFloorSegments[static_cast<size_t>(I)];
if(bPreviousUnmarked&&bNextUnmarked)
RawFloor[static_cast<size_t>(I)]+=static_cast<float>(FloorSurfaceRelief(
Points[I].X,Points[I].Y,static_cast<uint32>(Owner->Seed)));
}
const std::vector<double> FloorTangents=BuildFloorTangents(
Points,RawFloor,MarkedFloorSegments);
for(int32 Join=1;Join+1<Points.Num();++Join)
{
const FVector A=Points[Join]-Points[Join-1],B=Points[Join+1]-Points[Join];
const double AL=std::sqrt(A.X*A.X+A.Y*A.Y+A.Z*A.Z);
const double BL=std::sqrt(B.X*B.X+B.Y*B.Y+B.Z*B.Z);
if(AL>1.0e-6&&BL>1.0e-6)
{
const double Dot=(A.X*B.X+A.Y*B.Y+A.Z*B.Z)/(AL*BL);
const double Angle=std::acos(std::clamp(Dot,-1.0,1.0))*57.29577951308232;
if(Angle>Stats.MaxTunnelBendDegrees)
{
Stats.MaxTunnelBendDegrees=Angle;Stats.MaxTunnelBendVoxels=Points[Join];
const double DX=A.X/AL+B.X/BL,DY=A.Y/AL+B.Y/BL,DZ=A.Z/AL+B.Z/BL;
const double DL=std::sqrt(DX*DX+DY*DY+DZ*DZ);
if(DL>1.0e-6)Stats.MaxTunnelBendDirection=FVector(DX/DL,DY/DL,DZ/DL);
}
}
const float RPrev=std::max(2.0f,Radii.IsValidIndex(Join-1)?Radii[Join-1]:T.RadiusA);
const float RHere=std::max(2.0f,Radii.IsValidIndex(Join)?Radii[Join]:T.RadiusA);
const float RNext=std::max(2.0f,Radii.IsValidIndex(Join+1)?Radii[Join+1]:T.RadiusB);
Stats.LegacySegmentRadiusStep=std::max(Stats.LegacySegmentRadiusStep,
static_cast<double>(std::abs(std::max(RPrev,RHere)-std::max(RHere,RNext))));
if(FloorProfiles.IsValidIndex(Join-1)&&FloorProfiles.IsValidIndex(Join))
{
Stats.MaxCapsuleJoinFloorDelta=std::max(Stats.MaxCapsuleJoinFloorDelta,
static_cast<double>(std::abs(FloorProfiles[Join-1].EndFloorZ
-FloorProfiles[Join].StartFloorZ)));
const FVector PrevD=Points[Join]-Points[Join-1];
const FVector NextD=Points[Join+1]-Points[Join];
const double PrevXY=std::sqrt(PrevD.X*PrevD.X+PrevD.Y*PrevD.Y);
const double NextXY=std::sqrt(NextD.X*NextD.X+NextD.Y*NextD.Y);
const double RawPrevSlope=PrevXY>1.0e-6
?(FloorProfiles[Join-1].EndFloorZ-FloorProfiles[Join-1].StartFloorZ)/PrevXY:0.0;
const double RawNextSlope=NextXY>1.0e-6
?(FloorProfiles[Join].EndFloorZ-FloorProfiles[Join].StartFloorZ)/NextXY:0.0;
const double RawSlopeDelta=std::abs(RawPrevSlope-RawNextSlope);
if(FloorProfiles[Join-1].bLedgeTransition||FloorProfiles[Join].bLedgeTransition)
{
++Stats.MarkedFloorSlopeJoins;
Stats.MaxMarkedFloorSlopeDelta=std::max(Stats.MaxMarkedFloorSlopeDelta,RawSlopeDelta);
}
else
{
Stats.MaxRawUnmarkedFloorSlopeDelta=std::max(Stats.MaxRawUnmarkedFloorSlopeDelta,RawSlopeDelta);
}
}
++Stats.CapsuleJoinCount;
}
if(Points.Num()>1)
{
const size_t SegmentPrimitiveStart=Primitives.size();
for(int32 I=0;I+1<Points.Num();++I)
{
const float R0=SmoothedControlRadius(Radii,I,T.RadiusA,2.0f);
const float R1=SmoothedControlRadius(Radii,I+1,T.RadiusB,2.0f);
const double RadiusSlope0=ControlRadiusSlopeAt(Points,Radii,I,T.RadiusA,2.0f);
const double RadiusSlope1=ControlRadiusSlopeAt(Points,Radii,I+1,T.RadiusB,2.0f);
Stats.SmoothedProfileRadiusStep=std::max(Stats.SmoothedProfileRadiusStep,
static_cast<double>(std::abs(R1-R0)));
const FVector Segment=Points[I+1]-Points[I];
const double SegmentLength=std::sqrt(Segment.X*Segment.X+Segment.Y*Segment.Y+Segment.Z*Segment.Z);
const double Extension=ContinuousJoinExtension(R0,R1,SegmentLength);
const float ZA=RawFloor[static_cast<size_t>(I)];
const float ZB=RawFloor[static_cast<size_t>(I+1)];
AddTaperedCapsule(Points[I],Points[I+1],R0,R1,true,false,true,ZA,ZB,
I>0?Extension:0.0,I+2<Points.Num()?Extension:0.0);
if(!MarkedFloorSegments[static_cast<size_t>(I)])
{
SetCurvedFloorClip(Primitives.back(),Points[I],Points[I+1],ZA,ZB,
FloorTangents[static_cast<size_t>(I)],FloorTangents[static_cast<size_t>(I+1)]);
++Stats.CurvedFloorSegments;
Stats.MaxFloorCurveDeviation=std::max(Stats.MaxFloorCurveDeviation,
Primitives.back().FloorCurveMaxDeviation);
}
FitCapsuleRadiiAtEndpoints(Primitives.back(),Points[I],R0,Points[I+1],R1);
SetCapsuleRadiusProfile(Primitives.back(),Points[I],R0,RadiusSlope0,
Points[I+1],R1,RadiusSlope1);
}
for(int32 Join=1;Join+1<Points.Num();++Join)
{
const FPrimitive& Previous=Primitives[SegmentPrimitiveStart+static_cast<size_t>(Join-1)];
const FPrimitive& Next=Primitives[SegmentPrimitiveStart+static_cast<size_t>(Join)];
const float PreviousEnd=CapsuleRadiusAt(Previous,Points[Join]);
const float NextStart=CapsuleRadiusAt(Next,Points[Join]);
Stats.MaxCapsuleJoinRadiusDelta=std::max(Stats.MaxCapsuleJoinRadiusDelta,
static_cast<double>(std::abs(PreviousEnd-NextStart)));
Stats.MaxCapsuleJoinRadiusSlopeDelta=std::max(
Stats.MaxCapsuleJoinRadiusSlopeDelta,
std::abs(Previous.CapsuleRadiusSlopeEnd-Next.CapsuleRadiusSlopeStart));
if(!MarkedFloorSegments[static_cast<size_t>(Join-1)]
&&!MarkedFloorSegments[static_cast<size_t>(Join)])
{
const double TangentDelta=Previous.bCurvedFloorClip&&Next.bCurvedFloorClip
?std::abs(Previous.FloorCurveTangentEnd-Next.FloorCurveTangentStart)
:std::abs(FloorSlopeAtEndpoint(Previous,true)-FloorSlopeAtEndpoint(Next,false));
const double DirectionalSlopeChange=std::abs(FloorSlopeAtEndpoint(Previous,true)
-FloorSlopeAtEndpoint(Next,false));
Stats.MaxCapsuleJoinFloorSlopeDelta=std::max(
Stats.MaxCapsuleJoinFloorSlopeDelta,TangentDelta);
Stats.MaxUnmarkedFloorSlopeDelta=std::max(
Stats.MaxUnmarkedFloorSlopeDelta,TangentDelta);
Stats.MaxUnmarkedFloorDirectionSlopeDelta=std::max(
Stats.MaxUnmarkedFloorDirectionSlopeDelta,DirectionalSlopeChange);
}
}
}
else AddCapsule(T.EndpointA,T.EndpointB,std::max(T.RadiusA,T.RadiusB),true);
}
ActiveBlendRadius=RoomBlendRadius;
ActiveFloorBlendRadius=0.0;
for(const FCachedPit& P:Cache.Pits)
AddCapsule(FVector(P.CenterX,P.CenterY,P.TopZ-P.Depth),
FVector(P.CenterX,P.CenterY,P.TopZ),std::max(2.0f,P.Radius),true);
for(const FCachedChimney& C:Cache.Chimneys)
AddCapsule(FVector(C.CenterX,C.CenterY,C.BottomZ),
FVector(C.CenterX,C.CenterY,C.BottomZ+C.Height),std::max(2.0f,C.Radius),true);
for(const FCachedColumn& C:Cache.Columns)
{
FVector Center(C.CenterX,C.CenterY,(LayerRanges[0].Lo+LayerRanges[0].Hi)*0.5);
const double HalfZ=std::max(8.0,(LayerRanges[0].Hi-LayerRanges[0].Lo)*0.48);
AddEllipsoid(Center,{std::max(2.0f,C.Radius),std::max(2.0f,C.Radius),HalfZ},
{1,0,0},{0,1,0},{0,0,1},false);
}
// Reproduce the owner's finite, top-anchored origin room in every strate.
// Reproduire dans chaque strate la salle d'origine finie et ancrée en haut.
AddOriginLandingRoom(FieldForStrate(Slot.StrateIndex));
}
const double PassageJoinBlend=ActiveBlendRadius;ActiveBlendRadius=JoinBlendRadius;
ActiveFloorBlendRadius=FloorClipBlendRadius;
for(const FVoxelPassage& P:Owner->Manager->GetPassages())
{
++Stats.PassageCount;
const TArray<FVector>& Points=P.ControlPoints;
const TArray<float>& Radii=P.ControlRadii;
const size_t PassagePrimitiveStart=Primitives.size();
std::vector<float> RawFloor(static_cast<size_t>(Points.Num()));
std::vector<uint8_t> MarkedFloorSegments(Points.Num()>0?static_cast<size_t>(Points.Num()-1):0,0);
for(int32 I=0;I<Points.Num();++I)
RawFloor[static_cast<size_t>(I)]=static_cast<float>(Points[I].Z
-SmoothedControlRadius(Radii,I,P.Radius,6.0f));
if(P.NativeFloorProfileZ.Num()==Points.Num())
for(int32 I=0;I<Points.Num();++I)RawFloor[static_cast<size_t>(I)]=P.NativeFloorProfileZ[I];
for(int32 I=0;I<Points.Num();++I)
RawFloor[static_cast<size_t>(I)]+=static_cast<float>(FloorSurfaceRelief(
Points[I].X,Points[I].Y,static_cast<uint32>(Owner->Seed)));
const std::vector<double> FloorTangents=BuildFloorTangents(
Points,RawFloor,MarkedFloorSegments);
for(int32 Join=1;Join+1<Points.Num();++Join)
{
const float RPrev=std::max(6.0f,Radii.IsValidIndex(Join-1)?Radii[Join-1]:P.Radius);
const float RHere=std::max(6.0f,Radii.IsValidIndex(Join)?Radii[Join]:P.Radius);
const float RNext=std::max(6.0f,Radii.IsValidIndex(Join+1)?Radii[Join+1]:P.Radius);
Stats.LegacySegmentRadiusStep=std::max(Stats.LegacySegmentRadiusStep,
static_cast<double>(std::abs(std::max(RPrev,RHere)-std::max(RHere,RNext))));
if(P.NativeFloorProfileZ.IsValidIndex(Join-1)&&P.NativeFloorProfileZ.IsValidIndex(Join))
{
const FVector PrevD=Points[Join]-Points[Join-1];
const FVector NextD=Points[Join+1]-Points[Join];
const double PrevXY=std::sqrt(PrevD.X*PrevD.X+PrevD.Y*PrevD.Y);
const double NextXY=std::sqrt(NextD.X*NextD.X+NextD.Y*NextD.Y);
const double PrevSlope=PrevXY>1.0e-6
?(P.NativeFloorProfileZ[Join]-P.NativeFloorProfileZ[Join-1])/PrevXY:0.0;
const double NextSlope=NextXY>1.0e-6
?(P.NativeFloorProfileZ[Join+1]-P.NativeFloorProfileZ[Join])/NextXY:0.0;
const double RawSlopeDelta=std::abs(PrevSlope-NextSlope);
Stats.MaxRawUnmarkedFloorSlopeDelta=std::max(Stats.MaxRawUnmarkedFloorSlopeDelta,RawSlopeDelta);
}
++Stats.CapsuleJoinCount;
}
if(Points.Num()>1)
{
for(int32 I=0;I+1<Points.Num();++I)
{
const float RadiusA=SmoothedControlRadius(Radii,I,P.Radius,6.0f);
const float RadiusB=SmoothedControlRadius(Radii,I+1,P.Radius,6.0f);
const double RadiusSlopeA=ControlRadiusSlopeAt(Points,Radii,I,P.Radius,6.0f);
const double RadiusSlopeB=ControlRadiusSlopeAt(Points,Radii,I+1,P.Radius,6.0f);
Stats.SmoothedProfileRadiusStep=std::max(Stats.SmoothedProfileRadiusStep,
static_cast<double>(std::abs(RadiusB-RadiusA)));
const FVector Segment=Points[I+1]-Points[I];
const double SegmentLength=std::sqrt(Segment.X*Segment.X+Segment.Y*Segment.Y+Segment.Z*Segment.Z);
const float ZA=RawFloor[static_cast<size_t>(I)];
const float ZB=RawFloor[static_cast<size_t>(I+1)];
const double Extension=ContinuousJoinExtension(RadiusA,RadiusB,SegmentLength);
const double CoreExtension=ContinuousJoinExtension(std::min(2.5f,RadiusA*0.45f),
std::min(2.5f,RadiusB*0.45f),SegmentLength);
const double ExtendStart=I>0?Extension:0.0,ExtendEnd=I+2<Points.Num()?Extension:0.0;
const double CoreExtendStart=I>0?CoreExtension:0.0,CoreExtendEnd=I+2<Points.Num()?CoreExtension:0.0;
AddTaperedCapsule(Points[I],Points[I+1],RadiusA,RadiusB,true,false,true,ZA,ZB,
ExtendStart,ExtendEnd);
Primitives.back().bPassageSurface=true;
SetCurvedFloorClip(Primitives.back(),Points[I],Points[I+1],ZA,ZB,
FloorTangents[static_cast<size_t>(I)],FloorTangents[static_cast<size_t>(I+1)]);
++Stats.CurvedFloorSegments;
Stats.MaxFloorCurveDeviation=std::max(Stats.MaxFloorCurveDeviation,
Primitives.back().FloorCurveMaxDeviation);
FitCapsuleRadiiAtEndpoints(Primitives.back(),Points[I],RadiusA,Points[I+1],RadiusB);
SetCapsuleRadiusProfile(Primitives.back(),Points[I],RadiusA,RadiusSlopeA,
Points[I+1],RadiusB,RadiusSlopeB);
const float CoreRadiusA=std::min(2.5f,RadiusA*0.45f);
const float CoreRadiusB=std::min(2.5f,RadiusB*0.45f);
const double CoreSlopeA=CoreRadiusA<2.5f-1.0e-4?RadiusSlopeA*0.45:0.0;
const double CoreSlopeB=CoreRadiusB<2.5f-1.0e-4?RadiusSlopeB*0.45:0.0;
// Keep a narrow, un-clipped air core through every authored control segment.
// The larger outer tube owns the D-section floor; its interpolated floor curve
// can bow above the path on sharp drops, so clipping this safety bore would
// close the passage precisely where Cascading/Worm need the continuity repair.
// Garder un petit noyau d'air sans coupe de plancher dans chaque segment.
// Le tube large porte le plancher en D; sa courbe peut depasser l'axe dans les
// chutes brusques et fermer la route si ce noyau herite de la meme coupe.
AddTaperedCapsule(Points[I],Points[I+1],CoreRadiusA,
CoreRadiusB,true,true,false,ZA,ZB,
CoreExtendStart,CoreExtendEnd);
FitCapsuleRadiiAtEndpoints(Primitives.back(),Points[I],CoreRadiusA,
Points[I+1],CoreRadiusB);
SetCapsuleRadiusProfile(Primitives.back(),Points[I],CoreRadiusA,CoreSlopeA,
Points[I+1],CoreRadiusB,CoreSlopeB);
}
for(int32 Join=1;Join+1<Points.Num();++Join)
{
const size_t PreviousBase=PassagePrimitiveStart+static_cast<size_t>(Join-1)*2;
const size_t NextBase=PassagePrimitiveStart+static_cast<size_t>(Join)*2;
for(size_t Tube=0;Tube<2;++Tube)
{
const FPrimitive& Previous=Primitives[PreviousBase+Tube];
const FPrimitive& Next=Primitives[NextBase+Tube];
const float PreviousEnd=CapsuleRadiusAt(Previous,Points[Join]);
const float NextStart=CapsuleRadiusAt(Next,Points[Join]);
Stats.MaxCapsuleJoinRadiusDelta=std::max(Stats.MaxCapsuleJoinRadiusDelta,
static_cast<double>(std::abs(PreviousEnd-NextStart)));
Stats.MaxCapsuleJoinRadiusSlopeDelta=std::max(
Stats.MaxCapsuleJoinRadiusSlopeDelta,
std::abs(Previous.CapsuleRadiusSlopeEnd-Next.CapsuleRadiusSlopeStart));
if(Tube==0)
{
const double TangentDelta=Previous.bCurvedFloorClip&&Next.bCurvedFloorClip
?std::abs(Previous.FloorCurveTangentEnd-Next.FloorCurveTangentStart)
:std::abs(FloorSlopeAtEndpoint(Previous,true)-FloorSlopeAtEndpoint(Next,false));
const double DirectionalSlopeChange=std::abs(FloorSlopeAtEndpoint(Previous,true)
-FloorSlopeAtEndpoint(Next,false));
Stats.MaxCapsuleJoinFloorSlopeDelta=std::max(
Stats.MaxCapsuleJoinFloorSlopeDelta,TangentDelta);
Stats.MaxUnmarkedFloorSlopeDelta=std::max(
Stats.MaxUnmarkedFloorSlopeDelta,TangentDelta);
Stats.MaxUnmarkedFloorDirectionSlopeDelta=std::max(
Stats.MaxUnmarkedFloorDirectionSlopeDelta,DirectionalSlopeChange);
}
}
}
}
else
{
AddCapsule(P.UpperPoint,P.LowerPoint,std::max(6.0f,P.Radius),true,true);
Primitives.back().bPassageSurface=true;
}
AddLanding(P.UpperLanding,true);
AddLanding(P.LowerLanding,true);
}
ActiveBlendRadius=PassageJoinBlend;ActiveFloorBlendRadius=0.0;
}
const FLayerField* FieldForStrate(int32 StrateIndex) const
{
for(const FLayerField& Field:LayerFields)
if(Field.StrateIndex==StrateIndex)return &Field;
return nullptr;
}
void AddOriginLandingRoom(const FLayerField* Field)
{
if(Field==nullptr||Owner==nullptr||Owner->Settings==nullptr)return;
const double Radius=Owner->Settings->OriginSpineRadius;
if(!(Radius>0.0)||!std::isfinite(Radius))return;
// These constants mirror VoxelPassageGeometry in legacy voxel units.
// Ces constantes reprennent VoxelPassageGeometry en voxels historiques.
constexpr double MinimumHalfWidth=12.0;
constexpr double MinimumRoomHeight=24.0;
constexpr double PlayerHeight=14.08;
constexpr double Headroom=8.0;
constexpr double FloorThickness=6.0;
constexpr double SealSafetyMargin=8.0;
constexpr double RoomRounding=3.0;
constexpr double ShaftFloorClearance=8.0;
const double SafeRadius=std::max(std::abs(Radius),2.0);
const double HalfWidth=std::max({MinimumHalfWidth,SafeRadius+4.0,
SafeRadius+ShaftFloorClearance});
const double DesiredHeight=std::max({MinimumRoomHeight,PlayerHeight+Headroom,
2.0*SafeRadius+4.0});
const double Seal=std::max(0.0,static_cast<double>(Field->Params.BoundarySealThickness));
const double InnerBottom=static_cast<double>(Field->Lo)+Seal;
const double InnerTop=static_cast<double>(Field->Hi)-Seal;
const double MinimumFloor=InnerBottom+FloorThickness+SealSafetyMargin;
const double MaximumFloor=InnerTop-DesiredHeight-SealSafetyMargin;
if(!(InnerTop>InnerBottom)||MaximumFloor<MinimumFloor)return;
const double FloorZ=MaximumFloor;
const double CeilingZ=FloorZ+DesiredHeight;
const double HalfZ=(CeilingZ-FloorZ)*0.5;
const double HalfExtent=std::max(HalfWidth-RoomRounding,0.5);
const double RoundedHalfZ=std::max(HalfZ-RoomRounding,0.5);
const size_t Before=Primitives.size();
AddRoundedBox(FVector(0,0,(FloorZ+CeilingZ)*0.5),
{HalfExtent,HalfExtent,RoundedHalfZ},RoomRounding,true);
for(size_t I=Before;I<Primitives.size();++I)
{
// Keep the authored room floor hard and level while leaving all lower seal rock intact.
// Garder un sol horizontal net et préserver la roche du joint inférieur.
ApplyFloorClip(Primitives[I],FloorZ+0.5,0.0,0.0,0.0);
}
}
void AddLanding(const FVoxelPassageLanding& L,bool bCritical=false)
{
if(!(L.HalfWidth>0.0f)||!(L.CeilingZ>L.FloorZ))return;
const double HalfZ=std::max(2.0,(L.CeilingZ-L.FloorZ)*0.5);
const FVector Center(L.StandingPoint.X,L.StandingPoint.Y,(L.CeilingZ+L.FloorZ)*0.5);
const double Width=std::max(6.0f,L.HalfWidth);
AddRoundedBox(Center,{Width,Width,HalfZ},2.0,true);
if(bCritical)
{
const size_t Before=Primitives.size();
AddEllipsoid(Center,{2.5,2.5,HalfZ},{1,0,0},{0,1,0},{0,0,1},true);
for(size_t I=Before;I<Primitives.size();++I)Primitives[I].bCritical=true;
}
// The rounded landing carve and native floor already provide support.
// A shallow positive ellipsoid under this floor creates a broad shaft/floor plate.
}
void BuildRoundTunnelPrimitives()
{
const double PreviousBlendRadius=ActiveBlendRadius;ActiveBlendRadius=0.0;
constexpr double Pitch=144.0,RadiusBase=28.0,HalfHeight=26.0;
const int32 CX0=FloorDiv(Min[0],144)-1,CY0=FloorDiv(Min[1],144)-1;
const int32 CX1=FloorDiv(Max[0],144)+1,CY1=FloorDiv(Max[1],144)+1;
const uint32 Seed=static_cast<uint32>(Owner->Seed);
auto Unit=[](uint32 H){return static_cast<double>(FStandaloneResearchTunnels::Unit(H));};
for(int32 CY=CY0;CY<=CY1;++CY)for(int32 CX=CX0;CX<=CX1;++CX)
for(int32 Axis=0;Axis<2;++Axis)
{
const uint32 H=FStandaloneResearchTunnels::EdgeHash(CX,CY,Axis,Seed);
if(Unit(H^0x68E31DA4u)>0.56)continue;
const int32 NX=CX+(Axis==0?1:0),NY=CY+(Axis==1?1:0);
auto Cell=[&](int32 X,int32 Y,double& OX,double& OY)
{
const uint32 CH=FStandaloneResearchTunnels::EdgeHash(X,Y,3,Seed);
OX=(X+0.5)*Pitch+(Unit(CH^0xA511E9B3u)-0.5)*Pitch*0.18;
OY=(Y+0.5)*Pitch+(Unit(CH^0x63D83595u)-0.5)*Pitch*0.18;
};
double AX,AY,BX,BY;Cell(CX,CY,AX,AY);Cell(NX,NY,BX,BY);
const double DX=BX-AX,DY=BY-AY,L2=DX*DX+DY*DY,L=std::sqrt(std::max(1.0e-8,L2));
const double NXrm=-DY/L,NYrm=DX/L;
const double BA=(Unit(H^0xB5297A4Du)*2.0-1.0)*Pitch*0.16;
const double BB=(Unit(H^0x1B56C4E9u)*2.0-1.0)*Pitch*0.16;
const std::array<double,3> P0={AX,AY,FStandaloneResearchTunnels::WorldCenterZVoxels};
const std::array<double,3> C1={AX+DX/3.0+NXrm*BA,AY+DY/3.0+NYrm*BA,-80.0};
const std::array<double,3> C2={AX+DX*2.0/3.0+NXrm*BB,AY+DY*2.0/3.0+NYrm*BB,-80.0};
const std::array<double,3> P3={BX,BY,FStandaloneResearchTunnels::WorldCenterZVoxels};
const uint32 PhaseHash=H^0xA511E9B3u;
const double Phase=Unit(PhaseHash)*6.2831853071795864769;
auto Bezier=[&](double T)
{
const double O=1.0-T;
std::array<double,3> P{};
for(int32 A=0;A<3;++A)
P[A]=O*O*O*P0[A]+3*O*O*T*C1[A]+3*O*T*T*C2[A]+T*T*T*P3[A];
const double R=RadiusBase*(0.82+0.36*Unit(H^0x63D83595u))
*(1.0+0.22*VoxelMath::DetSin(static_cast<float>(6.2831853071795864769*T+Phase)));
const double VW=4.0*VoxelMath::DetSin(static_cast<float>(3.141592653589793*T))
*VoxelMath::DetSin(static_cast<float>(6.2831853071795864769*T+Phase));
P[2]+=VW;
return std::pair{P,R};
};
constexpr int32 Segments=12;
auto Last=Bezier(0.0);
for(int32 I=1;I<=Segments;++I)
{
const auto Next=Bezier(static_cast<double>(I)/Segments);
const FVector A(Last.first[0],Last.first[1],Last.first[2]);
const FVector B(Next.first[0],Next.first[1],Next.first[2]);
const FVector Segment=B-A;
const double SegmentLength=std::sqrt(Segment.X*Segment.X+Segment.Y*Segment.Y+Segment.Z*Segment.Z);
const double Extension=ContinuousJoinExtension(static_cast<float>(Last.second),
static_cast<float>(Next.second),SegmentLength);
AddTaperedCapsule(A,B,static_cast<float>(Last.second),static_cast<float>(Next.second),
true,false,false,0.0,0.0,I>1?Extension:0.0,I<Segments?Extension:0.0);
Last=Next;
}
}
ActiveBlendRadius=PreviousBlendRadius;
}
void BuildSpatialBins(int32 BinResolution,bool bBuildAxisBins=true)
{
InvalidateFarFieldGpuInput();
SpatialBins.clear();
CriticalAirBins.clear();
ActiveSpatialBinSize=BinResolution;
// 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;
B.MinA=FloorDiv(Min[A],LineBinSize);B.MinB=FloorDiv(Min[C],LineBinSize);
const int32 MaxA=FloorDiv(Max[A],LineBinSize),MaxB=FloorDiv(Max[C],LineBinSize);
B.CountA=MaxA-B.MinA+1;B.CountB=MaxB-B.MinB+1;
B.Items.resize(static_cast<size_t>(B.CountA)*B.CountB);
for(uint32 Id=0;Id<Primitives.size();++Id)
{
const FPrimitive& P=Primitives[Id];
const int32 A0=std::max(B.MinA,FloorDiv(P.Min[A],LineBinSize));
const int32 A1=std::min(MaxA,FloorDiv(P.Max[A],LineBinSize));
const int32 B0=std::max(B.MinB,FloorDiv(P.Min[C],LineBinSize));
const int32 B1=std::min(MaxB,FloorDiv(P.Max[C],LineBinSize));
if(A0>A1||B0>B1)continue;
for(int32 IB=B0;IB<=B1;++IB)for(int32 IA=A0;IA<=A1;++IA)
B.Items[static_cast<size_t>(IB-B.MinB)*B.CountA+(IA-B.MinA)].push_back(Id);
}
}
for(uint32 Id=0;Id<Primitives.size();++Id)
{
const FPrimitive& P=Primitives[Id];
const bool bCriticalAir=P.bCarve&&P.bCritical;
const int32 X0=FloorDiv(P.Min[0]-DetailBand,BinResolution),X1=FloorDiv(P.Max[0]+DetailBand,BinResolution);
const int32 Y0=FloorDiv(P.Min[1]-DetailBand,BinResolution),Y1=FloorDiv(P.Max[1]+DetailBand,BinResolution);
const int32 Z0=FloorDiv(P.Min[2]-DetailBand,BinResolution),Z1=FloorDiv(P.Max[2]+DetailBand,BinResolution);
const int64 BoundingBinCount=static_cast<int64>(X1-X0+1)*(Y1-Y0+1)*(Z1-Z0+1);
if(BoundingBinCount>100000)
{
// Long diagonal passages have a huge AABB even though their capsule is narrow.
// Rasterize the centerline with a conservative bin halo instead of dropping the
// primitive from analytic queries outside the packed near region.
// Les longues galeries diagonales ont une grande AABB mais un tube fin.
// Echantillonner l'axe avec une marge prudente garde les requetes analytiques.
if(!P.bCapsule)continue;
const double BinWidth=static_cast<double>(BinResolution);
const double AxisStep=std::max(1.0,BinWidth*0.5);
const int32 AxisSamples=std::max(1,static_cast<int32>(std::ceil(
P.CapsuleAxisLength/AxisStep)));
const double CapsuleRadius=std::max(P.CapsuleStartRadius,P.CapsuleEndRadius);
const int32 Halo=std::clamp(static_cast<int32>(std::ceil(
(CapsuleRadius+DetailBand)/BinWidth))+1,1,8);
std::vector<FBinKey> PrimitiveBins;
const int64 HaloWidth=2*Halo+1;
const uint64 Reserve=std::min<uint64>(
static_cast<uint64>(AxisSamples+1)*HaloWidth*HaloWidth*HaloWidth,
1000000ull);
PrimitiveBins.reserve(static_cast<size_t>(Reserve));
for(int32 Sample=0;Sample<=AxisSamples;++Sample)
{
const double T=static_cast<double>(Sample)/AxisSamples;
const int32 CX=static_cast<int32>(std::floor(P.CapsuleStartWorld[0]
+P.CapsuleAxisDelta[0]*T));
const int32 CY=static_cast<int32>(std::floor(P.CapsuleStartWorld[1]
+P.CapsuleAxisDelta[1]*T));
const int32 CZ=static_cast<int32>(std::floor(P.CapsuleStartWorld[2]
+P.CapsuleAxisDelta[2]*T));
const int32 BX=FloorDiv(CX,BinResolution),BY=FloorDiv(CY,BinResolution);
const int32 BZ=FloorDiv(CZ,BinResolution);
for(int32 DZ=-Halo;DZ<=Halo;++DZ)
for(int32 DY=-Halo;DY<=Halo;++DY)
for(int32 DX=-Halo;DX<=Halo;++DX)
PrimitiveBins.push_back({BX+DX,BY+DY,BZ+DZ});
}
std::sort(PrimitiveBins.begin(),PrimitiveBins.end(),[](const FBinKey& A,const FBinKey& B)
{
if(A.X!=B.X)return A.X<B.X;
if(A.Y!=B.Y)return A.Y<B.Y;
return A.Z<B.Z;
});
PrimitiveBins.erase(std::unique(PrimitiveBins.begin(),PrimitiveBins.end()),
PrimitiveBins.end());
for(const FBinKey& Key:PrimitiveBins)
{
SpatialBins[Key].push_back(Id);
if(bCriticalAir)CriticalAirBins[Key].push_back(Id);
}
continue;
}
for(int32 Z=Z0;Z<=Z1;++Z)for(int32 Y=Y0;Y<=Y1;++Y)for(int32 X=X0;X<=X1;++X)
{
const FBinKey Key{X,Y,Z};
SpatialBins[Key].push_back(Id);
if(bCriticalAir)CriticalAirBins[Key].push_back(Id);
}
}
}
void BuildFarParentShapes()
{
constexpr int32 ParentPitch=2048;
FarConnectorSegments.clear();
struct FParent { int32 GX=0,GY=0,Strate=0; FVector Center; double Rx=0,Ry=0,Rz=0; };
const int32 GX0=FloorDiv(ClipMin[0],ParentPitch)-1,GY0=FloorDiv(ClipMin[1],ParentPitch)-1;
const int32 GX1=FloorDiv(ClipMax[0],ParentPitch)+1,GY1=FloorDiv(ClipMax[1],ParentPitch)+1;
std::vector<FParent> Parents;
std::unordered_map<uint64,size_t> ByCell;
const auto& Layout=Owner->Manager->GetLayout();
const uint32 Seed=static_cast<uint32>(Owner->Seed);
const auto Key=[](int32 GX,int32 GY,int32 Strate)
{return (static_cast<uint64>(static_cast<uint32>(GX+524288)&0xFFFFFu)<<44)
|(static_cast<uint64>(static_cast<uint32>(GY+524288)&0xFFFFFu)<<24)
|(static_cast<uint64>(static_cast<uint32>(Strate)&0xFFFFu)<<8);};
auto Hash=[](uint32 X)
{X^=X>>16;X*=0x7FEB352Du;X^=X>>15;X*=0x846CA68Bu;X^=X>>16;return X;};
auto Unit=[&](uint32 H){return static_cast<double>(Hash(H)>>8)*(1.0/16777216.0);};
for(const FStrateSlot& Slot:Layout)
{
const double ZBottom=static_cast<double>(Slot.BottomChunkZ)*CHUNK_SIZE;
const double ZTop=static_cast<double>(Slot.TopChunkZ+1)*CHUNK_SIZE;
for(int32 GY=GY0;GY<=GY1;++GY)for(int32 GX=GX0;GX<=GX1;++GX)
{
uint32 H=Hash(Seed^static_cast<uint32>(GX)*0x9E3779B9u
^static_cast<uint32>(GY)*0x85EBCA6Bu
^static_cast<uint32>(Slot.StrateIndex)*0xC2B2AE35u);
const double X=GX*static_cast<double>(ParentPitch)+ParentPitch*0.5
+(Unit(H^0xA511E9B3u)-0.5)*192.0;
const double Y=GY*static_cast<double>(ParentPitch)+ParentPitch*0.5
+(Unit(H^0x63D83595u)-0.5)*192.0;
const double Rx=300.0+Unit(H^0xB5297A4Du)*100.0;
const double Ry=280.0+Unit(H^0x1B56C4E9u)*120.0;
const double Rz=100.0+Unit(H^0x68E31DA4u)*60.0;
if(X+Rx>=Min[0]&&X-Rx<=Max[0]&&Y+Ry>=Min[1]&&Y-Ry<=Max[1])continue;
const double Z=ZBottom+(ZTop-ZBottom)*(0.35+0.30*Unit(H^0x27D4EB2Fu));
FParent P{GX,GY,Slot.StrateIndex,FVector(X,Y,Z),Rx,Ry,Rz};
const size_t Index=Parents.size();Parents.push_back(P);
ByCell.emplace(Key(GX,GY,Slot.StrateIndex),Index);
const std::array<double,3> Axis0={1,0,0},Axis1={0,1,0},Axis2={0,0,1};
AddEllipsoid(P.Center,{P.Rx,P.Ry,P.Rz},Axis0,Axis1,Axis2,true);
}
}
for(const FParent& P:Parents)
{
for(const auto [DX,DY]:std::array<std::pair<int32,int32>,2>{{{1,0},{0,1}}})
{
const auto It=ByCell.find(Key(P.GX+DX,P.GY+DY,P.Strate));
if(It==ByCell.end())continue;
const FVector A(P.Center.X,P.Center.Y,P.Center.Z);
const FVector B=Parents[It->second].Center;
const int32 MinX=static_cast<int32>(std::floor(std::min(A.X,B.X)-64.0));
const int32 MaxX=static_cast<int32>(std::ceil(std::max(A.X,B.X)+64.0));
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;
AddTaperedCapsule(A,B,64.0f,64.0f,true);
Primitives.back().BlendRadius=0.0;
FarConnectorSegments.push_back({A,B,64.0f});
}
}
BuildFarConnectorSpatialBins();
if(!Parents.empty())
std::printf("[FarParents] pitch=%d rooms=%zu links=%llu clip=(%d,%d)..(%d,%d)\n",
ParentPitch,Parents.size(),static_cast<unsigned long long>(Primitives.size()),
ClipMin[0],ClipMin[1],ClipMax[0],ClipMax[1]);
}
void BuildFarConnectorSpatialBins()
{
constexpr int32 BinSize=2048;
FarConnectorSpatialBins.clear();
FarConnectorSpatialBins.reserve(FarConnectorSegments.size());
for(uint32 Index=0;Index<FarConnectorSegments.size();++Index)
{
const FFarConnectorSegment& Connector=FarConnectorSegments[Index];
const int32 MinX=static_cast<int32>(std::floor(
(std::min(Connector.Start.X,Connector.End.X)-Connector.Radius)/BinSize));
const int32 MaxX=static_cast<int32>(std::floor(
(std::max(Connector.Start.X,Connector.End.X)+Connector.Radius)/BinSize));
const int32 MinY=static_cast<int32>(std::floor(
(std::min(Connector.Start.Y,Connector.End.Y)-Connector.Radius)/BinSize));
const int32 MaxY=static_cast<int32>(std::floor(
(std::max(Connector.Start.Y,Connector.End.Y)+Connector.Radius)/BinSize));
const int32 MinZ=static_cast<int32>(std::floor(
(std::min(Connector.Start.Z,Connector.End.Z)-Connector.Radius)/BinSize));
const int32 MaxZ=static_cast<int32>(std::floor(
(std::max(Connector.Start.Z,Connector.End.Z)+Connector.Radius)/BinSize));
for(int32 Z=MinZ;Z<=MaxZ;++Z)for(int32 Y=MinY;Y<=MaxY;++Y)
for(int32 X=MinX;X<=MaxX;++X)
FarConnectorSpatialBins[{X,Y,Z}].push_back(Index);
}
}
const std::vector<uint32>& GetLineCandidates(int32 Axis,int32 A,int32 B) const
{
static const std::vector<uint32> Empty;
const FAxisBins& G=AxisBins[Axis];
const int32 IA=FloorDiv(A,LineBinSize)-G.MinA,IB=FloorDiv(B,LineBinSize)-G.MinB;
if(IA<0||IB<0||IA>=G.CountA||IB>=G.CountB)return Empty;
return G.Items[static_cast<size_t>(IB)*G.CountA+IA];
}
FBinKey PointBin(double X,double Y,double Z) const
{return {FloorDiv(static_cast<int32>(std::floor(X)),ActiveSpatialBinSize),FloorDiv(static_cast<int32>(std::floor(Y)),ActiveSpatialBinSize),FloorDiv(static_cast<int32>(std::floor(Z)),ActiveSpatialBinSize)};}
bool ShapeContains(const FPrimitive& P,int32 X,int32 Y,int32 Z) const
{
bool bInside=false;
if(P.bCapsule)
{
const double Position[3]={static_cast<double>(X),static_cast<double>(Y),static_cast<double>(Z)};
double V[3],D[3];double D2=0.0,VD=0.0,V2=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];V2+=V[A]*V[A];
}
const double R0=P.CapsuleStartRadius,R1=P.CapsuleEndRadius,DR=R1-R0;
const double QA=D2-DR*DR,QB=-2.0*(VD+R0*DR),QC=V2-R0*R0;
double Minimum=std::min(QC,QA+QB+QC);
if(QA>1.0e-12)
{
const double T=std::clamp(-QB/(2.0*QA),0.0,1.0);
Minimum=std::min(Minimum,(QA*T+QB)*T+QC);
}
bInside=Minimum<=0.0;
}
else if(P.bRoundedBox)
{
const double D[3]={std::abs(X-P.CenterSub[0]/16.0)-P.RoundedHalfExtent[0],
std::abs(Y-P.CenterSub[1]/16.0)-P.RoundedHalfExtent[1],
std::abs(Z-P.CenterSub[2]/16.0)-P.RoundedHalfExtent[2]};
const double Ox=std::max(D[0],0.0),Oy=std::max(D[1],0.0),Oz=std::max(D[2],0.0);
const double Outside=std::sqrt(Ox*Ox+Oy*Oy+Oz*Oz);
const double Inside=std::min(std::max({D[0],D[1],D[2]}),0.0);
bInside=Outside+Inside-P.Rounding<=0.0;
}
else
{
const int32 World[3]={X,Y,Z};int64 D[3];
for(int32 A=0;A<3;++A)D[A]=static_cast<int64>(World[A])*SubPerVoxel-P.CenterSub[A];
int64 Q=0;
for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J)Q+=P.Q[I][J]*D[I]*D[J];
bInside=Q<=static_cast<int64>(QScale);
}
if(!bInside)return false;
if(P.bHasFloorClip)
{
const double Floor=FloorAt(P,X,Y);
if(P.FloorBlendRadius<=0.0)return Z>=Floor;
return SmoothMaxDistance(ShapeDistance(P,X,Y,Z),static_cast<float>(Floor-Z),
static_cast<float>(P.FloorBlendRadius))<=0.0f;
}
return true;
}
static float CapsuleRadiusAtT(const FPrimitive& P,double T,double AxisLength)
{
T=std::clamp(T,0.0,1.0);
if(!P.bSmoothCapsuleRadius)
return static_cast<float>(P.CapsuleStartRadius
+(P.CapsuleEndRadius-P.CapsuleStartRadius)*T);
const double T0=P.CapsuleProfileStartT,T1=P.CapsuleProfileEndT;
if(T<T0)return static_cast<float>(P.CapsuleProfileStartRadius
+P.CapsuleRadiusSlopeStart*AxisLength*(T-T0));
if(T>T1)return static_cast<float>(P.CapsuleProfileEndRadius
+P.CapsuleRadiusSlopeEnd*AxisLength*(T-T1));
const double SpanT=std::max(1.0e-9,T1-T0);
const double U=std::clamp((T-T0)/SpanT,0.0,1.0),U2=U*U,U3=U2*U;
const double SpanDistance=AxisLength*SpanT;
const double H00=2.0*U3-3.0*U2+1.0;
const double H10=U3-2.0*U2+U;
const double H01=-2.0*U3+3.0*U2;
const double H11=U3-U2;
return static_cast<float>(H00*P.CapsuleProfileStartRadius
+H10*SpanDistance*P.CapsuleRadiusSlopeStart
+H01*P.CapsuleProfileEndRadius
+H11*SpanDistance*P.CapsuleRadiusSlopeEnd);
}
float ShapeDistance(const FPrimitive& P,double X,double Y,double Z) const
{
if(P.bCapsule)
{
const double Position[3]={X,Y,Z};
double V[3],D[3],D2=0.0,VD=0.0;
for(int32 A=0;A<3;++A)
{
V[A]=Position[A]-P.CapsuleStartWorld[A];
D[A]=P.CapsuleAxisDelta[A];
VD+=V[A]*D[A];
}
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,P.CapsuleAxisLength);
return static_cast<float>(std::sqrt(DistanceSquared)-Radius);
}
if(P.bRoundedBox)
{
const double D[3]={std::abs(X-P.CenterSub[0]/16.0)-P.RoundedHalfExtent[0],
std::abs(Y-P.CenterSub[1]/16.0)-P.RoundedHalfExtent[1],
std::abs(Z-P.CenterSub[2]/16.0)-P.RoundedHalfExtent[2]};
const double Ox=std::max(D[0],0.0),Oy=std::max(D[1],0.0),Oz=std::max(D[2],0.0);
const double Outside=std::sqrt(Ox*Ox+Oy*Oy+Oz*Oz);
const double Inside=std::min(std::max({D[0],D[1],D[2]}),0.0);
return static_cast<float>(Outside+Inside-P.Rounding);
}
const double D[3]={X-P.CenterSub[0]/16.0,Y-P.CenterSub[1]/16.0,Z-P.CenterSub[2]/16.0};
double Q=0.0;
for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J)Q+=static_cast<double>(P.Q[I][J])/QScale*D[I]*D[J]*256.0;
return static_cast<float>((std::sqrt(std::max(0.0,Q))-1.0)*P.MinRadius);
}
bool SolveLine(const FPrimitive& P,int32 Axis,const int32 Fixed[3],FInterval& Out) const
{
const int32 A=(Axis+1)%3,B=(Axis+2)%3;
const int32 Lo=P.Min[Axis],Hi=P.Max[Axis];
if(Fixed[A]<P.Min[A]||Fixed[A]>P.Max[A]||Fixed[B]<P.Min[B]||Fixed[B]>P.Max[B])return false;
int32 L=Lo,H=Hi;
if(!P.bRoundedBox&&!P.bCapsule)
{
double CoefA=static_cast<double>(P.Q[Axis][Axis]);
double CoefB=0.0,CoefC=-static_cast<double>(QScale);
int64 D[3];D[Axis]=0;
for(int32 J=0;J<3;++J)if(J!=Axis)D[J]=static_cast<int64>(Fixed[J])*SubPerVoxel-P.CenterSub[J];
for(int32 J=0;J<3;++J)if(J!=Axis)
{
CoefB+=2.0*P.Q[Axis][J]*D[J];
for(int32 K=0;K<3;++K)if(K!=Axis)CoefC+=static_cast<double>(P.Q[J][K])*D[J]*D[K];
}
if(!(CoefA>0.0))return false;
const double Disc=CoefB*CoefB-4.0*CoefA*CoefC;
if(Disc<0.0)return false;
const double S=std::sqrt(Disc);
const double U0=(-CoefB-S)/(2.0*CoefA),U1=(-CoefB+S)/(2.0*CoefA);
const double T0=(U0+P.CenterSub[Axis])/SubPerVoxel;
const double T1=(U1+P.CenterSub[Axis])/SubPerVoxel;
L=std::max(Lo,static_cast<int32>(std::floor(T0))-1);
H=std::min(Hi,static_cast<int32>(std::ceil(T1))+1);
}
if(L>H)return false;
if(P.bCarve&&P.bHasFloorClip&&(!P.bCurvedFloorClip||Axis==2))
{
double X0=Fixed[0],Y0=Fixed[1],Z0=Fixed[2];
if(Axis==0)X0=0.0;else if(Axis==1)Y0=0.0;else Z0=0.0;
const double Floor0=FloorAt(P,X0,Y0);
const double SmoothAllowance=P.FloorBlendRadius>0.0?P.FloorBlendRadius/6.0:0.0;
const double Margin0=Z0-(Floor0-SmoothAllowance);
const double MarginSlope=Axis==0?-P.FloorSlopeX:Axis==1?-P.FloorSlopeY:1.0;
if(std::abs(MarginSlope)<1.0e-12)
{if(Margin0<0.0)return false;}
else
{
const double Boundary=-Margin0/MarginSlope;
if(MarginSlope>0.0)L=std::max(L,static_cast<int32>(std::ceil(Boundary)));
else H=std::min(H,static_cast<int32>(std::floor(Boundary)));
}
}
if(L>H)return false;
int32 Mid=L+(H-L)/2;
if(P.bCapsule)
{
double V2=0.0,VD=0.0,D2=0.0;
for(int32 J=0;J<3;++J)if(J!=Axis)
{
const double Start=P.CapsuleStartSub[J]/static_cast<double>(SubPerVoxel);
const double End=P.CapsuleEndSub[J]/static_cast<double>(SubPerVoxel);
const double V=Fixed[J]-Start,D=End-Start;
V2+=V*V;VD+=V*D;D2+=D*D;
}
const double R0=P.CapsuleStartRadius,DR=P.CapsuleEndRadius-R0;
const double QA=D2-DR*DR,QB=-2.0*(VD+R0*DR),QC=V2-R0*R0;
const auto Value=[&](double T){return (QA*T+QB)*T+QC;};
double TBest=Value(1.0)<Value(0.0)?1.0:0.0;
if(QA>1.0e-12)
{
const double T=std::clamp(-QB/(2.0*QA),0.0,1.0);
if(Value(T)<Value(TBest))TBest=T;
}
if(Value(TBest)>0.0)return false;
const double Start=P.CapsuleStartSub[Axis]/static_cast<double>(SubPerVoxel);
const double End=P.CapsuleEndSub[Axis]/static_cast<double>(SubPerVoxel);
Mid=std::clamp(static_cast<int32>(std::llround(Start+(End-Start)*TBest)),L,H);
}
if(!ShapeContains(P,Axis==0?Mid:Fixed[0],Axis==1?Mid:Fixed[1],Axis==2?Mid:Fixed[2]))
{
bool Found=false;
const int32 SearchLimit=P.bCapsule?std::min(H-L,8):2;
for(int32 Delta=1;Delta<=SearchLimit&&!Found;++Delta)
{
if(Mid-Delta>=L&&ShapeContains(P,Axis==0?Mid-Delta:Fixed[0],Axis==1?Mid-Delta:Fixed[1],Axis==2?Mid-Delta:Fixed[2])){Mid-=Delta;Found=true;}
else if(Mid+Delta<=H&&ShapeContains(P,Axis==0?Mid+Delta:Fixed[0],Axis==1?Mid+Delta:Fixed[1],Axis==2?Mid+Delta:Fixed[2])){Mid+=Delta;Found=true;}
}
if(!Found&&P.bCapsule)
{
for(int32 Sample=L;Sample<=H;Sample+=2)
if(ShapeContains(P,Axis==0?Sample:Fixed[0],Axis==1?Sample:Fixed[1],Axis==2?Sample:Fixed[2]))
{Mid=Sample;Found=true;break;}
if(!Found&&((H-L)&1)
&&ShapeContains(P,Axis==0?H:Fixed[0],Axis==1?H:Fixed[1],Axis==2?H:Fixed[2]))
{Mid=H;Found=true;}
}
if(!Found)return false;
}
int32 Left=L,Right=Mid;
while(Left<Right)
{
const int32 M=Left+(Right-Left)/2;
const bool Inside=ShapeContains(P,Axis==0?M:Fixed[0],Axis==1?M:Fixed[1],Axis==2?M:Fixed[2]);
if(Inside)Right=M;else Left=M+1;
}
const int32 First=Left;Left=Mid;Right=H;
while(Left<Right)
{
const int32 M=Left+(Right-Left+1)/2;
const bool Inside=ShapeContains(P,Axis==0?M:Fixed[0],Axis==1?M:Fixed[1],Axis==2?M:Fixed[2]);
if(Inside)Left=M;else Right=M-1;
}
Out={First,Left};return true;
}
static void MergeIntervals(std::vector<FInterval>& Ranges)
{
if(Ranges.size()<2)return;
std::sort(Ranges.begin(),Ranges.end(),[](const FInterval& A,const FInterval& B){return A.Lo==B.Lo?A.Hi<B.Hi:A.Lo<B.Lo;});
size_t Out=0;
for(size_t I=1;I<Ranges.size();++I)
{
if(Ranges[I].Lo<=Ranges[Out].Hi+1)Ranges[Out].Hi=std::max(Ranges[Out].Hi,Ranges[I].Hi);
else Ranges[++Out]=Ranges[I];
}
Ranges.resize(Out+1);
}
static void FillBits(std::vector<uint64>& Words,size_t Base,int32 Lo,int32 Hi,bool bSet)
{
if(Lo>Hi)return;
const size_t First=static_cast<size_t>(Lo)>>6,Last=static_cast<size_t>(Hi)>>6;
for(size_t W=First;W<=Last;++W)
{
const uint32 Bit0=W==First?(static_cast<uint32>(Lo)&63u):0u;
const uint32 Bit1=W==Last?(static_cast<uint32>(Hi)&63u):63u;
const uint64 Mask=(~uint64{0}<<Bit0)&(~uint64{0}>>(63u-Bit1));
if(bSet)Words[Base+W]|=Mask;else Words[Base+W]&=~Mask;
}
}
void BuildPackedRockIntervals(FParallelTiming& Timing)
{
const int32 NX=Size[0],NY=Size[1],NZ=Size[2];
CandidateAxisWords[2].assign(static_cast<size_t>(NX)*NY*WordsPerZ,0);
struct FIntervalScratch
{
std::vector<FInterval> Carve,Solid,ProtectedAir;
};
std::array<FIntervalScratch,16> WorkerScratch;
for(int32 Worker=0;Worker<WorkerCount;++Worker)
{
WorkerScratch[static_cast<size_t>(Worker)].Carve.reserve(24);
WorkerScratch[static_cast<size_t>(Worker)].Solid.reserve(8);
WorkerScratch[static_cast<size_t>(Worker)].ProtectedAir.reserve(4);
}
ParallelLines(static_cast<size_t>(NX)*NY,[&](size_t Line,int32 Worker)
{
const int32 X=Min[0]+static_cast<int32>(Line%NX),Y=Min[1]+static_cast<int32>(Line/NX);
FIntervalScratch& Scratch=WorkerScratch[static_cast<size_t>(Worker)];
std::vector<FInterval>& Carve=Scratch.Carve;
std::vector<FInterval>& Solid=Scratch.Solid;
std::vector<FInterval>& ProtectedAir=Scratch.ProtectedAir;
Carve.clear();Solid.clear();ProtectedAir.clear();
const int32 Fixed[3]={X,Y,0};
for(uint32 Id:GetLineCandidates(2,X,Y))
{
const FPrimitive& P=Primitives[Id];FInterval R;
if(SolveLine(P,2,Fixed,R))
{
if(!P.bCritical)
{
const size_t CandidateBase=Line*WordsPerZ;
const int32 CandidateLo=std::max(Min[2],R.Lo-DetailBand);
const int32 CandidateHi=std::min(Max[2],R.Lo+DetailBand);
const int32 CandidateLo2=std::max(Min[2],R.Hi-DetailBand);
const int32 CandidateHi2=std::min(Max[2],R.Hi+DetailBand);
FillBits(CandidateAxisWords[2],CandidateBase,
CandidateLo-Min[2],CandidateHi-Min[2],true);
FillBits(CandidateAxisWords[2],CandidateBase,
CandidateLo2-Min[2],CandidateHi2-Min[2],true);
}
R.Lo=std::max(R.Lo,Min[2])-Min[2];R.Hi=std::min(R.Hi,Max[2])-Min[2];
if(R.Lo<=R.Hi)
{
if(!P.bCarve)Solid.push_back(R);
else if(P.bCritical)ProtectedAir.push_back(R);
else Carve.push_back(R);
}
}
}
MergeIntervals(Carve);MergeIntervals(Solid);MergeIntervals(ProtectedAir);
const size_t Base=Line*WordsPerZ;
const int32 ZLo=LayerRanges[0].Lo, ZHi=LayerRanges[0].Hi;
const int32 BaseLo=std::max(ZLo,Min[2])-Min[2],BaseHi=std::min(ZHi,Max[2])-Min[2];
if(BaseLo<=BaseHi)FillBits(RockWords,Base,BaseLo,BaseHi,true);
for(const FInterval& R:Carve)FillBits(RockWords,Base,R.Lo,R.Hi,false);
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,nullptr,16);
}
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)
{
if(Axis==2)continue;
const int32 A=(Axis+1)%3,B=(Axis+2)%3;
const size_t Lines=static_cast<size_t>(Size[A])*Size[B];
const size_t Words=(static_cast<size_t>(N[Axis])+63u)/64u;
CandidateAxisWords[Axis].assign(Lines*Words,0);
ParallelLines(Lines,[&,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]);
int32 Fixed[3]{};Fixed[A]=CA;Fixed[B]=CB;
for(uint32 Id:GetLineCandidates(Axis,CA,CB))
{
const FPrimitive& P=Primitives[Id];if(P.bCritical)continue;
FInterval R;if(!SolveLine(P,Axis,Fixed,R))continue;
const int32 Lo=std::max(Min[Axis],R.Lo-DetailBand);
const int32 Hi=std::min(Max[Axis],R.Lo+DetailBand);
const int32 Lo2=std::max(Min[Axis],R.Hi-DetailBand);
const int32 Hi2=std::min(Max[Axis],R.Hi+DetailBand);
const size_t Base=Line*Words;
FillBits(CandidateAxisWords[Axis],Base,Lo-Min[Axis],Hi-Min[Axis],true);
FillBits(CandidateAxisWords[Axis],Base,Lo2-Min[Axis],Hi2-Min[Axis],true);
}
},&AxisTiming,nullptr,16);
}
const auto MergeStart=std::chrono::steady_clock::now();
CandidateWords=std::move(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;
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]);
for(size_t W=0;W<Words;++W)
{
uint64 Bits=CandidateAxisWords[Axis][Line*Words+W];
while(Bits)
{
const uint32 Bit=std::countr_zero(Bits);Bits&=Bits-1;
const int32 T=Min[Axis]+static_cast<int32>(W*64+Bit);
if(T>Max[Axis])continue;
int32 Coord[3]{};Coord[Axis]=T;Coord[A]=CA;Coord[B]=CB;
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]);
std::atomic_ref<uint64>(CandidateWords[Column*WordsPerZ+(Zi>>6)])
.fetch_or(uint64{1}<<(Zi&63u),std::memory_order_relaxed);
}
}
},&MergeTiming,nullptr,16);
}
const int32 LayerLo=LayerRanges.empty()?1:LayerRanges[0].Lo;
const int32 LayerHi=LayerRanges.empty()?0:LayerRanges[0].Hi;
if(LayerLo<=LayerHi)
{
for(size_t Line=0;Line<static_cast<size_t>(Size[0])*Size[1];++Line)
{
const size_t Base=Line*WordsPerZ;
const int32 Lo0=std::max(Min[2],LayerLo-DetailBand)-Min[2];
const int32 Hi0=std::min(Max[2],LayerLo+DetailBand)-Min[2];
const int32 Lo1=std::max(Min[2],LayerHi-DetailBand)-Min[2];
const int32 Hi1=std::min(Max[2],LayerHi+DetailBand)-Min[2];
FillBits(CandidateWords,Base,Lo0,Hi0,true);
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);
}
bool BuildGpuVisualDetailNodeSets(
const std::vector<VoxelForgeCore::DetailBandTileRegion>& GpuTiles,
const std::vector<VoxelForgeCore::DetailBandTileRegion>& CpuTiles,
std::string& Error)
{
if(GpuTiles.empty()&&CpuTiles.empty())
{
GpuVisualDetailWords.clear();CpuVisualDetailWords.clear();
if(bProfileDetailBand)
{
GpuVisualDetailWords.assign(CandidateWords.size(),0u);
CpuVisualDetailWords.assign(CandidateWords.size(),0u);
}
GpuVisualDetailTileCount=CpuVisualDetailTileCount=0;
GpuVisualDetailGridSamples=CpuVisualDetailGridSamples=0;
GpuVisualDetailNodeCount=CpuVisualDetailNodeCount=0;
return true;
}
const auto BuildSet=[&](const std::vector<VoxelForgeCore::DetailBandTileRegion>& Tiles,
std::vector<uint64>& Nodes,uint64& GridSamples)->bool
{
Nodes.clear();GridSamples=0;
uint64 ReserveSamples=0;
for(const auto& Tile:Tiles)
{
if(Tile.cells<=0||Tile.cells>256||Tile.step<=0)
{Error="GPU visual detail-band tile region has invalid cells or step";return false;}
const uint64 Dim=static_cast<uint64>(Tile.cells)+3u;
const uint64 TileSamples=Dim*Dim*Dim;
if(TileSamples>std::numeric_limits<uint64>::max()-ReserveSamples)
{Error="GPU visual detail-band tile sample count overflowed 64 bits";return false;}
ReserveSamples+=TileSamples;
}
GridSamples=ReserveSamples;
const auto IsCandidate=[&](int32 X,int32 Y,int32 Z)
{
if(X<Min[0]||X>Max[0]||Y<Min[1]||Y>Max[1]||Z<Min[2]||Z>Max[2])
return false;
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]);
return (CandidateWords[Column*WordsPerZ+(Zi>>6)]
&(uint64{1}<<(Zi&63u)))!=0;
};
Nodes.assign(CandidateWords.size(),0u);
for(const auto& Tile:Tiles)
for(int32 Z=-1;Z<=Tile.cells+1;++Z)
for(int32 Y=-1;Y<=Tile.cells+1;++Y)
for(int32 X=-1;X<=Tile.cells+1;++X)
{
const int32 WX=Tile.originVoxel.x+X*Tile.step;
const int32 WY=Tile.originVoxel.y+Y*Tile.step;
const int32 WZ=Tile.originVoxel.z+Z*Tile.step;
if(!IsCandidate(WX,WY,WZ))continue;
const size_t Column=static_cast<size_t>(WY-Min[1])*Size[0]
+static_cast<size_t>(WX-Min[0]);
const size_t Zi=static_cast<size_t>(WZ-Min[2]);
Nodes[Column*WordsPerZ+(Zi>>6)]|=uint64{1}<<(Zi&63u);
}
return true;
};
if(!BuildSet(GpuTiles,GpuVisualDetailWords,GpuVisualDetailGridSamples)
||!BuildSet(CpuTiles,CpuVisualDetailWords,CpuVisualDetailGridSamples))return false;
GpuVisualDetailNodeCount=0;CpuVisualDetailNodeCount=0;
for(const uint64 Bits:GpuVisualDetailWords)GpuVisualDetailNodeCount+=std::popcount(Bits);
for(const uint64 Bits:CpuVisualDetailWords)CpuVisualDetailNodeCount+=std::popcount(Bits);
GpuVisualDetailTileCount=GpuTiles.size();CpuVisualDetailTileCount=CpuTiles.size();
if(!GpuTiles.empty()||!CpuTiles.empty())
{
std::printf("[GpuVisualDetailBandPlan] gpu_tiles=%llu gpu_sample_nodes=%llu gpu_unique_candidate_nodes=%llu cpu_near_tiles=%llu cpu_sample_nodes=%llu cpu_unique_candidate_nodes=%llu\n",
static_cast<unsigned long long>(GpuVisualDetailTileCount),
static_cast<unsigned long long>(GpuVisualDetailGridSamples),
static_cast<unsigned long long>(GpuVisualDetailNodeCount),
static_cast<unsigned long long>(CpuVisualDetailTileCount),
static_cast<unsigned long long>(CpuVisualDetailGridSamples),
static_cast<unsigned long long>(CpuVisualDetailNodeCount));
}
return true;
}
bool IsGpuVisualDetailNode(int32 X,int32 Y,int32 Z) const
{
if(!Contains(X,Y,Z)||GpuVisualDetailWords.empty())return false;
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]);
return (GpuVisualDetailWords[Column*WordsPerZ+(Zi>>6)]
&(uint64{1}<<(Zi&63u)))!=0;
}
bool IsCpuVisualDetailNode(int32 X,int32 Y,int32 Z) const
{
if(!Contains(X,Y,Z)||CpuVisualDetailWords.empty())return false;
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]);
return (CpuVisualDetailWords[Column*WordsPerZ+(Zi>>6)]
&(uint64{1}<<(Zi&63u)))!=0;
}
void ApplyDetailBand(FParallelTiming& Timing)
{
using Clock=std::chrono::steady_clock;
const bool bProfile=bProfileDetailBand;
std::array<FDetailWorkerProfile,16> WorkerProfile{};
std::array<double,16> WorkerActiveSeconds{};
std::atomic<uint64> CandidateCount{0},ProxyCount{0},FlipCount{0};
ParallelLines(static_cast<size_t>(Size[0])*Size[1],[&](size_t Line,int32 Worker)
{
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));
const FLayerField* LineField=nullptr;
bool bLineFieldKnown=false;
int32 LineFieldLo=0,LineFieldHi=-1;
const auto FindLineLayerField=[&](double Z)->const FLayerField*
{
if(bLineFieldKnown&&LineField&&Z>=LineFieldLo&&Z<=LineFieldHi)
return LineField;
LineField=FindLayerField(Z);
bLineFieldKnown=LineField!=nullptr;
if(LineField){LineFieldLo=LineField->Lo;LineFieldHi=LineField->Hi;}
return LineField;
};
const int32 QueryBinX=FloorDiv(X,ActiveSpatialBinSize);
const int32 QueryBinY=FloorDiv(Y,ActiveSpatialBinSize);
int32 CachedQueryBinZ=std::numeric_limits<int32>::min();
const FBinItems* CachedQueryBinItems=nullptr;
FDetailWorkerProfile& Profile=WorkerProfile[static_cast<size_t>(Worker)];
if(bProfile)++Profile.Lines;
uint64 LocalCandidates=0,LocalProxies=0,LocalFlips=0;
const size_t WordBase=Line*WordsPerZ;
for(size_t W=0;W<WordsPerZ;++W)
{
uint64 Bits=CandidateWords[WordBase+W];
const uint64 OriginalBits=Bits;
uint64 ValidBits=0;
int32 BatchZ[8]{};
int32 BatchCount=0,BatchQueryBinZ=std::numeric_limits<int32>::min();
const FBinItems* BatchBinItems=nullptr;
const auto ProcessBatch=[&]()
{
if(BatchCount==0)return;
FEffectiveRockSample Samples[8];
int32 ProfileDistanceSamples=0;
if(bProfile)
{
for(int32 Lane=0;Lane<BatchCount;++Lane)
if((Profile.Candidates++&255u)==0)++ProfileDistanceSamples;
}
const auto DistanceStart=ProfileDistanceSamples>0
?Clock::now():Clock::time_point{};
EffectiveRockDistanceZ8(X,Y,BatchZ,BatchCount,BatchBinItems,Samples);
if(ProfileDistanceSamples>0)
{
Profile.DistanceSampleNanoseconds+=static_cast<uint64>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
Clock::now()-DistanceStart).count());
Profile.DistanceSamples+=static_cast<uint64>(BatchCount);
}
for(int32 Lane=0;Lane<BatchCount;++Lane)
{
const int32 Z=BatchZ[Lane];
const size_t Zi=static_cast<size_t>(Z-Min[2]);
++LocalCandidates;
const FEffectiveRockSample& Sample=Samples[Lane];
const float SDF=Sample.Distance;
if(std::abs(SDF)>DetailSupremum+0.125f)continue;
const size_t VisualWordIndex=WordBase+(Zi>>6);
const uint64 VisualMask=uint64{1}<<(Zi&63u);
const bool bGpuVisualNode=bProfile
&&(GpuVisualDetailWords[VisualWordIndex]&VisualMask)!=0;
const bool bCpuVisualNode=bProfile
&&(CpuVisualDetailWords[VisualWordIndex]&VisualMask)!=0;
if(bProfile)
{
if(bGpuVisualNode)++Profile.GpuVisualCandidates;
if(bCpuVisualNode)++Profile.CpuVisualCandidates;
if(bGpuVisualNode&&!bCpuVisualNode)++Profile.GpuOnlyCandidates;
}
const float SurfaceSDF=Sample.SurfaceSDF;
const float CaveInfluence=Sample.CaveInfluence;
const float FloorInfluence=Sample.FloorInfluence;
const float PassageInfluence=Sample.PassageInfluence;
const bool bCriticalAir=Sample.bCriticalAir;
float DetailFade=DetailBandFade(SDF);
float MacroValue=0.0f;
bool bMacroKnown=false;
const FLayerField* DetailField=nullptr;
bool bDetailFieldKnown=false;
if(DetailVariant==EDetailVariant::Coherent
&&std::abs(SDF)>DetailBandInnerRadius)
{
MacroValue=NoiseLine.Macro.Sample(static_cast<float>(Z));
bMacroKnown=true;
DetailField=FindLineLayerField(Z);
bDetailFieldKnown=true;
DetailFade=DetailBandFade(SDF,LocalDetailBandEdge(MacroValue,
SurfaceSDF,CaveInfluence,FloorInfluence,DetailField));
}
if(DetailFade<=0.0f&&bProfile)++Profile.LocalBandSkips;
const bool bTimeDetail=DetailFade>0.0f&&bProfile
&&((Profile.DetailEvaluations&63u)==0);
const auto DetailStart=bTimeDetail?Clock::now():Clock::time_point{};
float Displaced=SDF;
if(DetailFade>0.0f)
{
if(bProfile)++Profile.DetailEvaluations;
if(bProfile&&bGpuVisualNode)++Profile.GpuVisualDetailEvaluations;
if(bProfile&&bGpuVisualNode&&!bCpuVisualNode)
++Profile.GpuOnlyDetailEvaluations;
Displaced+=DetailNoiseOnLine(X,Y,Z,Sample.SurfaceSDF,
CaveInfluence,FloorInfluence,PassageInfluence,NoiseLine,bTimeDetail?&Profile:nullptr,
DetailField,bDetailFieldKnown,bMacroKnown,MacroValue)*DetailFade;
}
if(bTimeDetail)
{
Profile.DetailSampleNanoseconds+=static_cast<uint64>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
Clock::now()-DetailStart).count());
++Profile.DetailSamples;
}
if(bCriticalAir)Displaced=std::min(Displaced,-1.0f/16.0f);
const bool bNewRock=Displaced>=0.0f;
const bool bOldRock=((RockWords[WordBase+(Zi>>6)]>>(Zi&63u))&1u)!=0;
if(bNewRock!=bOldRock)
{
RockWords[WordBase+(Zi>>6)]^=uint64{1}<<(Zi&63u);
++LocalFlips;
if(bProfile)++Profile.Flips;
}
const size_t Node=(Zi*static_cast<size_t>(Size[1])
+static_cast<size_t>(Y-Min[1]))*Size[0]
+static_cast<size_t>(X-Min[0]);
StoreSurfaceDistance(Node,-Displaced);
ValidBits|=uint64{1}<<(Zi&63u);
++LocalProxies;
}
BatchCount=0;
};
while(Bits)
{
const uint32 Bit=std::countr_zero(Bits);Bits&=Bits-1;
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);
const int32 QueryBinZ=FloorDiv(Z,ActiveSpatialBinSize);
if(BatchCount>0&&QueryBinZ!=BatchQueryBinZ)ProcessBatch();
if(QueryBinZ!=CachedQueryBinZ)
{
const auto Bin=SpatialBins.find({QueryBinX,QueryBinY,QueryBinZ});
CachedQueryBinItems=Bin==SpatialBins.end()?nullptr:&Bin->second;
CachedQueryBinZ=QueryBinZ;
}
if(BatchCount==0)BatchQueryBinZ=QueryBinZ,BatchBinItems=CachedQueryBinItems;
BatchZ[BatchCount++]=Z;
if(BatchCount==8)ProcessBatch();
}
ProcessBatch();
if(OriginalBits!=ValidBits)CandidateWords[WordBase+W]=ValidBits;
}
CandidateCount.fetch_add(LocalCandidates,std::memory_order_relaxed);
ProxyCount.fetch_add(LocalProxies,std::memory_order_relaxed);
FlipCount.fetch_add(LocalFlips,std::memory_order_relaxed);
// Each X/Y column is independent. Claim eight adjacent columns at a time to
// reduce scheduler atomics while preserving per-column scalar evaluation order.
},&Timing,&WorkerActiveSeconds,8);
Stats.SurfaceCandidates=CandidateCount.load(std::memory_order_relaxed);
Stats.SurfaceProxyNodes=ProxyCount.load(std::memory_order_relaxed);
Stats.DetailFlips=FlipCount.load(std::memory_order_relaxed);
if(bProfile)
{
uint64 DistanceCalls=0,DetailCalls=0,DistanceSamples=0,DistanceSampleNs=0;
uint64 DetailSamples=0,DetailSampleNs=0,ReliefSampleNs=0,LocalBandSkips=0;
uint64 LayerLookupSampleNs=0,OwnerSampleNs=0,OwnerFieldCalls=0,OwnerActiveCalls=0;
uint64 OwnerCacheHits=0,OwnerCacheMisses=0;
uint64 GpuVisualCandidates=0,CpuVisualCandidates=0,GpuOnlyCandidates=0;
uint64 GpuVisualDetailCalls=0,GpuOnlyDetailCalls=0;
for(int32 Worker=0;Worker<WorkerCount;++Worker)
{
const FDetailWorkerProfile& P=WorkerProfile[static_cast<size_t>(Worker)];
DistanceCalls+=P.Candidates;DetailCalls+=P.DetailEvaluations;
DistanceSamples+=P.DistanceSamples;DistanceSampleNs+=P.DistanceSampleNanoseconds;
DetailSamples+=P.DetailSamples;DetailSampleNs+=P.DetailSampleNanoseconds;
LocalBandSkips+=P.LocalBandSkips;
ReliefSampleNs+=P.ReliefSampleNanoseconds;
LayerLookupSampleNs+=P.LayerLookupSampleNanoseconds;
OwnerSampleNs+=P.OwnerRoughnessSampleNanoseconds;
OwnerFieldCalls+=P.OwnerRoughnessFieldCalls;
OwnerActiveCalls+=P.OwnerRoughnessActiveCalls;
OwnerCacheHits+=P.OwnerNoiseCacheHits;
OwnerCacheMisses+=P.OwnerNoiseCacheMisses;
GpuVisualCandidates+=P.GpuVisualCandidates;
CpuVisualCandidates+=P.CpuVisualCandidates;
GpuOnlyCandidates+=P.GpuOnlyCandidates;
GpuVisualDetailCalls+=P.GpuVisualDetailEvaluations;
GpuOnlyDetailCalls+=P.GpuOnlyDetailEvaluations;
}
const double DistanceAverageNs=DistanceSamples?static_cast<double>(DistanceSampleNs)/DistanceSamples:0.0;
const double DetailAverageNs=DetailSamples?static_cast<double>(DetailSampleNs)/DetailSamples:0.0;
const double CandidateWorkerNs=Stats.SurfaceCandidates
?Timing.WorkerSeconds*1.0e9/static_cast<double>(Stats.SurfaceCandidates):0.0;
std::printf("[DetailBandProfile] candidates=%llu distance_evaluations=%llu detail_noise_evaluations=%llu proxies=%llu flips=%llu candidate_worker_ns=%.1f distance_sample_ns=%.1f distance_estimated_worker_ms=%.1f detail_sample_ns=%.1f detail_estimated_worker_ms=%.1f worker_sum_ms=%.3f wall_ms=%.3f sampling=(distance_1_of_256,detail_1_of_64)\n",
static_cast<unsigned long long>(Stats.SurfaceCandidates),
static_cast<unsigned long long>(DistanceCalls),static_cast<unsigned long long>(DetailCalls),
static_cast<unsigned long long>(Stats.SurfaceProxyNodes),static_cast<unsigned long long>(Stats.DetailFlips),
CandidateWorkerNs,DistanceAverageNs,DistanceAverageNs*static_cast<double>(DistanceCalls)/1.0e6,
DetailAverageNs,DetailAverageNs*static_cast<double>(DetailCalls)/1.0e6,
Timing.WorkerSeconds*1000.0,Timing.WallSeconds*1000.0);
std::printf("[DetailBandSubprofile] relief_sample_ns=%.1f relief_estimated_worker_ms=%.1f layer_lookup_sample_ns=%.1f layer_lookup_estimated_worker_ms=%.1f owner_sample_ns=%.1f owner_estimated_worker_ms=%.1f owner_sampled_field_calls=%llu owner_sampled_noise_calls=%llu owner_sampled_core_cache_hits=%llu owner_sampled_core_cache_misses=%llu local_band_skipped=%llu\n",
DetailSamples?static_cast<double>(ReliefSampleNs)/DetailSamples:0.0,
DetailSamples?static_cast<double>(ReliefSampleNs)/DetailSamples*DetailCalls/1.0e6:0.0,
DetailSamples?static_cast<double>(LayerLookupSampleNs)/DetailSamples:0.0,
DetailSamples?static_cast<double>(LayerLookupSampleNs)/DetailSamples*DetailCalls/1.0e6:0.0,
DetailSamples?static_cast<double>(OwnerSampleNs)/DetailSamples:0.0,
DetailSamples?static_cast<double>(OwnerSampleNs)/DetailSamples*DetailCalls/1.0e6:0.0,
static_cast<unsigned long long>(OwnerFieldCalls),
static_cast<unsigned long long>(OwnerActiveCalls),
static_cast<unsigned long long>(OwnerCacheHits),
static_cast<unsigned long long>(OwnerCacheMisses),
static_cast<unsigned long long>(LocalBandSkips));
std::printf("[GpuDetailConsumerProfile] gpu_near_proxy_nodes=%llu cpu_near_mesh_nodes=%llu gpu_only_nodes=%llu gpu_near_detail_noise_evaluations=%llu gpu_only_detail_noise_evaluations=%llu\n",
static_cast<unsigned long long>(GpuVisualCandidates),
static_cast<unsigned long long>(CpuVisualCandidates),
static_cast<unsigned long long>(GpuOnlyCandidates),
static_cast<unsigned long long>(GpuVisualDetailCalls),
static_cast<unsigned long long>(GpuOnlyDetailCalls));
for(int32 Worker=0;Worker<WorkerCount;++Worker)
{
const FDetailWorkerProfile& P=WorkerProfile[static_cast<size_t>(Worker)];
std::printf("[DetailBandWorker] id=%d active_ms=%.3f lines=%llu candidates=%llu detail_evaluations=%llu flips=%llu\n",
Worker,WorkerActiveSeconds[static_cast<size_t>(Worker)]*1000.0,
static_cast<unsigned long long>(P.Lines),static_cast<unsigned long long>(P.Candidates),
static_cast<unsigned long long>(P.DetailEvaluations),static_cast<unsigned long long>(P.Flips));
}
}
}
uint64 CompleteSignEdgeDistances()
{
uint64 AddedTotal=0,SignCorrections=0;
const size_t Columns=static_cast<size_t>(Size[0])*Size[1];
for(int32 Pass=0;Pass<4;++Pass)
{
uint64 AddedThisPass=0;
bool bSignChanged=false;
const auto EnsureDistance=[&](int32 X,int32 Y,int32 Z)
{
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)return;
const float Density=AnalyticDensityAt(static_cast<float>(X),
static_cast<float>(Y),static_cast<float>(Z));
const size_t Node=(Zi*static_cast<size_t>(Size[1])
+static_cast<size_t>(Y-Min[1]))*Size[0]
+static_cast<size_t>(X-Min[0]);
StoreSurfaceDistance(Node,Density);
CandidateWords[Word]|=Mask;
++AddedThisPass;
const bool bExactRock=Density<0.0f;
const bool bPackedRock=(RockWords[Word]&Mask)!=0;
if(bExactRock!=bPackedRock)
{
if(bExactRock)RockWords[Word]|=Mask;
else RockWords[Word]&=~Mask;
++SignCorrections;
bSignChanged=true;
}
};
for(size_t Line=0;Line<Columns;++Line)
{
const int32 X=Min[0]+static_cast<int32>(Line%Size[0]);
const int32 Y=Min[1]+static_cast<int32>(Line/Size[0]);
const size_t Base=Line*WordsPerZ;
for(size_t W=0;W<WordsPerZ;++W)
{
const size_t Word=Base+W;
const uint64 Rock=RockWords[Word],Candidates=CandidateWords[Word];
const uint64 PreviousRock=W?RockWords[Word-1]>>63:0;
const uint64 PreviousCandidates=W?CandidateWords[Word-1]>>63:0;
uint64 Valid=(Size[2]-W*64)>=64?~uint64{0}
:((uint64{1}<<(Size[2]-W*64))-1);
if(W==0)Valid&=~uint64{1};
const uint64 ShiftedRock=(Rock<<1)|PreviousRock;
const uint64 ShiftedCandidates=(Candidates<<1)|PreviousCandidates;
uint64 Missing=((Rock^ShiftedRock)&Valid)
&~(Candidates&ShiftedCandidates);
while(Missing)
{
const uint32 Bit=std::countr_zero(Missing);Missing&=Missing-1;
const int32 Z=Min[2]+static_cast<int32>(W*64+Bit);
EnsureDistance(X,Y,Z);
EnsureDistance(X,Y,Z-1);
}
if(X<Max[0])
{
const size_t Other=Word+WordsPerZ;
Missing=(Rock^RockWords[Other])&Valid
&~(Candidates&CandidateWords[Other]);
while(Missing)
{
const uint32 Bit=std::countr_zero(Missing);Missing&=Missing-1;
const int32 Z=Min[2]+static_cast<int32>(W*64+Bit);
EnsureDistance(X,Y,Z);
EnsureDistance(X+1,Y,Z);
}
}
if(Y<Max[1])
{
const size_t Other=Word+static_cast<size_t>(Size[0])*WordsPerZ;
Missing=(Rock^RockWords[Other])&Valid
&~(Candidates&CandidateWords[Other]);
while(Missing)
{
const uint32 Bit=std::countr_zero(Missing);Missing&=Missing-1;
const int32 Z=Min[2]+static_cast<int32>(W*64+Bit);
EnsureDistance(X,Y,Z);
EnsureDistance(X,Y+1,Z);
}
}
}
}
AddedTotal+=AddedThisPass;
if(!bSignChanged)break;
}
std::printf("[GridStairDistanceRepair] exact_endpoint_proxies=%llu occupancy_corrections=%llu\n",
static_cast<unsigned long long>(AddedTotal),
static_cast<unsigned long long>(SignCorrections));
return AddedTotal;
}
void StoreSurfaceDistance(size_t Node,float Density)
{
int32 Q=static_cast<int32>(std::lround(Density*256.0f));
Q=std::clamp(Q,-32767,32767);
if(Q==0)Q=Density<0.0f?-1:1;
SurfaceDistanceQ[Node]=static_cast<int16>(Q);
}
bool IsInsideSolidOverlay(int32 X,int32 Y,int32 Z) const
{
const FBinKey K=PointBin(X,Y,Z);const auto It=SpatialBins.find(K);
if(It==SpatialBins.end())return false;
for(uint32 Id:It->second)
{
const FPrimitive& P=Primitives[Id];if(P.bCarve)continue;
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])continue;
if(ShapeContains(P,X,Y,Z))return true;
}
return false;
}
bool IsInsideCriticalAir(int32 X,int32 Y,int32 Z) const
{
const auto It=CriticalAirBins.find(PointBin(X,Y,Z));
if(It==CriticalAirBins.end())return false;
for(uint32 Id:It->second)
{
const FPrimitive& P=Primitives[Id];if(!P.bCarve||!P.bCritical)continue;
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])continue;
if(ShapeContains(P,X,Y,Z))return true;
}
return false;
}
bool IsNearCriticalAirBoundary(int32 X,int32 Y,int32 Z,double Margin) const
{
const auto It=CriticalAirBins.find(PointBin(X,Y,Z));
if(It==CriticalAirBins.end())return false;
for(uint32 Id:It->second)
{
const FPrimitive& P=Primitives[Id];if(!P.bCarve||!P.bCritical)continue;
if(X<P.Min[0]-Margin||X>P.Max[0]+Margin
||Y<P.Min[1]-Margin||Y>P.Max[1]+Margin
||Z<P.Min[2]-Margin||Z>P.Max[2]+Margin)continue;
float Distance=ShapeDistance(P,X,Y,Z);
if(P.bHasFloorClip)
{
const float FloorDistance=static_cast<float>(FloorAt(P,X,Y)-Z);
Distance=SmoothMaxDistance(Distance,FloorDistance,static_cast<float>(P.FloorBlendRadius));
}
if(std::abs(Distance)<=Margin)return true;
}
return false;
}
bool IsInsideSolidOverlay(double X,double Y,double Z) const
{
const auto It=SpatialBins.find(PointBin(X,Y,Z));
if(It==SpatialBins.end())return false;
for(uint32 Id:It->second)
{
const FPrimitive& P=Primitives[Id];if(P.bCarve)continue;
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])continue;
if(ShapeDistance(P,X,Y,Z)<=0.0f)return true;
}
return false;
}
float SolidOverlayDistance(double X,double Y,double Z) const
{
const auto It=SpatialBins.find(PointBin(X,Y,Z));
if(It==SpatialBins.end())return -100000.0f;
float Best=-100000.0f;
for(uint32 Id:It->second)
{
const FPrimitive& P=Primitives[Id];if(P.bCarve)continue;
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])continue;
Best=std::max(Best,-ShapeDistance(P,X,Y,Z));
}
return Best;
}
bool IsInsideCriticalAir(double X,double Y,double Z) const
{
const auto It=CriticalAirBins.find(PointBin(X,Y,Z));
if(It==CriticalAirBins.end())return false;
for(uint32 Id:It->second)
{
const FPrimitive& P=Primitives[Id];if(!P.bCarve||!P.bCritical)continue;
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])continue;
if(ShapeContains(P,X,Y,Z))return true;
}
return false;
}
float CaveDistance(double X,double Y,double Z,bool* OutFloorSurface=nullptr,
float* OutFloorInfluence=nullptr) const
{
const auto It=SpatialBins.find(PointBin(X,Y,Z));
if(It==SpatialBins.end())
{
if(OutFloorSurface)*OutFloorSurface=false;
if(OutFloorInfluence)*OutFloorInfluence=0.0f;
return 100000.0f;
}
float Best=100000.0f;
float BestFloorInfluence=0.0f;
for(uint32 Id:It->second)
{
const FPrimitive& P=Primitives[Id];if(!P.bCarve)continue;
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+(Best-Distance)/(2.0f*MetadataWidth));
const float Combined=SmoothMinDistance(Best,Distance,static_cast<float>(P.BlendRadius));
BestFloorInfluence=std::lerp(BestFloorInfluence,PrimitiveFloorInfluence,PrimitiveWeight);
Best=Combined;
}
if(OutFloorInfluence)*OutFloorInfluence=BestFloorInfluence;
if(OutFloorSurface)*OutFloorSurface=BestFloorInfluence>=0.5f;
return Best;
}
float StrataDistance(double Z) const
{
if(LayerRanges.empty())return -100000.0f;
return static_cast<float>(std::min(Z-LayerRanges[0].Lo,LayerRanges[0].Hi-Z));
}
const FLayerField* FindLayerField(double Z) const
{
for(const FLayerField& Field:LayerFields)
if(Z>=Field.Lo&&Z<=Field.Hi)return &Field;
return nullptr;
}
float EffectiveRockDistance(double X,double Y,double Z,
ESurfaceFamily* OutFamily=nullptr,
float* OutSurfaceSDF=nullptr,
bool* OutFloorSurface=nullptr,
float* OutCaveInfluence=nullptr,
float* OutFloorInfluence=nullptr,
bool* OutCriticalAir=nullptr,
const FBinItems* KnownBinItems=nullptr,
bool bKnownBin=false,
float* OutPassageInfluence=nullptr) const
{
const float Base=StrataDistance(Z);
float Cave=100000.0f;
float Overlay=-100000.0f;
float CaveFloorInfluence=0.0f;
float CavePassageInfluence=0.0f;
bool bCriticalAir=false;
const FBinItems* BinItems=KnownBinItems;
if(!bKnownBin)
{
const auto Bin=SpatialBins.find(PointBin(X,Y,Z));
if(Bin!=SpatialBins.end())BinItems=&Bin->second;
}
if(BinItems)
{
for(uint32 Id:*BinItems)
{
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);
if(OutPassageInfluence&&(CavePassageInfluence>0.0f||P.bPassageSurface))
CavePassageInfluence=std::lerp(CavePassageInfluence,
P.bPassageSurface?1.0f:0.0f,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);
constexpr float FamilyBlendWidth=6.0f;
const float CaveVsOverlay=SmoothUnit(0.5f+(Cave-Overlay)/(2.0f*FamilyBlendWidth));
const float CarveInfluence=SmoothUnit(0.5f+(Base-CarveOrOverlay)/(2.0f*FamilyBlendWidth));
const float CaveInfluence=CarveInfluence*CaveVsOverlay;
const float FloorInfluence=CaveInfluence*CaveFloorInfluence;
const float PassageInfluence=CaveInfluence*CavePassageInfluence;
const float RockDistance=SmoothMinDistance(Base,CarveOrOverlay,StrataCaveBlendRadius);
if(OutFamily)
*OutFamily=Family;
if(OutSurfaceSDF)
*OutSurfaceSDF=Family==ESurfaceFamily::Strata?Base:
(Family==ESurfaceFamily::Cave?Cave:Overlay);
if(OutCaveInfluence)*OutCaveInfluence=CaveInfluence;
if(OutFloorInfluence)*OutFloorInfluence=FloorInfluence;
if(OutPassageInfluence)*OutPassageInfluence=PassageInfluence;
if(OutFloorSurface)*OutFloorSurface=FloorInfluence>=0.5f;
return RockDistance;
}
struct FEffectiveRockSample
{
float Distance=0.0f;
float SurfaceSDF=0.0f;
float CaveInfluence=0.0f;
float FloorInfluence=0.0f;
float PassageInfluence=0.0f;
ESurfaceFamily Family=ESurfaceFamily::Strata;
bool bFloorSurface=false;
bool bCriticalAir=false;
};
void ShapeDistanceZ8(const FPrimitive& P,int32 X,int32 Y,const int32 Z[8],int32 Count,
float OutDistance[8]) const
{
const __m256d Zero=_mm256_setzero_pd();
const __m256d One=_mm256_set1_pd(1.0);
const __m256d SignMask=_mm256_set1_pd(-0.0);
for(int32 Base=0;Base<Count;Base+=4)
{
double ZLane[4];
const int32 Lanes=std::min(4,Count-Base);
for(int32 Lane=0;Lane<4;++Lane)
ZLane[Lane]=static_cast<double>(Z[Base+std::min(Lane,Lanes-1)]);
const __m256d VZ=_mm256_setr_pd(ZLane[0],ZLane[1],ZLane[2],ZLane[3]);
__m256d Distance;
if(P.bCapsule)
{
const double VX=static_cast<double>(X)-P.CapsuleStartWorld[0];
const double VY=static_cast<double>(Y)-P.CapsuleStartWorld[1];
const __m256d VXv=_mm256_set1_pd(VX);
const __m256d VYv=_mm256_set1_pd(VY);
const __m256d VZv=_mm256_sub_pd(VZ,_mm256_set1_pd(P.CapsuleStartWorld[2]));
__m256d VD=_mm256_mul_pd(VXv,_mm256_set1_pd(P.CapsuleAxisDelta[0]));
VD=_mm256_add_pd(VD,_mm256_mul_pd(VYv,_mm256_set1_pd(P.CapsuleAxisDelta[1])));
VD=_mm256_add_pd(VD,_mm256_mul_pd(VZv,_mm256_set1_pd(P.CapsuleAxisDelta[2])));
const __m256d T=P.CapsuleAxisLengthSquared>1.0e-12
?_mm256_max_pd(Zero,_mm256_min_pd(One,
_mm256_div_pd(VD,_mm256_set1_pd(P.CapsuleAxisLengthSquared))))
:Zero;
const __m256d EX=_mm256_sub_pd(VXv,
_mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[0])));
const __m256d EY=_mm256_sub_pd(VYv,
_mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[1])));
const __m256d EZ=_mm256_sub_pd(VZv,
_mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[2])));
__m256d DistanceSquared=_mm256_mul_pd(EX,EX);
DistanceSquared=_mm256_add_pd(DistanceSquared,_mm256_mul_pd(EY,EY));
DistanceSquared=_mm256_add_pd(DistanceSquared,_mm256_mul_pd(EZ,EZ));
const __m256d Root=_mm256_sqrt_pd(DistanceSquared);
double TValues[4],RootValues[4];
_mm256_storeu_pd(TValues,T);
_mm256_storeu_pd(RootValues,Root);
for(int32 Lane=0;Lane<Lanes;++Lane)
OutDistance[Base+Lane]=static_cast<float>(RootValues[Lane]
-CapsuleRadiusAtT(P,TValues[Lane],P.CapsuleAxisLength));
continue;
}
if(P.bRoundedBox)
{
const double DX=std::abs(static_cast<double>(X)-P.CenterSub[0]/16.0)
-P.RoundedHalfExtent[0];
const double DY=std::abs(static_cast<double>(Y)-P.CenterSub[1]/16.0)
-P.RoundedHalfExtent[1];
const double DZCenter=P.CenterSub[2]/16.0;
const __m256d DZv=_mm256_sub_pd(_mm256_andnot_pd(SignMask,
_mm256_sub_pd(VZ,_mm256_set1_pd(DZCenter))),
_mm256_set1_pd(P.RoundedHalfExtent[2]));
const __m256d DXv=_mm256_set1_pd(DX),DYv=_mm256_set1_pd(DY);
const __m256d Ox=_mm256_max_pd(DXv,Zero),Oy=_mm256_max_pd(DYv,Zero);
const __m256d Oz=_mm256_max_pd(DZv,Zero);
__m256d OutsideSquared=_mm256_mul_pd(Ox,Ox);
OutsideSquared=_mm256_add_pd(OutsideSquared,_mm256_mul_pd(Oy,Oy));
OutsideSquared=_mm256_add_pd(OutsideSquared,_mm256_mul_pd(Oz,Oz));
const __m256d MaxXY=_mm256_max_pd(DXv,DYv);
const __m256d MaxQ=_mm256_max_pd(MaxXY,DZv);
const __m256d Inside=_mm256_min_pd(MaxQ,Zero);
Distance=_mm256_sub_pd(_mm256_add_pd(_mm256_sqrt_pd(OutsideSquared),Inside),
_mm256_set1_pd(P.Rounding));
}
else
{
const __m256d D[3]={
_mm256_set1_pd(static_cast<double>(X)-P.CenterSub[0]/16.0),
_mm256_set1_pd(static_cast<double>(Y)-P.CenterSub[1]/16.0),
_mm256_sub_pd(VZ,_mm256_set1_pd(P.CenterSub[2]/16.0))};
__m256d Q=_mm256_setzero_pd();
for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J)
{
const double Coefficient=static_cast<double>(P.Q[I][J])/QScale;
__m256d Term=_mm256_mul_pd(_mm256_set1_pd(Coefficient),D[I]);
Term=_mm256_mul_pd(Term,D[J]);
Term=_mm256_mul_pd(Term,_mm256_set1_pd(256.0));
Q=_mm256_add_pd(Q,Term);
}
Distance=_mm256_mul_pd(_mm256_sub_pd(
_mm256_sqrt_pd(_mm256_max_pd(Q,Zero)),_mm256_set1_pd(1.0)),
_mm256_set1_pd(P.MinRadius));
}
double Values[4];
_mm256_storeu_pd(Values,Distance);
for(int32 Lane=0;Lane<Lanes;++Lane)
OutDistance[Base+Lane]=static_cast<float>(Values[Lane]);
}
}
void EffectiveRockDistanceZ8(int32 X,int32 Y,const int32 Z[8],int32 Count,
const FBinItems* BinItems,FEffectiveRockSample Out[8]) const
{
float Base[8],Cave[8],Overlay[8],CaveFloorInfluence[8],CavePassageInfluence[8];
bool CriticalAir[8]{};
for(int32 Lane=0;Lane<Count;++Lane)
{
Base[Lane]=StrataDistance(Z[Lane]);
Cave[Lane]=100000.0f;
Overlay[Lane]=-100000.0f;
CaveFloorInfluence[Lane]=0.0f;
CavePassageInfluence[Lane]=0.0f;
}
if(BinItems)
{
for(uint32 Id:*BinItems)
{
const FPrimitive& P=Primitives[Id];
bool Eligible[8]{};
bool AnyEligible=false;
if(P.bCarve)
{
for(int32 Lane=0;Lane<Count;++Lane)
{
const int32 ZValue=Z[Lane];
if(P.bCritical&&X>=P.Min[0]&&X<=P.Max[0]
&&Y>=P.Min[1]&&Y<=P.Max[1]
&&ZValue>=P.Min[2]&&ZValue<=P.Max[2]
&&ShapeContains(P,X,Y,ZValue))
CriticalAir[Lane]=true;
if(X<P.Min[0]-DetailBand||X>P.Max[0]+DetailBand
||Y<P.Min[1]-DetailBand||Y>P.Max[1]+DetailBand
||ZValue<P.Min[2]-DetailBand||ZValue>P.Max[2]+DetailBand)
continue;
Eligible[Lane]=true;
AnyEligible=true;
}
}
else
{
for(int32 Lane=0;Lane<Count;++Lane)
{
const int32 ZValue=Z[Lane];
Eligible[Lane]=X>=P.Min[0]&&X<=P.Max[0]
&&Y>=P.Min[1]&&Y<=P.Max[1]
&&ZValue>=P.Min[2]&&ZValue<=P.Max[2];
AnyEligible=AnyEligible||Eligible[Lane];
}
}
if(!AnyEligible)continue;
float Distances[8];
ShapeDistanceZ8(P,X,Y,Z,Count,Distances);
if(P.bCarve)
{
const double Floor=P.bHasFloorClip?FloorAt(P,X,Y):0.0;
const float MetadataWidth=static_cast<float>(std::max(P.BlendRadius,6.0));
for(int32 Lane=0;Lane<Count;++Lane)
{
if(!Eligible[Lane])continue;
float Distance=Distances[Lane];
float PrimitiveFloorInfluence=0.0f;
if(P.bHasFloorClip)
{
const float FloorT=(static_cast<float>(std::abs(Z[Lane]-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[Lane]),
static_cast<float>(P.FloorBlendRadius));
}
const float PrimitiveWeight=SmoothUnit(0.5f
+(Cave[Lane]-Distance)/(2.0f*MetadataWidth));
const float Combined=SmoothMinDistance(Cave[Lane],Distance,
static_cast<float>(P.BlendRadius));
CaveFloorInfluence[Lane]=std::lerp(CaveFloorInfluence[Lane],
PrimitiveFloorInfluence,PrimitiveWeight);
if(CavePassageInfluence[Lane]>0.0f||P.bPassageSurface)
CavePassageInfluence[Lane]=std::lerp(CavePassageInfluence[Lane],
P.bPassageSurface?1.0f:0.0f,PrimitiveWeight);
Cave[Lane]=Combined;
}
}
else
{
for(int32 Lane=0;Lane<Count;++Lane)
if(Eligible[Lane])Overlay[Lane]=std::max(Overlay[Lane],-Distances[Lane]);
}
}
}
for(int32 Lane=0;Lane<Count;++Lane)
{
const float CarveOrOverlay=std::max(Cave[Lane],Overlay[Lane]);
const ESurfaceFamily Family=Base[Lane]<=CarveOrOverlay?ESurfaceFamily::Strata:
(Cave[Lane]>=Overlay[Lane]?ESurfaceFamily::Cave:ESurfaceFamily::SolidOverlay);
constexpr float FamilyBlendWidth=6.0f;
const float CaveVsOverlay=SmoothUnit(0.5f
+(Cave[Lane]-Overlay[Lane])/(2.0f*FamilyBlendWidth));
const float CarveInfluence=SmoothUnit(0.5f
+(Base[Lane]-CarveOrOverlay)/(2.0f*FamilyBlendWidth));
const float CaveInfluence=CarveInfluence*CaveVsOverlay;
const float FloorInfluence=CaveInfluence*CaveFloorInfluence[Lane];
const float PassageInfluence=CaveInfluence*CavePassageInfluence[Lane];
Out[Lane].Distance=SmoothMinDistance(Base[Lane],CarveOrOverlay,StrataCaveBlendRadius);
Out[Lane].Family=Family;
Out[Lane].SurfaceSDF=Family==ESurfaceFamily::Strata?Base[Lane]:
(Family==ESurfaceFamily::Cave?Cave[Lane]:Overlay[Lane]);
Out[Lane].CaveInfluence=CaveInfluence;
Out[Lane].FloorInfluence=FloorInfluence;
Out[Lane].PassageInfluence=PassageInfluence;
Out[Lane].bFloorSurface=FloorInfluence>=0.5f;
Out[Lane].bCriticalAir=CriticalAir[Lane];
}
}
public:
void DensityAtLatticeZRun(int32 X,int32 Y,int32 ZStart,int32 ZStep,
int32 Count,float* OutDensity) const
{
if(Count<=0||OutDensity==nullptr)return;
bool bHasLegacySurfaceSample=false;
for(int32 Lane=0;Lane<Count;++Lane)
bHasLegacySurfaceSample|=UsesLegacySurfaceField(static_cast<float>(X),
static_cast<float>(Y),static_cast<float>(ZStart+Lane*ZStep));
if(bHasLegacySurfaceSample)
{
for(int32 Lane=0;Lane<Count;++Lane)
OutDensity[Lane]=DensityAtLattice(X,Y,ZStart+Lane*ZStep);
return;
}
if(!bAnalyticOnly)
{
for(int32 Lane=0;Lane<Count;++Lane)
OutDensity[Lane]=DensityAtLattice(X,Y,ZStart+Lane*ZStep);
return;
}
FDetailNoiseLineContext DetailNoiseLine(
static_cast<float>(X),static_cast<float>(Y),static_cast<uint32>(Owner->Seed));
for(int32 Offset=0;Offset<Count;)
{
int32 Z[8];
const int32 Remaining=std::min(8,Count-Offset);
for(int32 Lane=0;Lane<Remaining;++Lane)
Z[Lane]=ZStart+(Offset+Lane)*ZStep;
const int32 BinZ=FloorDiv(Z[0],ActiveSpatialBinSize);
const auto Found=SpatialBins.find({FloorDiv(X,ActiveSpatialBinSize),
FloorDiv(Y,ActiveSpatialBinSize),BinZ});
const FBinItems* BinItems=Found==SpatialBins.end()?nullptr:&Found->second;
int32 BatchCount=1;
while(BatchCount<Remaining
&&FloorDiv(Z[BatchCount],ActiveSpatialBinSize)==BinZ)++BatchCount;
FEffectiveRockSample Samples[8]{};
EffectiveRockDistanceZ8(X,Y,Z,BatchCount,BinItems,Samples);
for(int32 Lane=0;Lane<BatchCount;++Lane)
{
const int32 SampleZ=Z[Lane];
const bool bInsideFine=X>=Min[0]&&X<=Max[0]
&&Y>=Min[1]&&Y<=Max[1]&&SampleZ>=Min[2]&&SampleZ<=Max[2];
float RockDistance=Samples[Lane].Distance;
if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth
&&std::abs(RockDistance)<=DetailSupremum+0.125f)
{
const float Fade=DetailBandFadeAt(static_cast<float>(X),
static_cast<float>(Y),static_cast<float>(SampleZ),RockDistance,
Samples[Lane].SurfaceSDF,Samples[Lane].CaveInfluence,
Samples[Lane].FloorInfluence);
if(Fade>0.0f)RockDistance+=DetailNoiseOnLine(static_cast<float>(X),
static_cast<float>(Y),static_cast<float>(SampleZ),
Samples[Lane].SurfaceSDF,Samples[Lane].CaveInfluence,
Samples[Lane].FloorInfluence,Samples[Lane].PassageInfluence,
DetailNoiseLine)*Fade;
}
if(Samples[Lane].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);
OutDensity[Offset+Lane]=ApplyEditBrushes(Density,X,Y,SampleZ);
}
Offset+=BatchCount;
}
}
void DensityAtLatticeZ8(int32 X,int32 Y,int32 ZStart,int32 ZStep,
int32 Count,float OutDensity[8]) const
{
DensityAtLatticeZRun(X,Y,ZStart,ZStep,std::clamp(Count,0,8),OutDensity);
}
private:
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 FBinKey QueryKey=PointBin(X,Y,Z);
const auto QueryBin=SpatialBins.find(QueryKey);
const FBinItems* QueryItems=QueryBin==SpatialBins.end()?nullptr:&QueryBin->second;
if(QueryItems)
{
for(uint32 Id:*QueryItems)
{
const FPrimitive& P=Primitives[Id];
const VF_FieldPrimitive& Shared=SharedFieldPrimitives[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=VF_FieldPrimitiveDistance(Shared,X,Y,Z);
if(P.bHasFloorClip)
{
const double Floor=VF_FieldPrimitiveFloorAt(Shared,X,Y);
Distance=VF_FieldSmoothMax(Distance,static_cast<float>(Floor-Z),
static_cast<float>(P.FloorBlendRadius));
}
Cave=VF_FieldSmoothMin(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,-VF_FieldPrimitiveDistance(Shared,X,Y,Z));
}
}
if(OutCriticalAir)*OutCriticalAir=bCriticalAir;
return VF_FieldSmoothMin(Base,std::max(Cave,Overlay),StrataCaveBlendRadius);
}
struct FOwnerNoiseLineContext
{
struct FAnchorCache
{
const FLayerField* Field=nullptr;
int32 GridZ0=MIN_int32;
int32 SampleZ0=MIN_int32,SampleZ1=MIN_int32;
float LowerCore=0.0f,UpperCore=0.0f;
} Step8,Step2;
};
float OwnerCaveRoughnessCore(double X,double Y,double Z,
const FStrateGenerationParams& Params) const
{
// Keep CPU prediction and the near-field HLSL on one scalar source. The helper
// preserves the legacy seed offsets, octave order, and roughness families.
return VF_FieldOwnerRoughnessCore(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z),static_cast<uint32>(Owner->Seed),Params.SurfaceRoughness,
Params.RoughnessFrequency,Params.VerticalScale,Params.DomainWarpStrength,
Params.DomainWarpFrequency,static_cast<uint32>(Params.RoughnessNoiseType));
}
float OwnerCaveRoughness(double X,double Y,double Z,float SurfaceSDF,
const FLayerField& Field,float CaveInfluence,
FOwnerNoiseLineContext& LineContext,
FDetailWorkerProfile* Profile=nullptr,
int32 AnchorStep=8) const
{
const FStrateGenerationParams& Params=Field.Params;
const float Depth=Params.SurfaceRoughness*2.0f;
if(!(Depth>0.0f)||std::abs(SurfaceSDF)>=Depth||!(Params.RoughnessFrequency>0.0f))return 0.0f;
if(Profile)++Profile->OwnerRoughnessActiveCalls;
const int32 ZStep=std::max(1,AnchorStep);
const int32 GridZ0=static_cast<int32>(std::floor(Z/static_cast<double>(ZStep)))*ZStep;
const int32 SampleZ0=std::max(GridZ0,Field.Lo);
const int32 SampleZ1=std::min(GridZ0+ZStep,Field.Hi);
float Total=0.0f;
if(ZStep==1)
Total=OwnerCaveRoughnessCore(X,Y,Z,Params);
else if(SampleZ1>SampleZ0)
{
FOwnerNoiseLineContext::FAnchorCache& Cache=ZStep==2
?LineContext.Step2:LineContext.Step8;
const bool bCacheHit=Cache.Field==&Field&&Cache.GridZ0==GridZ0
&&Cache.SampleZ0==SampleZ0&&Cache.SampleZ1==SampleZ1;
if(Profile)
{
if(bCacheHit)++Profile->OwnerNoiseCacheHits;
else ++Profile->OwnerNoiseCacheMisses;
}
if(!bCacheHit)
{
const bool bReusesPreviousUpper=Cache.Field==&Field
&&Cache.SampleZ1==SampleZ0;
Cache.Field=&Field;
Cache.GridZ0=GridZ0;
Cache.SampleZ0=SampleZ0;
Cache.SampleZ1=SampleZ1;
Cache.LowerCore=bReusesPreviousUpper?Cache.UpperCore
:OwnerCaveRoughnessCore(X,Y,SampleZ0,Params);
Cache.UpperCore=OwnerCaveRoughnessCore(X,Y,SampleZ1,Params);
}
const float Alpha=static_cast<float>((Z-SampleZ0)/(SampleZ1-SampleZ0));
Total=FMath::Lerp(Cache.LowerCore,Cache.UpperCore,Alpha);
}
else
Total=OwnerCaveRoughnessCore(X,Y,Z,Params);
if(SurfaceSDF<0.0f)
Total=std::lerp(Total,std::min(Total,0.0f),std::clamp(CaveInfluence,0.0f,1.0f));
const float Fade=1.0f-std::abs(SurfaceSDF)/Depth;
return Total*Fade*Fade;
}
float OwnerCaveRoughnessForDetail(double X,double Y,double Z,float SurfaceSDF,
const FLayerField& Field,float CaveInfluence,
float PassageInfluence,
FOwnerNoiseLineContext& LineContext,
FDetailWorkerProfile* Profile=nullptr) const
{
constexpr float DirectStart=0.40f,DirectEnd=0.60f;
constexpr int32 PassageAnchorStep=2;
const float PassageWeight=std::clamp(PassageInfluence,0.0f,1.0f);
if(PassageWeight>=DirectEnd)
return OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field,CaveInfluence,
LineContext,Profile,PassageAnchorStep);
const float Anchored=OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field,
CaveInfluence,LineContext,Profile);
if(PassageWeight<=DirectStart)return Anchored;
const float Direct=OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field,
CaveInfluence,LineContext,Profile,PassageAnchorStep);
const float DirectWeight=SmoothUnit((PassageWeight-DirectStart)
/(DirectEnd-DirectStart));
const float AnchoredWeight=1.0f-DirectWeight;
const float Normalization=1.0f/std::sqrt(
AnchoredWeight*AnchoredWeight+DirectWeight*DirectWeight);
return (AnchoredWeight*Anchored+DirectWeight*Direct)*Normalization;
}
static float SurfaceRelief(float X,float Y,float Z,uint32 Seed,
float* OutPassageBandRelief=nullptr)
{
// Keep all active bands at four voxels or wider: the full-detail lattice can
// resolve them without turning the surface into per-voxel steps. The broad
// owner comparison showed that the shortest octave needed more usable relief.
const float Macro=FStandaloneResearchTunnels::ValueNoise(X,Y,Z,32,Seed^0xB5297A4Du);
const float Mid=FStandaloneResearchTunnels::ValueNoise(X,Y,Z,16,Seed^0x63D83595u);
const float Fine=FStandaloneResearchTunnels::ValueNoise(X,Y,Z,4,Seed^0xA511E9B3u);
if(OutPassageBandRelief)
*OutPassageBandRelief=1.20f*Macro+(0.80f+0.25f*Macro)*(0.40f*Mid);
return 1.20f*Macro+(0.80f+0.25f*Macro)*(0.40f*Mid+0.60f*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[2]=Corners[6];
Corners[1]=Corners[5];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;
FOwnerNoiseLineContext OwnerNoise;
FDetailNoiseLineContext(float X,float Y,uint32 Seed)
:Macro(X,Y,32,Seed^0xB5297A4Du),Mid(X,Y,16,Seed^0x63D83595u),
Fine(X,Y,4,Seed^0xA511E9B3u)
{}
float SurfaceReliefAt(float Z,bool bMacroKnown=false,float KnownMacro=0.0f,
float* OutPassageBandRelief=nullptr)
{
const float MacroValue=bMacroKnown?KnownMacro:Macro.Sample(Z);
const float MidValue=Mid.Sample(Z);
const float FineValue=Fine.Sample(Z);
if(OutPassageBandRelief)
*OutPassageBandRelief=1.20f*MacroValue
+(0.80f+0.25f*MacroValue)*(0.40f*MidValue);
return 1.20f*MacroValue+(0.80f+0.25f*MacroValue)
*(0.40f*MidValue+0.60f*FineValue);
}
};
float DetailNoise(double X,double Y,double Z,float SurfaceSDF,
float CaveInfluence,float FloorInfluence,float PassageInfluence) const
{
if(DetailVariant==EDetailVariant::Smooth)return 0.0f;
if(DetailVariant==EDetailVariant::PerVoxel)
{
const int32 IX=static_cast<int32>(std::floor(X)),IY=static_cast<int32>(std::floor(Y)),IZ=static_cast<int32>(std::floor(Z));
return FStandaloneResearchTunnels::NoiseValue(IX,IY,IZ,static_cast<uint32>(Owner->Seed))
*DetailSupremum;
}
float PassageBandRelief=0.0f;
const float Relief=SurfaceRelief(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z),static_cast<uint32>(Owner->Seed),&PassageBandRelief);
const float PassageWeight=std::clamp(PassageInfluence,0.0f,1.0f);
const FLayerField* Field=FindLayerField(Z);
FOwnerNoiseLineContext OwnerNoise;
float OwnerRelief=0.0f;
if(Field)
OwnerRelief=OwnerCaveRoughnessForDetail(X,Y,Z,SurfaceSDF,*Field,
CaveInfluence,PassageWeight,OwnerNoise);
const float Detail=std::clamp(BlendDetailFamilies(Relief,PassageBandRelief,
OwnerRelief,CaveInfluence,FloorInfluence,PassageWeight),-3.9f,3.9f);
return Detail;
}
float DetailNoiseOnLine(double X,double Y,double Z,float SurfaceSDF,
float CaveInfluence,float FloorInfluence,float PassageInfluence,
FDetailNoiseLineContext& NoiseLine,
FDetailWorkerProfile* Profile=nullptr,
const FLayerField* KnownField=nullptr,
bool bFieldKnown=false,bool bMacroKnown=false,
float KnownMacro=0.0f) const
{
if(DetailVariant!=EDetailVariant::Coherent)
return DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence,PassageInfluence);
using Clock=std::chrono::steady_clock;
const auto ReliefStart=Profile?Clock::now():Clock::time_point{};
float PassageBandRelief=0.0f;
const float Relief=NoiseLine.SurfaceReliefAt(static_cast<float>(Z),
bMacroKnown,KnownMacro,&PassageBandRelief);
if(Profile)
Profile->ReliefSampleNanoseconds+=static_cast<uint64>(
std::chrono::duration_cast<std::chrono::nanoseconds>(Clock::now()-ReliefStart).count());
const auto LookupStart=Profile?Clock::now():Clock::time_point{};
const FLayerField* Field=bFieldKnown?KnownField:FindLayerField(Z);
if(Profile)
Profile->LayerLookupSampleNanoseconds+=static_cast<uint64>(
std::chrono::duration_cast<std::chrono::nanoseconds>(Clock::now()-LookupStart).count());
const auto OwnerStart=Profile?Clock::now():Clock::time_point{};
if(Profile&&Field)++Profile->OwnerRoughnessFieldCalls;
const float PassageWeight=std::clamp(PassageInfluence,0.0f,1.0f);
float OwnerRelief=0.0f;
if(Field)
OwnerRelief=OwnerCaveRoughnessForDetail(X,Y,Z,SurfaceSDF,*Field,
CaveInfluence,PassageWeight,NoiseLine.OwnerNoise,Profile);
if(Profile)
Profile->OwnerRoughnessSampleNanoseconds+=static_cast<uint64>(
std::chrono::duration_cast<std::chrono::nanoseconds>(Clock::now()-OwnerStart).count());
const float Detail=std::clamp(BlendDetailFamilies(Relief,PassageBandRelief,
OwnerRelief,CaveInfluence,FloorInfluence,PassageWeight),-3.9f,3.9f);
return Detail;
}
bool EvaluateFast(double X,double Y,double Z) const
{
ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
float PassageInfluence=0.0f;
float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,
&bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir,nullptr,false,
&PassageInfluence);
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)
{
const float Fade=DetailBandFadeAt(X,Y,Z,RockDistance,SurfaceSDF,
CaveInfluence,FloorInfluence);
if(Fade>0.0f)RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,
FloorInfluence,PassageInfluence)*Fade;
}
if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f);
return RockDistance>=0.0f;
}
bool ValidatePassageRoutes()
{
constexpr double SampleStep=0.5;
uint32 PassageIndex=0;
for(const FVoxelPassage& P:Owner->Manager->GetPassages())
{
const auto& Points=P.ControlPoints;
if(Points.Num()<2){++PassageIndex;continue;}
uint64 RouteSamples=0,RouteFailures=0,RouteCritical=0;
int32 FirstFailure[3]={0,0,0};bool bFirstFailureCritical=false;
for(int32 I=0;I+1<Points.Num();++I)
{
const FVector A=Points[I],B=Points[I+1];
const double L=std::sqrt((B.X-A.X)*(B.X-A.X)+(B.Y-A.Y)*(B.Y-A.Y)+(B.Z-A.Z)*(B.Z-A.Z));
const int32 Count=std::max(1,static_cast<int32>(std::ceil(L/SampleStep)));
uint64 SegmentSamples=0,SegmentFailures=0,SegmentCritical=0;
for(int32 J=0;J<=Count;++J)
{
const double T=static_cast<double>(J)/Count;
const double X=A.X+(B.X-A.X)*T,Y=A.Y+(B.Y-A.Y)*T,Z=A.Z+(B.Z-A.Z)*T;
++Stats.WalkSamples;
++RouteSamples;++SegmentSamples;
const int32 VX=static_cast<int32>(std::lround(X));
const int32 VY=static_cast<int32>(std::lround(Y));
const int32 VZ=static_cast<int32>(std::lround(Z));
const bool bCritical=IsInsideCriticalAir(VX,VY,VZ);
if(bCritical){++RouteCritical;++SegmentCritical;}
if(IsRock(VX,VY,VZ))
{
++Stats.WalkFailures;++RouteFailures;++SegmentFailures;
if(RouteFailures==1)
{FirstFailure[0]=VX;FirstFailure[1]=VY;FirstFailure[2]=VZ;bFirstFailureCritical=bCritical;}
}
}
if(SegmentFailures)
std::fprintf(stderr,"[WalkabilitySegment] passage=%u segment=%d failures=%llu/%llu critical=%llu length=%.2f start=(%.2f,%.2f,%.2f) end=(%.2f,%.2f,%.2f)\n",
PassageIndex,I,static_cast<unsigned long long>(SegmentFailures),
static_cast<unsigned long long>(SegmentSamples),
static_cast<unsigned long long>(SegmentCritical),L,
A.X,A.Y,A.Z,B.X,B.Y,B.Z);
}
if(RouteFailures)
std::fprintf(stderr,"[WalkabilityRoute] passage=%u type=%u points=%d failures=%llu/%llu critical_centerline=%llu first_solid=(%d,%d,%d) critical=%u\n",
PassageIndex,static_cast<uint32>(P.PassageType),Points.Num(),
static_cast<unsigned long long>(RouteFailures),
static_cast<unsigned long long>(RouteSamples),
static_cast<unsigned long long>(RouteCritical),FirstFailure[0],
FirstFailure[1],FirstFailure[2],bFirstFailureCritical?1u:0u);
++PassageIndex;
}
if(Stats.WalkFailures)
std::fprintf(stderr,"[Walkability] FAIL %llu/%llu passage centerline samples are solid\n",
static_cast<unsigned long long>(Stats.WalkFailures),static_cast<unsigned long long>(Stats.WalkSamples));
else
std::printf("[Walkability] PASS %llu/%llu passage centerline samples remain open; native landing floors retained\n",
static_cast<unsigned long long>(Stats.WalkSamples),static_cast<unsigned long long>(Stats.WalkSamples));
return Stats.WalkFailures==0;
}
static double DetailAutocorrelation(EDetailVariant Variant)
{
constexpr int32 N=24;double Sum=0.0,SumSq=0.0,PairA=0.0,PairB=0.0,PairAA=0.0,PairBB=0.0,PairAB=0.0;uint64 Count=0;
auto Value=[&](int32 X,int32 Y,int32 Z)->double
{
if(Variant==EDetailVariant::PerVoxel)return FStandaloneResearchTunnels::NoiseValue(X,Y,Z,11);
if(Variant==EDetailVariant::Smooth)return 0.0;
return SurfaceRelief(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z),11);
};
for(int32 Z=0;Z<N;++Z)for(int32 Y=0;Y<N;++Y)for(int32 X=0;X<N;++X)
{const double V=Value(X,Y,Z);Sum+=V;SumSq+=V*V;}
const double Mean=Sum/(N*N*N),Variance=SumSq/(N*N*N)-Mean*Mean;
if(Variance<1.0e-9)return 1.0;
for(int32 Z=0;Z<N;++Z)for(int32 Y=0;Y<N;++Y)for(int32 X=0;X+1<N;++X)
{
const double A=Value(X,Y,Z)-Mean,B=Value(X+1,Y,Z)-Mean;
PairAA+=A*A;PairBB+=B*B;PairAB+=A*B;++Count;
}
return Count?PairAB/std::sqrt(std::max(1.0e-12,PairAA*PairBB)):0.0;
}
template<class Fn> void ParallelLines(size_t Count,Fn&& Work,FParallelTiming* Timing=nullptr,
std::array<double,16>* WorkerActiveSeconds=nullptr,size_t WorkBatch=1) const
{
const auto WallStart=std::chrono::steady_clock::now();
if(WorkerCount<=1||Count<512||!Scheduler)
{
const auto WorkerStart=std::chrono::steady_clock::now();
for(size_t I=0;I<Count;++I)
{
if constexpr(std::is_invocable_v<Fn&,size_t,int32>)Work(I,0);
else Work(I);
}
if(WorkerActiveSeconds)
(*WorkerActiveSeconds)[0]=std::chrono::duration<double>(
std::chrono::steady_clock::now()-WorkerStart).count();
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;
}
WorkBatch=std::max<size_t>(WorkBatch,1);
using WorkType=std::remove_reference_t<Fn>;
struct FDispatchContext { WorkType* Work; } Dispatch{&Work};
const auto Invoke=[](void* Opaque,size_t Index,size_t Worker)
{
auto& Context=*static_cast<FDispatchContext*>(Opaque);
if constexpr(std::is_invocable_v<WorkType&,size_t,int32>)
(*Context.Work)(Index,static_cast<int32>(Worker));
else
(*Context.Work)(Index);
};
std::array<double,16> ActiveSeconds{};
const double WorkerSeconds=Scheduler->parallel_for(Count,WorkBatch,
static_cast<size_t>(WorkerCount),&Dispatch,Invoke,ActiveSeconds.data(),ActiveSeconds.size());
if(WorkerActiveSeconds)*WorkerActiveSeconds=ActiveSeconds;
if(Timing)
{
Timing->WallSeconds+=std::chrono::duration<double>(std::chrono::steady_clock::now()-WallStart).count();
Timing->WorkerSeconds+=WorkerSeconds;
}
}
void InvalidateFarFieldGpuInput()
{
std::lock_guard<std::mutex> Lock(FarFieldGpuInputMutex);
CachedFarFieldGpuInput.reset();
}
FStandaloneOwnerWorld* Owner=nullptr;
const VoxelForgeCore::WorldAssemblyManifest* WorldAssembly=nullptr;
int32 WorkerCount=6;
const VoxelForgeCore::JobScheduler* Scheduler=nullptr;
bool RoundTunnelsEnabled=false;
bool bProfileDetailBand=false;
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;
size_t WordsPerZ=0;
EDetailVariant DetailVariant=EDetailVariant::Coherent;
bool Built=false;
bool FarBuilt=false;
std::vector<FInterval> LayerRanges;
std::vector<FLayerField> LayerFields;
std::vector<FPrimitive> Primitives;
std::vector<VF_FieldPrimitive> SharedFieldPrimitives;
std::vector<FFarConnectorSegment> FarConnectorSegments;
std::unordered_map<FBinKey,std::vector<uint32>,FBinHash> FarConnectorSpatialBins;
std::vector<FEditBrush> EditBrushes;
std::array<FAxisBins,3> AxisBins;
std::unordered_map<FBinKey,FBinItems,FBinHash> SpatialBins;
std::unordered_map<FBinKey,FBinItems,FBinHash> CriticalAirBins;
mutable std::mutex FarFieldGpuInputMutex;
mutable std::shared_ptr<const VoxelForgeCore::FarFieldWorldInput> CachedFarFieldGpuInput;
std::vector<uint64> RockWords;
std::unique_ptr<int16[]> SurfaceDistanceQ;
std::array<std::vector<uint64>,3> CandidateAxisWords;
std::vector<uint64> CandidateWords;
std::vector<uint64> GpuVisualDetailWords,CpuVisualDetailWords;
uint64 GpuVisualDetailTileCount=0,CpuVisualDetailTileCount=0;
uint64 GpuVisualDetailGridSamples=0,CpuVisualDetailGridSamples=0;
uint64 GpuVisualDetailNodeCount=0,CpuVisualDetailNodeCount=0;
FBuildStats Stats;
bool bAnalyticOnly=false;
};