VoxelHash::Cell/Pair, legacy Cellular3D and the shared Core cellular hash added 0x7FFFFFFF to a signed int32 before casting to uint32; overflow is undefined, so Clang and GCC placed cave passage 1 differently (first differing sample at LOD1 tile (0,64,-512)). The bias is now done in unsigned arithmetic, in both the standalone copy and the Unreal plugin header (Source/VoxelForge/Public/VoxelCaveMorphology.h). Native caves, motifs and lattice now stream LOD0-8 with skirts and cross-level transitions. New verify.py --speed-matrix runs every generator on one asserted-identical view (5 interleaved runs): motifs 1.33, profiles 1.30-1.32, caves 1.41, natural SW 1.67, lattice 1.70, FI 4.93 s whole view. fast/medium/full PASS, Clang/GCC peer hashes equal. Research R29 (open-edge ledge) and R30 (compiler-divergence suspects). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
10804 lines
578 KiB
C++
10804 lines
578 KiB
C++
#pragma once
|
|
|
|
#include "OwnerWorldLoader.h"
|
|
#include "StandaloneResearchTunnels.h"
|
|
#include "VoxelCaveMorphology.h"
|
|
#include "VoxelNoise.h"
|
|
#include "VoxelStrateManager.h"
|
|
#include "VoxelDensityPrimitives.h"
|
|
#include "VoxelForgeCore/VoxelForgeCore.h"
|
|
#include "VoxelForgeFieldShared.h"
|
|
#include "VoxelForgeCore/FloatingIslandInstrumentation.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 <variant>
|
|
#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:
|
|
const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout=nullptr;
|
|
struct FResolvedStratumBounds
|
|
{
|
|
double BottomZ=0.0;
|
|
double TopZ=0.0;
|
|
double SealThickness=0.0;
|
|
};
|
|
|
|
// Immutable adapters share densityAt(x,y,z,out) without a virtual call in the sample
|
|
// loop. The variant is selected once per resolved strate; callers retain concrete access
|
|
// to fixed-point and interval operations where the legacy arithmetic requires them.
|
|
struct FSurfaceWorldFieldAdapter
|
|
{
|
|
VoxelForgeCore::Heightfield2D Evaluator;
|
|
FSurfaceWorldFieldAdapter(const VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
|
const VoxelForgeCore::WorldAssemblyStratumPlanV1& Plan) noexcept
|
|
:Evaluator(Recipe,Plan){}
|
|
|
|
bool densityAt(float X,float Y,float Z,float& OutDensity) const noexcept
|
|
{
|
|
if(!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))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));};
|
|
if(!Evaluator.isValid())return false;
|
|
OutDensity=static_cast<float>(Evaluator.point(ToFixed(X),ToFixed(Y),ToFixed(Z))
|
|
.densitySubvoxels/Scale);
|
|
return true;
|
|
}
|
|
};
|
|
|
|
struct FFloatingIslandFieldAdapter
|
|
{
|
|
VoxelForgeCore::FloatingIslandFieldV1 Evaluator;
|
|
int32 BottomChunkZ=0;
|
|
std::vector<FFloatingIslandParams> LegacyBoundaryParams;
|
|
|
|
FFloatingIslandFieldAdapter(const VoxelForgeCore::FloatingIslandRecipeV1& Recipe,
|
|
const VoxelForgeCore::FloatingIslandPlanV1& Plan,int32 InBottomChunkZ,
|
|
int32 InTopChunkZ,const UVoxelStrateManager* Manager)
|
|
:Evaluator(Recipe,Plan),BottomChunkZ(InBottomChunkZ)
|
|
{
|
|
if(Manager&&Recipe.geometryVersion<4&&InTopChunkZ>=InBottomChunkZ)
|
|
{
|
|
LegacyBoundaryParams.reserve(static_cast<std::size_t>(
|
|
static_cast<int64>(InTopChunkZ)-InBottomChunkZ+1));
|
|
for(int32 ChunkZ=InBottomChunkZ;ChunkZ<=InTopChunkZ;++ChunkZ)
|
|
LegacyBoundaryParams.push_back(Manager->GetFloatingIslandParamsForChunk(
|
|
FIntVector(0,0,ChunkZ)));
|
|
}
|
|
}
|
|
|
|
std::int64_t densitySubvoxelsAt(std::int64_t X,std::int64_t Y,
|
|
std::int64_t Z) const noexcept
|
|
{return Evaluator.densitySubvoxels(X,Y,Z);}
|
|
|
|
const FFloatingIslandParams* legacyBoundaryParamsAt(int32 ChunkZ) const noexcept
|
|
{
|
|
const int64 Index=static_cast<int64>(ChunkZ)-BottomChunkZ;
|
|
return Index>=0&&static_cast<std::size_t>(Index)<LegacyBoundaryParams.size()
|
|
?&LegacyBoundaryParams[static_cast<std::size_t>(Index)]:nullptr;
|
|
}
|
|
|
|
bool densityAt(float X,float Y,float Z,float& OutDensity) const noexcept
|
|
{
|
|
if(!Evaluator.isValid()||!std::isfinite(X)||!std::isfinite(Y)
|
|
||!std::isfinite(Z))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));};
|
|
OutDensity=static_cast<float>(static_cast<double>(densitySubvoxelsAt(
|
|
ToFixed(X),ToFixed(Y),ToFixed(Z)))/Scale);
|
|
return std::isfinite(OutDensity);
|
|
}
|
|
};
|
|
|
|
struct FLegacyGeneratorFieldAdapter
|
|
{
|
|
const UVoxelGenerator* Generator=nullptr;
|
|
bool densityAt(float X,float Y,float Z,float& OutDensity) const
|
|
{
|
|
if(!Generator)return false;
|
|
OutDensity=Generator->GetDensityAt(X,Y,Z);
|
|
return std::isfinite(OutDensity);
|
|
}
|
|
};
|
|
|
|
struct FCavesFieldAdapter
|
|
{
|
|
std::uint32_t CoreVoxelSizeMicrometres=0;
|
|
bool densityAt(float X,float Y,float Z,float& OutDensity) const noexcept
|
|
{
|
|
if(CoreVoxelSizeMicrometres==0||!std::isfinite(X)
|
|
||!std::isfinite(Y)||!std::isfinite(Z))return false;
|
|
const double metresPerVoxel=CoreVoxelSizeMicrometres/1000000.0;
|
|
// A negative metre distance denotes host rock. Cave features are composed
|
|
// into this solid adapter once, in ApplyResolvedCavernGraph.
|
|
OutDensity=static_cast<float>(-32.0/metresPerVoxel);
|
|
return std::isfinite(OutDensity);
|
|
}
|
|
};
|
|
|
|
using FGeneratorFieldAdapterV1=std::variant<std::monostate,
|
|
FSurfaceWorldFieldAdapter,FFloatingIslandFieldAdapter,
|
|
FCavesFieldAdapter,FLegacyGeneratorFieldAdapter>;
|
|
|
|
struct FBoundStratumField
|
|
{
|
|
enum class EKind : uint8 { Legacy, SurfaceWorld, FloatingIslands, Caves };
|
|
const VoxelForgeCore::ResolvedLayoutStratumV2* Resolved=nullptr;
|
|
const VoxelForgeCore::WorldAssemblyStratumPlanV1* SurfacePlan=nullptr;
|
|
const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1* IslandPlan=nullptr;
|
|
FGeneratorFieldAdapterV1 Evaluator;
|
|
int32 StrateIndex=-1;
|
|
int32 BottomChunkZ=0;
|
|
int32 TopChunkZ=-1;
|
|
EKind Kind=EKind::Legacy;
|
|
};
|
|
|
|
const std::vector<FStandalonePassageVoidCrossing>& GetPassageVoidCrossings() const
|
|
{return PassageVoidCrossings;}
|
|
|
|
class FScopedPassageTileQueries
|
|
{
|
|
public:
|
|
FScopedPassageTileQueries(const FStandaloneShapeWorld& shape,
|
|
const std::array<std::int64_t,3>& minimumSubvoxels,
|
|
const std::array<std::int64_t,3>& maximumSubvoxels)
|
|
:shape_(&shape),minimumSubvoxels_(minimumSubvoxels),
|
|
maximumSubvoxels_(maximumSubvoxels),previous_(ActivePassageTileQueries)
|
|
{
|
|
mayContainResolvedBoundaryBand_=shape.ResolvedBoundaryBandsIntersectSubvoxelRange(
|
|
minimumSubvoxels_[2],maximumSubvoxels_[2]);
|
|
mayContainTransitionPassageFade_=shape.ResolvedTransitionPassageFadeIntersectsTileXY(
|
|
minimumSubvoxels_,maximumSubvoxels_);
|
|
featureIndices_=shape.BuildPassageFeatureTileList(
|
|
minimumSubvoxels_,maximumSubvoxels_);
|
|
stampIndices_=shape.BuildStampTileList(minimumSubvoxels_,maximumSubvoxels_);
|
|
cavernFeatureIndices_=shape.BuildCavernFeatureTileList(
|
|
minimumSubvoxels_,maximumSubvoxels_);
|
|
passageRoutes_.assign(shape.ResolvedLayout?shape.ResolvedLayout->passages.size():0,0);
|
|
passageColumns_=passageRoutes_;
|
|
passageMouthSupports_=passageRoutes_;
|
|
for(const auto& crossing:shape.PassageVoidCrossings)
|
|
if(crossing.needsColumn
|
|
&&crossing.bottomZSubvoxels<=maximumSubvoxels_[2]
|
|
&&crossing.topZSubvoxels>=minimumSubvoxels_[2])
|
|
columnZOverlap_=true;
|
|
for(const std::uint32_t featureIndex:featureIndices_)
|
|
{
|
|
if(featureIndex>=shape.PassageTileFeatures.size())continue;
|
|
const auto& feature=shape.PassageTileFeatures[featureIndex];
|
|
if(feature.passageIndex>=passageRoutes_.size())continue;
|
|
if(feature.bColumn)passageColumns_[feature.passageIndex]=1;
|
|
else if(feature.bMouthSupport)passageMouthSupports_[feature.passageIndex]=1;
|
|
else
|
|
{
|
|
passageRoutes_[feature.passageIndex]=1;
|
|
routeFeatureOverlap_=true;
|
|
}
|
|
}
|
|
ActivePassageTileQueries=this;
|
|
}
|
|
~FScopedPassageTileQueries(){ActivePassageTileQueries=previous_;}
|
|
FScopedPassageTileQueries(const FScopedPassageTileQueries&)=delete;
|
|
FScopedPassageTileQueries& operator=(const FScopedPassageTileQueries&)=delete;
|
|
|
|
bool matches(const FStandaloneShapeWorld& shape) const{return shape_==&shape;}
|
|
bool hasFeature(std::size_t passageIndex,bool columnFeature) const
|
|
{
|
|
const auto& candidates=columnFeature?passageColumns_:passageRoutes_;
|
|
return passageIndex<candidates.size()&&candidates[passageIndex]!=0;
|
|
}
|
|
bool hasMouthSupport(std::size_t passageIndex) const
|
|
{return passageIndex<passageMouthSupports_.size()&&passageMouthSupports_[passageIndex]!=0;}
|
|
bool hasAnyMouthSupport() const
|
|
{return std::any_of(passageMouthSupports_.begin(),passageMouthSupports_.end(),
|
|
[](std::uint8_t value){return value!=0;});}
|
|
bool hasRouteFeatureOverlap() const{return routeFeatureOverlap_;}
|
|
bool hasColumnZOverlap() const{return columnZOverlap_;}
|
|
bool mayContainResolvedBoundaryBand() const{return mayContainResolvedBoundaryBand_;}
|
|
bool mayContainTransitionPassageFade() const{return mayContainTransitionPassageFade_;}
|
|
bool mayContainResolvedBoundaryBandAtZ(double zMicrometres) const
|
|
{
|
|
return mayContainResolvedBoundaryBand_
|
|
&&shape_->ResolvedBoundaryBandMayAffectAtZMicrometres(zMicrometres);
|
|
}
|
|
std::size_t featureCount() const{return featureIndices_.size();}
|
|
bool hasStampCandidates() const{return !stampIndices_.empty();}
|
|
const std::vector<std::uint32_t>& stampCandidates() const{return stampIndices_;}
|
|
bool hasCavernCandidates() const{return !cavernFeatureIndices_.empty();}
|
|
const std::vector<std::uint32_t>& cavernFeatureCandidates() const
|
|
{return cavernFeatureIndices_;}
|
|
|
|
private:
|
|
const FStandaloneShapeWorld* shape_;
|
|
std::array<std::int64_t,3> minimumSubvoxels_,maximumSubvoxels_;
|
|
std::vector<std::uint32_t> featureIndices_;
|
|
std::vector<std::uint32_t> stampIndices_;
|
|
std::vector<std::uint32_t> cavernFeatureIndices_;
|
|
std::vector<std::uint8_t> passageRoutes_,passageColumns_,passageMouthSupports_;
|
|
bool routeFeatureOverlap_=false,columnZOverlap_=false;
|
|
bool mayContainResolvedBoundaryBand_=true;
|
|
bool mayContainTransitionPassageFade_=true;
|
|
const FScopedPassageTileQueries* previous_;
|
|
};
|
|
|
|
bool IsResolvedPassageColumnSample(std::size_t PassageIndex,std::size_t SegmentIndex,
|
|
const VoxelForgeCore::Vec3d& Position) const
|
|
{
|
|
(void)SegmentIndex;
|
|
for(const auto& range:PassageVoidCrossings)
|
|
{
|
|
if(!range.needsColumn||range.passageIndex!=PassageIndex)continue;
|
|
if(PassageColumnSignedDistance(range,static_cast<float>(Position.x),
|
|
static_cast<float>(Position.y),static_cast<float>(Position.z))<=0.0)return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool IsResolvedPassageShortSpanSample(std::size_t PassageIndex,std::size_t SegmentIndex,
|
|
const VoxelForgeCore::Vec3d& Position) const
|
|
{
|
|
return IsPassageShortSpanSegment(PassageIndex,SegmentIndex)
|
|
&&PassageBridgeSignedDistance(PassageIndex,SegmentIndex,
|
|
static_cast<float>(Position.x),static_cast<float>(Position.y),
|
|
static_cast<float>(Position.z))<=0.0;
|
|
}
|
|
|
|
double GetCoreVoxelSizeCm() const
|
|
{
|
|
return Owner == nullptr || Owner->CoreVoxelSizeMicrons == 0
|
|
? static_cast<double>(VOXEL_SIZE)
|
|
: static_cast<double>(Owner->CoreVoxelSizeMicrons) / 10000.0;
|
|
}
|
|
|
|
double GetResolvedLayoutChunkWidthVoxels() const
|
|
{
|
|
return ResolvedLayout&&ResolvedLayout->strateLayoutChunkWidthSubvoxels>0
|
|
?static_cast<double>(ResolvedLayout->strateLayoutChunkWidthSubvoxels)
|
|
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel
|
|
:Owner?static_cast<double>(Owner->StrateLayoutChunkWidthSubvoxels)
|
|
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel:0.0;
|
|
}
|
|
|
|
const VoxelForgeCore::ResolvedLayoutStratumV2* FindResolvedLayoutStratum(int32 StrateIndex) const
|
|
{
|
|
if(!ResolvedLayout)return nullptr;
|
|
if(StrateIndex>=0&&static_cast<std::size_t>(StrateIndex)<ResolvedLayout->strates.size())
|
|
{
|
|
const auto& direct=ResolvedLayout->strates[static_cast<std::size_t>(StrateIndex)];
|
|
if(direct.strateIndex==StrateIndex)return &direct;
|
|
}
|
|
const auto it=std::find_if(ResolvedLayout->strates.begin(),ResolvedLayout->strates.end(),
|
|
[StrateIndex](const auto& item){return item.strateIndex==StrateIndex;});
|
|
return it==ResolvedLayout->strates.end()?nullptr:&*it;
|
|
}
|
|
|
|
void BindResolvedStratumFields()
|
|
{
|
|
BoundStratumFields.clear();
|
|
BoundManagerLayoutChunkWidthVoxels=Owner&&Owner->Manager
|
|
?Owner->Manager->GetStrateLayoutChunkWidthVoxels():0.0f;
|
|
if(!ResolvedLayout||ResolvedLayout->strateLayoutChunkSizeMicrometres<=0)return;
|
|
const int64 ChunkWidthMicrometres=ResolvedLayout->strateLayoutChunkSizeMicrometres;
|
|
const auto FloorDiv64=[](int64 Value,int64 Divisor)
|
|
{
|
|
const int64 Quotient=Value/Divisor;
|
|
const int64 Remainder=Value%Divisor;
|
|
return Remainder<0?Quotient-1:Quotient;
|
|
};
|
|
BoundStratumFields.reserve(ResolvedLayout->strates.size());
|
|
for(const auto& Stratum:ResolvedLayout->strates)
|
|
{
|
|
if(Stratum.strateIndex<0
|
|
||Stratum.interiorTopMicrometres<=Stratum.interiorBottomMicrometres)
|
|
continue;
|
|
const int64 Bottom=FloorDiv64(Stratum.interiorBottomMicrometres,
|
|
ChunkWidthMicrometres);
|
|
const int64 Top=FloorDiv64(Stratum.interiorTopMicrometres-1,
|
|
ChunkWidthMicrometres);
|
|
if(Bottom<std::numeric_limits<int32>::min()
|
|
||Bottom>std::numeric_limits<int32>::max()
|
|
||Top<std::numeric_limits<int32>::min()
|
|
||Top>std::numeric_limits<int32>::max()||Top<Bottom)continue;
|
|
|
|
FBoundStratumField Bound;
|
|
Bound.Resolved=&Stratum;
|
|
Bound.StrateIndex=Stratum.strateIndex;
|
|
Bound.BottomChunkZ=static_cast<int32>(Bottom);
|
|
Bound.TopChunkZ=static_cast<int32>(Top);
|
|
if(Stratum.generatorKind=="SurfaceWorld")
|
|
{
|
|
Bound.Kind=FBoundStratumField::EKind::SurfaceWorld;
|
|
if(WorldAssembly)
|
|
{
|
|
const auto Plan=std::find_if(WorldAssembly->plan.strates.begin(),
|
|
WorldAssembly->plan.strates.end(),[&](const auto& Candidate)
|
|
{return Candidate.strateIndex==Bound.StrateIndex
|
|
&&Candidate.bottomChunkZ==Bound.BottomChunkZ
|
|
&&Candidate.topChunkZ==Bound.TopChunkZ;});
|
|
if(Plan!=WorldAssembly->plan.strates.end())
|
|
{
|
|
Bound.SurfacePlan=&*Plan;
|
|
FSurfaceWorldFieldAdapter Adapter(WorldAssembly->recipe,*Plan);
|
|
if(Adapter.Evaluator.isValid())
|
|
Bound.Evaluator.emplace<FSurfaceWorldFieldAdapter>(
|
|
std::move(Adapter));
|
|
}
|
|
}
|
|
}
|
|
else if(Stratum.generatorKind=="FloatingIslands")
|
|
{
|
|
Bound.Kind=FBoundStratumField::EKind::FloatingIslands;
|
|
if(WorldAssembly)
|
|
{
|
|
const auto Plan=std::find_if(WorldAssembly->floatingIslands.begin(),
|
|
WorldAssembly->floatingIslands.end(),[&](const auto& Candidate)
|
|
{return Candidate.strateIndex==Bound.StrateIndex
|
|
&&Candidate.bottomChunkZ==Bound.BottomChunkZ
|
|
&&Candidate.topChunkZ==Bound.TopChunkZ;});
|
|
if(Plan!=WorldAssembly->floatingIslands.end())
|
|
{
|
|
Bound.IslandPlan=&*Plan;
|
|
Bound.Evaluator.emplace<FFloatingIslandFieldAdapter>(
|
|
Plan->manifest.recipe,Plan->manifest.plan,
|
|
Bound.BottomChunkZ,Bound.TopChunkZ,
|
|
Owner?Owner->Manager.get():nullptr);
|
|
}
|
|
}
|
|
}
|
|
else if(VoxelForgeCore::HasNativeGraphField(Stratum)
|
|
&&(VoxelForgeCore::IsNativeCaveFieldStratum(Stratum)
|
|
?Stratum.passageCapabilities.hasNativeCaveField
|
|
:Stratum.passageCapabilities.hasNativeLatticeField))
|
|
{
|
|
Bound.Kind=FBoundStratumField::EKind::Caves;
|
|
Bound.Evaluator.emplace<FCavesFieldAdapter>(
|
|
FCavesFieldAdapter{ResolvedLayout->coreVoxelSizeMicrometres});
|
|
}
|
|
if(std::holds_alternative<std::monostate>(Bound.Evaluator))
|
|
Bound.Evaluator.emplace<FLegacyGeneratorFieldAdapter>(
|
|
FLegacyGeneratorFieldAdapter{Owner?Owner->Generator.get():nullptr});
|
|
BoundStratumFields.push_back(std::move(Bound));
|
|
}
|
|
std::sort(BoundStratumFields.begin(),BoundStratumFields.end(),
|
|
[](const auto& A,const auto& B){return A.BottomChunkZ<B.BottomChunkZ;});
|
|
BuildOreVeinSpatialIndex();
|
|
}
|
|
|
|
const FBoundStratumField* FindBoundStratumForChunk(int32 ChunkZ) const
|
|
{
|
|
const auto Upper=std::upper_bound(BoundStratumFields.begin(),
|
|
BoundStratumFields.end(),ChunkZ,[](int32 Z,const auto& Field)
|
|
{return Z<Field.BottomChunkZ;});
|
|
if(Upper==BoundStratumFields.begin())return nullptr;
|
|
const auto& Candidate=*std::prev(Upper);
|
|
return ChunkZ<=Candidate.TopChunkZ?&Candidate:nullptr;
|
|
}
|
|
|
|
const FBoundStratumField* FindBoundStratumForZ(float Z) const
|
|
{
|
|
if(!std::isfinite(Z))return nullptr;
|
|
const float LayoutWidth=static_cast<float>(GetResolvedLayoutChunkWidthVoxels());
|
|
if(!(LayoutWidth>0.0f))return nullptr;
|
|
return FindBoundStratumForChunk(FMath::FloorToInt(Z/LayoutWidth));
|
|
}
|
|
|
|
bool IsFloatingIslandFieldAtZ(float Z) const
|
|
{
|
|
const FBoundStratumField* Bound=FindBoundStratumForZ(Z);
|
|
return Bound&&Bound->Kind==FBoundStratumField::EKind::FloatingIslands;
|
|
}
|
|
|
|
const FBoundStratumField* FindBoundStratumForManagerZ(float Z) const
|
|
{
|
|
if(!std::isfinite(Z)||!Owner||!(BoundManagerLayoutChunkWidthVoxels>0.0f))
|
|
return nullptr;
|
|
const float WorldZCentimetres=static_cast<float>(
|
|
static_cast<double>(Z)*GetCoreVoxelSizeCm());
|
|
const int32 ChunkZ=FMath::FloorToInt((WorldZCentimetres/VOXEL_SIZE)
|
|
/BoundManagerLayoutChunkWidthVoxels);
|
|
return FindBoundStratumForChunk(ChunkZ);
|
|
}
|
|
|
|
const VoxelForgeCore::ResolvedLayoutStratumV2* FindResolvedLayoutStratumByOrder(int32 Order) const
|
|
{
|
|
if(!ResolvedLayout||Order<0)return nullptr;
|
|
if(static_cast<std::size_t>(Order)<ResolvedLayout->strates.size())
|
|
{
|
|
const auto& direct=ResolvedLayout->strates[static_cast<std::size_t>(Order)];
|
|
if(direct.order==Order)return &direct;
|
|
}
|
|
const auto it=std::find_if(ResolvedLayout->strates.begin(),ResolvedLayout->strates.end(),
|
|
[Order](const auto& item){return item.order==Order;});
|
|
return it==ResolvedLayout->strates.end()?nullptr:&*it;
|
|
}
|
|
|
|
bool IsResolvedLayoutTransitionActive(const VoxelForgeCore::ResolvedLayoutStratumV2& Upper) const
|
|
{
|
|
return Upper.transitionMode!="Hard"&&!Upper.transitionFallsBackToHard
|
|
&&Upper.transitionWidthMicrometres>0
|
|
&&Upper.transitionHonouredInCore;
|
|
}
|
|
|
|
bool ResolvedBoundaryBandsIntersectSubvoxelRange(std::int64_t minimumZSubvoxels,
|
|
std::int64_t maximumZSubvoxels) const
|
|
{
|
|
if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0
|
|
||minimumZSubvoxels>maximumZSubvoxels)return false;
|
|
constexpr double Subvoxels=static_cast<double>(
|
|
VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
|
const double queryMinimumMicrometres=static_cast<double>(minimumZSubvoxels)
|
|
*Owner->CoreVoxelSizeMicrons/Subvoxels;
|
|
const double queryMaximumMicrometres=static_cast<double>(maximumZSubvoxels)
|
|
*Owner->CoreVoxelSizeMicrons/Subvoxels;
|
|
const double sampleMarginMicrometres=static_cast<double>(Owner->CoreVoxelSizeMicrons);
|
|
for(const auto& interval:ResolvedBoundaryZIntervalsMicrometres)
|
|
{
|
|
if(queryMaximumMicrometres+sampleMarginMicrometres>=interval[0]-1.0
|
|
&&queryMinimumMicrometres-sampleMarginMicrometres<interval[1]+1.0)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ResolvedBoundaryBandMayAffectAtZMicrometres(double zMicrometres) const
|
|
{
|
|
if(!std::isfinite(zMicrometres)||ResolvedBoundaryZIntervalsMicrometres.empty())return false;
|
|
const auto it=std::upper_bound(ResolvedBoundaryZIntervalsMicrometres.begin(),
|
|
ResolvedBoundaryZIntervalsMicrometres.end(),zMicrometres,
|
|
[](double z,const auto& interval){return z<interval[0];});
|
|
return it!=ResolvedBoundaryZIntervalsMicrometres.begin()
|
|
&&zMicrometres<std::prev(it)->at(1);
|
|
}
|
|
|
|
void BuildResolvedBoundaryZIntervals()
|
|
{
|
|
ResolvedBoundaryZIntervalsMicrometres.clear();
|
|
if(!ResolvedLayout)return;
|
|
for(const auto& stratum:ResolvedLayout->strates)
|
|
{
|
|
if(stratum.ceiling.presentInCurrentField
|
|
&&stratum.ceiling.bottomMicrometres<stratum.ceiling.topMicrometres)
|
|
ResolvedBoundaryZIntervalsMicrometres.push_back({
|
|
static_cast<double>(stratum.ceiling.bottomMicrometres),
|
|
static_cast<double>(stratum.ceiling.topMicrometres)});
|
|
if(stratum.bedrock.presentInCurrentField
|
|
&&stratum.bedrock.bottomMicrometres<stratum.bedrock.topMicrometres)
|
|
ResolvedBoundaryZIntervalsMicrometres.push_back({
|
|
static_cast<double>(stratum.bedrock.bottomMicrometres),
|
|
static_cast<double>(stratum.bedrock.topMicrometres)});
|
|
}
|
|
for(std::int32_t order=0;order+1<static_cast<std::int32_t>(ResolvedLayout->strates.size());
|
|
++order)
|
|
{
|
|
const auto* upper=FindResolvedLayoutStratumByOrder(order);
|
|
const auto* lower=FindResolvedLayoutStratumByOrder(order+1);
|
|
if(!upper||!lower||!IsResolvedLayoutTransitionActive(*upper))continue;
|
|
const auto displacement=ResolvedTransitionMaximumFaceDisplacementMicrometres(
|
|
*upper,*lower);
|
|
ResolvedBoundaryZIntervalsMicrometres.push_back({
|
|
static_cast<double>(std::min(upper->bedrock.bottomMicrometres,
|
|
lower->ceiling.bottomMicrometres)-displacement),
|
|
static_cast<double>(std::max(upper->bedrock.topMicrometres,
|
|
lower->ceiling.topMicrometres)+displacement)});
|
|
}
|
|
std::sort(ResolvedBoundaryZIntervalsMicrometres.begin(),
|
|
ResolvedBoundaryZIntervalsMicrometres.end(),
|
|
[](const auto& a,const auto& b){return a[0]<b[0];});
|
|
std::size_t write=0;
|
|
for(const auto& interval:ResolvedBoundaryZIntervalsMicrometres)
|
|
{
|
|
if(write>0&&interval[0]<=ResolvedBoundaryZIntervalsMicrometres[write-1][1])
|
|
ResolvedBoundaryZIntervalsMicrometres[write-1][1]=std::max(
|
|
ResolvedBoundaryZIntervalsMicrometres[write-1][1],interval[1]);
|
|
else
|
|
ResolvedBoundaryZIntervalsMicrometres[write++]=interval;
|
|
}
|
|
ResolvedBoundaryZIntervalsMicrometres.resize(write);
|
|
}
|
|
|
|
std::int64_t ResolvedTransitionMaximumFaceDisplacementMicrometres(
|
|
const VoxelForgeCore::ResolvedLayoutStratumV2& Upper,
|
|
const VoxelForgeCore::ResolvedLayoutStratumV2& Lower) const
|
|
{
|
|
const std::int64_t width=std::max<std::int64_t>(0,Upper.transitionWidthMicrometres);
|
|
if(Upper.transitionMode=="Gradient")
|
|
return width/8;
|
|
if(Upper.transitionMode!="Interleaved")return 0;
|
|
const std::int64_t upperThickness=std::max<std::int64_t>(0,
|
|
Upper.bedrock.topMicrometres-Upper.bedrock.bottomMicrometres);
|
|
const std::int64_t lowerThickness=std::max<std::int64_t>(0,
|
|
Lower.ceiling.topMicrometres-Lower.ceiling.bottomMicrometres);
|
|
const std::int64_t maximumCrossing=std::min(width,upperThickness+lowerThickness);
|
|
return (maximumCrossing+1)/2;
|
|
}
|
|
|
|
bool ResolvedTransitionPassageFadeIntersectsTileXY(
|
|
const std::array<std::int64_t,3>& minimumSubvoxels,
|
|
const std::array<std::int64_t,3>& maximumSubvoxels) const
|
|
{
|
|
if(!Owner||!ResolvedLayout||Owner->CoreVoxelSizeMicrons==0)return true;
|
|
for(std::size_t axis=0;axis<2;++axis)
|
|
if(minimumSubvoxels[axis]>maximumSubvoxels[axis])return true;
|
|
constexpr std::int64_t Subvoxels=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
constexpr std::int64_t SupportMarginMicrometres=1000000;
|
|
constexpr std::int64_t FadeWidthMicrometres=2000000;
|
|
const double voxelMicrometres=static_cast<double>(Owner->CoreVoxelSizeMicrons);
|
|
const double minimumX=static_cast<double>(minimumSubvoxels[0])*voxelMicrometres
|
|
/Subvoxels-voxelMicrometres;
|
|
const double maximumX=static_cast<double>(maximumSubvoxels[0])*voxelMicrometres
|
|
/Subvoxels+voxelMicrometres;
|
|
const double minimumY=static_cast<double>(minimumSubvoxels[1])*voxelMicrometres
|
|
/Subvoxels-voxelMicrometres;
|
|
const double maximumY=static_cast<double>(maximumSubvoxels[1])*voxelMicrometres
|
|
/Subvoxels+voxelMicrometres;
|
|
for(std::int32_t order=0;order+1<static_cast<std::int32_t>(ResolvedLayout->strates.size());
|
|
++order)
|
|
{
|
|
const auto* upper=FindResolvedLayoutStratumByOrder(order);
|
|
const auto* lower=FindResolvedLayoutStratumByOrder(order+1);
|
|
if(!upper||!lower||!IsResolvedLayoutTransitionActive(*upper))continue;
|
|
const std::int64_t seam=upper->bedrock.bottomMicrometres;
|
|
for(const auto& passage:ResolvedLayout->passages)
|
|
{
|
|
if(passage.upperStrateIndex!=upper->strateIndex
|
|
||passage.lowerStrateIndex!=lower->strateIndex)continue;
|
|
const std::int64_t mouthRadius=std::max<std::int64_t>(0,
|
|
passage.mouthRadiusSubvoxels*Owner->CoreVoxelSizeMicrons/Subvoxels);
|
|
const std::int64_t protectedRadius=mouthRadius+SupportMarginMicrometres;
|
|
const double fadeEnd=static_cast<double>(protectedRadius+FadeWidthMicrometres);
|
|
for(const auto* mouth:{&passage.upperMouthSubvoxels,&passage.lowerMouthSubvoxels})
|
|
{
|
|
const std::int64_t mouthZ=mouth->at(2)*Owner->CoreVoxelSizeMicrons/Subvoxels;
|
|
if(std::abs(mouthZ-seam)
|
|
>upper->transitionWidthMicrometres/2+mouthRadius)continue;
|
|
const double mouthX=static_cast<double>(mouth->at(0)
|
|
*Owner->CoreVoxelSizeMicrons/Subvoxels);
|
|
const double mouthY=static_cast<double>(mouth->at(1)
|
|
*Owner->CoreVoxelSizeMicrons/Subvoxels);
|
|
const double dx=mouthX<minimumX?minimumX-mouthX
|
|
:mouthX>maximumX?mouthX-maximumX:0.0;
|
|
const double dy=mouthY<minimumY?minimumY-mouthY
|
|
:mouthY>maximumY?mouthY-maximumY:0.0;
|
|
if(dx*dx+dy*dy<fadeEnd*fadeEnd)return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::int64_t ResolvedTransitionPassageWeightQ16(
|
|
const VoxelForgeCore::ResolvedLayoutStratumV2& Upper,
|
|
const VoxelForgeCore::ResolvedLayoutStratumV2& Lower,
|
|
std::int64_t XMicrometres,std::int64_t YMicrometres) const
|
|
{
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().TransitionPassageFadeCalls;
|
|
#endif
|
|
constexpr std::int64_t Q16=1ll<<16;
|
|
constexpr std::int64_t SupportMarginMicrometres=1000000;
|
|
constexpr std::int64_t FadeWidthMicrometres=2000000;
|
|
if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0)return Q16;
|
|
const std::int64_t seam=Upper.bedrock.bottomMicrometres;
|
|
const std::int64_t coreMicrometres=Owner->CoreVoxelSizeMicrons;
|
|
const std::int64_t subvoxelsPerVoxel=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
std::int64_t weight=Q16;
|
|
for(const auto& passage:ResolvedLayout->passages)
|
|
{
|
|
if(passage.upperStrateIndex!=Upper.strateIndex
|
|
||passage.lowerStrateIndex!=Lower.strateIndex)continue;
|
|
const std::int64_t mouthRadiusMicrometres=std::max<std::int64_t>(0,
|
|
passage.mouthRadiusSubvoxels*coreMicrometres/subvoxelsPerVoxel);
|
|
const std::int64_t protectedRadius=mouthRadiusMicrometres
|
|
+SupportMarginMicrometres;
|
|
for(const auto* mouth:{&passage.upperMouthSubvoxels,&passage.lowerMouthSubvoxels})
|
|
{
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().TransitionPassageMouthTests;
|
|
#endif
|
|
const std::int64_t mouthZMicrometres=mouth->at(2)*coreMicrometres
|
|
/subvoxelsPerVoxel;
|
|
if(std::abs(mouthZMicrometres-seam)
|
|
>Upper.transitionWidthMicrometres/2+mouthRadiusMicrometres)continue;
|
|
const double dx=static_cast<double>(XMicrometres-mouth->at(0)*coreMicrometres
|
|
/subvoxelsPerVoxel);
|
|
const double dy=static_cast<double>(YMicrometres-mouth->at(1)*coreMicrometres
|
|
/subvoxelsPerVoxel);
|
|
const std::int64_t fadeEnd=protectedRadius+FadeWidthMicrometres;
|
|
const double distanceSquared=dx*dx+dy*dy;
|
|
if(distanceSquared>=static_cast<double>(fadeEnd)*fadeEnd)continue;
|
|
if(distanceSquared<=static_cast<double>(protectedRadius)*protectedRadius)return 0;
|
|
const double distance=std::sqrt(distanceSquared);
|
|
const double t=static_cast<double>(static_cast<std::int64_t>(distance)
|
|
-protectedRadius)/FadeWidthMicrometres;
|
|
const double smooth=t*t*(3.0-2.0*t);
|
|
weight=std::min(weight,static_cast<std::int64_t>(std::llround(smooth*Q16)));
|
|
}
|
|
}
|
|
return weight;
|
|
}
|
|
|
|
VoxelForgeCore::ResolvedLayoutTransitionBandsV1 ResolvedTransitionBandsAtXY(
|
|
const VoxelForgeCore::ResolvedLayoutStratumV2& Upper,
|
|
const VoxelForgeCore::ResolvedLayoutStratumV2& Lower,
|
|
std::int64_t XMicrometres,std::int64_t YMicrometres,bool SkipPassageFade=false) const
|
|
{
|
|
auto bands=VoxelForgeCore::EvaluateResolvedLayoutTransitionBandsV1(Upper,Lower,
|
|
Owner?Owner->Seed:0,ResolvedLayout?ResolvedLayout->strateLayoutChunkSizeMicrometres:0,
|
|
XMicrometres,YMicrometres);
|
|
constexpr std::int64_t TransitionQ16=1ll<<16;
|
|
const std::int64_t weight=SkipPassageFade?TransitionQ16:
|
|
ResolvedTransitionPassageWeightQ16(Upper,Lower,XMicrometres,YMicrometres);
|
|
if(weight>=TransitionQ16)return bands;
|
|
const std::int64_t seam=Upper.bedrock.bottomMicrometres;
|
|
const std::int64_t upperThickness=Upper.bedrock.topMicrometres-seam;
|
|
const std::int64_t lowerThickness=Lower.ceiling.topMicrometres
|
|
-Lower.ceiling.bottomMicrometres;
|
|
const auto scaled=[&](std::int64_t value){return (value*weight)/TransitionQ16;};
|
|
bands.upperBedrockBottomMicrometres=seam+scaled(
|
|
bands.upperBedrockBottomMicrometres-seam);
|
|
bands.upperBedrockTopMicrometres=bands.upperBedrockBottomMicrometres+upperThickness;
|
|
bands.lowerCeilingTopMicrometres=seam+scaled(
|
|
bands.lowerCeilingTopMicrometres-seam);
|
|
bands.lowerCeilingBottomMicrometres=bands.lowerCeilingTopMicrometres-lowerThickness;
|
|
bands.crossingDepthMicrometres=std::max<std::int64_t>(0,
|
|
bands.lowerCeilingTopMicrometres-bands.upperBedrockBottomMicrometres);
|
|
return bands;
|
|
}
|
|
|
|
bool ResolvedBedrockTopAt(const VoxelForgeCore::ResolvedLayoutStratumV2& Upper,
|
|
double X,double Y,double& TopZ) const
|
|
{
|
|
const auto* lower=FindResolvedLayoutStratumByOrder(Upper.order+1);
|
|
if(!lower||!Owner||Owner->CoreVoxelSizeMicrons==0
|
|
||!IsResolvedLayoutTransitionActive(Upper))return false;
|
|
const auto toMicrometres=[&](double value)
|
|
{return static_cast<std::int64_t>(std::llround(value*Owner->CoreVoxelSizeMicrons));};
|
|
const auto* tileQueries=ActivePassageTileQueries;
|
|
const bool skipPassageFade=tileQueries&&tileQueries->matches(*this)
|
|
&&!tileQueries->mayContainTransitionPassageFade();
|
|
const auto bands=ResolvedTransitionBandsAtXY(Upper,*lower,toMicrometres(X),
|
|
toMicrometres(Y),skipPassageFade);
|
|
TopZ=static_cast<double>(bands.upperBedrockTopMicrometres)
|
|
/Owner->CoreVoxelSizeMicrons;
|
|
return std::isfinite(TopZ);
|
|
}
|
|
|
|
bool ResolvedCeilingBottomAt(const VoxelForgeCore::ResolvedLayoutStratumV2& Lower,
|
|
double X,double Y,double& BottomZ) const
|
|
{
|
|
const auto* upper=FindResolvedLayoutStratumByOrder(Lower.order-1);
|
|
if(!upper||!Owner||Owner->CoreVoxelSizeMicrons==0
|
|
||!IsResolvedLayoutTransitionActive(*upper))return false;
|
|
const auto toMicrometres=[&](double value)
|
|
{return static_cast<std::int64_t>(std::llround(value*Owner->CoreVoxelSizeMicrons));};
|
|
const auto* tileQueries=ActivePassageTileQueries;
|
|
const bool skipPassageFade=tileQueries&&tileQueries->matches(*this)
|
|
&&!tileQueries->mayContainTransitionPassageFade();
|
|
const auto bands=ResolvedTransitionBandsAtXY(*upper,Lower,toMicrometres(X),
|
|
toMicrometres(Y),skipPassageFade);
|
|
BottomZ=static_cast<double>(bands.lowerCeilingBottomMicrometres)
|
|
/Owner->CoreVoxelSizeMicrons;
|
|
return std::isfinite(BottomZ);
|
|
}
|
|
|
|
bool IsInsideResolvedStrateBoundaryBand(float Z) const
|
|
{
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().BoundaryBandCalls;
|
|
#endif
|
|
const auto* tileQueries=ActivePassageTileQueries;
|
|
if(tileQueries&&tileQueries->matches(*this))
|
|
{
|
|
if(!tileQueries->mayContainResolvedBoundaryBand())
|
|
{
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().BoundaryTileCullRejects;
|
|
#endif
|
|
return false;
|
|
}
|
|
if(Owner&&!tileQueries->mayContainResolvedBoundaryBandAtZ(
|
|
static_cast<double>(Z)*Owner->CoreVoxelSizeMicrons))
|
|
{
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().BoundaryPointCullRejects;
|
|
#endif
|
|
return false;
|
|
}
|
|
// This predicate only selects the scalar path for boundary-capable Z
|
|
// samples. The merged intervals are conservative, so a positive is safe.
|
|
return true;
|
|
}
|
|
if(!ResolvedLayout||!Owner||!std::isfinite(Z))return false;
|
|
const FBoundStratumField* bound=FindBoundStratumForManagerZ(Z);
|
|
const auto* stratum=bound?FindResolvedLayoutStratum(bound->StrateIndex):nullptr;
|
|
if(!stratum)return false;
|
|
const double zMicrometres=static_cast<double>(Z)*Owner->CoreVoxelSizeMicrons;
|
|
const auto solidAt=[&](const VoxelForgeCore::ResolvedLayoutBandV2& band)
|
|
{return band.presentInCurrentField&&zMicrometres>=band.bottomMicrometres
|
|
&&zMicrometres<band.topMicrometres;};
|
|
const auto mayTouchTransition=[&](const VoxelForgeCore::ResolvedLayoutStratumV2* Upper,
|
|
const VoxelForgeCore::ResolvedLayoutStratumV2* Lower)
|
|
{
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().TransitionWindowChecks;
|
|
#endif
|
|
if(!Upper||!Lower||!IsResolvedLayoutTransitionActive(*Upper))return false;
|
|
const auto maxDisplacement=ResolvedTransitionMaximumFaceDisplacementMicrometres(
|
|
*Upper,*Lower);
|
|
const double minimum=std::min(Upper->bedrock.bottomMicrometres,
|
|
Lower->ceiling.bottomMicrometres)-maxDisplacement;
|
|
const double maximum=std::max(Upper->bedrock.topMicrometres,
|
|
Lower->ceiling.topMicrometres)+maxDisplacement;
|
|
const bool touches=zMicrometres>=minimum&&zMicrometres<maximum;
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
if(touches)++ThreadClassificationStats().TransitionWindowHits;
|
|
#endif
|
|
return touches;
|
|
};
|
|
const auto* lower=FindResolvedLayoutStratumByOrder(stratum->order+1);
|
|
const auto* upper=stratum->order>0?FindResolvedLayoutStratumByOrder(stratum->order-1):nullptr;
|
|
if(mayTouchTransition(stratum,lower)||mayTouchTransition(upper,stratum))return true;
|
|
return solidAt(stratum->ceiling)||solidAt(stratum->bedrock);
|
|
}
|
|
|
|
float ApplyResolvedStrateBoundaryBand(float density,float X,float Y,float Z) const
|
|
{
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().BoundaryBandCalls;
|
|
#endif
|
|
(void)X;(void)Y;
|
|
const auto* tileQueries=ActivePassageTileQueries;
|
|
if(tileQueries&&tileQueries->matches(*this))
|
|
{
|
|
if(!tileQueries->mayContainResolvedBoundaryBand())
|
|
{
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().BoundaryTileCullRejects;
|
|
#endif
|
|
return density;
|
|
}
|
|
if(Owner&&!tileQueries->mayContainResolvedBoundaryBandAtZ(
|
|
static_cast<double>(Z)*Owner->CoreVoxelSizeMicrons))
|
|
{
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().BoundaryPointCullRejects;
|
|
#endif
|
|
return density;
|
|
}
|
|
}
|
|
if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0
|
|
||!std::isfinite(Z))return density;
|
|
const double zMicrometres=static_cast<double>(Z)*Owner->CoreVoxelSizeMicrons;
|
|
if(ResolvedLayout->strates.size()==2)
|
|
{
|
|
const auto* transitionUpper=FindResolvedLayoutStratumByOrder(0);
|
|
const auto* transitionLower=FindResolvedLayoutStratumByOrder(1);
|
|
if(transitionUpper&&transitionLower
|
|
&&IsResolvedLayoutTransitionActive(*transitionUpper))
|
|
{
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().TransitionWindowChecks;
|
|
#endif
|
|
const auto maximumDisplacement=ResolvedTransitionMaximumFaceDisplacementMicrometres(
|
|
*transitionUpper,*transitionLower);
|
|
const double minimum=std::min(transitionUpper->bedrock.bottomMicrometres,
|
|
transitionLower->ceiling.bottomMicrometres)-maximumDisplacement;
|
|
const double maximum=std::max(transitionUpper->bedrock.topMicrometres,
|
|
transitionLower->ceiling.topMicrometres)+maximumDisplacement;
|
|
if(zMicrometres>=minimum&&zMicrometres<maximum)
|
|
{
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().TransitionWindowHits;
|
|
++ThreadClassificationStats().TransitionEvaluatorCalls;
|
|
#endif
|
|
const auto* tileQueries=ActivePassageTileQueries;
|
|
const bool skipPassageFade=tileQueries&&tileQueries->matches(*this)
|
|
&&!tileQueries->mayContainTransitionPassageFade();
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
if(skipPassageFade)
|
|
++ThreadClassificationStats().TransitionPassageTileSkips;
|
|
#endif
|
|
const auto xMicrometres=static_cast<std::int64_t>(std::llround(
|
|
static_cast<double>(X)*Owner->CoreVoxelSizeMicrons));
|
|
const auto yMicrometres=static_cast<std::int64_t>(std::llround(
|
|
static_cast<double>(Y)*Owner->CoreVoxelSizeMicrons));
|
|
const auto bands=ResolvedTransitionBandsAtXY(*transitionUpper,
|
|
*transitionLower,xMicrometres,yMicrometres,skipPassageFade);
|
|
const std::int64_t solidBottom=std::min(
|
|
bands.upperBedrockBottomMicrometres,bands.lowerCeilingBottomMicrometres);
|
|
const std::int64_t solidTop=std::max(
|
|
bands.upperBedrockTopMicrometres,bands.lowerCeilingTopMicrometres);
|
|
const double signedDistanceMicrometres=zMicrometres<solidBottom
|
|
?static_cast<double>(solidBottom-zMicrometres)
|
|
:zMicrometres>solidTop
|
|
?static_cast<double>(zMicrometres-solidTop)
|
|
:-static_cast<double>(std::min(zMicrometres-solidBottom,
|
|
solidTop-zMicrometres));
|
|
const std::int64_t seam=transitionUpper->bedrock.bottomMicrometres;
|
|
const bool floatingIslandAirGap=
|
|
transitionUpper->lowerInterfaceCapabilities.preserveUpperBedrockAirGap
|
|
&&bands.upperBedrockTopMicrometres<seam
|
|
&&zMicrometres>=bands.upperBedrockTopMicrometres
|
|
&&zMicrometres<seam;
|
|
if(!(signedDistanceMicrometres>0.0&&density<0.0f
|
|
&&!floatingIslandAirGap))
|
|
return static_cast<float>(signedDistanceMicrometres
|
|
/Owner->CoreVoxelSizeMicrons);
|
|
}
|
|
}
|
|
}
|
|
const FBoundStratumField* bound=FindBoundStratumForManagerZ(Z);
|
|
const auto* stratum=bound?FindResolvedLayoutStratum(bound->StrateIndex):nullptr;
|
|
if(!stratum)return density;
|
|
const auto solidAt=[&](const VoxelForgeCore::ResolvedLayoutBandV2& band)
|
|
{return band.presentInCurrentField&&zMicrometres>=band.bottomMicrometres
|
|
&&zMicrometres<band.topMicrometres;};
|
|
|
|
bool bCeilingHandledByTransition=false,bBedrockHandledByTransition=false;
|
|
const auto applyInterface=[&](const VoxelForgeCore::ResolvedLayoutStratumV2* upper,
|
|
const VoxelForgeCore::ResolvedLayoutStratumV2* lower)
|
|
{
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().TransitionWindowChecks;
|
|
#endif
|
|
if(!upper||!lower||!IsResolvedLayoutTransitionActive(*upper))return false;
|
|
const auto maxDisplacement=ResolvedTransitionMaximumFaceDisplacementMicrometres(
|
|
*upper,*lower);
|
|
const double minimum=std::min(upper->bedrock.bottomMicrometres,
|
|
lower->ceiling.bottomMicrometres)-maxDisplacement;
|
|
const double maximum=std::max(upper->bedrock.topMicrometres,
|
|
lower->ceiling.topMicrometres)+maxDisplacement;
|
|
if(zMicrometres<minimum||zMicrometres>=maximum)return false;
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().TransitionWindowHits;
|
|
++ThreadClassificationStats().TransitionEvaluatorCalls;
|
|
#endif
|
|
if(upper==stratum)bBedrockHandledByTransition=true;
|
|
if(lower==stratum)bCeilingHandledByTransition=true;
|
|
const auto* tileQueries=ActivePassageTileQueries;
|
|
const std::int64_t xMicrometres=static_cast<std::int64_t>(std::llround(
|
|
static_cast<double>(X)*Owner->CoreVoxelSizeMicrons));
|
|
const std::int64_t yMicrometres=static_cast<std::int64_t>(std::llround(
|
|
static_cast<double>(Y)*Owner->CoreVoxelSizeMicrons));
|
|
const bool skipPassageFade=tileQueries&&tileQueries->matches(*this)
|
|
&&!tileQueries->mayContainTransitionPassageFade();
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
if(skipPassageFade)++ThreadClassificationStats().TransitionPassageTileSkips;
|
|
#endif
|
|
const auto resolvedBands=ResolvedTransitionBandsAtXY(*upper,*lower,
|
|
xMicrometres,yMicrometres,skipPassageFade);
|
|
// Treat the two authored bands as one continuous solid interval. Its
|
|
// exterior faces remain the lower ceiling bottom and upper bedrock top;
|
|
// measuring signed distance from those faces also clears native generator
|
|
// density on both air sides of the displaced interface.
|
|
const std::int64_t solidBottom=std::min(
|
|
resolvedBands.upperBedrockBottomMicrometres,
|
|
resolvedBands.lowerCeilingBottomMicrometres);
|
|
const std::int64_t solidTop=std::max(
|
|
resolvedBands.upperBedrockTopMicrometres,
|
|
resolvedBands.lowerCeilingTopMicrometres);
|
|
const double signedDistanceMicrometres=zMicrometres<solidBottom
|
|
?static_cast<double>(solidBottom-zMicrometres)
|
|
:zMicrometres>solidTop
|
|
?static_cast<double>(zMicrometres-solidTop)
|
|
:-static_cast<double>(std::min(zMicrometres-solidBottom,
|
|
solidTop-zMicrometres));
|
|
const std::int64_t seam=upper->bedrock.bottomMicrometres;
|
|
const bool floatingIslandAirGap=
|
|
upper->lowerInterfaceCapabilities.preserveUpperBedrockAirGap
|
|
&&resolvedBands.upperBedrockTopMicrometres<seam
|
|
&&zMicrometres>=resolvedBands.upperBedrockTopMicrometres
|
|
&&zMicrometres<seam;
|
|
// A native solid may continue beyond either authored face. Keep that
|
|
// connected rock; the FI gap is the explicit exception that must stay open.
|
|
if(signedDistanceMicrometres>0.0&&density<0.0f&&!floatingIslandAirGap)
|
|
return false;
|
|
density=static_cast<float>(signedDistanceMicrometres
|
|
/std::max<std::uint32_t>(1,Owner->CoreVoxelSizeMicrons));
|
|
return true;
|
|
};
|
|
|
|
const auto* lower=FindResolvedLayoutStratumByOrder(stratum->order+1);
|
|
const auto* upper=stratum->order>0?FindResolvedLayoutStratumByOrder(stratum->order-1):nullptr;
|
|
if(applyInterface(stratum,lower)||applyInterface(upper,stratum))return density;
|
|
const auto* band=(!bCeilingHandledByTransition&&solidAt(stratum->ceiling))
|
|
?&stratum->ceiling:(!bBedrockHandledByTransition&&solidAt(stratum->bedrock))
|
|
?&stratum->bedrock:nullptr;
|
|
if(!band)return density;
|
|
return -std::max(0.05f,static_cast<float>(band->solidDensityQ16)/65536.0f);
|
|
}
|
|
|
|
float ApplyResolvedStratePassageCarves(float density,float X,float Y,float Z) const
|
|
{
|
|
if(!ResolvedLayout||ResolvedLayout->passages.empty()||!std::isfinite(X)
|
|
||!std::isfinite(Y)||!std::isfinite(Z))return density;
|
|
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
|
const double qPerMetre=Q*1000000.0/
|
|
std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres);
|
|
const double qx=static_cast<double>(X)*Q,qy=static_cast<double>(Y)*Q,
|
|
qz=static_cast<double>(Z)*Q;
|
|
const auto* passageTileQueries=ActivePassageTileQueries;
|
|
const bool passageTileScopeMatches=passageTileQueries
|
|
&&passageTileQueries->matches(*this);
|
|
bool queryNearUpperMouthSupport=false;
|
|
for(const auto& passage:ResolvedLayout->passages)
|
|
{
|
|
const double dx=qx-static_cast<double>(passage.upperMouthSubvoxels[0]);
|
|
const double dy=qy-static_cast<double>(passage.upperMouthSubvoxels[1]);
|
|
const double dz=qz-static_cast<double>(passage.upperMouthSubvoxels[2]);
|
|
const double reach=15.0*qPerMetre;
|
|
if(dx*dx+dy*dy<=reach*reach&&std::abs(dz)<=8.0*qPerMetre)
|
|
{queryNearUpperMouthSupport=true;break;}
|
|
}
|
|
if(passageTileScopeMatches&&density>=0.0f
|
|
&&!passageTileQueries->hasRouteFeatureOverlap()
|
|
&&!passageTileQueries->hasColumnZOverlap()
|
|
&&!passageTileQueries->hasAnyMouthSupport()
|
|
&&!queryNearUpperMouthSupport)return density;
|
|
const double floorThicknessVoxels=qPerMetre/Q;
|
|
const double playerCenterAboveFloorQ=1.13*qPerMetre;
|
|
float composed=density;
|
|
const auto composeRouteMouthSupport=[&](std::int32_t strateIndex,
|
|
const std::array<std::int64_t,3>& mouth,bool departure)
|
|
{
|
|
// Surface-route treads may reinforce native solid terrain only.
|
|
if(!(density<0.0f))return;
|
|
const double mouthDx=qx-static_cast<double>(mouth[0]);
|
|
const double mouthDy=qy-static_cast<double>(mouth[1]);
|
|
const double routeSupportReach=13.0*qPerMetre;
|
|
if(mouthDx*mouthDx+mouthDy*mouthDy>routeSupportReach*routeSupportReach)return;
|
|
const auto stratum=std::find_if(ResolvedLayout->strates.begin(),
|
|
ResolvedLayout->strates.end(),[strateIndex](const auto& value)
|
|
{return value.strateIndex==strateIndex;});
|
|
if(stratum==ResolvedLayout->strates.end())return;
|
|
std::array<const VoxelForgeCore::ResolvedLayoutAnchorV2*,5> route{};
|
|
std::size_t routeCount=0;
|
|
for(const auto& anchor:stratum->anchors)
|
|
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
|
{
|
|
if(departure)
|
|
{
|
|
if(routeCount<route.size())route[routeCount++]=&anchor;
|
|
else
|
|
{
|
|
std::move(route.begin()+1,route.end(),route.begin());
|
|
route.back()=&anchor;
|
|
}
|
|
}
|
|
else if(routeCount<route.size())route[routeCount++]=&anchor;
|
|
}
|
|
if(routeCount<2)return;
|
|
const std::size_t lastSegment=routeCount-1;
|
|
constexpr double MouthSupportBlendMetres=0.45;
|
|
const double mouthBlendWidth=MouthSupportBlendMetres*qPerMetre;
|
|
double mouthSupportDistance=std::numeric_limits<double>::infinity();
|
|
for(std::size_t index=0;index<lastSegment;++index)
|
|
{
|
|
const auto& a=*route[index];const auto& b=*route[index+1];
|
|
const double dx=static_cast<double>(b.values[0]-a.values[0]);
|
|
const double dy=static_cast<double>(b.values[1]-a.values[1]);
|
|
const double lengthSq=dx*dx+dy*dy;
|
|
const double t=lengthSq>0.0?std::clamp(((qx-a.values[0])*dx
|
|
+(qy-a.values[1])*dy)/lengthSq,0.0,1.0):0.0;
|
|
const double centerX=a.values[0]+t*dx,centerY=a.values[1]+t*dy;
|
|
const double centerZ=a.values[2]+t*(static_cast<double>(b.values[2])-a.values[2]);
|
|
const double routeRadius=a.values.size()>=4
|
|
?a.values[3]+t*(static_cast<double>(b.values[3])-a.values[3]):0.0;
|
|
const double halfWidth=std::clamp(routeRadius,
|
|
0.42*qPerMetre,0.56*qPerMetre);
|
|
const double horizontalX=qx-centerX,horizontalY=qy-centerY;
|
|
const double horizontalDistance=DeterministicSqrt(
|
|
horizontalX*horizontalX+horizontalY*horizontalY);
|
|
const double floorQ=centerZ-(VoxelForgeCore::WorldAssemblyEvaluatorVersion>=184
|
|
?playerCenterAboveFloorQ+0.25*qPerMetre:1.20*qPerMetre);
|
|
const double sideDistance=horizontalDistance-halfWidth;
|
|
const double topDistance=qz-floorQ;
|
|
const double bottomDistance=floorQ-qPerMetre-qz;
|
|
const double verticalDistance=std::max(topDistance,bottomDistance);
|
|
if(sideDistance>mouthBlendWidth||verticalDistance>mouthBlendWidth)continue;
|
|
const double outside=DeterministicSqrt(
|
|
std::max(sideDistance,0.0)*std::max(sideDistance,0.0)
|
|
+std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0));
|
|
const double inside=std::min(std::max(sideDistance,verticalDistance),0.0);
|
|
mouthSupportDistance=std::min(mouthSupportDistance,outside+inside);
|
|
}
|
|
if(std::isfinite(mouthSupportDistance))
|
|
composed=SmoothMinDistance(composed,
|
|
static_cast<float>(mouthSupportDistance/Q),
|
|
static_cast<float>(mouthBlendWidth/Q));
|
|
};
|
|
// The connected passage tread continues the surface route into the tunnel.
|
|
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
|
{
|
|
const auto& passage=ResolvedLayout->passages[passageIndex];
|
|
if(passage.controlPoints.size()<2)continue;
|
|
const auto supportStratum=[&](std::int32_t strateIndex)
|
|
->const VoxelForgeCore::ResolvedLayoutStratumV2*
|
|
{
|
|
const auto it=std::find_if(ResolvedLayout->strates.begin(),
|
|
ResolvedLayout->strates.end(),[strateIndex](const auto& stratum)
|
|
{return stratum.strateIndex==strateIndex;});
|
|
return it==ResolvedLayout->strates.end()?nullptr:&*it;
|
|
};
|
|
const auto* supportUpper=supportStratum(passage.upperStrateIndex);
|
|
const auto* supportLower=supportStratum(passage.lowerStrateIndex);
|
|
const bool floatingIslandPassage=supportUpper
|
|
&&supportUpper->passageCapabilities.hasFloatingIslandLanding;
|
|
const bool surfaceToTunnelSupport=supportUpper&&supportLower
|
|
&&supportUpper->lowerInterfaceCapabilities.surfaceToTunnelLandingSupport;
|
|
const double firstFloorQ=passage.controlPoints.front().positionSubvoxels[2]
|
|
-passage.controlPoints.front().radiusSubvoxels+playerCenterAboveFloorQ;
|
|
const double lastFloorQ=passage.controlPoints.back().positionSubvoxels[2]
|
|
-passage.controlPoints.back().radiusSubvoxels+playerCenterAboveFloorQ;
|
|
for(const auto& crossing:PassageVoidCrossings)
|
|
{
|
|
if(!crossing.needsColumn||crossing.passageIndex!=passageIndex
|
|
||qz>crossing.topZSubvoxels||qz<crossing.bottomZSubvoxels)continue;
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().ColumnZOverlapCalls;
|
|
#endif
|
|
double columnDistance;
|
|
const bool tileProvesColumnExterior=passageTileScopeMatches
|
|
&&!ActivePassageTileQueries->hasFeature(passageIndex,true);
|
|
if(tileProvesColumnExterior)
|
|
{
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().ColumnGuardCalls;
|
|
#endif
|
|
if(composed<0.0f)
|
|
{
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().ColumnGuardNonpositiveSkips;
|
|
#endif
|
|
continue;
|
|
}
|
|
const double dx=qx-crossing.centerXSubvoxels;
|
|
const double dy=qy-crossing.centerYSubvoxels;
|
|
const double horizontalSquared=dx*dx+dy*dy;
|
|
const double maximumRadius=static_cast<double>(crossing.maximumRadiusSubvoxels);
|
|
const double exteriorLimit=maximumRadius
|
|
+static_cast<double>(composed)*Q+1.0e-4;
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
if(horizontalSquared>exteriorLimit*exteriorLimit)
|
|
{
|
|
++ThreadClassificationStats().ColumnGuardRadialRejects;
|
|
continue;
|
|
}
|
|
++ThreadClassificationStats().ColumnSignedDistanceCalls;
|
|
#else
|
|
if(horizontalSquared>exteriorLimit*exteriorLimit)continue;
|
|
#endif
|
|
columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z);
|
|
}
|
|
else
|
|
{
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
++ThreadClassificationStats().ColumnSignedDistanceCalls;
|
|
#endif
|
|
columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z);
|
|
}
|
|
composed=std::min(composed,static_cast<float>(columnDistance/Q));
|
|
}
|
|
const bool tilePassageFree=passageTileScopeMatches
|
|
&&!ActivePassageTileQueries->hasFeature(passageIndex,false);
|
|
if(composed>=0.0f&&tilePassageFree)
|
|
{
|
|
continue;
|
|
}
|
|
if(qz>firstFloorQ+2.0*passage.mouthRadiusSubvoxels
|
|
||qz<lastFloorQ-2.0*passage.mouthRadiusSubvoxels)
|
|
{
|
|
continue;
|
|
}
|
|
const auto floorProfile=[&](const auto& point)
|
|
{return point.positionSubvoxels[2]-point.radiusSubvoxels+playerCenterAboveFloorQ;};
|
|
const std::ptrdiff_t lastIndex=static_cast<std::ptrdiff_t>(passage.controlPoints.size())-1;
|
|
const std::ptrdiff_t lowerMouthIndex=std::clamp<std::ptrdiff_t>(
|
|
static_cast<std::ptrdiff_t>(passage.lowerMouthControlIndex),1,lastIndex);
|
|
const auto mainEnd=passage.controlPoints.begin()+lowerMouthIndex+1;
|
|
// The tube radius is measured from its centerline, while floorProfile is
|
|
// one player-center offset away. Search the full vertical radius of the SDF;
|
|
// a narrow point window misses earlier turns of a low-pitch helix.
|
|
const double carveSearchMargin=std::max(passage.mouthRadiusSubvoxels,
|
|
passage.midRadiusSubvoxels)+0.25*qPerMetre;
|
|
const auto carveHigh=std::lower_bound(passage.controlPoints.begin(),mainEnd,
|
|
qz+carveSearchMargin,
|
|
[&](const auto& point,double value){return floorProfile(point)>value;});
|
|
const auto carveLow=std::lower_bound(passage.controlPoints.begin(),mainEnd,
|
|
qz-carveSearchMargin,
|
|
[&](const auto& point,double value){return floorProfile(point)>value;});
|
|
const std::ptrdiff_t carveBegin=std::max<std::ptrdiff_t>(0,
|
|
(carveHigh-passage.controlPoints.begin())-1);
|
|
const std::ptrdiff_t carveEnd=std::min<std::ptrdiff_t>(lowerMouthIndex-1,
|
|
(carveLow-passage.controlPoints.begin())+1);
|
|
const std::ptrdiff_t mouthSearchBegin=std::max<std::ptrdiff_t>(0,lowerMouthIndex-8);
|
|
const std::ptrdiff_t mouthSearchEnd=std::min<std::ptrdiff_t>(lastIndex,
|
|
lowerMouthIndex+8);
|
|
if(density<0.0f&&tilePassageFree
|
|
&&!PassageMayAffectDensity(passageIndex,carveBegin,carveEnd,
|
|
lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,qz,
|
|
-static_cast<double>(composed)*Q+0.24*qPerMetre))
|
|
{
|
|
continue;
|
|
}
|
|
// For a positive source sample, the tube can only change it within relief reach;
|
|
// the supported column tread is within 2.25 m of its centerline. A short-span
|
|
// bridge's positive exterior SDF is not bounded this way, so keep its full query.
|
|
// Pour un échantillon source positif, le tube ne change le champ qu'à portée du relief ;
|
|
// le plancher du pilier reste à 2,25 m de son axe. Le SDF extérieur d'une courte passerelle
|
|
// n'a pas cette borne, donc sa requête complète est conservée.
|
|
const bool canCullPositiveRoute=passageTileScopeMatches&&density>=0.0f
|
|
&&!PassageHasShortSpanSegments(passageIndex);
|
|
if(canCullPositiveRoute&&!PassageMayAffectDensity(passageIndex,carveBegin,carveEnd,
|
|
lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,qz,
|
|
std::max(2.25*qPerMetre,-static_cast<double>(composed)*Q
|
|
+0.24*qPerMetre)))continue;
|
|
double bestSignedDistance=std::numeric_limits<double>::infinity();
|
|
double bestPlayerCapsuleSignedDistance=std::numeric_limits<double>::infinity();
|
|
double bestT=0.0;
|
|
std::ptrdiff_t bestIndex=0;
|
|
int bestScanPhase=std::numeric_limits<int>::max();
|
|
std::ptrdiff_t bestScanIndex=std::numeric_limits<std::ptrdiff_t>::max();
|
|
std::ptrdiff_t bestSupportSegmentIndex=0;
|
|
bool hasSupportFloor=false;
|
|
bool bestSupportIsMouthTread=false;
|
|
double bestSupportMouthBlend=1.0;
|
|
double bestSupportDistanceSquared=std::numeric_limits<double>::infinity();
|
|
double bestSupportDensity=std::numeric_limits<double>::infinity();
|
|
double bestSupportNativeBlend=0.0;
|
|
double bestSupportFloorQ=0.0;
|
|
bool bestSupportUsesSignedDistance=false;
|
|
std::ptrdiff_t selectedSupportSegmentIndex=-1;
|
|
const double lowerMouthDx=qx-passage.lowerMouthSubvoxels[0];
|
|
const double lowerMouthDy=qy-passage.lowerMouthSubvoxels[1];
|
|
const double lowerMouthDz=qz-passage.lowerMouthSubvoxels[2];
|
|
const double lowerMouthDistance=DeterministicSqrt(
|
|
lowerMouthDx*lowerMouthDx+lowerMouthDy*lowerMouthDy
|
|
+lowerMouthDz*lowerMouthDz)/qPerMetre;
|
|
const double lowerMouthT=std::clamp((20.0-lowerMouthDistance)/12.0,0.0,1.0);
|
|
const double lowerMouthBlend=lowerMouthT*lowerMouthT
|
|
*(3.0-2.0*lowerMouthT);
|
|
const auto considerCarveSegment=[&](std::size_t segmentIndex,int scanPhase)
|
|
{
|
|
const std::ptrdiff_t index=static_cast<std::ptrdiff_t>(segmentIndex);
|
|
const auto& a=passage.controlPoints[static_cast<std::size_t>(index)];
|
|
const auto& b=passage.controlPoints[static_cast<std::size_t>(index+1)];
|
|
const double ax=static_cast<double>(a.positionSubvoxels[0]);
|
|
const double ay=static_cast<double>(a.positionSubvoxels[1]);
|
|
const double az=static_cast<double>(a.positionSubvoxels[2]);
|
|
const double dx=static_cast<double>(b.positionSubvoxels[0])-ax;
|
|
const double dy=static_cast<double>(b.positionSubvoxels[1])-ay;
|
|
const double dz=static_cast<double>(b.positionSubvoxels[2])-az;
|
|
const double lengthSq=dx*dx+dy*dy+dz*dz;
|
|
if(lengthSq<=0.0)return;
|
|
const double t=std::clamp(((qx-ax)*dx+(qy-ay)*dy+(qz-az)*dz)/lengthSq,0.0,1.0);
|
|
if(!passageTileScopeMatches)
|
|
{
|
|
const double horizontalLengthSq=dx*dx+dy*dy;
|
|
const double playerT=horizontalLengthSq>0.0
|
|
?std::clamp(((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):t;
|
|
const double playerDx=qx-(ax+playerT*dx);
|
|
const double playerDy=qy-(ay+playerT*dy);
|
|
const double playerRadius=static_cast<double>(a.radiusSubvoxels)
|
|
+playerT*(b.radiusSubvoxels-a.radiusSubvoxels);
|
|
const double playerCenterZ=az+playerT*dz-playerRadius+playerCenterAboveFloorQ;
|
|
const double playerVertical=std::max(0.0,
|
|
std::abs(qz-playerCenterZ)-0.54*qPerMetre);
|
|
bestPlayerCapsuleSignedDistance=std::min(bestPlayerCapsuleSignedDistance,
|
|
DeterministicSqrt(playerDx*playerDx+playerDy*playerDy
|
|
+playerVertical*playerVertical)-0.34*qPerMetre);
|
|
}
|
|
double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz;
|
|
const bool smoothCarveJoin=index>2&&index+12<lastIndex
|
|
&&DeterministicSqrt(lengthSq)<=8.0*qPerMetre
|
|
&&!IsPassageShortSpanSegment(passageIndex,segmentIndex-1)
|
|
&&!IsPassageShortSpanSegment(passageIndex,segmentIndex)
|
|
&&!IsPassageShortSpanSegment(passageIndex,segmentIndex+1);
|
|
if(smoothCarveJoin)
|
|
{
|
|
const auto& p0=passage.controlPoints[static_cast<std::size_t>(index-1)];
|
|
const auto& p3=passage.controlPoints[static_cast<std::size_t>(index+2)];
|
|
constexpr double SmoothJoinWeight=1.0;
|
|
const double t2=t*t,t3=t2*t;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const double v0=static_cast<double>(p0.positionSubvoxels[axis]);
|
|
const double v1=static_cast<double>(a.positionSubvoxels[axis]);
|
|
const double v2=static_cast<double>(b.positionSubvoxels[axis]);
|
|
const double v3=static_cast<double>(p3.positionSubvoxels[axis]);
|
|
const double curve=0.5*((2.0*v1)+(-v0+v2)*t
|
|
+(2.0*v0-5.0*v1+4.0*v2-v3)*t2
|
|
+(-v0+3.0*v1-3.0*v2+v3)*t3);
|
|
const double line=axis==0?cx:axis==1?cy:cz;
|
|
const double blended=line+(curve-line)*SmoothJoinWeight;
|
|
if(axis==0)cx=blended;
|
|
else if(axis==1)cy=blended;
|
|
else cz=blended;
|
|
}
|
|
}
|
|
const double ex=qx-cx,ey=qy-cy,ez=qz-cz;
|
|
const double radius=static_cast<double>(a.radiusSubvoxels)
|
|
+t*(b.radiusSubvoxels-a.radiusSubvoxels);
|
|
const double signedDistance=DeterministicSqrt(ex*ex+ey*ey+ez*ez)-radius;
|
|
if(signedDistance<bestSignedDistance
|
|
||(signedDistance==bestSignedDistance
|
|
&&(scanPhase<bestScanPhase
|
|
||(scanPhase==bestScanPhase&&index<bestScanIndex))))
|
|
{
|
|
bestSignedDistance=signedDistance;bestT=t;bestIndex=index;
|
|
bestScanPhase=scanPhase;bestScanIndex=index;
|
|
}
|
|
};
|
|
// Alternate styles can bend locally at either mouth, so floorProfile is
|
|
// not monotone over those short endpoint runs. Keep the binary search for
|
|
// the long descent and inspect both endpoint neighborhoods directly.
|
|
// The route-bound landing can rise and fall after the lower mouth. Scan
|
|
// that short tail directly; binary search is only valid on the descent.
|
|
if(passageTileScopeMatches)
|
|
{
|
|
VisitNearestPassageSegmentRange(passageIndex,carveBegin,carveEnd,qx,qy,qz,
|
|
0,bestSignedDistance,considerCarveSegment);
|
|
VisitNearestPassageSegmentRange(passageIndex,0,
|
|
std::min<std::ptrdiff_t>(8,lowerMouthIndex)-1,qx,qy,qz,
|
|
1,bestSignedDistance,considerCarveSegment);
|
|
VisitNearestPassageSegmentRange(passageIndex,mouthSearchBegin,mouthSearchEnd-1,
|
|
qx,qy,qz,2,bestSignedDistance,considerCarveSegment);
|
|
VisitNearestPassageSegmentRange(passageIndex,lowerMouthIndex,lastIndex-1,
|
|
qx,qy,qz,3,bestSignedDistance,considerCarveSegment);
|
|
}
|
|
else
|
|
{
|
|
for(std::ptrdiff_t index=carveBegin;index<=carveEnd;++index)
|
|
considerCarveSegment(static_cast<std::size_t>(index),0);
|
|
for(std::ptrdiff_t index=0;index<std::min<std::ptrdiff_t>(8,lowerMouthIndex);++index)
|
|
considerCarveSegment(static_cast<std::size_t>(index),1);
|
|
for(std::ptrdiff_t index=mouthSearchBegin;index<mouthSearchEnd;++index)
|
|
considerCarveSegment(static_cast<std::size_t>(index),2);
|
|
for(std::ptrdiff_t index=lowerMouthIndex;index<lastIndex;++index)
|
|
considerCarveSegment(static_cast<std::size_t>(index),3);
|
|
}
|
|
if(!std::isfinite(bestSignedDistance))continue;
|
|
const auto& nearestA=passage.controlPoints[static_cast<std::size_t>(bestIndex)];
|
|
const auto& nearestB=passage.controlPoints[static_cast<std::size_t>(bestIndex+1)];
|
|
const double nearestRadius=nearestA.radiusSubvoxels+bestT*(
|
|
static_cast<double>(nearestB.radiusSubvoxels)-nearestA.radiusSubvoxels);
|
|
const double distanceToCenter=std::max(1.0e-9,bestSignedDistance+nearestRadius);
|
|
const double verticalWallAlignment=distanceToCenter>1.0e-9
|
|
?std::clamp(-(qz-(nearestA.positionSubvoxels[2]+bestT*(
|
|
static_cast<double>(nearestB.positionSubvoxels[2])-nearestA.positionSubvoxels[2])))
|
|
/distanceToCenter,0.0,1.0):0.0;
|
|
const float relief=VF_FieldPassageWallRelief(static_cast<float>(X),static_cast<float>(Y),
|
|
static_cast<float>(Z),static_cast<std::uint32_t>(Owner?Owner->Seed:0));
|
|
const float mouthSurfaceRelief=VF_FieldSurfaceRelief(static_cast<float>(X)*1.5f,
|
|
static_cast<float>(Y)*1.5f,static_cast<float>(Z)*1.5f,
|
|
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x4D4F5554u);
|
|
const float mouthTerrainRelief=VF_FieldSurfaceRelief(static_cast<float>(X)*1.5f,
|
|
static_cast<float>(Y)*1.5f,static_cast<float>(Z)*1.5f,
|
|
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x4D4F5554u);
|
|
const float mouthTreadFieldRelief=VF_FieldSurfaceRelief(static_cast<float>(X)*0.75f,
|
|
static_cast<float>(Y)*0.75f,static_cast<float>(Z)*0.75f,
|
|
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x54485244u);
|
|
const auto routeSegmentLengthMetres=[&](std::ptrdiff_t routeIndex)
|
|
{
|
|
const auto& before=passage.controlPoints[static_cast<std::size_t>(routeIndex)];
|
|
const auto& after=passage.controlPoints[static_cast<std::size_t>(routeIndex+1)];
|
|
const double dx=static_cast<double>(after.positionSubvoxels[0])
|
|
-before.positionSubvoxels[0];
|
|
const double dy=static_cast<double>(after.positionSubvoxels[1])
|
|
-before.positionSubvoxels[1];
|
|
const double dz=static_cast<double>(after.positionSubvoxels[2])
|
|
-before.positionSubvoxels[2];
|
|
return DeterministicSqrt(dx*dx+dy*dy+dz*dz)/qPerMetre;
|
|
};
|
|
double distanceFromOpenMouth=std::numeric_limits<double>::infinity();
|
|
if(bestIndex<=2)
|
|
{
|
|
distanceFromOpenMouth=bestT*routeSegmentLengthMetres(bestIndex);
|
|
for(std::ptrdiff_t routeIndex=0;routeIndex<bestIndex;++routeIndex)
|
|
distanceFromOpenMouth+=routeSegmentLengthMetres(routeIndex);
|
|
}
|
|
else if(bestIndex>=lowerMouthIndex-2&&bestIndex<=lowerMouthIndex+2)
|
|
{
|
|
if(bestIndex<lowerMouthIndex)
|
|
{
|
|
distanceFromOpenMouth=(1.0-bestT)*routeSegmentLengthMetres(bestIndex);
|
|
for(std::ptrdiff_t routeIndex=bestIndex+1;
|
|
routeIndex<lowerMouthIndex;++routeIndex)
|
|
distanceFromOpenMouth+=routeSegmentLengthMetres(routeIndex);
|
|
}
|
|
else
|
|
{
|
|
distanceFromOpenMouth=bestT*routeSegmentLengthMetres(bestIndex);
|
|
for(std::ptrdiff_t routeIndex=lowerMouthIndex;routeIndex<bestIndex;++routeIndex)
|
|
distanceFromOpenMouth+=routeSegmentLengthMetres(routeIndex);
|
|
}
|
|
}
|
|
double mouthEntryFloorAlignment=0.0;
|
|
double mouthEntryPlanDistance=std::numeric_limits<double>::infinity();
|
|
double mouthEntryFloorQ=0.0;
|
|
bool upperMouthFloorFade=false;
|
|
{
|
|
// The large mouth bore overlaps several inner coils. Its global
|
|
// nearest SDF can belong to a later turn even at the lip, so use the
|
|
// projected entrance path to recover the local floor orientation.
|
|
double prefixMetres=0.0;
|
|
double nearestEntryPlanDistance=std::numeric_limits<double>::infinity();
|
|
double projectedEntryDistance=std::numeric_limits<double>::infinity();
|
|
for(std::ptrdiff_t routeIndex=0;
|
|
routeIndex<std::min<std::ptrdiff_t>(3,lowerMouthIndex);++routeIndex)
|
|
{
|
|
const auto& before=passage.controlPoints[static_cast<std::size_t>(routeIndex)];
|
|
const auto& after=passage.controlPoints[static_cast<std::size_t>(routeIndex+1)];
|
|
const double ax=static_cast<double>(before.positionSubvoxels[0]);
|
|
const double ay=static_cast<double>(before.positionSubvoxels[1]);
|
|
const double az=static_cast<double>(before.positionSubvoxels[2]);
|
|
const double dx=static_cast<double>(after.positionSubvoxels[0])-ax;
|
|
const double dy=static_cast<double>(after.positionSubvoxels[1])-ay;
|
|
const double dz=static_cast<double>(after.positionSubvoxels[2])-az;
|
|
const double horizontalLengthSq=dx*dx+dy*dy;
|
|
const double t=horizontalLengthSq>0.0?std::clamp(
|
|
((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):0.0;
|
|
double projectedX=ax+t*dx,projectedY=ay+t*dy;
|
|
const double horizontalLength=DeterministicSqrt(horizontalLengthSq);
|
|
if(horizontalLength>1.0e-6)
|
|
{
|
|
const double pathRelief=std::clamp(static_cast<double>(
|
|
VF_FieldSurfaceRelief(static_cast<float>(projectedX/qPerMetre*1.5),
|
|
static_cast<float>(projectedY/qPerMetre*1.5),
|
|
static_cast<float>((az+t*dz)/qPerMetre*1.5),
|
|
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x4D4F5554u)),
|
|
-1.0,1.0);
|
|
const double lateralOffset=pathRelief*0.18*qPerMetre;
|
|
projectedX-=dy/horizontalLength*lateralOffset;
|
|
projectedY+=dx/horizontalLength*lateralOffset;
|
|
}
|
|
const double planDx=qx-projectedX,planDy=qy-projectedY;
|
|
const double planDistance=DeterministicSqrt(planDx*planDx+planDy*planDy);
|
|
if(planDistance<nearestEntryPlanDistance)
|
|
{
|
|
nearestEntryPlanDistance=planDistance;
|
|
const double centerZ=az+t*dz;
|
|
const double radius=before.radiusSubvoxels+t*(
|
|
static_cast<double>(after.radiusSubvoxels)-before.radiusSubvoxels);
|
|
mouthEntryFloorQ=centerZ-radius+0.20*qPerMetre;
|
|
const double centerDistance=DeterministicSqrt(planDx*planDx
|
|
+planDy*planDy+(qz-centerZ)*(qz-centerZ));
|
|
mouthEntryFloorAlignment=centerDistance>1.0e-9
|
|
?std::clamp(-(qz-centerZ)/centerDistance,0.0,1.0):0.0;
|
|
projectedEntryDistance=prefixMetres
|
|
+t*routeSegmentLengthMetres(routeIndex);
|
|
}
|
|
prefixMetres+=routeSegmentLengthMetres(routeIndex);
|
|
}
|
|
if(nearestEntryPlanDistance<=2.0*qPerMetre
|
|
&&projectedEntryDistance<=8.0
|
|
&&std::abs(qz-mouthEntryFloorQ)<=4.0*qPerMetre)
|
|
{
|
|
distanceFromOpenMouth=projectedEntryDistance;
|
|
mouthEntryPlanDistance=nearestEntryPlanDistance/qPerMetre;
|
|
upperMouthFloorFade=true;
|
|
}
|
|
}
|
|
double mouthFloorBlend=0.0;
|
|
if(std::isfinite(distanceFromOpenMouth))
|
|
{
|
|
const double fade=std::clamp(distanceFromOpenMouth/11.0,0.0,1.0);
|
|
const double smooth=fade*fade*(3.0-2.0*fade);
|
|
mouthFloorBlend=1.0-smooth;
|
|
}
|
|
const double floorReliefT=std::clamp((verticalWallAlignment-0.55)/0.45,0.0,1.0);
|
|
const double floorReliefWeight=floorReliefT*floorReliefT
|
|
*(3.0-2.0*floorReliefT);
|
|
const double floorReliefOffset=std::clamp(
|
|
static_cast<double>(mouthTerrainRelief),-1.0,1.0)
|
|
*0.20*qPerMetre*mouthFloorBlend*floorReliefWeight
|
|
*(upperMouthFloorFade?1.0:0.0);
|
|
const double wallReliefWeight=0.05+0.95*(1.0-verticalWallAlignment);
|
|
const double roughSignedDistance=bestSignedDistance
|
|
+static_cast<double>(std::clamp(relief,-1.0f,1.0f))*0.21*qPerMetre*wallReliefWeight
|
|
+floorReliefOffset;
|
|
if(IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(bestIndex)))
|
|
{
|
|
const double bridgeDistance=PassageBridgeSignedDistance(passageIndex,
|
|
static_cast<std::size_t>(bestIndex),X,Y,Z);
|
|
composed=std::min(composed,static_cast<float>(bridgeDistance/Q));
|
|
}
|
|
// Leave native crater ground at the lip. The bore opens through the walls
|
|
// immediately, while its floor cut eases in along the passage centerline.
|
|
double mouthCarveFade=1.0;
|
|
if(upperMouthFloorFade)
|
|
{
|
|
constexpr double mouthCarveFadeMetres=8.0;
|
|
mouthCarveFade=std::clamp(distanceFromOpenMouth/mouthCarveFadeMetres,
|
|
0.0,1.0);
|
|
mouthCarveFade=mouthCarveFade*mouthCarveFade*(3.0-2.0*mouthCarveFade);
|
|
}
|
|
const double floorAlignment=std::max(verticalWallAlignment,
|
|
mouthEntryFloorAlignment)*std::max(verticalWallAlignment,
|
|
mouthEntryFloorAlignment);
|
|
double floorBelowWeight=0.0;
|
|
if(upperMouthFloorFade)
|
|
{
|
|
const double belowFloor=(mouthEntryFloorQ-qz)/qPerMetre;
|
|
const double supportRamp=std::clamp(belowFloor/0.15,0.0,1.0);
|
|
const double supportFade=std::clamp((1.0-belowFloor)/0.35,0.0,1.0);
|
|
const double rampSmooth=supportRamp*supportRamp*(3.0-2.0*supportRamp);
|
|
const double fadeSmooth=supportFade*supportFade*(3.0-2.0*supportFade);
|
|
floorBelowWeight=rampSmooth*fadeSmooth;
|
|
}
|
|
// Preserve native terrain only along a capsule-scale worn lane. The
|
|
// former bore-wide alignment feather left a straight platform-width
|
|
// bridge across the FI mouth.
|
|
double mouthWalkLaneT=std::clamp((1.05-mouthEntryPlanDistance)/0.60,0.0,1.0);
|
|
mouthWalkLaneT=mouthWalkLaneT*mouthWalkLaneT*(3.0-2.0*mouthWalkLaneT);
|
|
const double mouthWalkLaneWeight=upperMouthFloorFade?mouthWalkLaneT:0.0;
|
|
const double floorCarveWeight=1.0-(1.0-mouthCarveFade)
|
|
*floorAlignment*floorAlignment*floorBelowWeight*mouthWalkLaneWeight;
|
|
const float fullyCarvedDensity=std::max(composed,
|
|
static_cast<float>(-roughSignedDistance/Q));
|
|
composed=std::lerp(composed,fullyCarvedDensity,
|
|
static_cast<float>(floorCarveWeight));
|
|
const auto considerSupportSegment=[&](std::ptrdiff_t index)
|
|
{
|
|
const auto& segmentA=passage.controlPoints[static_cast<std::size_t>(index)];
|
|
const auto& segmentB=passage.controlPoints[static_cast<std::size_t>(index+1)];
|
|
const double dx=static_cast<double>(segmentB.positionSubvoxels[0])-segmentA.positionSubvoxels[0];
|
|
const double dy=static_cast<double>(segmentB.positionSubvoxels[1])-segmentA.positionSubvoxels[1];
|
|
const double dz=static_cast<double>(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2];
|
|
const double radiusA=segmentA.radiusSubvoxels;
|
|
const double radiusB=segmentB.radiusSubvoxels;
|
|
const bool entryMouthShoulder=index<=2;
|
|
// Keep the tread within the probe band and below the capsule's
|
|
// lowest sampled point.
|
|
const bool bridgeDeck=IsPassageShortSpanSegment(passageIndex,
|
|
static_cast<std::size_t>(index))
|
|
||IsPassageColumnSegment(passageIndex,static_cast<std::size_t>(index));
|
|
// Keep a small capsule margin at regular treads and restore most of
|
|
// the nominal height smoothly as a tread approaches its landing.
|
|
double floorOffset=bridgeDeck
|
|
?PassageBridgeDeckOffsetMetres(passageIndex,static_cast<std::size_t>(index))
|
|
:std::lerp(0.15,0.20,lowerMouthBlend);
|
|
const double horizontalLengthSq=dx*dx+dy*dy;
|
|
const bool overlapSupportJoin=index+12<lastIndex
|
|
&&!IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(index))
|
|
&&(index==0||(!IsPassageShortSpanSegment(passageIndex,
|
|
static_cast<std::size_t>(index-1))));
|
|
const bool smoothSupportJoin=index>0&&overlapSupportJoin
|
|
&&!IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(index+1));
|
|
const double projection=horizontalLengthSq>0.0
|
|
?((qx-segmentA.positionSubvoxels[0])*dx
|
|
+(qy-segmentA.positionSubvoxels[1])*dy)/horizontalLengthSq:bestT;
|
|
// Extend tread segments by a fixed distance through route knots. A
|
|
// fractional extension made the support width change with segment length.
|
|
const double overlapFraction=overlapSupportJoin&&horizontalLengthSq>0.0
|
|
?std::min(0.5,2.0*qPerMetre/DeterministicSqrt(horizontalLengthSq)):0.0;
|
|
const double supportT=horizontalLengthSq>0.0
|
|
?std::clamp(projection,-overlapFraction,1.0+overlapFraction):bestT;
|
|
double centerX=segmentA.positionSubvoxels[0]+supportT*dx;
|
|
double centerY=segmentA.positionSubvoxels[1]+supportT*dy;
|
|
double centerZ=segmentA.positionSubvoxels[2]+supportT*(
|
|
static_cast<double>(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]);
|
|
double radius=segmentA.radiusSubvoxels+supportT*(
|
|
static_cast<double>(segmentB.radiusSubvoxels)-segmentA.radiusSubvoxels);
|
|
if(smoothSupportJoin)
|
|
{
|
|
const auto& p0=passage.controlPoints[static_cast<std::size_t>(index-1)];
|
|
const auto& p3=passage.controlPoints[static_cast<std::size_t>(index+2)];
|
|
const double t2=supportT*supportT,t3=t2*supportT;
|
|
const auto interpolate=[&](double v0,double v1,double v2,double v3)
|
|
{
|
|
return 0.5*((2.0*v1)+(-v0+v2)*supportT
|
|
+(2.0*v0-5.0*v1+4.0*v2-v3)*t2
|
|
+(-v0+3.0*v1-3.0*v2+v3)*t3);
|
|
};
|
|
constexpr double SmoothSupportHeightWeight=0.25;
|
|
centerZ+=(interpolate(p0.positionSubvoxels[2],segmentA.positionSubvoxels[2],
|
|
segmentB.positionSubvoxels[2],p3.positionSubvoxels[2])-centerZ)
|
|
*SmoothSupportHeightWeight;
|
|
radius+=(interpolate(p0.radiusSubvoxels,segmentA.radiusSubvoxels,
|
|
segmentB.radiusSubvoxels,p3.radiusSubvoxels)-radius)
|
|
*SmoothSupportHeightWeight;
|
|
}
|
|
double treadRelief=0.0;
|
|
if(entryMouthShoulder
|
|
||(index>=lowerMouthIndex-2&&index<=lowerMouthIndex+2))
|
|
{
|
|
const double horizontalLength=DeterministicSqrt(horizontalLengthSq);
|
|
if(horizontalLength>1.0e-6)
|
|
{
|
|
const double pathRelief=std::clamp(static_cast<double>(
|
|
VF_FieldSurfaceRelief(static_cast<float>(centerX/qPerMetre*1.5),
|
|
static_cast<float>(centerY/qPerMetre*1.5),
|
|
static_cast<float>(centerZ/qPerMetre*1.5),
|
|
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x4D4F5554u)),
|
|
-1.0,1.0);
|
|
treadRelief=pathRelief;
|
|
const double lateralOffset=pathRelief*0.18*qPerMetre;
|
|
centerX-=dy/horizontalLength*lateralOffset;
|
|
centerY+=dx/horizontalLength*lateralOffset;
|
|
}
|
|
}
|
|
const double horizontalX=qx-centerX,horizontalY=qy-centerY;
|
|
const double horizontalDistance=DeterministicSqrt(horizontalX*horizontalX
|
|
+horizontalY*horizontalY);
|
|
const double supportPlayerCenterZ=centerZ-radius+playerCenterAboveFloorQ;
|
|
const double supportQueryDepth=supportPlayerCenterZ-qz;
|
|
// Support belongs just below the avatar's foot plane. Queries within
|
|
// the capsule body, or a neighbouring coil's overlapping bore, must
|
|
// not turn that tread into a ceiling or mid-air shelf.
|
|
// Sparse authored routes use the full signed distance around the top;
|
|
// dense routes retain the legacy one-sided tread density.
|
|
const double supportSegmentLength=DeterministicSqrt(
|
|
horizontalLengthSq+dz*dz)/qPerMetre;
|
|
const double sparseRouteThreshold=VoxelForgeCore::WorldAssemblyEvaluatorVersion>=188
|
|
&&floatingIslandPassage&&passage.style==0?1.0:1.5;
|
|
const bool sparseRouteSupport=supportSegmentLength>sparseRouteThreshold;
|
|
const double minimumSupportQueryDepth=index==selectedSupportSegmentIndex
|
|
&&sparseRouteSupport?0.65:0.93;
|
|
if(supportQueryDepth<minimumSupportQueryDepth*qPerMetre
|
|
||supportQueryDepth>1.30*qPerMetre)return;
|
|
const bool fallbackSupport=qz>=supportPlayerCenterZ-1.50*qPerMetre;
|
|
const double supportThicknessVoxels=fallbackSupport
|
|
?qPerMetre/Q:0.5*qPerMetre/Q;
|
|
// Keep the supported ramp to a comfortable walking width. Extending a
|
|
// full bore-radius shelf across overlapping helix coils can block the
|
|
// capsule on the neighbouring turn.
|
|
const bool lowerMouthBodyOverlap=index>=lowerMouthIndex-2
|
|
&&supportQueryDepth<0.98*qPerMetre;
|
|
// Keep a narrow walk core, but place its top on the native surface
|
|
// so the support remains a worn ramp instead of a route-height slab.
|
|
const bool mouthTread=entryMouthShoulder
|
|
&&(!floatingIslandPassage||passage.style==0);
|
|
double distanceFromMouthMetres=std::numeric_limits<double>::infinity();
|
|
if(entryMouthShoulder)
|
|
{
|
|
double prefixMetres=0.0;
|
|
for(std::ptrdiff_t routeIndex=0;routeIndex<index;++routeIndex)
|
|
{
|
|
const auto& before=passage.controlPoints[
|
|
static_cast<std::size_t>(routeIndex)];
|
|
const auto& after=passage.controlPoints[
|
|
static_cast<std::size_t>(routeIndex+1)];
|
|
const double dx=static_cast<double>(after.positionSubvoxels[0])
|
|
-before.positionSubvoxels[0];
|
|
const double dy=static_cast<double>(after.positionSubvoxels[1])
|
|
-before.positionSubvoxels[1];
|
|
const double dz=static_cast<double>(after.positionSubvoxels[2])
|
|
-before.positionSubvoxels[2];
|
|
prefixMetres+=DeterministicSqrt(dx*dx+dy*dy+dz*dz)/qPerMetre;
|
|
}
|
|
distanceFromMouthMetres=std::max(0.0,
|
|
prefixMetres+supportT*supportSegmentLength);
|
|
}
|
|
else if(lowerMouthBodyOverlap)
|
|
{
|
|
double suffixMetres=0.0;
|
|
for(std::ptrdiff_t routeIndex=index+1;
|
|
routeIndex<lowerMouthIndex;++routeIndex)
|
|
{
|
|
const auto& before=passage.controlPoints[
|
|
static_cast<std::size_t>(routeIndex)];
|
|
const auto& after=passage.controlPoints[
|
|
static_cast<std::size_t>(routeIndex+1)];
|
|
const double dx=static_cast<double>(after.positionSubvoxels[0])
|
|
-before.positionSubvoxels[0];
|
|
const double dy=static_cast<double>(after.positionSubvoxels[1])
|
|
-before.positionSubvoxels[1];
|
|
const double dz=static_cast<double>(after.positionSubvoxels[2])
|
|
-before.positionSubvoxels[2];
|
|
suffixMetres+=DeterministicSqrt(dx*dx+dy*dy+dz*dz)/qPerMetre;
|
|
}
|
|
distanceFromMouthMetres=std::max(0.0,
|
|
suffixMetres+(1.0-supportT)*supportSegmentLength);
|
|
}
|
|
double mouthReliefT=std::clamp(distanceFromMouthMetres/8.0,0.0,1.0);
|
|
mouthReliefT=mouthReliefT*mouthReliefT*(3.0-2.0*mouthReliefT);
|
|
double mouthSupportT=std::clamp((distanceFromMouthMetres-0.25)/4.0,0.0,1.0);
|
|
mouthSupportT=mouthSupportT*mouthSupportT*(3.0-2.0*mouthSupportT);
|
|
const double mouthSupportWidth=std::lerp(0.82,1.00,mouthSupportT);
|
|
const double supportHalfWidthMetres=mouthTread?mouthSupportWidth
|
|
:lowerMouthBodyOverlap?0.90:1.25;
|
|
double supportHalfWidth=std::min(radius,
|
|
supportHalfWidthMetres*qPerMetre);
|
|
const double shoulderWarp=mouthTread
|
|
?std::clamp(static_cast<double>(mouthSurfaceRelief),-1.0,1.0)
|
|
*0.12*qPerMetre:0.0;
|
|
supportHalfWidth=mouthTread
|
|
?std::clamp(supportHalfWidth+shoulderWarp,
|
|
std::lerp(0.52,0.62,mouthSupportT)*qPerMetre,
|
|
std::lerp(0.92,1.10,mouthSupportT)*qPerMetre)
|
|
:std::max(0.0,supportHalfWidth+shoulderWarp);
|
|
// Keep the walk core within the player footprint, then ease the outer
|
|
// edge into native ground over a long enough shoulder to avoid a berm.
|
|
const double supportEdgeCoreMetres=mouthTread?0.08
|
|
:entryMouthShoulder?0.60:0.90;
|
|
const double supportEdgeBlendMetres=std::min(
|
|
mouthTread?1.40:entryMouthShoulder?0.85:0.35,
|
|
std::max(0.0,supportHalfWidth/qPerMetre-supportEdgeCoreMetres));
|
|
const double supportEdgeT=supportEdgeBlendMetres>1.0e-6
|
|
?std::clamp((horizontalDistance
|
|
-(supportHalfWidth-supportEdgeBlendMetres*qPerMetre))
|
|
/(supportEdgeBlendMetres*qPerMetre),0.0,1.0):0.0;
|
|
const double supportEdgeSmooth=supportEdgeT*supportEdgeT
|
|
*(3.0-2.0*supportEdgeT);
|
|
const double mouthSupportThicknessVoxels=std::lerp(
|
|
0.35*supportThicknessVoxels,supportThicknessVoxels,mouthSupportT);
|
|
const double localSupportThicknessVoxels=entryMouthShoulder
|
|
?mouthSupportThicknessVoxels*(1.0-0.72*supportEdgeSmooth)
|
|
:supportThicknessVoxels;
|
|
double mouthSupportBlend=1.0;
|
|
if(mouthTread&&std::isfinite(distanceFromMouthMetres))
|
|
{
|
|
const double mouthBlendT=std::clamp(
|
|
distanceFromMouthMetres/0.75,0.0,1.0);
|
|
mouthSupportBlend=0.75+0.25*mouthBlendT*mouthBlendT
|
|
*(3.0-2.0*mouthBlendT);
|
|
}
|
|
const double supportEdgeDepth=mouthTread?0.25:0.35;
|
|
const double mouthFloorOffset=mouthTread
|
|
?std::lerp(0.15,0.19,lowerMouthBlend):floorOffset;
|
|
const double baseFloorQ=centerZ-radius+mouthFloorOffset*qPerMetre
|
|
-supportEdgeSmooth*supportEdgeDepth*qPerMetre;
|
|
const double nativeSurfaceQ=static_cast<double>(qz)
|
|
-static_cast<double>(density)*Q;
|
|
// Keep support near the route floor; visible mouth preservation below
|
|
// blends the source field directly instead of extending this SDF top.
|
|
const double nativeFieldOffset=std::clamp(nativeSurfaceQ-baseFloorQ,
|
|
-0.80*qPerMetre,0.80*qPerMetre);
|
|
const double nativeSurfaceBlend=mouthTread?1.0:1.0-mouthReliefT;
|
|
const double nativeTreadOffset=std::clamp(
|
|
nativeFieldOffset*nativeSurfaceBlend+floorReliefOffset
|
|
-std::abs(treadRelief)*0.08*qPerMetre
|
|
*(1.0-0.25*mouthReliefT),
|
|
-0.80*qPerMetre,0.80*qPerMetre);
|
|
const double floorQ=baseFloorQ
|
|
+(mouthTread?std::clamp(nativeTreadOffset
|
|
+std::clamp(static_cast<double>(mouthTreadFieldRelief),-1.0,1.0)
|
|
*0.40*qPerMetre,-0.80*qPerMetre,0.0):0.0);
|
|
const double floorVoxels=floorQ/Q;
|
|
const bool signedDistanceSupport=sparseRouteSupport||mouthTread;
|
|
const double supportPositiveRangeVoxels=signedDistanceSupport
|
|
?0.35*qPerMetre/Q:1.0/Q;
|
|
if(Z<floorVoxels-localSupportThicknessVoxels
|
|
||Z>floorVoxels+supportPositiveRangeVoxels)return;
|
|
const double supportQueryHalfWidth=supportHalfWidth
|
|
+(mouthTread?0.45*qPerMetre:0.0);
|
|
if(horizontalDistance>supportQueryHalfWidth)return;
|
|
const double supportDistanceSquared=horizontalDistance*horizontalDistance
|
|
+(Z-floorVoxels)*(Z-floorVoxels);
|
|
if(supportDistanceSquared>=bestSupportDistanceSquared)return;
|
|
bestSupportDistanceSquared=supportDistanceSquared;
|
|
hasSupportFloor=true;
|
|
bestSupportSegmentIndex=index;
|
|
bestSupportIsMouthTread=mouthTread;
|
|
if(signedDistanceSupport)
|
|
{
|
|
// Sparse support profiles need both sides of a signed distance
|
|
// so the sampled zero crossing stays on the tread top.
|
|
const double topDistance=qz-floorQ;
|
|
const double bottomDistance=floorQ-localSupportThicknessVoxels*Q-qz;
|
|
const double verticalDistance=std::max(topDistance,bottomDistance);
|
|
const double sideDistance=horizontalDistance-supportHalfWidth;
|
|
bestSupportDensity=SmoothMaxDistance(
|
|
static_cast<float>(sideDistance/Q),
|
|
static_cast<float>(verticalDistance/Q),
|
|
entryMouthShoulder?0.80f:0.20f);
|
|
}
|
|
else
|
|
{
|
|
bestSupportDensity=std::clamp(Z-floorVoxels,
|
|
-localSupportThicknessVoxels,0.0);
|
|
}
|
|
bestSupportFloorQ=floorQ;
|
|
bestSupportUsesSignedDistance=signedDistanceSupport;
|
|
bestSupportMouthBlend=mouthSupportBlend;
|
|
bestSupportNativeBlend=mouthTread
|
|
?0.85*mouthSupportBlend*(1.0-supportEdgeSmooth):0.0;
|
|
};
|
|
std::ptrdiff_t supportCenterIndex=bestIndex;
|
|
if(bestIndex<lowerMouthIndex)
|
|
{
|
|
// A helix can have multiple centerlines at one XY. Select the tread
|
|
// whose floor is just below this density query, not the closest wall.
|
|
const double supportProfileOffset=surfaceToTunnelSupport?1.13:0.93;
|
|
const double supportProfileTarget=qz+supportProfileOffset*qPerMetre;
|
|
const auto supportProfile=std::lower_bound(passage.controlPoints.begin(),mainEnd,
|
|
supportProfileTarget,[&](const auto& point,double value)
|
|
{return floorProfile(point)>value;});
|
|
const std::ptrdiff_t profileIndex=std::clamp<std::ptrdiff_t>(
|
|
supportProfile-passage.controlPoints.begin(),0,lowerMouthIndex);
|
|
// At floor height the nearest 3D centerline can belong to the next
|
|
// coil below. The descending floor profile identifies the coil that
|
|
// owns this tread, even when it is more than eight control points away.
|
|
supportCenterIndex=profileIndex;
|
|
}
|
|
const std::ptrdiff_t supportBegin=std::max<std::ptrdiff_t>(0,supportCenterIndex-1);
|
|
const std::ptrdiff_t supportEnd=std::min<std::ptrdiff_t>(lastIndex-1,
|
|
supportCenterIndex+1);
|
|
selectedSupportSegmentIndex=supportCenterIndex>0
|
|
?supportCenterIndex-1:supportCenterIndex;
|
|
for(std::ptrdiff_t index=supportBegin;index<=supportEnd;++index)
|
|
considerSupportSegment(index);
|
|
// Keep route treads inside the native rock envelope. Carving may reopen the
|
|
// floor above this point, but a support cannot create a new surface in air.
|
|
const bool passageColumnFloorSupport=hasSupportFloor
|
|
&&IsPassageColumnSegment(passageIndex,
|
|
static_cast<std::size_t>(bestSupportSegmentIndex));
|
|
const bool shortSpanFloorSupport=hasSupportFloor
|
|
&&IsPassageShortSpanSegment(passageIndex,
|
|
static_cast<std::size_t>(bestSupportSegmentIndex));
|
|
const double capsuleClearanceMetres=bestSupportUsesSignedDistance?0.045:0.05;
|
|
const double capsuleQueryZ=bestSupportUsesSignedDistance?bestSupportFloorQ:qz;
|
|
const bool supportCapsuleProfileAvailable=passageIndex<PassageNearestNodes.size()
|
|
&&!PassageNearestNodes[passageIndex].empty();
|
|
const bool capsuleAllowsSupport=(passageTileScopeMatches
|
|
||(bestSupportUsesSignedDistance&&supportCapsuleProfileAvailable))
|
|
?!PassagePlayerCapsuleBlocksSupport(passageIndex,carveBegin,carveEnd,
|
|
lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,
|
|
capsuleQueryZ,qPerMetre,capsuleClearanceMetres)
|
|
:bestPlayerCapsuleSignedDistance>=capsuleClearanceMetres*qPerMetre;
|
|
// Outside bridge and short-span segments, add the tread only where the
|
|
// native field already provides a solid envelope; the capsule check keeps
|
|
// that support below the player's walk space.
|
|
if(hasSupportFloor&&(density<0.0f||passageColumnFloorSupport
|
|
||shortSpanFloorSupport)
|
|
&&(capsuleAllowsSupport||bestSupportDensity>=0.0))
|
|
{
|
|
if(bestSupportIsMouthTread)
|
|
{
|
|
// Preserve the owner's native ground through the shallow mouth
|
|
// carve. Blending back the native field keeps its crater curvature
|
|
// and noise. The capsule test keeps neighbouring coils open.
|
|
const bool nativeRestoreCapsuleClear=density<0.0f
|
|
&&bestSupportNativeBlend>0.0&&capsuleAllowsSupport
|
|
&&!PassagePlayerCapsuleBlocksSupport(passageIndex,carveBegin,
|
|
carveEnd,lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,
|
|
lastIndex,qx,qy,qz,qPerMetre,capsuleClearanceMetres);
|
|
if(nativeRestoreCapsuleClear)
|
|
{
|
|
const double nativeSurfaceDetail=std::clamp(
|
|
static_cast<double>(mouthTerrainRelief),-1.0,1.0)
|
|
*0.18*qPerMetre/Q;
|
|
const float nativeGroundWithRelief=static_cast<float>(density
|
|
-nativeSurfaceDetail);
|
|
composed=std::lerp(composed,nativeGroundWithRelief,
|
|
static_cast<float>(bestSupportNativeBlend));
|
|
}
|
|
}
|
|
else composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
|
}
|
|
}
|
|
// Restore the surface route where the passage cut crossed otherwise solid
|
|
// native ground. The support stays native-solid-only, so it cannot float.
|
|
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
|
{
|
|
const auto& passage=ResolvedLayout->passages[passageIndex];
|
|
if(passage.controlPoints.size()<2)continue;
|
|
const bool tileHasSupport=passageTileScopeMatches;
|
|
if(!tileHasSupport
|
|
||ActivePassageTileQueries->hasMouthSupport(passageIndex))
|
|
composeRouteMouthSupport(passage.upperStrateIndex,
|
|
passage.upperMouthSubvoxels,true);
|
|
}
|
|
return composed;
|
|
}
|
|
|
|
float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const
|
|
{
|
|
const float graphDensity=ApplyResolvedCavernGraph(density,X,Y,Z);
|
|
const auto* bound=FindBoundStratumForManagerZ(Z);
|
|
const bool nativeLattice=bound&&bound->Resolved
|
|
&&VoxelForgeCore::IsNativeLatticeFieldStratum(*bound->Resolved)
|
|
&&bound->Resolved->passageCapabilities.hasNativeLatticeField;
|
|
// The native lattice field is a complete host-rock SDF: it owns both the
|
|
// voids and the surrounding solid. Replacing its air-side distances with
|
|
// the generic stratum transition distance creates a discontinuity at a
|
|
// mouth that crosses a wide transition band. Keep that opt-in field whole;
|
|
// its host-rock value already supplies the ceiling and bedrock.
|
|
const float boundary=nativeLattice?graphDensity
|
|
:ApplyResolvedStrateBoundaryBand(graphDensity,X,Y,Z);
|
|
// The native lattice owns its resolved arrival/departure portals and their
|
|
// supported spine. Reapplying legacy passage carves here can erase the
|
|
// tread immediately above a broad legacy mouth or support column.
|
|
const float composed=nativeLattice?boundary
|
|
:ApplyResolvedStratePassageCarves(boundary,X,Y,Z);
|
|
const float stamped=StampRuntime.empty()?composed:ApplyResolvedSdfStamps(composed,X,Y,Z);
|
|
if(bound&&bound->Kind==FBoundStratumField::EKind::Caves
|
|
&&bound->Resolved&&VoxelForgeCore::HasNativeGraphField(*bound->Resolved)
|
|
&&(VoxelForgeCore::IsNativeCaveFieldStratum(*bound->Resolved)
|
|
?bound->Resolved->passageCapabilities.hasNativeCaveField
|
|
:bound->Resolved->passageCapabilities.hasNativeLatticeField))
|
|
return stamped;
|
|
// The legacy landmark demo retains its compatibility floor cap. Native caves
|
|
// already combine each floor with its wall before the single void carve above.
|
|
return ApplyResolvedCavernFloorCaps(stamped,X,Y,Z);
|
|
}
|
|
|
|
bool ResolvedPassageIntersectsBox(const int32 minimum[3],const int32 maximum[3]) const
|
|
{
|
|
if(!ResolvedLayout||ResolvedLayout->passages.empty())return false;
|
|
if(PassageProofNodes.empty())
|
|
{
|
|
// Native-only readiness tiles have not built the passage BVH. Keep their
|
|
// exact conservative fallback instead of indexing an empty tree.
|
|
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
|
const double voxelsPerMetre=Owner&&Owner->CoreVoxelSizeMicrons>0
|
|
?1000000.0/Owner->CoreVoxelSizeMicrons:4.0;
|
|
const double bridgeHalfWidthVoxels=3.5*voxelsPerMetre;
|
|
const double bridgeFootDepthVoxels=(5.0-0.13)*voxelsPerMetre;
|
|
const auto overlaps=[&](const double low[3],const double high[3])
|
|
{
|
|
for(int axis=0;axis<3;++axis)
|
|
if(high[axis]<minimum[axis]||low[axis]>maximum[axis])return false;
|
|
return true;
|
|
};
|
|
for(const auto& passage:ResolvedLayout->passages)
|
|
{
|
|
for(std::size_t i=1;i<passage.controlPoints.size();++i)
|
|
{
|
|
const auto& a=passage.controlPoints[i-1];const auto& b=passage.controlPoints[i];
|
|
const double radius=static_cast<double>(std::max(a.radiusSubvoxels,b.radiusSubvoxels))/Q;
|
|
const double floorThicknessVoxels=voxelsPerMetre;
|
|
const double horizontalExtent=std::max(radius,bridgeHalfWidthVoxels);
|
|
const double footDepth=std::max(floorThicknessVoxels,bridgeFootDepthVoxels);
|
|
const double low[3]={
|
|
std::min(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q-horizontalExtent,
|
|
std::min(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q-horizontalExtent,
|
|
std::min(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q-radius-footDepth};
|
|
const double high[3]={
|
|
std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+horizontalExtent,
|
|
std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+horizontalExtent,
|
|
std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius};
|
|
if(overlaps(low,high))return true;
|
|
}
|
|
const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(),
|
|
ResolvedLayout->strates.end(),[&passage](const auto& value)
|
|
{return value.strateIndex==passage.upperStrateIndex;});
|
|
if(upperStratum!=ResolvedLayout->strates.end())
|
|
{
|
|
std::array<const VoxelForgeCore::ResolvedLayoutAnchorV2*,5> route{};
|
|
std::size_t routeCount=0;
|
|
for(const auto& anchor:upperStratum->anchors)
|
|
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
|
{
|
|
if(routeCount<route.size())route[routeCount++]=&anchor;
|
|
else
|
|
{
|
|
std::move(route.begin()+1,route.end(),route.begin());
|
|
route.back()=&anchor;
|
|
}
|
|
}
|
|
if(routeCount>=2)
|
|
{
|
|
const double mouthReach=13.0*voxelsPerMetre*Q;
|
|
double minimumZ=std::numeric_limits<double>::infinity();
|
|
double maximumZ=-std::numeric_limits<double>::infinity();
|
|
for(std::size_t index=0;index<routeCount;++index)
|
|
{
|
|
minimumZ=std::min(minimumZ,static_cast<double>(route[index]->values[2]));
|
|
maximumZ=std::max(maximumZ,static_cast<double>(route[index]->values[2]));
|
|
}
|
|
const double mouthX=passage.upperMouthSubvoxels[0];
|
|
const double mouthY=passage.upperMouthSubvoxels[1];
|
|
const double low[3]={(mouthX-mouthReach)/Q,(mouthY-mouthReach)/Q,
|
|
(minimumZ-3.0*voxelsPerMetre*Q)/Q};
|
|
const double high[3]={(mouthX+mouthReach)/Q,(mouthY+mouthReach)/Q,
|
|
(maximumZ+3.0*voxelsPerMetre*Q)/Q};
|
|
if(overlaps(low,high))return true;
|
|
}
|
|
}
|
|
}
|
|
for(const auto& crossing:PassageVoidCrossings)
|
|
{
|
|
if(!crossing.needsColumn)continue;
|
|
const double radius=static_cast<double>(crossing.maximumRadiusSubvoxels)/Q;
|
|
const double low[3]={crossing.centerXSubvoxels/Q-radius,
|
|
crossing.centerYSubvoxels/Q-radius,crossing.bottomZSubvoxels/Q};
|
|
const double high[3]={crossing.centerXSubvoxels/Q+radius,
|
|
crossing.centerYSubvoxels/Q+radius,crossing.topZSubvoxels/Q};
|
|
if(overlaps(low,high))return true;
|
|
}
|
|
return false;
|
|
}
|
|
const double low[3]={static_cast<double>(minimum[0]),static_cast<double>(minimum[1]),
|
|
static_cast<double>(minimum[2])};
|
|
const double high[3]={static_cast<double>(maximum[0]),static_cast<double>(maximum[1]),
|
|
static_cast<double>(maximum[2])};
|
|
return PassageProofBoundsOverlapNode(0,low,high);
|
|
}
|
|
|
|
static bool MakeResolvedStratumBounds(double BottomZ,double TopZ,double SealThickness,
|
|
FResolvedStratumBounds& Out)
|
|
{
|
|
if(!std::isfinite(BottomZ)||!std::isfinite(TopZ)||!std::isfinite(SealThickness)
|
|
||TopZ<=BottomZ||SealThickness<0.0)return false;
|
|
Out={BottomZ,TopZ,SealThickness};
|
|
return true;
|
|
}
|
|
|
|
bool GetResolvedStratumBounds(const FStrateSlot& Slot,FResolvedStratumBounds& Out) const
|
|
{
|
|
if(!Owner)return false;
|
|
if(const auto* resolved=FindResolvedLayoutStratum(Slot.StrateIndex))
|
|
{
|
|
const double coreMicrons=std::max<std::uint32_t>(1,Owner->CoreVoxelSizeMicrons);
|
|
const double bottom=static_cast<double>(resolved->interiorBottomMicrometres)/coreMicrons;
|
|
const double top=static_cast<double>(resolved->interiorTopMicrometres)/coreMicrons;
|
|
const double ceiling=static_cast<double>(resolved->ceiling.topMicrometres
|
|
-resolved->ceiling.bottomMicrometres)/coreMicrons;
|
|
const double bedrock=static_cast<double>(resolved->bedrock.topMicrometres
|
|
-resolved->bedrock.bottomMicrometres)/coreMicrons;
|
|
return MakeResolvedStratumBounds(bottom,top,std::max(ceiling,bedrock),Out);
|
|
}
|
|
const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2;
|
|
const FBoundStratumField* Bound=FindBoundStratumForChunk(MidChunkZ);
|
|
if(bUseNativeManifestFields&&WorldAssembly&&Bound
|
|
&&Bound->Kind==FBoundStratumField::EKind::FloatingIslands)
|
|
{
|
|
const auto* Plan=Bound->IslandPlan;
|
|
if(Plan&&Plan->manifest.recipe.geometryVersion>=3)
|
|
{
|
|
constexpr double Scale=static_cast<double>(
|
|
VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
|
return MakeResolvedStratumBounds(
|
|
static_cast<double>(Plan->manifest.recipe.layoutMinimumZSubvoxels)/Scale,
|
|
static_cast<double>(Plan->manifest.recipe.layoutMaximumZSubvoxels)/Scale,
|
|
static_cast<double>(Plan->manifest.recipe.boundarySealSubvoxels)/Scale,Out);
|
|
}
|
|
}
|
|
const FStrateGenerationParams Params=Owner->Manager->GetGenerationParams(
|
|
FIntVector(0,0,MidChunkZ));
|
|
const double chunkWidth=GetResolvedLayoutChunkWidthVoxels();
|
|
return MakeResolvedStratumBounds(static_cast<double>(Slot.BottomChunkZ)*chunkWidth,
|
|
static_cast<double>(Slot.TopChunkZ+1)*chunkWidth,
|
|
static_cast<double>(Params.BoundarySealThickness),Out);
|
|
}
|
|
|
|
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;
|
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
|
uint64 BoundaryBandCalls=0,TransitionWindowChecks=0,TransitionWindowHits=0;
|
|
uint64 TransitionEvaluatorCalls=0,TransitionPassageFadeCalls=0,TransitionPassageMouthTests=0;
|
|
uint64 BoundaryTileCullRejects=0,BoundaryPointCullRejects=0;
|
|
uint64 ColumnZOverlapCalls=0,ColumnGuardCalls=0,ColumnGuardNonpositiveSkips=0;
|
|
uint64 ColumnGuardRadialRejects=0,ColumnSignedDistanceCalls=0;
|
|
uint64 TransitionPassageTileSkips=0;
|
|
#endif
|
|
};
|
|
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 MaxFloorProfileEndpointValueError = 0.0;
|
|
double MaxFloorProfileEndpointSlopeError = 0.0;
|
|
bool FloorProfileEndpointStepControlDetected = false;
|
|
bool FloorProfileEndpointSlopeControlDetected = false;
|
|
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;
|
|
double FloorCurveEndpointValueError = 0.0;
|
|
double FloorCurveEndpointSlopeError = 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,
|
|
const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout=nullptr,
|
|
bool bInParallelSignEdgeRepair=false)
|
|
{
|
|
using Clock=std::chrono::steady_clock;
|
|
const auto TotalStart=Clock::now();
|
|
Owner=&World;WorldAssembly=InWorldAssembly;ResolvedLayout=InResolvedLayout;
|
|
WorkerCount=std::clamp(Workers,1,16);DetailVariant=Variant;
|
|
Scheduler=InScheduler;RoundTunnelsEnabled=bInRoundTunnels;
|
|
bUseNativeManifestFields=true;
|
|
BindResolvedStratumFields();
|
|
bAnalyticOnly=false;
|
|
bProfileDetailBand=bInProfileDetailBand;
|
|
bParallelSignEdgeRepair=bInParallelSignEdgeRepair;
|
|
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;
|
|
AuditFloorProfileBlendNegativeControls();
|
|
|
|
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();
|
|
const auto SharedStage=Clock::now();
|
|
RebuildSharedFieldPrimitives();
|
|
Stats.SharedFieldSeconds=std::chrono::duration<double>(Clock::now()-SharedStage).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);
|
|
// Native density queries for non-heightfield strates use the exact interval
|
|
// evaluator. Build those native intervals before scanning passage support;
|
|
// the scan itself still completes before any resolved column is composed.
|
|
BuildPassageVoidCrossingPlan();
|
|
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();
|
|
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.MaxFloorProfileEndpointValueError<=0.01
|
|
&&Stats.MaxFloorProfileEndpointSlopeError<=0.001
|
|
&&Stats.FloorProfileEndpointStepControlDetected
|
|
&&Stats.FloorProfileEndpointSlopeControlDetected;
|
|
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_chord_deviation=%.3f vox informational endpoint_value_residual=%.8f vox endpoint_slope_residual=%.8f vox_per_m thresholds=(0.010,0.001) negative_controls=%s/%s status=%s\n",
|
|
static_cast<unsigned long long>(Stats.CurvedFloorSegments),Stats.MaxFloorCurveDeviation,
|
|
Stats.MaxFloorProfileEndpointValueError,Stats.MaxFloorProfileEndpointSlopeError,
|
|
Stats.FloorProfileEndpointStepControlDetected?"step_detected":"step_missed",
|
|
Stats.FloorProfileEndpointSlopeControlDetected?"slope_detected":"slope_missed",
|
|
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,
|
|
const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly,
|
|
const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout,
|
|
std::string& Error)
|
|
{
|
|
using Clock=std::chrono::steady_clock;
|
|
Owner=&World;WorldAssembly=InWorldAssembly;ResolvedLayout=InResolvedLayout;
|
|
WorkerCount=1;DetailVariant=Variant;Scheduler=nullptr;
|
|
// Analytic pages retain their bounded owner bins for route bookkeeping. Once published,
|
|
// the Core world binds each page to its base shape as the single density evaluator.
|
|
bUseNativeManifestFields=true;
|
|
BindResolvedStratumFields();
|
|
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();
|
|
// Analytic regions never build packed interval lines. Their exact-density fallback only
|
|
// reads the sparse 3D primitive bins; allocating the dense per-axis line-bin grids here
|
|
// scales with the region's XY bounding rectangle and is both unnecessary and unbounded.
|
|
// Les regions analytiques n'utilisent pas les intervalles compactes; leur repli exact lit
|
|
// les bins 3D clairsemes. Les grilles denses par axe croitraient avec la surface XY sans
|
|
// servir ce chemin.
|
|
BuildSpatialBins(LineBinSize,false);
|
|
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;
|
|
}
|
|
|
|
void InitializeNativeSurfaceOnly(FStandaloneOwnerWorld& World,
|
|
const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly,
|
|
const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout,
|
|
const FIntVector& InMinimum,const FIntVector& InMaximum)
|
|
{
|
|
Owner=&World;WorldAssembly=InWorldAssembly;ResolvedLayout=InResolvedLayout;
|
|
NativeOnlyMinimum=InMinimum;NativeOnlyMaximum=InMaximum;
|
|
bUseNativeManifestFields=true;bNativeOnly=true;bAnalyticOnly=true;Built=true;
|
|
BindResolvedStratumFields();
|
|
NativeOnlyViolations.store(0,std::memory_order_relaxed);
|
|
}
|
|
|
|
bool BuildShapeDependencyProof(FStandaloneOwnerWorld& World,
|
|
const FIntVector& MinVoxels,const FIntVector& MaxVoxels,bool bInRoundTunnels,
|
|
const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout=nullptr)
|
|
{
|
|
Owner=&World;ResolvedLayout=InResolvedLayout;WorkerCount=1;RoundTunnelsEnabled=bInRoundTunnels;
|
|
bAnalyticOnly=true;bNativeOnly=false;Stats={};
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
{
|
|
Min[Axis]=(&MinVoxels.X)[Axis]-2;Max[Axis]=(&MaxVoxels.X)[Axis]+2;
|
|
ClipMin[Axis]=(&MinVoxels.X)[Axis];ClipMax[Axis]=(&MaxVoxels.X)[Axis];
|
|
Size[Axis]=Max[Axis]-Min[Axis]+1;
|
|
if(Size[Axis]<=0||Size[Axis]>4096||ClipMax[Axis]<ClipMin[Axis])return false;
|
|
}
|
|
BuildLayerBounds();BuildOwnerPrimitives();
|
|
if(RoundTunnelsEnabled)BuildRoundTunnelPrimitives();
|
|
return true;
|
|
}
|
|
|
|
bool HasShapeDependencyInBox(const FIntVector& Origin,int32 Step,int32 Cells) const
|
|
{
|
|
const int32 Pad=std::max(Step,1);
|
|
const int32 BMin[3]={Origin.X-Pad,Origin.Y-Pad,Origin.Z-Pad};
|
|
const int32 BMax[3]={Origin.X+(Cells+1)*Step,Origin.Y+(Cells+1)*Step,
|
|
Origin.Z+(Cells+1)*Step};
|
|
for(const FPrimitive& Primitive:Primitives)
|
|
{
|
|
bool bOverlaps=true;
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
bOverlaps=bOverlaps&&Primitive.Max[Axis]+DetailBand>=BMin[Axis]
|
|
&&Primitive.Min[Axis]-DetailBand<=BMax[Axis];
|
|
if(bOverlaps)return true;
|
|
}
|
|
for(const FInterval& Range:LayerRanges)
|
|
if(Range.Lo<=BMax[2]&&Range.Hi>=BMin[2]
|
|
&&(Range.Lo>=BMin[2]||Range.Hi<=BMax[2]))return true;
|
|
return false;
|
|
}
|
|
|
|
bool IsNativeOnly() const { return bNativeOnly; }
|
|
uint32 GetNativeOnlyViolationCount() const
|
|
{ return NativeOnlyViolations.load(std::memory_order_relaxed); }
|
|
void RecordNativeOnlyViolation() const
|
|
{ NativeOnlyViolations.fetch_add(1,std::memory_order_relaxed); }
|
|
|
|
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())
|
|
{
|
|
FResolvedStratumBounds Bounds;
|
|
if(!GetResolvedStratumBounds(Slot,Bounds))continue;
|
|
const float HalfWidth=static_cast<float>(std::max(8.0,
|
|
Bounds.SealThickness+8.0));
|
|
const float Boundaries[2]={static_cast<float>(Bounds.BottomZ),
|
|
static_cast<float>(Bounds.TopZ)};
|
|
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)));
|
|
}
|
|
}
|
|
}
|
|
|
|
void SetSharedDensitySource(const FStandaloneShapeWorld* Source)
|
|
{ SharedDensitySource=Source==this?nullptr:Source; }
|
|
|
|
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
|
|
{
|
|
if(SharedDensitySource)return SharedDensitySource->DensityAt(X,Y,Z);
|
|
if(bNativeOnly)
|
|
{
|
|
if(!NativeOnlyContains(X,Y,Z))
|
|
{RecordNativeOnlyViolation();return 0.0f;}
|
|
float NativeDensity=0.0f;
|
|
if(!NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
|
{RecordNativeOnlyViolation();return 0.0f;}
|
|
return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z);
|
|
}
|
|
float IslandDensity=0.0f;
|
|
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
|
|
{
|
|
++ThreadClassificationStats().NativeFloatFieldCalls;
|
|
return ApplyEditBrushes(ApplyResolvedStrateComposition(IslandDensity,X,Y,Z),X,Y,Z);
|
|
}
|
|
float NativeDensity=0.0f;
|
|
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
|
{
|
|
++ThreadClassificationStats().NativeFloatFieldCalls;
|
|
return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),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);
|
|
}
|
|
|
|
std::vector<std::uint32_t> OreVeinTileCandidates(
|
|
const std::array<std::int64_t,3>& minimum,
|
|
const std::array<std::int64_t,3>& maximum) const
|
|
{return BuildOreVeinTileList(minimum,maximum);}
|
|
|
|
VoxelForgeCore::MaterialSampleV1 EvaluateMaterialAt(float X,float Y,float Z,
|
|
std::span<const std::uint32_t> candidates,
|
|
std::uint64_t* candidateChecks=nullptr) const
|
|
{
|
|
if(SharedDensitySource)
|
|
return SharedDensitySource->MaterialAt(X,Y,Z,candidates,candidateChecks);
|
|
return MaterialAt(X,Y,Z,candidates,candidateChecks);
|
|
}
|
|
|
|
VoxelForgeCore::MaterialSampleV1 MaterialAt(float X,float Y,float Z) const
|
|
{
|
|
if(SharedDensitySource)return SharedDensitySource->MaterialAt(X,Y,Z);
|
|
const auto candidates=BuildOreVeinPointList(X,Y,Z);
|
|
return EvaluateMaterialAt(X,Y,Z,candidates);
|
|
}
|
|
|
|
std::string DensityDispatchPathAt(float X,float Y,float Z) const
|
|
{
|
|
if(SharedDensitySource)return SharedDensitySource->DensityDispatchPathAt(X,Y,Z);
|
|
float Density=0.0f;
|
|
if(NativeFloatingIslandDensityAt(X,Y,Z,Density))return "FloatingIslands-native";
|
|
const auto* Bound=FindBoundStratumForZ(Z);
|
|
if(Bound&&Bound->Kind==FBoundStratumField::EKind::Caves
|
|
&&NativeSurfaceDensityAt(X,Y,Z,Density))
|
|
return Bound->Resolved&&VoxelForgeCore::IsNativeLatticeFieldStratum(*Bound->Resolved)
|
|
?"Lattice-native":"Caves-native";
|
|
if(NativeSurfaceDensityAt(X,Y,Z,Density))return "SurfaceWorld-native";
|
|
if(UsesLegacySurfaceField(X,Y,Z))return "SurfaceWorld-legacy";
|
|
if(Bound&&Bound->Resolved)
|
|
{
|
|
const std::string& Kind=Bound->Resolved->generatorKind;
|
|
return "compiled-analytic/"+Kind;
|
|
}
|
|
return Owner?"compiled-analytic/TunnelNetwork":"compiled-analytic/fallback";
|
|
}
|
|
|
|
float DensityAtLattice(int32 X,int32 Y,int32 Z) const
|
|
{
|
|
if(SharedDensitySource)return SharedDensitySource->DensityAtLattice(X,Y,Z);
|
|
if(bNativeOnly)
|
|
{
|
|
if(!NativeOnlyContains(static_cast<float>(X),static_cast<float>(Y),
|
|
static_cast<float>(Z)))
|
|
{RecordNativeOnlyViolation();return 0.0f;}
|
|
float NativeDensity=0.0f;
|
|
if(!NativeSurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
|
|
static_cast<float>(Z),NativeDensity))
|
|
{RecordNativeOnlyViolation();return 0.0f;}
|
|
return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z);
|
|
}
|
|
float IslandDensity=0.0f;
|
|
if(NativeFloatingIslandDensityAt(static_cast<float>(X),static_cast<float>(Y),
|
|
static_cast<float>(Z),IslandDensity))
|
|
{
|
|
++ThreadClassificationStats().NativeLatticeFieldCalls;
|
|
return ApplyEditBrushes(ApplyResolvedStrateComposition(IslandDensity,X,Y,Z),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(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),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
|
|
{
|
|
if(SharedDensitySource)return SharedDensitySource->DensityAtUnclamped(X,Y,Z);
|
|
if(bNativeOnly)
|
|
{
|
|
if(!NativeOnlyContains(X,Y,Z))
|
|
{RecordNativeOnlyViolation();return 0.0f;}
|
|
float NativeDensity=0.0f;
|
|
if(!NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
|
{RecordNativeOnlyViolation();return 0.0f;}
|
|
return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z);
|
|
}
|
|
float IslandDensity=0.0f;
|
|
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
|
|
{
|
|
++ThreadClassificationStats().NativeUnclampedFieldCalls;
|
|
return ApplyEditBrushes(ApplyResolvedStrateComposition(IslandDensity,X,Y,Z),X,Y,Z);
|
|
}
|
|
float NativeDensity=0.0f;
|
|
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
|
{
|
|
++ThreadClassificationStats().NativeUnclampedFieldCalls;
|
|
return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),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
|
|
{
|
|
if(SharedDensitySource)return SharedDensitySource->DetailSampleAt(X,Y,Z);
|
|
VoxelForgeCore::DetailFieldSample Sample;
|
|
if(bNativeOnly)
|
|
{RecordNativeOnlyViolation();return 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
|
|
{
|
|
if(SharedDensitySource)
|
|
{
|
|
SharedDensitySource->DensityAtLatticeX8(XStart,XStep,Y,Z,Count,OutDensity);
|
|
return;
|
|
}
|
|
Count=std::clamp(Count,0,8);
|
|
if(Count==0)return;
|
|
if(bNativeOnly)
|
|
{
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
OutDensity[Lane]=DensityAtLattice(XStart+Lane*XStep,Y,Z);
|
|
return;
|
|
}
|
|
if(IsInsideResolvedStrateBoundaryBand(static_cast<float>(Z)))
|
|
{
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
OutDensity[Lane]=DensityAtLattice(XStart+Lane*XStep,Y,Z);
|
|
return;
|
|
}
|
|
bool bHasLegacySurfaceSample=false;
|
|
bool bHasNativeIslandSample=false;
|
|
bool bHasNativeGraphSample=false;
|
|
std::array<float,8> NativeIslandDensity{};
|
|
std::array<bool,8> HasNativeIslandSample{};
|
|
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);
|
|
const auto* managerBound=FindBoundStratumForManagerZ(SampleZ);
|
|
const auto* resolvedBound=FindBoundStratumForZ(SampleZ);
|
|
bHasNativeGraphSample|=(managerBound
|
|
&&managerBound->Kind==FBoundStratumField::EKind::Caves)
|
|
||(resolvedBound&&resolvedBound->Kind==FBoundStratumField::EKind::Caves);
|
|
HasNativeIslandSample[static_cast<std::size_t>(Lane)]=NativeFloatingIslandDensityAt(
|
|
SampleX,SampleY,SampleZ,NativeIslandDensity[static_cast<std::size_t>(Lane)]);
|
|
bHasNativeIslandSample|=HasNativeIslandSample[static_cast<std::size_t>(Lane)];
|
|
}
|
|
if(bHasLegacySurfaceSample||bHasNativeIslandSample||bHasNativeGraphSample)
|
|
{
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
{
|
|
const std::size_t Index=static_cast<std::size_t>(Lane);
|
|
if(HasNativeIslandSample[Index])
|
|
{
|
|
++ThreadClassificationStats().NativeFloatFieldCalls;
|
|
const float SampleX=static_cast<float>(XStart+Lane*XStep);
|
|
const float SampleY=static_cast<float>(Y);
|
|
const float SampleZ=static_cast<float>(Z);
|
|
OutDensity[Lane]=ApplyEditBrushes(ApplyResolvedStrateComposition(
|
|
NativeIslandDensity[Index],SampleX,SampleY,SampleZ),
|
|
SampleX,SampleY,SampleZ);
|
|
}
|
|
else
|
|
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:
|
|
struct FPassageTileFeature
|
|
{
|
|
std::size_t passageIndex=0,segmentIndex=0;
|
|
bool bColumn=false,bMouthSupport=false;
|
|
};
|
|
struct FPassageNearestNode
|
|
{
|
|
std::array<double,3> minimum{},maximum{};
|
|
double maximumRadius=0.0;
|
|
std::vector<std::size_t> segments;
|
|
int left=-1,right=-1;
|
|
};
|
|
struct FPassageProofBound
|
|
{
|
|
std::array<double,3> minimum{},maximum{};
|
|
};
|
|
struct FPassageProofNode
|
|
{
|
|
std::array<double,3> minimum{},maximum{};
|
|
std::size_t first=0,end=0;
|
|
int left=-1,right=-1;
|
|
};
|
|
struct FPassageFeatureNode
|
|
{
|
|
std::array<std::int64_t,3> minimum{},maximum{};
|
|
std::size_t first=0,end=0;
|
|
int left=-1,right=-1;
|
|
};
|
|
struct FStampSpatialBound
|
|
{
|
|
std::array<std::int64_t,3> minimumSubvoxels{},maximumSubvoxels{};
|
|
};
|
|
struct FStampSpatialNode
|
|
{
|
|
std::array<std::int64_t,3> minimumSubvoxels{},maximumSubvoxels{};
|
|
std::size_t first=0,end=0;
|
|
int left=-1,right=-1;
|
|
};
|
|
struct FStampRuntime
|
|
{
|
|
const VoxelForgeCore::ResolvedLayoutStampV1* record=nullptr;
|
|
std::array<float,3> positionVoxels{},halfExtentsVoxels{};
|
|
float smoothWidthVoxels=0.0f;
|
|
float irregularityAmplitudeVoxels=0.0f;
|
|
float irregularityWavelengthVoxels=0.0f;
|
|
float roundingRadiusVoxels=0.0f;
|
|
std::uint64_t irregularitySeed=0;
|
|
std::uint8_t quarterTurns=0;
|
|
};
|
|
struct FOreVeinSpatialBound
|
|
{
|
|
std::array<std::int64_t,3> minimumSubvoxels{},maximumSubvoxels{};
|
|
};
|
|
struct FOreVeinSpatialNode
|
|
{
|
|
std::array<std::int64_t,3> minimumSubvoxels{},maximumSubvoxels{};
|
|
std::size_t first=0,end=0;
|
|
int left=-1,right=-1;
|
|
};
|
|
struct FOreVeinRuntime
|
|
{
|
|
const VoxelForgeCore::ResolvedLayoutOreVeinV1* record=nullptr;
|
|
std::array<std::array<double,3>,2> endpointsVoxels{};
|
|
double radiusVoxels=0.0,irregularityAmplitudeVoxels=0.0;
|
|
double irregularityWavelengthVoxels=0.0,blendWidthVoxels=0.0;
|
|
std::uint64_t irregularitySeed=0;
|
|
std::uint16_t resourceMaterialId=0;
|
|
};
|
|
struct FCavernRuntimeFeature
|
|
{
|
|
const VoxelForgeCore::ResolvedCavernRoomV1* room=nullptr;
|
|
const VoxelForgeCore::ResolvedCavernEdgeV1* edge=nullptr;
|
|
bool nativeGraph=false;
|
|
std::array<std::int64_t,3> boundsMinimumMicrometres{},boundsMaximumMicrometres{};
|
|
std::array<double,3> centerMetres{},halfExtentsMetres{};
|
|
struct FSegment
|
|
{
|
|
std::array<double,3> start{},end{},minimum{},maximum{};
|
|
};
|
|
struct FSegmentNode
|
|
{
|
|
std::array<double,3> minimum{},maximum{};
|
|
std::size_t first=0,end=0;
|
|
int left=-1,right=-1;
|
|
};
|
|
std::vector<FSegment> segments;
|
|
std::vector<std::uint32_t> segmentOrder;
|
|
std::vector<FSegmentNode> segmentNodes;
|
|
double minimumFloorMetres=std::numeric_limits<double>::infinity();
|
|
double maximumFloorMetres=-std::numeric_limits<double>::infinity();
|
|
double maximumSegmentGrade=0.0;
|
|
std::array<float,3> centerVoxels{},halfExtentsVoxels{};
|
|
std::array<std::array<float,3>,4> controlPointsVoxels{};
|
|
float floorVoxels=0.0f;
|
|
float boreRadiusVoxels=0.0f,boreHeightVoxels=0.0f;
|
|
float floorSupportDepthVoxels=0.0f;
|
|
float smoothWidthVoxels=0.0f;
|
|
float irregularityAmplitudeVoxels=0.0f;
|
|
float irregularityWavelengthVoxels=0.0f;
|
|
std::uint64_t irregularitySeed=0;
|
|
};
|
|
struct FCavernSpatialNode
|
|
{
|
|
std::array<std::int64_t,3> minimumMicrometres{},maximumMicrometres{};
|
|
std::size_t first=0,end=0;
|
|
int left=-1,right=-1;
|
|
};
|
|
struct FMetricInterval
|
|
{
|
|
double minimum=0.0,maximum=0.0;
|
|
bool known=true;
|
|
};
|
|
|
|
int BuildStampSpatialNode(std::size_t first,std::size_t end)
|
|
{
|
|
FStampSpatialNode node;node.first=first;node.end=end;
|
|
node.minimumSubvoxels.fill(std::numeric_limits<std::int64_t>::max());
|
|
node.maximumSubvoxels.fill(std::numeric_limits<std::int64_t>::min());
|
|
for(std::size_t offset=first;offset<end;++offset)
|
|
{
|
|
const auto& bound=StampSpatialBounds[StampSpatialOrder[offset]];
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
node.minimumSubvoxels[axis]=std::min(node.minimumSubvoxels[axis],
|
|
bound.minimumSubvoxels[axis]);
|
|
node.maximumSubvoxels[axis]=std::max(node.maximumSubvoxels[axis],
|
|
bound.maximumSubvoxels[axis]);
|
|
}
|
|
}
|
|
const int nodeIndex=static_cast<int>(StampSpatialNodes.size());
|
|
StampSpatialNodes.push_back(node);
|
|
if(end-first>8)
|
|
{
|
|
std::size_t splitAxis=0;
|
|
for(std::size_t axis=1;axis<3;++axis)
|
|
if(static_cast<long double>(node.maximumSubvoxels[axis])
|
|
-node.minimumSubvoxels[axis]
|
|
>static_cast<long double>(node.maximumSubvoxels[splitAxis])
|
|
-node.minimumSubvoxels[splitAxis])splitAxis=axis;
|
|
const std::size_t middle=first+(end-first)/2;
|
|
std::nth_element(StampSpatialOrder.begin()+first,
|
|
StampSpatialOrder.begin()+middle,StampSpatialOrder.begin()+end,
|
|
[this,splitAxis](std::uint32_t a,std::uint32_t b)
|
|
{
|
|
const auto& left=StampSpatialBounds[a];const auto& right=StampSpatialBounds[b];
|
|
const long double leftCenter=static_cast<long double>(
|
|
left.minimumSubvoxels[splitAxis])+left.maximumSubvoxels[splitAxis];
|
|
const long double rightCenter=static_cast<long double>(
|
|
right.minimumSubvoxels[splitAxis])+right.maximumSubvoxels[splitAxis];
|
|
return leftCenter==rightCenter?a<b:leftCenter<rightCenter;
|
|
});
|
|
StampSpatialNodes[nodeIndex].left=BuildStampSpatialNode(first,middle);
|
|
StampSpatialNodes[nodeIndex].right=BuildStampSpatialNode(middle,end);
|
|
}
|
|
return nodeIndex;
|
|
}
|
|
|
|
void QueryStampSpatialNode(int nodeIndex,
|
|
const std::array<std::int64_t,3>& minimum,
|
|
const std::array<std::int64_t,3>& maximum,
|
|
std::vector<std::uint32_t>& result) const
|
|
{
|
|
if(nodeIndex<0)return;
|
|
const auto& node=StampSpatialNodes[static_cast<std::size_t>(nodeIndex)];
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
if(node.maximumSubvoxels[axis]<minimum[axis]
|
|
||node.minimumSubvoxels[axis]>maximum[axis])return;
|
|
if(node.left<0)
|
|
{
|
|
for(std::size_t offset=node.first;offset<node.end;++offset)
|
|
{
|
|
const auto index=StampSpatialOrder[offset];
|
|
const auto& bound=StampSpatialBounds[index];bool overlap=true;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
overlap=overlap&&bound.minimumSubvoxels[axis]<=maximum[axis]
|
|
&&bound.maximumSubvoxels[axis]>=minimum[axis];
|
|
if(overlap)result.push_back(index);
|
|
}
|
|
return;
|
|
}
|
|
QueryStampSpatialNode(node.left,minimum,maximum,result);
|
|
QueryStampSpatialNode(node.right,minimum,maximum,result);
|
|
}
|
|
|
|
void BuildStampSpatialIndex()
|
|
{
|
|
StampSpatialBounds.clear();StampSpatialOrder.clear();StampSpatialNodes.clear();
|
|
StampRuntime.clear();
|
|
if(!ResolvedLayout||ResolvedLayout->stamps.empty()||!Owner
|
|
||Owner->CoreVoxelSizeMicrons==0)return;
|
|
constexpr long double Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
const long double scale=Q/Owner->CoreVoxelSizeMicrons;
|
|
StampSpatialBounds.reserve(ResolvedLayout->stamps.size());
|
|
StampRuntime.reserve(ResolvedLayout->stamps.size());
|
|
for(const auto& stamp:ResolvedLayout->stamps)
|
|
{
|
|
FStampSpatialBound bound;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
bound.minimumSubvoxels[axis]=static_cast<std::int64_t>(std::floor(
|
|
static_cast<long double>(stamp.boundsMinimumMicrometres[axis])*scale));
|
|
bound.maximumSubvoxels[axis]=static_cast<std::int64_t>(std::ceil(
|
|
static_cast<long double>(stamp.boundsMaximumMicrometres[axis])*scale));
|
|
}
|
|
StampSpatialBounds.push_back(bound);
|
|
FStampRuntime runtime;runtime.record=&stamp;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
runtime.positionVoxels[axis]=static_cast<float>(
|
|
static_cast<double>(stamp.positionMicrometres[axis])
|
|
/Owner->CoreVoxelSizeMicrons);
|
|
const std::size_t xAxis=stamp.yawQuarterTurns%2?1:0;
|
|
const std::size_t yAxis=stamp.yawQuarterTurns%2?0:1;
|
|
runtime.halfExtentsVoxels={
|
|
static_cast<float>(static_cast<double>(stamp.halfExtentsMicrometres[0])
|
|
/Owner->CoreVoxelSizeMicrons),
|
|
static_cast<float>(static_cast<double>(stamp.halfExtentsMicrometres[1])
|
|
/Owner->CoreVoxelSizeMicrons),
|
|
static_cast<float>(static_cast<double>(stamp.halfExtentsMicrometres[2])
|
|
/Owner->CoreVoxelSizeMicrons)};
|
|
(void)xAxis;(void)yAxis;
|
|
runtime.smoothWidthVoxels=static_cast<float>(
|
|
static_cast<double>(stamp.smoothWidthMicrometres)/Owner->CoreVoxelSizeMicrons);
|
|
runtime.irregularityAmplitudeVoxels=static_cast<float>(
|
|
static_cast<double>(stamp.irregularityAmplitudeMicrometres)
|
|
/Owner->CoreVoxelSizeMicrons);
|
|
runtime.irregularityWavelengthVoxels=static_cast<float>(
|
|
static_cast<double>(stamp.irregularityWavelengthMicrometres)
|
|
/Owner->CoreVoxelSizeMicrons);
|
|
runtime.roundingRadiusVoxels=static_cast<float>(
|
|
static_cast<double>(stamp.roundingRadiusMicrometres)
|
|
/Owner->CoreVoxelSizeMicrons);
|
|
runtime.irregularitySeed=stamp.irregularitySeed;
|
|
runtime.quarterTurns=stamp.yawQuarterTurns;
|
|
StampRuntime.push_back(runtime);
|
|
}
|
|
StampSpatialOrder.resize(StampSpatialBounds.size());
|
|
for(std::size_t i=0;i<StampSpatialOrder.size();++i)
|
|
StampSpatialOrder[i]=static_cast<std::uint32_t>(i);
|
|
if(!StampSpatialOrder.empty())BuildStampSpatialNode(0,StampSpatialOrder.size());
|
|
}
|
|
|
|
int BuildOreVeinSpatialNode(std::size_t first,std::size_t end)
|
|
{
|
|
FOreVeinSpatialNode node;node.first=first;node.end=end;
|
|
node.minimumSubvoxels.fill(std::numeric_limits<std::int64_t>::max());
|
|
node.maximumSubvoxels.fill(std::numeric_limits<std::int64_t>::min());
|
|
for(std::size_t offset=first;offset<end;++offset)
|
|
{
|
|
const auto& bound=OreVeinSpatialBounds[OreVeinSpatialOrder[offset]];
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
node.minimumSubvoxels[axis]=std::min(node.minimumSubvoxels[axis],
|
|
bound.minimumSubvoxels[axis]);
|
|
node.maximumSubvoxels[axis]=std::max(node.maximumSubvoxels[axis],
|
|
bound.maximumSubvoxels[axis]);
|
|
}
|
|
}
|
|
const int nodeIndex=static_cast<int>(OreVeinSpatialNodes.size());
|
|
OreVeinSpatialNodes.push_back(node);
|
|
if(end-first>8)
|
|
{
|
|
std::size_t splitAxis=0;
|
|
for(std::size_t axis=1;axis<3;++axis)
|
|
if(static_cast<long double>(node.maximumSubvoxels[axis])
|
|
-node.minimumSubvoxels[axis]
|
|
>static_cast<long double>(node.maximumSubvoxels[splitAxis])
|
|
-node.minimumSubvoxels[splitAxis])splitAxis=axis;
|
|
const std::size_t middle=first+(end-first)/2;
|
|
std::nth_element(OreVeinSpatialOrder.begin()+first,
|
|
OreVeinSpatialOrder.begin()+middle,OreVeinSpatialOrder.begin()+end,
|
|
[this,splitAxis](std::uint32_t a,std::uint32_t b)
|
|
{
|
|
const auto& left=OreVeinSpatialBounds[a];
|
|
const auto& right=OreVeinSpatialBounds[b];
|
|
const long double leftCenter=static_cast<long double>(
|
|
left.minimumSubvoxels[splitAxis])+left.maximumSubvoxels[splitAxis];
|
|
const long double rightCenter=static_cast<long double>(
|
|
right.minimumSubvoxels[splitAxis])+right.maximumSubvoxels[splitAxis];
|
|
return leftCenter==rightCenter?a<b:leftCenter<rightCenter;
|
|
});
|
|
OreVeinSpatialNodes[nodeIndex].left=BuildOreVeinSpatialNode(first,middle);
|
|
OreVeinSpatialNodes[nodeIndex].right=BuildOreVeinSpatialNode(middle,end);
|
|
}
|
|
return nodeIndex;
|
|
}
|
|
|
|
void QueryOreVeinSpatialNode(int nodeIndex,
|
|
const std::array<std::int64_t,3>& minimum,
|
|
const std::array<std::int64_t,3>& maximum,
|
|
std::vector<std::uint32_t>& result) const
|
|
{
|
|
if(nodeIndex<0)return;
|
|
const auto& node=OreVeinSpatialNodes[static_cast<std::size_t>(nodeIndex)];
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
if(node.maximumSubvoxels[axis]<minimum[axis]
|
|
||node.minimumSubvoxels[axis]>maximum[axis])return;
|
|
if(node.left<0)
|
|
{
|
|
for(std::size_t offset=node.first;offset<node.end;++offset)
|
|
{
|
|
const std::uint32_t index=OreVeinSpatialOrder[offset];
|
|
const auto& bound=OreVeinSpatialBounds[index];bool overlap=true;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
overlap=overlap&&bound.minimumSubvoxels[axis]<=maximum[axis]
|
|
&&bound.maximumSubvoxels[axis]>=minimum[axis];
|
|
if(overlap)result.push_back(index);
|
|
}
|
|
return;
|
|
}
|
|
QueryOreVeinSpatialNode(node.left,minimum,maximum,result);
|
|
QueryOreVeinSpatialNode(node.right,minimum,maximum,result);
|
|
}
|
|
|
|
void BuildOreVeinSpatialIndex()
|
|
{
|
|
OreVeinSpatialBounds.clear();OreVeinSpatialOrder.clear();OreVeinSpatialNodes.clear();
|
|
OreVeinRuntime.clear();
|
|
if(!ResolvedLayout||ResolvedLayout->oreVeins.empty()||!Owner
|
|
||Owner->CoreVoxelSizeMicrons==0)return;
|
|
constexpr long double Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
const long double scale=Q/Owner->CoreVoxelSizeMicrons;
|
|
OreVeinSpatialBounds.reserve(ResolvedLayout->oreVeins.size());
|
|
OreVeinRuntime.reserve(ResolvedLayout->oreVeins.size());
|
|
for(const auto& vein:ResolvedLayout->oreVeins)
|
|
{
|
|
FOreVeinSpatialBound bound;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
bound.minimumSubvoxels[axis]=static_cast<std::int64_t>(std::floor(
|
|
static_cast<long double>(vein.boundsMinimumMicrometres[axis])*scale));
|
|
bound.maximumSubvoxels[axis]=static_cast<std::int64_t>(std::ceil(
|
|
static_cast<long double>(vein.boundsMaximumMicrometres[axis])*scale));
|
|
}
|
|
OreVeinSpatialBounds.push_back(bound);
|
|
FOreVeinRuntime runtime;runtime.record=&vein;
|
|
for(std::size_t endpoint=0;endpoint<2;++endpoint)
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
runtime.endpointsVoxels[endpoint][axis]=
|
|
static_cast<double>(vein.endpointsMicrometres[endpoint][axis])
|
|
/Owner->CoreVoxelSizeMicrons;
|
|
runtime.radiusVoxels=static_cast<double>(vein.radiusMicrometres)
|
|
/Owner->CoreVoxelSizeMicrons;
|
|
runtime.irregularityAmplitudeVoxels=
|
|
static_cast<double>(vein.irregularityAmplitudeMicrometres)
|
|
/Owner->CoreVoxelSizeMicrons;
|
|
runtime.irregularityWavelengthVoxels=
|
|
static_cast<double>(vein.irregularityWavelengthMicrometres)
|
|
/Owner->CoreVoxelSizeMicrons;
|
|
runtime.blendWidthVoxels=static_cast<double>(vein.blendWidthMicrometres)
|
|
/Owner->CoreVoxelSizeMicrons;
|
|
runtime.irregularitySeed=vein.irregularitySeed;
|
|
runtime.resourceMaterialId=vein.resourceMaterialId;
|
|
OreVeinRuntime.push_back(runtime);
|
|
}
|
|
OreVeinSpatialOrder.resize(OreVeinSpatialBounds.size());
|
|
for(std::size_t i=0;i<OreVeinSpatialOrder.size();++i)
|
|
OreVeinSpatialOrder[i]=static_cast<std::uint32_t>(i);
|
|
if(!OreVeinSpatialOrder.empty())BuildOreVeinSpatialNode(0,OreVeinSpatialOrder.size());
|
|
}
|
|
|
|
std::vector<std::uint32_t> BuildOreVeinTileList(
|
|
const std::array<std::int64_t,3>& minimum,
|
|
const std::array<std::int64_t,3>& maximum) const
|
|
{
|
|
std::vector<std::uint32_t> result;
|
|
if(OreVeinSpatialBounds.empty())return result;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
if(minimum[axis]>maximum[axis])
|
|
{
|
|
result.resize(OreVeinSpatialBounds.size());
|
|
for(std::size_t i=0;i<result.size();++i)
|
|
result[i]=static_cast<std::uint32_t>(i);
|
|
return result;
|
|
}
|
|
QueryOreVeinSpatialNode(0,minimum,maximum,result);
|
|
std::sort(result.begin(),result.end());
|
|
return result;
|
|
}
|
|
|
|
std::vector<std::uint32_t> BuildOreVeinPointList(float X,float Y,float Z) const
|
|
{
|
|
constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
const auto toQ=[](float value)
|
|
{
|
|
const long double scaled=static_cast<long double>(value)*Q;
|
|
if(!std::isfinite(value)||scaled<std::numeric_limits<std::int64_t>::min()
|
|
||scaled>std::numeric_limits<std::int64_t>::max())return std::int64_t{0};
|
|
return static_cast<std::int64_t>(std::llround(scaled));
|
|
};
|
|
const std::array<std::int64_t,3> point={toQ(X),toQ(Y),toQ(Z)};
|
|
return BuildOreVeinTileList(point,point);
|
|
}
|
|
|
|
VoxelForgeCore::MaterialSampleV1 MaterialAt(float X,float Y,float Z,
|
|
std::span<const std::uint32_t> candidates,
|
|
std::uint64_t* candidateChecks=nullptr) const
|
|
{
|
|
using namespace VoxelForgeCore;
|
|
MaterialSampleV1 result;
|
|
if(!ResolvedLayout||OreVeinRuntime.empty()||candidates.empty()
|
|
||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))return result;
|
|
const FBoundStratumField* bound=FindBoundStratumForZ(Z);
|
|
if(!bound)return result;
|
|
const std::array<double,3> point={X,Y,Z};
|
|
const auto noiseAt=[](std::uint64_t seed,std::int64_t x,std::int64_t y,std::int64_t z)
|
|
{
|
|
std::uint64_t hash=14695981039346656037ull;
|
|
for(const std::uint64_t word:{seed,static_cast<std::uint64_t>(x),
|
|
static_cast<std::uint64_t>(y),static_cast<std::uint64_t>(z)})
|
|
for(int byte=0;byte<8;++byte)
|
|
{hash^=static_cast<std::uint8_t>(word>>(byte*8));hash*=1099511628211ull;}
|
|
hash^=hash>>30;hash*=0xbf58476d1ce4e5b9ull;
|
|
hash^=hash>>27;hash*=0x94d049bb133111ebull;hash^=hash>>31;
|
|
return static_cast<double>(static_cast<std::uint32_t>(hash>>32))/2147483647.5-1.0;
|
|
};
|
|
const auto fade=[](double value){return value*value*(3.0-2.0*value);};
|
|
for(const std::uint32_t candidate:candidates)
|
|
{
|
|
if(candidateChecks)++*candidateChecks;
|
|
if(candidate>=OreVeinRuntime.size())continue;
|
|
const auto& vein=OreVeinRuntime[candidate];
|
|
if(!vein.record||vein.record->strateIndex!=bound->StrateIndex)continue;
|
|
const double wavelength=vein.irregularityWavelengthVoxels;
|
|
double irregularity=0.0;
|
|
if(vein.irregularityAmplitudeVoxels>0.0&&wavelength>0.0)
|
|
{
|
|
std::array<std::int64_t,3> cell{};std::array<double,3> t{};
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const double coordinate=point[axis]/wavelength;
|
|
cell[axis]=static_cast<std::int64_t>(std::floor(coordinate));
|
|
t[axis]=fade(coordinate-cell[axis]);
|
|
}
|
|
double layer[2][2]{};
|
|
for(int z=0;z<2;++z)for(int y=0;y<2;++y)
|
|
{
|
|
const double a=noiseAt(vein.irregularitySeed,cell[0],cell[1]+y,cell[2]+z);
|
|
const double b=noiseAt(vein.irregularitySeed,cell[0]+1,cell[1]+y,cell[2]+z);
|
|
layer[z][y]=std::lerp(a,b,t[0]);
|
|
}
|
|
irregularity=std::lerp(std::lerp(layer[0][0],layer[0][1],t[1]),
|
|
std::lerp(layer[1][0],layer[1][1],t[1]),t[2]);
|
|
}
|
|
const auto& a=vein.endpointsVoxels[0];const auto& b=vein.endpointsVoxels[1];
|
|
double lengthSquared=0.0,dot=0.0;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{const double delta=b[axis]-a[axis];lengthSquared+=delta*delta;dot+=(point[axis]-a[axis])*delta;}
|
|
const double t=lengthSquared>0.0?std::clamp(dot/lengthSquared,0.0,1.0):0.0;
|
|
double distanceSquared=0.0;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{const double delta=point[axis]-(a[axis]+t*(b[axis]-a[axis]));distanceSquared+=delta*delta;}
|
|
const double distance=std::sqrt(distanceSquared);
|
|
const double radius=std::max(0.0,vein.radiusVoxels
|
|
+irregularity*vein.irregularityAmplitudeVoxels);
|
|
if(distance>=radius+vein.blendWidthVoxels)continue;
|
|
double weight=1.0;
|
|
if(distance>radius)
|
|
{
|
|
const double u=std::clamp((radius+vein.blendWidthVoxels-distance)
|
|
/vein.blendWidthVoxels,0.0,1.0);
|
|
weight=u*u*(3.0-2.0*u);
|
|
}
|
|
const auto weightQ16=static_cast<std::uint16_t>(std::clamp(
|
|
std::llround(weight*65535.0),0ll,65535ll));
|
|
if(weightQ16==0)continue;
|
|
std::size_t slot=2;
|
|
for(std::size_t index=0;index<2;++index)
|
|
if(result.resourceMaterialIds[index]==vein.resourceMaterialId)
|
|
{slot=index;break;}
|
|
if(slot==2)
|
|
for(std::size_t index=0;index<2;++index)
|
|
if(result.resourceMaterialIds[index]==0){slot=index;break;}
|
|
if(slot<2)
|
|
{
|
|
result.resourceMaterialIds[slot]=vein.resourceMaterialId;
|
|
const std::uint32_t sum=static_cast<std::uint32_t>(
|
|
result.resourceWeightsQ16[slot])+weightQ16;
|
|
result.resourceWeightsQ16[slot]=static_cast<std::uint16_t>(
|
|
std::min<std::uint32_t>(65535,sum));
|
|
}
|
|
}
|
|
const std::uint32_t total=static_cast<std::uint32_t>(result.resourceWeightsQ16[0])
|
|
+result.resourceWeightsQ16[1];
|
|
if(total>65535)
|
|
{
|
|
result.resourceWeightsQ16[0]=static_cast<std::uint16_t>(
|
|
static_cast<std::uint64_t>(result.resourceWeightsQ16[0])*65535/total);
|
|
result.resourceWeightsQ16[1]=static_cast<std::uint16_t>(
|
|
65535-result.resourceWeightsQ16[0]);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::vector<std::uint32_t> BuildStampTileList(
|
|
const std::array<std::int64_t,3>& minimum,
|
|
const std::array<std::int64_t,3>& maximum) const
|
|
{
|
|
std::vector<std::uint32_t> result;
|
|
if(StampSpatialBounds.empty())return result;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
if(minimum[axis]>maximum[axis])
|
|
{
|
|
result.resize(StampSpatialBounds.size());
|
|
for(std::size_t i=0;i<result.size();++i)
|
|
result[i]=static_cast<std::uint32_t>(i);
|
|
return result;
|
|
}
|
|
QueryStampSpatialNode(0,minimum,maximum,result);
|
|
// Stamp union/carve composition follows canonical stable-ID order. The BVH query
|
|
// order is spatial and can differ per tile, which would let a later carve erase a
|
|
// fill on meshed tiles even though unscoped density queries use the canonical order.
|
|
std::sort(result.begin(),result.end());
|
|
return result;
|
|
}
|
|
|
|
void BuildCavernSpatialIndex()
|
|
{
|
|
CavernRuntime.clear();CavernSpatialNodes.clear();CavernSpatialOrder.clear();
|
|
if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0
|
|
||ResolvedLayout->cavernGraphVocabularyVersion
|
|
!=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV1)return;
|
|
const auto isNativeGraphStrate=[this](std::int32_t strateIndex)
|
|
{
|
|
const auto found=std::find_if(ResolvedLayout->strates.begin(),
|
|
ResolvedLayout->strates.end(),[strateIndex](const auto& stratum)
|
|
{return stratum.strateIndex==strateIndex;});
|
|
return found!=ResolvedLayout->strates.end()
|
|
&&VoxelForgeCore::HasNativeGraphField(*found)
|
|
&&(VoxelForgeCore::IsNativeCaveFieldStratum(*found)
|
|
?found->passageCapabilities.hasNativeCaveField
|
|
:found->passageCapabilities.hasNativeLatticeField);
|
|
};
|
|
const float micronsPerVoxel=static_cast<float>(Owner->CoreVoxelSizeMicrons);
|
|
for(const auto& room:ResolvedLayout->cavernRooms)
|
|
{
|
|
FCavernRuntimeFeature runtime;runtime.room=&room;
|
|
runtime.nativeGraph=isNativeGraphStrate(room.strateIndex);
|
|
runtime.boundsMinimumMicrometres=room.boundsMinimumMicrometres;
|
|
runtime.boundsMaximumMicrometres=room.boundsMaximumMicrometres;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
runtime.centerMetres[axis]=room.centerMicrometres[axis]/1000000.0;
|
|
runtime.halfExtentsMetres[axis]=room.halfExtentsMicrometres[axis]/1000000.0;
|
|
runtime.centerVoxels[axis]=static_cast<float>(
|
|
static_cast<double>(room.centerMicrometres[axis])/micronsPerVoxel);
|
|
runtime.halfExtentsVoxels[axis]=static_cast<float>(
|
|
static_cast<double>(room.halfExtentsMicrometres[axis])/micronsPerVoxel);
|
|
}
|
|
runtime.floorVoxels=static_cast<float>(
|
|
static_cast<double>(room.floorMicrometres)/micronsPerVoxel);
|
|
runtime.smoothWidthVoxels=static_cast<float>(
|
|
static_cast<double>(room.smoothWidthMicrometres)/micronsPerVoxel);
|
|
runtime.irregularityAmplitudeVoxels=static_cast<float>(
|
|
static_cast<double>(room.irregularityAmplitudeMicrometres)/micronsPerVoxel);
|
|
runtime.irregularityWavelengthVoxels=static_cast<float>(
|
|
static_cast<double>(room.irregularityWavelengthMicrometres)/micronsPerVoxel);
|
|
runtime.irregularitySeed=room.irregularitySeed;
|
|
CavernRuntime.push_back(runtime);
|
|
}
|
|
for(const auto& edge:ResolvedLayout->cavernEdges)
|
|
{
|
|
FCavernRuntimeFeature runtime;runtime.edge=&edge;
|
|
runtime.nativeGraph=isNativeGraphStrate(edge.strateIndex);
|
|
runtime.boundsMinimumMicrometres=edge.boundsMinimumMicrometres;
|
|
runtime.boundsMaximumMicrometres=edge.boundsMaximumMicrometres;
|
|
for(std::size_t point=0;point<4;++point)
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
runtime.controlPointsVoxels[point][axis]=static_cast<float>(
|
|
static_cast<double>(edge.controlPointsMicrometres[point][axis])
|
|
/micronsPerVoxel);
|
|
runtime.boreRadiusVoxels=static_cast<float>(
|
|
static_cast<double>(edge.boreRadiusMicrometres)/micronsPerVoxel);
|
|
runtime.boreHeightVoxels=static_cast<float>(
|
|
static_cast<double>(edge.boreHeightMicrometres)/micronsPerVoxel);
|
|
runtime.floorSupportDepthVoxels=static_cast<float>(
|
|
static_cast<double>(edge.floorSupportDepthMicrometres)/micronsPerVoxel);
|
|
runtime.smoothWidthVoxels=static_cast<float>(
|
|
static_cast<double>(edge.smoothWidthMicrometres)/micronsPerVoxel);
|
|
runtime.irregularityAmplitudeVoxels=static_cast<float>(
|
|
static_cast<double>(edge.irregularityAmplitudeMicrometres)/micronsPerVoxel);
|
|
runtime.irregularityWavelengthVoxels=static_cast<float>(
|
|
static_cast<double>(edge.irregularityWavelengthMicrometres)/micronsPerVoxel);
|
|
runtime.irregularitySeed=edge.irregularitySeed;
|
|
runtime.segments.reserve(edge.polylineMicrometres.size()>1
|
|
?edge.polylineMicrometres.size()-1:0);
|
|
for(std::size_t point=1;point<edge.polylineMicrometres.size();++point)
|
|
{
|
|
FCavernRuntimeFeature::FSegment segment;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
segment.start[axis]=edge.polylineMicrometres[point-1][axis]/1000000.0;
|
|
segment.end[axis]=edge.polylineMicrometres[point][axis]/1000000.0;
|
|
segment.minimum[axis]=std::min(segment.start[axis],segment.end[axis]);
|
|
segment.maximum[axis]=std::max(segment.start[axis],segment.end[axis]);
|
|
}
|
|
runtime.minimumFloorMetres=std::min(runtime.minimumFloorMetres,
|
|
std::min(segment.start[2],segment.end[2]));
|
|
runtime.maximumFloorMetres=std::max(runtime.maximumFloorMetres,
|
|
std::max(segment.start[2],segment.end[2]));
|
|
const double horizontal=std::hypot(segment.end[0]-segment.start[0],
|
|
segment.end[1]-segment.start[1]);
|
|
if(horizontal>1.0e-9)
|
|
runtime.maximumSegmentGrade=std::max(runtime.maximumSegmentGrade,
|
|
std::abs((segment.end[2]-segment.start[2])/horizontal));
|
|
runtime.segments.push_back(segment);
|
|
}
|
|
runtime.segmentOrder.resize(runtime.segments.size());
|
|
for(std::size_t index=0;index<runtime.segmentOrder.size();++index)
|
|
runtime.segmentOrder[index]=static_cast<std::uint32_t>(index);
|
|
if(!runtime.segmentOrder.empty())
|
|
{
|
|
const auto buildSegmentNode=[&](const auto& self,std::size_t first,
|
|
std::size_t end)->int
|
|
{
|
|
FCavernRuntimeFeature::FSegmentNode node;
|
|
node.first=first;node.end=end;
|
|
node.minimum.fill(std::numeric_limits<double>::infinity());
|
|
node.maximum.fill(-std::numeric_limits<double>::infinity());
|
|
for(std::size_t offset=first;offset<end;++offset)
|
|
{
|
|
const auto& segment=runtime.segments[
|
|
runtime.segmentOrder[offset]];
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
node.minimum[axis]=std::min(node.minimum[axis],segment.minimum[axis]);
|
|
node.maximum[axis]=std::max(node.maximum[axis],segment.maximum[axis]);
|
|
}
|
|
}
|
|
const int nodeIndex=static_cast<int>(runtime.segmentNodes.size());
|
|
runtime.segmentNodes.push_back(node);
|
|
if(end-first>4)
|
|
{
|
|
std::size_t splitAxis=0;
|
|
for(std::size_t axis=1;axis<3;++axis)
|
|
if(node.maximum[axis]-node.minimum[axis]
|
|
>node.maximum[splitAxis]-node.minimum[splitAxis])
|
|
splitAxis=axis;
|
|
const std::size_t middle=first+(end-first)/2;
|
|
std::sort(runtime.segmentOrder.begin()+first,
|
|
runtime.segmentOrder.begin()+end,
|
|
[&](std::uint32_t a,std::uint32_t b)
|
|
{
|
|
const auto& left=runtime.segments[a];
|
|
const auto& right=runtime.segments[b];
|
|
const double ca=left.minimum[splitAxis]+left.maximum[splitAxis];
|
|
const double cb=right.minimum[splitAxis]+right.maximum[splitAxis];
|
|
return ca==cb?a<b:ca<cb;
|
|
});
|
|
const int left=self(self,first,middle);
|
|
const int right=self(self,middle,end);
|
|
runtime.segmentNodes[nodeIndex].left=left;
|
|
runtime.segmentNodes[nodeIndex].right=right;
|
|
}
|
|
return nodeIndex;
|
|
};
|
|
buildSegmentNode(buildSegmentNode,0,runtime.segmentOrder.size());
|
|
}
|
|
CavernRuntime.push_back(runtime);
|
|
}
|
|
std::stable_sort(CavernRuntime.begin(),CavernRuntime.end(),[](const auto& a,const auto& b)
|
|
{
|
|
const std::int32_t leftStrate=a.room?a.room->strateIndex
|
|
:a.edge?a.edge->strateIndex:-1;
|
|
const std::int32_t rightStrate=b.room?b.room->strateIndex
|
|
:b.edge?b.edge->strateIndex:-1;
|
|
if(leftStrate!=rightStrate)return leftStrate<rightStrate;
|
|
if(a.nativeGraph!=b.nativeGraph)return a.nativeGraph;
|
|
if(!a.nativeGraph)return false;
|
|
const std::string& left=a.room?a.room->stableId:a.edge->stableId;
|
|
const std::string& right=b.room?b.room->stableId:b.edge->stableId;
|
|
return left<right;
|
|
});
|
|
CavernSpatialOrder.resize(CavernRuntime.size());
|
|
for(std::size_t i=0;i<CavernSpatialOrder.size();++i)
|
|
CavernSpatialOrder[i]=static_cast<std::uint32_t>(i);
|
|
if(!CavernSpatialOrder.empty())BuildCavernSpatialNode(0,CavernSpatialOrder.size());
|
|
}
|
|
|
|
int BuildCavernSpatialNode(std::size_t first,std::size_t end)
|
|
{
|
|
FCavernSpatialNode node;node.first=first;node.end=end;
|
|
node.minimumMicrometres.fill(std::numeric_limits<std::int64_t>::max());
|
|
node.maximumMicrometres.fill(std::numeric_limits<std::int64_t>::min());
|
|
for(std::size_t offset=first;offset<end;++offset)
|
|
{
|
|
const auto& feature=CavernRuntime[CavernSpatialOrder[offset]];
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
node.minimumMicrometres[axis]=std::min(node.minimumMicrometres[axis],
|
|
feature.boundsMinimumMicrometres[axis]);
|
|
node.maximumMicrometres[axis]=std::max(node.maximumMicrometres[axis],
|
|
feature.boundsMaximumMicrometres[axis]);
|
|
}
|
|
}
|
|
const int index=static_cast<int>(CavernSpatialNodes.size());
|
|
CavernSpatialNodes.push_back(node);
|
|
if(end-first>8)
|
|
{
|
|
std::size_t axis=0;
|
|
for(std::size_t candidate=1;candidate<3;++candidate)
|
|
if(static_cast<long double>(node.maximumMicrometres[candidate])
|
|
-node.minimumMicrometres[candidate]
|
|
>static_cast<long double>(node.maximumMicrometres[axis])
|
|
-node.minimumMicrometres[axis])axis=candidate;
|
|
const std::size_t middle=first+(end-first)/2;
|
|
std::nth_element(CavernSpatialOrder.begin()+first,
|
|
CavernSpatialOrder.begin()+middle,CavernSpatialOrder.begin()+end,
|
|
[this,axis](std::uint32_t leftIndex,std::uint32_t rightIndex)
|
|
{
|
|
const auto& left=CavernRuntime[leftIndex];
|
|
const auto& right=CavernRuntime[rightIndex];
|
|
const long double leftCenter=static_cast<long double>(
|
|
left.boundsMinimumMicrometres[axis])
|
|
+left.boundsMaximumMicrometres[axis];
|
|
const long double rightCenter=static_cast<long double>(
|
|
right.boundsMinimumMicrometres[axis])
|
|
+right.boundsMaximumMicrometres[axis];
|
|
const std::string& leftId=left.room?left.room->stableId:left.edge->stableId;
|
|
const std::string& rightId=right.room?right.room->stableId:right.edge->stableId;
|
|
return leftCenter==rightCenter?leftId<rightId:leftCenter<rightCenter;
|
|
});
|
|
CavernSpatialNodes[index].left=BuildCavernSpatialNode(first,middle);
|
|
CavernSpatialNodes[index].right=BuildCavernSpatialNode(middle,end);
|
|
}
|
|
return index;
|
|
}
|
|
|
|
void QueryCavernSpatialNode(int nodeIndex,
|
|
const std::array<std::int64_t,3>& minimum,
|
|
const std::array<std::int64_t,3>& maximum,
|
|
std::vector<std::uint32_t>& result) const
|
|
{
|
|
if(nodeIndex<0)return;
|
|
const auto& node=CavernSpatialNodes[static_cast<std::size_t>(nodeIndex)];
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
if(node.maximumMicrometres[axis]<minimum[axis]
|
|
||node.minimumMicrometres[axis]>maximum[axis])return;
|
|
if(node.left<0)
|
|
{
|
|
for(std::size_t offset=node.first;offset<node.end;++offset)
|
|
{
|
|
const auto index=CavernSpatialOrder[offset];
|
|
const auto& feature=CavernRuntime[index];bool overlaps=true;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
overlaps=overlaps&&feature.boundsMinimumMicrometres[axis]<=maximum[axis]
|
|
&&feature.boundsMaximumMicrometres[axis]>=minimum[axis];
|
|
if(overlaps)result.push_back(index);
|
|
}
|
|
return;
|
|
}
|
|
QueryCavernSpatialNode(node.left,minimum,maximum,result);
|
|
QueryCavernSpatialNode(node.right,minimum,maximum,result);
|
|
}
|
|
|
|
std::vector<std::uint32_t> BuildCavernFeatureTileList(
|
|
const std::array<std::int64_t,3>& minimumSubvoxels,
|
|
const std::array<std::int64_t,3>& maximumSubvoxels) const
|
|
{
|
|
std::vector<std::uint32_t> result;
|
|
if(CavernRuntime.empty()||CavernSpatialNodes.empty())return result;
|
|
if(!Owner||Owner->CoreVoxelSizeMicrons==0)
|
|
{
|
|
result.resize(CavernRuntime.size());
|
|
for(std::size_t i=0;i<result.size();++i)result[i]=static_cast<std::uint32_t>(i);
|
|
return result;
|
|
}
|
|
constexpr long double Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
std::array<std::int64_t,3> minimumMicrometres{},maximumMicrometres{};
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
if(minimumSubvoxels[axis]>maximumSubvoxels[axis])
|
|
{
|
|
result.resize(CavernRuntime.size());
|
|
for(std::size_t i=0;i<result.size();++i)
|
|
result[i]=static_cast<std::uint32_t>(i);
|
|
return result;
|
|
}
|
|
minimumMicrometres[axis]=static_cast<std::int64_t>(std::floor(
|
|
static_cast<long double>(minimumSubvoxels[axis])
|
|
*Owner->CoreVoxelSizeMicrons/Q));
|
|
maximumMicrometres[axis]=static_cast<std::int64_t>(std::ceil(
|
|
static_cast<long double>(maximumSubvoxels[axis])
|
|
*Owner->CoreVoxelSizeMicrons/Q));
|
|
}
|
|
QueryCavernSpatialNode(0,minimumMicrometres,maximumMicrometres,result);
|
|
// Runtime order is legacy insertion order for legacy cavern graphs and
|
|
// canonical stable-ID order within native caves strates.
|
|
std::sort(result.begin(),result.end());
|
|
return result;
|
|
}
|
|
|
|
int BuildPassageNearestNode(std::size_t passageIndex,std::vector<std::size_t> segments)
|
|
{
|
|
const auto& points=ResolvedLayout->passages[passageIndex].controlPoints;
|
|
FPassageNearestNode node;
|
|
node.minimum.fill(std::numeric_limits<double>::infinity());
|
|
node.maximum.fill(-std::numeric_limits<double>::infinity());
|
|
for(const std::size_t segment:segments)
|
|
{
|
|
const auto& a=points[segment];const auto& b=points[segment+1];
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
node.minimum[axis]=std::min({node.minimum[axis],
|
|
static_cast<double>(a.positionSubvoxels[axis]),
|
|
static_cast<double>(b.positionSubvoxels[axis])});
|
|
node.maximum[axis]=std::max({node.maximum[axis],
|
|
static_cast<double>(a.positionSubvoxels[axis]),
|
|
static_cast<double>(b.positionSubvoxels[axis])});
|
|
}
|
|
node.maximumRadius=std::max({node.maximumRadius,
|
|
static_cast<double>(a.radiusSubvoxels),static_cast<double>(b.radiusSubvoxels)});
|
|
}
|
|
node.segments=segments;
|
|
std::sort(node.segments.begin(),node.segments.end());
|
|
const int nodeIndex=static_cast<int>(PassageNearestNodes[passageIndex].size());
|
|
PassageNearestNodes[passageIndex].push_back(std::move(node));
|
|
if(segments.size()>4)
|
|
{
|
|
std::size_t splitAxis=0;
|
|
for(std::size_t axis=1;axis<3;++axis)
|
|
if(PassageNearestNodes[passageIndex][nodeIndex].maximum[axis]
|
|
-PassageNearestNodes[passageIndex][nodeIndex].minimum[axis]
|
|
>PassageNearestNodes[passageIndex][nodeIndex].maximum[splitAxis]
|
|
-PassageNearestNodes[passageIndex][nodeIndex].minimum[splitAxis])splitAxis=axis;
|
|
const std::size_t middle=segments.size()/2;
|
|
const auto center=[&](std::size_t segment)
|
|
{return static_cast<double>(points[segment].positionSubvoxels[splitAxis])
|
|
+static_cast<double>(points[segment+1].positionSubvoxels[splitAxis]);};
|
|
std::nth_element(segments.begin(),segments.begin()+middle,segments.end(),
|
|
[&](std::size_t a,std::size_t b)
|
|
{const double ca=center(a),cb=center(b);return ca==cb?a<b:ca<cb;});
|
|
std::vector<std::size_t> leftSegments(segments.begin(),segments.begin()+middle);
|
|
std::vector<std::size_t> rightSegments(segments.begin()+middle,segments.end());
|
|
const int left=BuildPassageNearestNode(passageIndex,std::move(leftSegments));
|
|
const int right=BuildPassageNearestNode(passageIndex,std::move(rightSegments));
|
|
PassageNearestNodes[passageIndex][nodeIndex].left=left;
|
|
PassageNearestNodes[passageIndex][nodeIndex].right=right;
|
|
}
|
|
return nodeIndex;
|
|
}
|
|
|
|
int BuildPassageProofNode(std::size_t first,std::size_t end)
|
|
{
|
|
FPassageProofNode node;node.first=first;node.end=end;
|
|
node.minimum.fill(std::numeric_limits<double>::infinity());
|
|
node.maximum.fill(-std::numeric_limits<double>::infinity());
|
|
for(std::size_t index=first;index<end;++index)
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
node.minimum[axis]=std::min(node.minimum[axis],PassageProofBounds[index].minimum[axis]);
|
|
node.maximum[axis]=std::max(node.maximum[axis],PassageProofBounds[index].maximum[axis]);
|
|
}
|
|
const int nodeIndex=static_cast<int>(PassageProofNodes.size());
|
|
PassageProofNodes.push_back(node);
|
|
if(end-first>8)
|
|
{
|
|
std::size_t splitAxis=0;
|
|
for(std::size_t axis=1;axis<3;++axis)
|
|
if(node.maximum[axis]-node.minimum[axis]
|
|
>node.maximum[splitAxis]-node.minimum[splitAxis])splitAxis=axis;
|
|
const std::size_t middle=first+(end-first)/2;
|
|
std::nth_element(PassageProofBounds.begin()+first,PassageProofBounds.begin()+middle,
|
|
PassageProofBounds.begin()+end,[splitAxis](const auto& a,const auto& b)
|
|
{return a.minimum[splitAxis]+a.maximum[splitAxis]
|
|
<b.minimum[splitAxis]+b.maximum[splitAxis];});
|
|
const int left=BuildPassageProofNode(first,middle);
|
|
const int right=BuildPassageProofNode(middle,end);
|
|
PassageProofNodes[nodeIndex].left=left;
|
|
PassageProofNodes[nodeIndex].right=right;
|
|
}
|
|
return nodeIndex;
|
|
}
|
|
|
|
int BuildPassageFeatureNode(std::size_t first,std::size_t end)
|
|
{
|
|
FPassageFeatureNode node;node.first=first;node.end=end;
|
|
node.minimum.fill(std::numeric_limits<std::int64_t>::max());
|
|
node.maximum.fill(std::numeric_limits<std::int64_t>::min());
|
|
for(std::size_t offset=first;offset<end;++offset)
|
|
{
|
|
const auto& feature=PassageFeatureBounds[PassageFeatureOrder[offset]];
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
node.minimum[axis]=std::min(node.minimum[axis],feature.minimumSubvoxels[axis]);
|
|
node.maximum[axis]=std::max(node.maximum[axis],feature.maximumSubvoxels[axis]);
|
|
}
|
|
}
|
|
const int nodeIndex=static_cast<int>(PassageFeatureNodes.size());
|
|
PassageFeatureNodes.push_back(node);
|
|
if(end-first>8)
|
|
{
|
|
std::size_t splitAxis=0;
|
|
for(std::size_t axis=1;axis<3;++axis)
|
|
if(static_cast<long double>(node.maximum[axis])-node.minimum[axis]
|
|
>static_cast<long double>(node.maximum[splitAxis])-node.minimum[splitAxis])
|
|
splitAxis=axis;
|
|
const std::size_t middle=first+(end-first)/2;
|
|
std::nth_element(PassageFeatureOrder.begin()+first,PassageFeatureOrder.begin()+middle,
|
|
PassageFeatureOrder.begin()+end,[this,splitAxis](std::uint32_t a,std::uint32_t b)
|
|
{
|
|
const auto& left=PassageFeatureBounds[a];
|
|
const auto& right=PassageFeatureBounds[b];
|
|
const double leftCenter=static_cast<double>(left.minimumSubvoxels[splitAxis])*0.5
|
|
+static_cast<double>(left.maximumSubvoxels[splitAxis])*0.5;
|
|
const double rightCenter=static_cast<double>(right.minimumSubvoxels[splitAxis])*0.5
|
|
+static_cast<double>(right.maximumSubvoxels[splitAxis])*0.5;
|
|
return leftCenter==rightCenter?a<b:leftCenter<rightCenter;
|
|
});
|
|
const int left=BuildPassageFeatureNode(first,middle);
|
|
const int right=BuildPassageFeatureNode(middle,end);
|
|
PassageFeatureNodes[nodeIndex].left=left;
|
|
PassageFeatureNodes[nodeIndex].right=right;
|
|
}
|
|
return nodeIndex;
|
|
}
|
|
|
|
void QueryPassageFeatureNode(int nodeIndex,
|
|
const std::array<std::int64_t,3>& minimum,
|
|
const std::array<std::int64_t,3>& maximum,
|
|
std::vector<std::uint32_t>& result) const
|
|
{
|
|
if(nodeIndex<0)return;
|
|
const auto& node=PassageFeatureNodes[static_cast<std::size_t>(nodeIndex)];
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
if(node.maximum[axis]<minimum[axis]||node.minimum[axis]>maximum[axis])return;
|
|
if(node.left<0)
|
|
{
|
|
for(std::size_t offset=node.first;offset<node.end;++offset)
|
|
{
|
|
const std::uint32_t featureIndex=PassageFeatureOrder[offset];
|
|
const auto& feature=PassageFeatureBounds[featureIndex];
|
|
bool overlaps=true;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
overlaps=overlaps&&feature.minimumSubvoxels[axis]<=maximum[axis]
|
|
&&feature.maximumSubvoxels[axis]>=minimum[axis];
|
|
if(overlaps)result.push_back(featureIndex);
|
|
}
|
|
return;
|
|
}
|
|
QueryPassageFeatureNode(node.left,minimum,maximum,result);
|
|
QueryPassageFeatureNode(node.right,minimum,maximum,result);
|
|
}
|
|
|
|
std::vector<std::uint32_t> BuildPassageFeatureTileList(
|
|
const std::array<std::int64_t,3>& minimum,
|
|
const std::array<std::int64_t,3>& maximum) const
|
|
{
|
|
std::vector<std::uint32_t> result;
|
|
if(PassageFeatureBounds.empty())return result;
|
|
bool failOpen=PassageFeatureBoundsFailOpen;
|
|
for(std::size_t axis=0;axis<3;++axis)failOpen=failOpen||minimum[axis]>maximum[axis];
|
|
if(failOpen)
|
|
{
|
|
result.resize(PassageFeatureBounds.size());
|
|
for(std::size_t i=0;i<result.size();++i)result[i]=static_cast<std::uint32_t>(i);
|
|
return result;
|
|
}
|
|
if(!PassageFeatureNodes.empty())QueryPassageFeatureNode(0,minimum,maximum,result);
|
|
return result;
|
|
}
|
|
|
|
void BuildPassageSpatialIndices()
|
|
{
|
|
BuildStampSpatialIndex();
|
|
BuildCavernSpatialIndex();
|
|
PassageTileFeatures.clear();PassageFeatureBounds.clear();
|
|
PassageFeatureOrder.clear();PassageFeatureNodes.clear();
|
|
PassageFeatureBoundsFailOpen=false;
|
|
PassageNearestNodes.clear();
|
|
PassageProofBounds.clear();PassageProofNodes.clear();
|
|
PassageNearestNodes.resize(ResolvedLayout?ResolvedLayout->passages.size():0);
|
|
PassageColumnSegmentFlags.clear();PassageShortSpanSegmentFlags.clear();
|
|
PassageHasShortSpanFlags.clear();
|
|
if(!ResolvedLayout||ResolvedLayout->passages.empty())return;
|
|
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
|
const double voxelsPerMetre=Owner&&Owner->CoreVoxelSizeMicrons>0
|
|
?1000000.0/Owner->CoreVoxelSizeMicrons:4.0;
|
|
const double bridgeHalfWidthVoxels=3.5*voxelsPerMetre;
|
|
const double bridgeFootDepthVoxels=(5.0-0.13)*voxelsPerMetre;
|
|
PassageColumnSegmentFlags.resize(ResolvedLayout->passages.size());
|
|
PassageShortSpanSegmentFlags.resize(ResolvedLayout->passages.size());
|
|
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
|
{
|
|
const auto& passage=ResolvedLayout->passages[passageIndex];
|
|
const std::size_t segmentCount=passage.controlPoints.size()>1
|
|
?passage.controlPoints.size()-1:0;
|
|
PassageColumnSegmentFlags[passageIndex].assign(segmentCount,0);
|
|
PassageShortSpanSegmentFlags[passageIndex].assign(segmentCount,0);
|
|
if(segmentCount>0)
|
|
{
|
|
std::vector<std::size_t> segmentIds(segmentCount);
|
|
for(std::size_t i=0;i<segmentCount;++i)segmentIds[i]=i;
|
|
BuildPassageNearestNode(passageIndex,std::move(segmentIds));
|
|
}
|
|
for(std::size_t segment=0;segment<segmentCount;++segment)
|
|
{
|
|
const auto& a=passage.controlPoints[segment];
|
|
const auto& b=passage.controlPoints[segment+1];
|
|
const double radius=static_cast<double>(std::max(a.radiusSubvoxels,b.radiusSubvoxels))/Q;
|
|
const double horizontalExtent=std::max(radius,bridgeHalfWidthVoxels);
|
|
const double footDepth=std::max(voxelsPerMetre,bridgeFootDepthVoxels);
|
|
FPassageProofBound proof;
|
|
proof.minimum={std::min(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q-horizontalExtent,
|
|
std::min(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q-horizontalExtent,
|
|
std::min(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q-radius-footDepth};
|
|
proof.maximum={std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+horizontalExtent,
|
|
std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+horizontalExtent,
|
|
std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius};
|
|
PassageProofBounds.push_back(proof);
|
|
const std::array<std::int64_t,3> featureMin={
|
|
static_cast<std::int64_t>(std::floor(proof.minimum[0]*Q)),
|
|
static_cast<std::int64_t>(std::floor(proof.minimum[1]*Q)),
|
|
static_cast<std::int64_t>(std::floor(proof.minimum[2]*Q))};
|
|
const std::array<std::int64_t,3> featureMax={
|
|
static_cast<std::int64_t>(std::ceil(proof.maximum[0]*Q)),
|
|
static_cast<std::int64_t>(std::ceil(proof.maximum[1]*Q)),
|
|
static_cast<std::int64_t>(std::ceil(proof.maximum[2]*Q))};
|
|
PassageFeatureBounds.push_back({static_cast<std::uint64_t>(PassageTileFeatures.size()),
|
|
featureMin,featureMax});
|
|
PassageTileFeatures.push_back({passageIndex,segment,false});
|
|
}
|
|
const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(),
|
|
ResolvedLayout->strates.end(),[&passage](const auto& value)
|
|
{return value.strateIndex==passage.upperStrateIndex;});
|
|
if(upperStratum!=ResolvedLayout->strates.end())
|
|
{
|
|
std::array<const VoxelForgeCore::ResolvedLayoutAnchorV2*,5> route{};
|
|
std::size_t routeCount=0;
|
|
for(const auto& anchor:upperStratum->anchors)
|
|
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
|
{
|
|
if(routeCount<route.size())route[routeCount++]=&anchor;
|
|
else
|
|
{
|
|
std::move(route.begin()+1,route.end(),route.begin());
|
|
route.back()=&anchor;
|
|
}
|
|
}
|
|
if(routeCount>=2)
|
|
{
|
|
const double mouthReach=13.0*voxelsPerMetre*Q;
|
|
double minimumZ=std::numeric_limits<double>::infinity();
|
|
double maximumZ=-std::numeric_limits<double>::infinity();
|
|
for(std::size_t index=0;index<routeCount;++index)
|
|
{
|
|
minimumZ=std::min(minimumZ,static_cast<double>(route[index]->values[2]));
|
|
maximumZ=std::max(maximumZ,static_cast<double>(route[index]->values[2]));
|
|
}
|
|
const double mouthX=passage.upperMouthSubvoxels[0];
|
|
const double mouthY=passage.upperMouthSubvoxels[1];
|
|
FPassageProofBound supportProof;
|
|
supportProof.minimum={(mouthX-mouthReach)/Q,(mouthY-mouthReach)/Q,
|
|
(minimumZ-3.0*voxelsPerMetre*Q)/Q};
|
|
supportProof.maximum={(mouthX+mouthReach)/Q,(mouthY+mouthReach)/Q,
|
|
(maximumZ+3.0*voxelsPerMetre*Q)/Q};
|
|
PassageProofBounds.push_back(supportProof);
|
|
const std::array<std::int64_t,3> featureMin={
|
|
static_cast<std::int64_t>(std::floor(supportProof.minimum[0]*Q)),
|
|
static_cast<std::int64_t>(std::floor(supportProof.minimum[1]*Q)),
|
|
static_cast<std::int64_t>(std::floor(supportProof.minimum[2]*Q))};
|
|
const std::array<std::int64_t,3> featureMax={
|
|
static_cast<std::int64_t>(std::ceil(supportProof.maximum[0]*Q)),
|
|
static_cast<std::int64_t>(std::ceil(supportProof.maximum[1]*Q)),
|
|
static_cast<std::int64_t>(std::ceil(supportProof.maximum[2]*Q))};
|
|
PassageFeatureBounds.push_back({static_cast<std::uint64_t>(PassageTileFeatures.size()),
|
|
featureMin,featureMax});
|
|
PassageTileFeatures.push_back({passageIndex,0,false,true});
|
|
}
|
|
}
|
|
}
|
|
for(const auto& crossing:PassageVoidCrossings)
|
|
{
|
|
if(crossing.passageIndex<PassageColumnSegmentFlags.size())
|
|
{
|
|
auto& flags= crossing.needsColumn
|
|
?PassageColumnSegmentFlags[crossing.passageIndex]
|
|
:PassageShortSpanSegmentFlags[crossing.passageIndex];
|
|
bool surfaceWorldUpper=false;
|
|
if(crossing.passageIndex<ResolvedLayout->passages.size())
|
|
{
|
|
const std::int32_t strate=ResolvedLayout->passages[
|
|
crossing.passageIndex].upperStrateIndex;
|
|
const auto upper=std::find_if(ResolvedLayout->strates.begin(),
|
|
ResolvedLayout->strates.end(),[strate](const auto& value)
|
|
{return value.strateIndex==strate;});
|
|
surfaceWorldUpper=upper!=ResolvedLayout->strates.end()
|
|
&&upper->passageCapabilities.hasSurfaceLanding;
|
|
}
|
|
const std::size_t shortSpanPadding=!crossing.needsColumn
|
|
&&surfaceWorldUpper&&!flags.empty()?2:0;
|
|
const std::size_t first=std::min(crossing.firstSegment>shortSpanPadding
|
|
?crossing.firstSegment-shortSpanPadding:0,flags.size());
|
|
const std::size_t last=std::min(crossing.lastSegment+shortSpanPadding,
|
|
flags.empty()?0:flags.size()-1);
|
|
if(!flags.empty()&&first<=last)
|
|
std::fill(flags.begin()+first,flags.begin()+last+1,1);
|
|
}
|
|
if(!crossing.needsColumn)continue;
|
|
FPassageProofBound proof;
|
|
const double radius=static_cast<double>(crossing.maximumRadiusSubvoxels)/Q;
|
|
proof.minimum={crossing.centerXSubvoxels/Q-radius,crossing.centerYSubvoxels/Q-radius,
|
|
crossing.bottomZSubvoxels/Q};
|
|
proof.maximum={crossing.centerXSubvoxels/Q+radius,crossing.centerYSubvoxels/Q+radius,
|
|
crossing.topZSubvoxels/Q};
|
|
PassageProofBounds.push_back(proof);
|
|
const std::array<std::int64_t,3> featureMin={
|
|
static_cast<std::int64_t>(std::floor(proof.minimum[0]*Q)),
|
|
static_cast<std::int64_t>(std::floor(proof.minimum[1]*Q)),
|
|
static_cast<std::int64_t>(std::floor(proof.minimum[2]*Q))};
|
|
const std::array<std::int64_t,3> featureMax={
|
|
static_cast<std::int64_t>(std::ceil(proof.maximum[0]*Q)),
|
|
static_cast<std::int64_t>(std::ceil(proof.maximum[1]*Q)),
|
|
static_cast<std::int64_t>(std::ceil(proof.maximum[2]*Q))};
|
|
PassageFeatureBounds.push_back({static_cast<std::uint64_t>(PassageTileFeatures.size()),
|
|
featureMin,featureMax});
|
|
PassageTileFeatures.push_back({crossing.passageIndex,0,true});
|
|
}
|
|
PassageHasShortSpanFlags.assign(PassageShortSpanSegmentFlags.size(),0);
|
|
for(std::size_t passageIndex=0;passageIndex<PassageShortSpanSegmentFlags.size();++passageIndex)
|
|
PassageHasShortSpanFlags[passageIndex]=static_cast<std::uint8_t>(
|
|
std::any_of(PassageShortSpanSegmentFlags[passageIndex].begin(),
|
|
PassageShortSpanSegmentFlags[passageIndex].end(),
|
|
[](std::uint8_t flag){return flag!=0;}));
|
|
for(std::size_t i=0;i<PassageFeatureBounds.size();++i)
|
|
PassageFeatureOrder.push_back(static_cast<std::uint32_t>(i));
|
|
for(const auto& feature:PassageFeatureBounds)
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
PassageFeatureBoundsFailOpen=PassageFeatureBoundsFailOpen
|
|
||feature.minimumSubvoxels[axis]>feature.maximumSubvoxels[axis];
|
|
if(!PassageFeatureOrder.empty())BuildPassageFeatureNode(0,PassageFeatureOrder.size());
|
|
if(!PassageProofBounds.empty())BuildPassageProofNode(0,PassageProofBounds.size());
|
|
}
|
|
|
|
double PassageNearestNodeLowerBound(const FPassageNearestNode& node,
|
|
double qx,double qy,double qz) const
|
|
{
|
|
const double dx=qx<node.minimum[0]?node.minimum[0]-qx:qx>node.maximum[0]?qx-node.maximum[0]:0.0;
|
|
const double dy=qy<node.minimum[1]?node.minimum[1]-qy:qy>node.maximum[1]?qy-node.maximum[1]:0.0;
|
|
const double dz=qz<node.minimum[2]?node.minimum[2]-qz:qz>node.maximum[2]?qz-node.maximum[2]:0.0;
|
|
return DeterministicSqrt(dx*dx+dy*dy+dz*dz)-node.maximumRadius-1.0e-6;
|
|
}
|
|
|
|
template<typename FVisit>
|
|
void VisitNearestPassageSpatialNode(std::size_t passageIndex,int nodeIndex,
|
|
std::size_t first,std::size_t end,double qx,double qy,double qz,int priority,
|
|
double& bestDistance,FVisit& visit) const
|
|
{
|
|
if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return;
|
|
const auto& nodes=PassageNearestNodes[passageIndex];
|
|
const auto& node=nodes[static_cast<std::size_t>(nodeIndex)];
|
|
const auto candidate=std::lower_bound(node.segments.begin(),node.segments.end(),first);
|
|
if(candidate==node.segments.end()||*candidate>=end)return;
|
|
const double lowerBound=PassageNearestNodeLowerBound(node,qx,qy,qz);
|
|
if(lowerBound>bestDistance)return;
|
|
if(node.left<0)
|
|
{
|
|
for(auto it=candidate;it!=node.segments.end()&&*it<end;++it)
|
|
visit(*it,priority);
|
|
return;
|
|
}
|
|
const double leftBound=PassageNearestNodeLowerBound(
|
|
nodes[static_cast<std::size_t>(node.left)],qx,qy,qz);
|
|
const double rightBound=PassageNearestNodeLowerBound(
|
|
nodes[static_cast<std::size_t>(node.right)],qx,qy,qz);
|
|
const int nearChild=leftBound<=rightBound?node.left:node.right;
|
|
const int farChild=leftBound<=rightBound?node.right:node.left;
|
|
VisitNearestPassageSpatialNode(passageIndex,nearChild,first,end,qx,qy,qz,
|
|
priority,bestDistance,visit);
|
|
VisitNearestPassageSpatialNode(passageIndex,farChild,first,end,qx,qy,qz,
|
|
priority,bestDistance,visit);
|
|
}
|
|
|
|
template<typename FVisit>
|
|
void VisitNearestPassageSegmentRange(std::size_t passageIndex,std::ptrdiff_t first,
|
|
std::ptrdiff_t last,double qx,double qy,double qz,int priority,
|
|
double& bestDistance,FVisit&& visit) const
|
|
{
|
|
if(first>last||passageIndex>=PassageNearestNodes.size()
|
|
||PassageNearestNodes[passageIndex].empty())return;
|
|
auto callback=std::forward<FVisit>(visit);
|
|
VisitNearestPassageSpatialNode(passageIndex,0,static_cast<std::size_t>(first),
|
|
static_cast<std::size_t>(last+1),qx,qy,qz,priority,bestDistance,callback);
|
|
}
|
|
|
|
bool PassagePotentialWithinNode(std::size_t passageIndex,int nodeIndex,
|
|
std::size_t first,std::size_t end,double qx,double qy,double qz,
|
|
double signedDistanceLimit) const
|
|
{
|
|
if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return false;
|
|
const auto& nodes=PassageNearestNodes[passageIndex];
|
|
const auto& node=nodes[static_cast<std::size_t>(nodeIndex)];
|
|
const auto candidate=std::lower_bound(node.segments.begin(),node.segments.end(),first);
|
|
if(candidate==node.segments.end()||*candidate>=end)return false;
|
|
if(PassageNearestNodeLowerBound(node,qx,qy,qz)>signedDistanceLimit)return false;
|
|
if(node.left<0)return true;
|
|
const double leftBound=PassageNearestNodeLowerBound(
|
|
nodes[static_cast<std::size_t>(node.left)],qx,qy,qz);
|
|
const double rightBound=PassageNearestNodeLowerBound(
|
|
nodes[static_cast<std::size_t>(node.right)],qx,qy,qz);
|
|
const int nearChild=leftBound<=rightBound?node.left:node.right;
|
|
const int farChild=leftBound<=rightBound?node.right:node.left;
|
|
return PassagePotentialWithinNode(passageIndex,nearChild,first,end,qx,qy,qz,
|
|
signedDistanceLimit)
|
|
||PassagePotentialWithinNode(passageIndex,farChild,first,end,qx,qy,qz,
|
|
signedDistanceLimit);
|
|
}
|
|
|
|
bool PassageMayAffectDensity(std::size_t passageIndex,std::ptrdiff_t carveBegin,
|
|
std::ptrdiff_t carveEnd,std::ptrdiff_t lowerMouthIndex,
|
|
std::ptrdiff_t mouthSearchBegin,std::ptrdiff_t mouthSearchEnd,std::ptrdiff_t lastIndex,
|
|
double qx,double qy,double qz,double signedDistanceLimit) const
|
|
{
|
|
const auto possible=[&](std::ptrdiff_t first,std::ptrdiff_t last)
|
|
{
|
|
return first<=last&&passageIndex<PassageNearestNodes.size()
|
|
&&!PassageNearestNodes[passageIndex].empty()
|
|
&&PassagePotentialWithinNode(passageIndex,0,static_cast<std::size_t>(first),
|
|
static_cast<std::size_t>(last+1),qx,qy,qz,signedDistanceLimit);
|
|
};
|
|
return possible(carveBegin,carveEnd)
|
|
||possible(0,std::min<std::ptrdiff_t>(8,lowerMouthIndex)-1)
|
|
||possible(mouthSearchBegin,mouthSearchEnd-1)
|
|
||possible(lowerMouthIndex,lastIndex-1);
|
|
}
|
|
|
|
bool PassagePlayerCapsuleBlocksSupportNode(std::size_t passageIndex,int nodeIndex,
|
|
std::size_t first,std::size_t end,double qx,double qy,double qz,double qPerMetre,
|
|
double minimumClearanceMetres) const
|
|
{
|
|
if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return false;
|
|
const auto& nodes=PassageNearestNodes[passageIndex];
|
|
const auto& node=nodes[static_cast<std::size_t>(nodeIndex)];
|
|
auto candidate=std::lower_bound(node.segments.begin(),node.segments.end(),first);
|
|
if(candidate==node.segments.end()||*candidate>=end)return false;
|
|
const double xyReach=0.39*qPerMetre;
|
|
if(qx+xyReach<node.minimum[0]||qx-xyReach>node.maximum[0]
|
|
||qy+xyReach<node.minimum[1]||qy-xyReach>node.maximum[1])return false;
|
|
const double minimumCenterZ=node.minimum[2]-node.maximumRadius+0.20*qPerMetre;
|
|
const double maximumCenterZ=node.maximum[2]+2.06*qPerMetre;
|
|
if(qz<minimumCenterZ||qz>maximumCenterZ)return false;
|
|
const auto& passage=ResolvedLayout->passages[passageIndex];
|
|
if(node.left<0)
|
|
{
|
|
for(auto it=candidate;it!=node.segments.end()&&*it<end;++it)
|
|
{
|
|
const std::size_t index=*it;
|
|
const auto& a=passage.controlPoints[index];const auto& b=passage.controlPoints[index+1];
|
|
const double ax=a.positionSubvoxels[0],ay=a.positionSubvoxels[1],az=a.positionSubvoxels[2];
|
|
const double dx=static_cast<double>(b.positionSubvoxels[0])-ax;
|
|
const double dy=static_cast<double>(b.positionSubvoxels[1])-ay;
|
|
const double dz=static_cast<double>(b.positionSubvoxels[2])-az;
|
|
const double lengthSq=dx*dx+dy*dy+dz*dz;
|
|
if(lengthSq<=0.0)continue;
|
|
const double horizontalLengthSq=dx*dx+dy*dy;
|
|
const double t=horizontalLengthSq>0.0
|
|
?std::clamp(((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):0.0;
|
|
const double centerX=ax+t*dx,centerY=ay+t*dy;
|
|
const double centerZ=az+t*dz-(static_cast<double>(a.radiusSubvoxels)
|
|
+t*(static_cast<double>(b.radiusSubvoxels)-a.radiusSubvoxels))+1.13*qPerMetre;
|
|
const double horizontalX=qx-centerX,horizontalY=qy-centerY;
|
|
const double vertical=std::max(0.0,std::abs(qz-centerZ)-0.54*qPerMetre);
|
|
const double distance=DeterministicSqrt(horizontalX*horizontalX+horizontalY*horizontalY
|
|
+vertical*vertical)-0.34*qPerMetre;
|
|
if(distance<minimumClearanceMetres*qPerMetre)return true;
|
|
}
|
|
return false;
|
|
}
|
|
return PassagePlayerCapsuleBlocksSupportNode(passageIndex,node.left,first,end,
|
|
qx,qy,qz,qPerMetre,minimumClearanceMetres)
|
|
||PassagePlayerCapsuleBlocksSupportNode(passageIndex,node.right,first,end,
|
|
qx,qy,qz,qPerMetre,minimumClearanceMetres);
|
|
}
|
|
|
|
bool PassagePlayerCapsuleBlocksSupport(std::size_t passageIndex,
|
|
std::ptrdiff_t carveBegin,std::ptrdiff_t carveEnd,std::ptrdiff_t lowerMouthIndex,
|
|
std::ptrdiff_t mouthSearchBegin,std::ptrdiff_t mouthSearchEnd,std::ptrdiff_t lastIndex,
|
|
double qx,double qy,double qz,double qPerMetre,double minimumClearanceMetres) const
|
|
{
|
|
const auto blocked=[&](std::ptrdiff_t first,std::ptrdiff_t last)
|
|
{
|
|
return first<=last&&PassagePlayerCapsuleBlocksSupportNode(passageIndex,0,
|
|
static_cast<std::size_t>(first),static_cast<std::size_t>(last+1),
|
|
qx,qy,qz,qPerMetre,minimumClearanceMetres);
|
|
};
|
|
return blocked(carveBegin,carveEnd)
|
|
||blocked(0,std::min<std::ptrdiff_t>(8,lowerMouthIndex)-1)
|
|
||blocked(mouthSearchBegin,mouthSearchEnd-1)
|
|
||blocked(lowerMouthIndex,lastIndex-1);
|
|
}
|
|
|
|
bool PassageProofBoundsOverlapNode(int nodeIndex,const double minimum[3],
|
|
const double maximum[3]) const
|
|
{
|
|
if(nodeIndex<0)return false;
|
|
const auto& node=PassageProofNodes[static_cast<std::size_t>(nodeIndex)];
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
if(node.maximum[axis]<minimum[axis]||node.minimum[axis]>maximum[axis])return false;
|
|
if(node.left<0)
|
|
{
|
|
for(std::size_t index=node.first;index<node.end;++index)
|
|
{
|
|
bool overlap=true;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
overlap=overlap&&PassageProofBounds[index].maximum[axis]>=minimum[axis]
|
|
&&PassageProofBounds[index].minimum[axis]<=maximum[axis];
|
|
if(overlap)return true;
|
|
}
|
|
return false;
|
|
}
|
|
return PassageProofBoundsOverlapNode(node.left,minimum,maximum)
|
|
||PassageProofBoundsOverlapNode(node.right,minimum,maximum);
|
|
}
|
|
|
|
bool IsPassageColumnSegment(std::size_t PassageIndex,std::size_t SegmentIndex) const
|
|
{
|
|
return PassageIndex<PassageColumnSegmentFlags.size()
|
|
&&SegmentIndex<PassageColumnSegmentFlags[PassageIndex].size()
|
|
&&PassageColumnSegmentFlags[PassageIndex][SegmentIndex]!=0;
|
|
}
|
|
|
|
bool IsPassageShortSpanSegment(std::size_t PassageIndex,std::size_t SegmentIndex) const
|
|
{
|
|
return PassageIndex<PassageShortSpanSegmentFlags.size()
|
|
&&SegmentIndex<PassageShortSpanSegmentFlags[PassageIndex].size()
|
|
&&PassageShortSpanSegmentFlags[PassageIndex][SegmentIndex]!=0;
|
|
}
|
|
|
|
bool PassageHasShortSpanSegments(std::size_t PassageIndex) const
|
|
{
|
|
return PassageIndex<PassageHasShortSpanFlags.size()
|
|
&&PassageHasShortSpanFlags[PassageIndex]!=0;
|
|
}
|
|
|
|
double PassageBridgeDeckOffsetMetres(std::size_t PassageIndex,
|
|
std::size_t SegmentIndex) const
|
|
{
|
|
(void)PassageIndex;(void)SegmentIndex;
|
|
return 0.13;
|
|
}
|
|
|
|
double PassageBridgeSignedDistance(std::size_t PassageIndex,std::size_t SegmentIndex,
|
|
float X,float Y,float Z) const
|
|
{
|
|
if(!ResolvedLayout||PassageIndex>=ResolvedLayout->passages.size())
|
|
return std::numeric_limits<double>::infinity();
|
|
const auto& passage=ResolvedLayout->passages[PassageIndex];
|
|
if(SegmentIndex+1>=passage.controlPoints.size())
|
|
return std::numeric_limits<double>::infinity();
|
|
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
|
const double qPerMetre=Q*1000000.0/std::max<std::uint32_t>(1,
|
|
ResolvedLayout->coreVoxelSizeMicrometres);
|
|
const auto& a=passage.controlPoints[SegmentIndex];
|
|
const auto& b=passage.controlPoints[SegmentIndex+1];
|
|
const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(),
|
|
ResolvedLayout->strates.end(),[&passage](const auto& value)
|
|
{return value.strateIndex==passage.upperStrateIndex;});
|
|
const bool surfaceWorldUpper=upperStratum!=ResolvedLayout->strates.end()
|
|
&&upperStratum->passageCapabilities.hasSurfaceLanding;
|
|
const double ax=static_cast<double>(a.positionSubvoxels[0]);
|
|
const double ay=static_cast<double>(a.positionSubvoxels[1]);
|
|
const double dx=static_cast<double>(b.positionSubvoxels[0])-ax;
|
|
const double dy=static_cast<double>(b.positionSubvoxels[1])-ay;
|
|
const double horizontalLengthSq=dx*dx+dy*dy;
|
|
const double qx=static_cast<double>(X)*Q,qy=static_cast<double>(Y)*Q;
|
|
const double qz=static_cast<double>(Z)*Q;
|
|
const double t=horizontalLengthSq>0.0
|
|
?std::clamp(((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):0.0;
|
|
const double centerX=ax+t*dx,centerY=ay+t*dy;
|
|
const double centerZ=a.positionSubvoxels[2]+t*(
|
|
static_cast<double>(b.positionSubvoxels[2])-a.positionSubvoxels[2]);
|
|
const double radius=a.radiusSubvoxels+t*(
|
|
static_cast<double>(b.radiusSubvoxels)-a.radiusSubvoxels);
|
|
// Embed the deck 13 cm above the bore bottom: within the 25 cm support
|
|
// probe, while leaving 16 cm between it and the capsule's lowest sample.
|
|
const double floorQ=centerZ-radius
|
|
+PassageBridgeDeckOffsetMetres(PassageIndex,SegmentIndex)*qPerMetre;
|
|
const double relief=std::clamp(static_cast<double>(VF_FieldSurfaceRelief(X,Y,Z,
|
|
static_cast<std::uint32_t>(Owner?Owner->Seed:0))),-1.0,1.0);
|
|
const double halfWidth=(surfaceWorldUpper?3.65+0.10*relief:3.30+0.20*relief)
|
|
*qPerMetre;
|
|
const double thickness=(4.0+1.0*relief)*qPerMetre;
|
|
const double horizontalDistance=DeterministicSqrt((qx-centerX)*(qx-centerX)
|
|
+(qy-centerY)*(qy-centerY));
|
|
const double sideDistance=horizontalDistance-halfWidth;
|
|
const double topDistance=qz-floorQ;
|
|
const double bottomDistance=floorQ-thickness-qz;
|
|
const double verticalDistance=std::max(topDistance,bottomDistance);
|
|
const double outside=DeterministicSqrt(
|
|
std::max(sideDistance,0.0)*std::max(sideDistance,0.0)
|
|
+std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0));
|
|
const double inside=std::min(std::max(sideDistance,verticalDistance),0.0);
|
|
return outside+inside;
|
|
}
|
|
|
|
double PassageColumnSignedDistance(const FStandalonePassageVoidCrossing& crossing,
|
|
float X,float Y,float Z) const
|
|
{
|
|
if(crossing.maximumRadiusSubvoxels<=0||crossing.topZSubvoxels
|
|
<=crossing.bottomZSubvoxels)return std::numeric_limits<double>::infinity();
|
|
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
|
const double qPerMetre=Q*1000000.0/std::max<std::uint32_t>(1,
|
|
ResolvedLayout?ResolvedLayout->coreVoxelSizeMicrometres:250000u);
|
|
const double qx=static_cast<double>(X)*Q,qy=static_cast<double>(Y)*Q,
|
|
qz=static_cast<double>(Z)*Q;
|
|
const double dx=qx-crossing.centerXSubvoxels;
|
|
const double dy=qy-crossing.centerYSubvoxels;
|
|
const double maximumRadius=static_cast<double>(crossing.maximumRadiusSubvoxels);
|
|
const double horizontalSquared=dx*dx+dy*dy;
|
|
if(horizontalSquared>maximumRadius*maximumRadius)
|
|
return DeterministicSqrt(horizontalSquared)-maximumRadius;
|
|
if(qz>crossing.topZSubvoxels)
|
|
return qz-crossing.topZSubvoxels;
|
|
if(qz<crossing.bottomZSubvoxels)
|
|
return crossing.bottomZSubvoxels-qz;
|
|
// The lower-strate route is already walkable in its native field and binds
|
|
// directly to the passage mouth. Preserve that player capsule as the column
|
|
// flares into the ground, so the pillar joins the route instead of sealing it.
|
|
if(ResolvedLayout&&crossing.passageIndex<ResolvedLayout->passages.size()
|
|
&&qz<crossing.bottomZSubvoxels+18.0*qPerMetre)
|
|
{
|
|
const auto& passage=ResolvedLayout->passages[crossing.passageIndex];
|
|
const auto lower=std::find_if(ResolvedLayout->strates.begin(),
|
|
ResolvedLayout->strates.end(),[&](const auto& value)
|
|
{return value.strateIndex==passage.lowerStrateIndex;});
|
|
if(lower!=ResolvedLayout->strates.end())
|
|
{
|
|
const double capsuleRadius=0.37*qPerMetre;
|
|
const double capsuleHalfHeight=0.86*qPerMetre;
|
|
const double capsuleRadiusSquared=capsuleRadius*capsuleRadius;
|
|
const VoxelForgeCore::ResolvedLayoutAnchorV2* previous=nullptr;
|
|
for(const auto& anchor:lower->anchors)
|
|
{
|
|
if(anchor.kind!="route_point"||anchor.values.size()<3)continue;
|
|
if(previous)
|
|
{
|
|
const double ax=previous->values[0],ay=previous->values[1];
|
|
const double dx=static_cast<double>(anchor.values[0])-ax;
|
|
const double dy=static_cast<double>(anchor.values[1])-ay;
|
|
const double lengthSquared=dx*dx+dy*dy;
|
|
const double t=lengthSquared>0.0?std::clamp(
|
|
((qx-ax)*dx+(qy-ay)*dy)/lengthSquared,0.0,1.0):0.0;
|
|
const double routeX=ax+t*dx,routeY=ay+t*dy;
|
|
const double routeZ=previous->values[2]+t*(
|
|
static_cast<double>(anchor.values[2])-previous->values[2]);
|
|
const double routeDx=qx-routeX,routeDy=qy-routeY;
|
|
if(routeDx*routeDx+routeDy*routeDy<=capsuleRadiusSquared
|
|
&&std::abs(qz-routeZ)<=capsuleHalfHeight)
|
|
return maximumRadius+qPerMetre;
|
|
}
|
|
previous=&anchor;
|
|
}
|
|
}
|
|
}
|
|
|
|
const double height=static_cast<double>(crossing.topZSubvoxels
|
|
-crossing.bottomZSubvoxels);
|
|
const double t=std::clamp((crossing.topZSubvoxels-qz)/height,0.0,1.0);
|
|
const auto smoothStep=[](double value)
|
|
{
|
|
value=std::clamp(value,0.0,1.0);
|
|
return value*value*(3.0-2.0*value);
|
|
};
|
|
const float z=static_cast<float>(Z);
|
|
const float scaledZ=z*0.12f;
|
|
const float driftX=VF_FieldSurfaceRelief(0.0f,0.0f,scaledZ,
|
|
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x434f4c58u);
|
|
const float driftY=VF_FieldSurfaceRelief(0.0f,0.0f,scaledZ,
|
|
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x434f4c59u);
|
|
const double axisX=crossing.centerXSubvoxels
|
|
+std::clamp(static_cast<double>(driftX),-1.0,1.0)*0.35*qPerMetre;
|
|
const double axisY=crossing.centerYSubvoxels
|
|
+std::clamp(static_cast<double>(driftY),-1.0,1.0)*0.35*qPerMetre;
|
|
const double centerDx=qx-axisX,centerDy=qy-axisY;
|
|
const double radialDistance=DeterministicSqrt(centerDx*centerDx+centerDy*centerDy);
|
|
const float broadRelief=VF_FieldSurfaceRelief(X*0.12f,Y*0.12f,Z*0.12f,
|
|
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x434f4c52u);
|
|
const float surfaceRelief=VF_FieldSurfaceRelief(X,Y,Z,
|
|
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x434f4c53u);
|
|
const double baseFlare=6.0*qPerMetre*smoothStep((t-0.82)/0.18);
|
|
const double ceilingFlare=2.0*qPerMetre*(1.0-smoothStep(t/0.075));
|
|
const double irregularity=1.25*qPerMetre
|
|
+std::clamp(static_cast<double>(broadRelief),-1.0,1.0)*1.10*qPerMetre
|
|
+std::clamp(static_cast<double>(surfaceRelief),-1.0,1.0)*0.35*qPerMetre;
|
|
const double radius=static_cast<double>(crossing.minimumRadiusSubvoxels)
|
|
+std::max(0.15*qPerMetre,irregularity)+baseFlare+ceilingFlare;
|
|
const double radialDistanceField=radialDistance-radius;
|
|
const double topDistance=qz-crossing.topZSubvoxels;
|
|
const double bottomDistance=crossing.bottomZSubvoxels-qz;
|
|
const double verticalDistance=std::max(topDistance,bottomDistance);
|
|
const double outside=DeterministicSqrt(
|
|
std::max(radialDistanceField,0.0)*std::max(radialDistanceField,0.0)
|
|
+std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0));
|
|
const double inside=std::min(std::max(radialDistanceField,verticalDistance),0.0);
|
|
return outside+inside;
|
|
}
|
|
|
|
float NativePassageDensityAt(float X,float Y,float Z) const
|
|
{
|
|
float density=0.0f;
|
|
if(!NativeFloatingIslandDensityAt(X,Y,Z,density)
|
|
&&!NativeSurfaceDensityAt(X,Y,Z,density))
|
|
{
|
|
if(UsesLegacySurfaceField(X,Y,Z)&&Owner&&Owner->Generator)
|
|
{
|
|
if(!LegacyGeneratorDensityAt(X,Y,Z,density))
|
|
density=Owner->Generator->GetDensityAt(X,Y,Z);
|
|
}
|
|
else
|
|
density=AnalyticDensityAt(X,Y,Z,true,nullptr,nullptr,nullptr,
|
|
false,false);
|
|
}
|
|
return ApplyResolvedStrateBoundaryBand(density,X,Y,Z);
|
|
}
|
|
|
|
void BuildPassageVoidCrossingPlan()
|
|
{
|
|
BuildResolvedBoundaryZIntervals();
|
|
PassageVoidCrossings.clear();
|
|
if(!Owner||!ResolvedLayout||ResolvedLayout->passages.empty())return;
|
|
if(!Owner->PassageVoidCrossingCache)
|
|
{BuildPassageSpatialIndices();return;}
|
|
auto& cache=*Owner->PassageVoidCrossingCache;
|
|
std::lock_guard<std::mutex> lock(cache.mutex);
|
|
const std::string cacheKey=ResolvedLayout->stableIdentity+"/passage-void-columns-v1";
|
|
const auto cached=cache.byLayout.find(cacheKey);
|
|
if(cached!=cache.byLayout.end())
|
|
{PassageVoidCrossings=cached->second;BuildPassageSpatialIndices();return;}
|
|
|
|
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
|
const double qPerMetre=Q*1000000.0/std::max<std::uint32_t>(1,
|
|
ResolvedLayout->coreVoxelSizeMicrometres);
|
|
constexpr std::array<std::array<double,2>,5> supportOffsets{{
|
|
{{0.0,0.0}},{{0.45,0.0}},{{-0.45,0.0}},{{0.0,0.45}},{{0.0,-0.45}}}};
|
|
constexpr std::array<double,2> supportDepthsMetres{{0.125,0.25}};
|
|
constexpr double shortSpanMaximumMetres=12.5; // 2.5 x the 5 m legacy support thickness.
|
|
struct FPathSample
|
|
{
|
|
std::size_t segment=0;
|
|
std::array<double,4> point{};
|
|
bool nativeSupported=false;
|
|
};
|
|
const auto nativeSupportedAt=[&](const std::array<double,4>& point)
|
|
{
|
|
const double floorQ=point[2]-point[3]+0.13*qPerMetre;
|
|
std::uint32_t supported=0;
|
|
for(const auto& offset:supportOffsets)
|
|
{
|
|
bool hasSolid=false;
|
|
for(const double depth:supportDepthsMetres)
|
|
{
|
|
const double sampleZ=floorQ-depth*qPerMetre;
|
|
const float density=NativePassageDensityAt(
|
|
static_cast<float>((point[0]+offset[0]*qPerMetre)/Q),
|
|
static_cast<float>((point[1]+offset[1]*qPerMetre)/Q),
|
|
static_cast<float>(sampleZ/Q));
|
|
if(density<0.0f){hasSolid=true;break;}
|
|
}
|
|
supported+=hasSolid;
|
|
}
|
|
return supported>=3;
|
|
};
|
|
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();
|
|
++passageIndex)
|
|
{
|
|
const auto& passage=ResolvedLayout->passages[passageIndex];
|
|
const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(),
|
|
ResolvedLayout->strates.end(),[&passage](const auto& value)
|
|
{return value.strateIndex==passage.upperStrateIndex;});
|
|
const bool surfaceWorldUpper=upperStratum!=ResolvedLayout->strates.end()
|
|
&&upperStratum->passageCapabilities.hasSurfaceLanding;
|
|
if(passage.controlPoints.size()<2)continue;
|
|
std::vector<FPathSample> samples;
|
|
samples.reserve(passage.controlPoints.size()*2);
|
|
const auto pointAt=[&](std::size_t segment,double t)
|
|
{
|
|
const auto& a=passage.controlPoints[segment];
|
|
const auto& b=passage.controlPoints[segment+1];
|
|
FPathSample sample;sample.segment=segment;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
sample.point[axis]=a.positionSubvoxels[axis]+t*(
|
|
static_cast<double>(b.positionSubvoxels[axis])
|
|
-a.positionSubvoxels[axis]);
|
|
sample.point[3]=a.radiusSubvoxels+t*(
|
|
static_cast<double>(b.radiusSubvoxels)-a.radiusSubvoxels);
|
|
sample.nativeSupported=nativeSupportedAt(sample.point);
|
|
return sample;
|
|
};
|
|
samples.push_back(pointAt(0,0.0));
|
|
for(std::size_t segment=0;segment+1<passage.controlPoints.size();++segment)
|
|
{
|
|
const auto& a=passage.controlPoints[segment];
|
|
const auto& b=passage.controlPoints[segment+1];
|
|
const double dx=static_cast<double>(b.positionSubvoxels[0])-a.positionSubvoxels[0];
|
|
const double dy=static_cast<double>(b.positionSubvoxels[1])-a.positionSubvoxels[1];
|
|
const double dz=static_cast<double>(b.positionSubvoxels[2])-a.positionSubvoxels[2];
|
|
const double lengthMetres=DeterministicSqrt(dx*dx+dy*dy+dz*dz)/qPerMetre;
|
|
const std::size_t intervals=std::max<std::size_t>(1,
|
|
static_cast<std::size_t>(std::ceil(lengthMetres/0.5)));
|
|
for(std::size_t step=1;step<=intervals;++step)
|
|
samples.push_back(pointAt(segment,static_cast<double>(step)/intervals));
|
|
}
|
|
std::size_t cursor=0;
|
|
while(cursor<samples.size())
|
|
{
|
|
if(samples[cursor].nativeSupported){++cursor;continue;}
|
|
const std::size_t gapBegin=cursor;
|
|
while(cursor<samples.size()&&!samples[cursor].nativeSupported)++cursor;
|
|
const std::size_t gapEnd=cursor-1;
|
|
const std::size_t startSample=gapBegin>0?gapBegin-1:gapBegin;
|
|
const std::size_t endSample=cursor<samples.size()?cursor:samples.size()-1;
|
|
double gapLength=0.0;
|
|
for(std::size_t index=startSample+1;index<=endSample;++index)
|
|
{
|
|
const auto& a=samples[index-1].point;
|
|
const auto& b=samples[index].point;
|
|
const double dx=b[0]-a[0],dy=b[1]-a[1],dz=b[2]-a[2];
|
|
gapLength+=DeterministicSqrt(dx*dx+dy*dy+dz*dz)/qPerMetre;
|
|
}
|
|
if(gapLength<1.0)continue;
|
|
FStandalonePassageVoidCrossing crossing;
|
|
crossing.passageIndex=passageIndex;
|
|
crossing.firstSegment=samples[startSample].segment;
|
|
crossing.lastSegment=samples[endSample].segment;
|
|
crossing.unsupportedSamples=gapEnd-gapBegin+1;
|
|
crossing.unsupportedLengthMetres=gapLength;
|
|
crossing.startPoint=samples[startSample].point;
|
|
crossing.endPoint=samples[endSample].point;
|
|
crossing.connectedAtStart=gapBegin>0&&samples[startSample].nativeSupported;
|
|
crossing.connectedAtEnd=cursor<samples.size()&&samples[endSample].nativeSupported;
|
|
crossing.needsColumn=gapLength>shortSpanMaximumMetres;
|
|
if(crossing.needsColumn&&passage.controlPoints.size()>1)
|
|
crossing.lastSegment=std::min(crossing.lastSegment+1,
|
|
passage.controlPoints.size()-2);
|
|
if(crossing.needsColumn)
|
|
{
|
|
std::int64_t sumX=0,sumY=0;
|
|
std::uint64_t centerSamples=0;
|
|
for(std::size_t index=gapBegin;index<=gapEnd;++index)
|
|
{
|
|
sumX+=static_cast<std::int64_t>(std::llround(samples[index].point[0]));
|
|
sumY+=static_cast<std::int64_t>(std::llround(samples[index].point[1]));
|
|
++centerSamples;
|
|
}
|
|
crossing.centerXSubvoxels=centerSamples>0
|
|
?static_cast<std::int64_t>(std::llround(static_cast<double>(sumX)/centerSamples)):0;
|
|
crossing.centerYSubvoxels=centerSamples>0
|
|
?static_cast<std::int64_t>(std::llround(static_cast<double>(sumY)/centerSamples)):0;
|
|
for(const auto& stratum:ResolvedLayout->strates)
|
|
if(stratum.strateIndex==passage.upperStrateIndex
|
|
&&stratum.passageCapabilities.usePassageAnchorForColumns)
|
|
for(const auto& anchor:stratum.anchors)
|
|
if(anchor.kind=="passage_anchor"&&anchor.values.size()>=2)
|
|
{
|
|
crossing.centerXSubvoxels=anchor.values[0];
|
|
crossing.centerYSubvoxels=anchor.values[1];
|
|
break;
|
|
}
|
|
double maximumOffsetSubvoxels=0.0;
|
|
double maximumBoreSubvoxels=0.0;
|
|
double minimumCrossingZ=std::min(crossing.startPoint[2],crossing.endPoint[2]);
|
|
double maximumCrossingZ=std::max(crossing.startPoint[2],crossing.endPoint[2]);
|
|
for(std::size_t sampleIndex=0;sampleIndex<samples.size();++sampleIndex)
|
|
{
|
|
const auto& sample=samples[sampleIndex];
|
|
// The audit and density field use whole control segments, including
|
|
// the neighboring segments that extend the column through its caps.
|
|
// Size the column from that same segment window instead of only the
|
|
// unsupported half-metre samples inside the original gap.
|
|
if(surfaceWorldUpper)
|
|
{
|
|
if(sample.segment<crossing.firstSegment
|
|
||sample.segment>crossing.lastSegment)continue;
|
|
}
|
|
else if(sampleIndex<startSample||sampleIndex>endSample)continue;
|
|
const auto& point=sample.point;
|
|
const double dx=point[0]-crossing.centerXSubvoxels;
|
|
const double dy=point[1]-crossing.centerYSubvoxels;
|
|
maximumOffsetSubvoxels=std::max(maximumOffsetSubvoxels,
|
|
DeterministicSqrt(dx*dx+dy*dy));
|
|
maximumBoreSubvoxels=std::max(maximumBoreSubvoxels,point[3]);
|
|
if(surfaceWorldUpper)
|
|
{
|
|
minimumCrossingZ=std::min(minimumCrossingZ,point[2]);
|
|
maximumCrossingZ=std::max(maximumCrossingZ,point[2]);
|
|
}
|
|
}
|
|
const double maximumOffsetMetres=maximumOffsetSubvoxels/qPerMetre;
|
|
const double maximumBoreMetres=maximumBoreSubvoxels/qPerMetre;
|
|
const double minimumRadiusMetres=maximumOffsetMetres+maximumBoreMetres+2.0;
|
|
const double nominalRadiusMetres=minimumRadiusMetres+1.25;
|
|
crossing.maximumRouteOffsetMetres=maximumOffsetMetres;
|
|
crossing.maximumBoreRadiusMetres=maximumBoreMetres;
|
|
crossing.minimumRadiusSubvoxels=static_cast<std::int64_t>(std::ceil(
|
|
minimumRadiusMetres*qPerMetre));
|
|
crossing.nominalRadiusSubvoxels=static_cast<std::int64_t>(std::llround(
|
|
nominalRadiusMetres*qPerMetre));
|
|
crossing.maximumRadiusSubvoxels=crossing.minimumRadiusSubvoxels
|
|
+static_cast<std::int64_t>(std::ceil(11.25*qPerMetre));
|
|
crossing.topZSubvoxels=static_cast<std::int64_t>(std::llround(
|
|
maximumCrossingZ+5.0*qPerMetre));
|
|
crossing.bottomZSubvoxels=static_cast<std::int64_t>(std::llround(
|
|
minimumCrossingZ-6.0*qPerMetre));
|
|
}
|
|
PassageVoidCrossings.push_back(crossing);
|
|
if(crossing.needsColumn)
|
|
{
|
|
const double coreRadius=crossing.nominalRadiusSubvoxels/qPerMetre;
|
|
const double minimumWall=coreRadius-crossing.maximumRouteOffsetMetres
|
|
-crossing.maximumBoreRadiusMetres;
|
|
std::printf("[PassageVoidColumnPlan] id=%s segments=%zu..%zu unsupported_samples=%llu span_m=%.3f threshold_m=%.3f connected=%d/%d axis_m=(%.3f,%.3f) z_m=%.3f..%.3f route_radius_m=%.3f bore_radius_m=%.3f min_radius_m=%.3f nominal_radius_m=%.3f max_radius_m=%.3f nominal_wall_m=%.3f flare_m=6.000 status=%s\n",
|
|
passage.stableId.c_str(),crossing.firstSegment,crossing.lastSegment,
|
|
static_cast<unsigned long long>(crossing.unsupportedSamples),gapLength,
|
|
shortSpanMaximumMetres,static_cast<int>(crossing.connectedAtStart),
|
|
static_cast<int>(crossing.connectedAtEnd),
|
|
crossing.centerXSubvoxels/qPerMetre,crossing.centerYSubvoxels/qPerMetre,
|
|
crossing.topZSubvoxels/qPerMetre,crossing.bottomZSubvoxels/qPerMetre,
|
|
crossing.maximumRouteOffsetMetres,crossing.maximumBoreRadiusMetres,
|
|
crossing.minimumRadiusSubvoxels/qPerMetre,coreRadius,
|
|
crossing.maximumRadiusSubvoxels/qPerMetre,minimumWall,
|
|
crossing.connectedAtStart&&crossing.connectedAtEnd?"PLANNED":"UNATTACHED");
|
|
}
|
|
else
|
|
std::printf("[PassageShortGroundedSpanPlan] id=%s segments=%zu..%zu span_m=%.3f threshold_m=%.3f connected=%d/%d width_m=7.300 thickness_m=3.000..5.000 status=%s\n",
|
|
passage.stableId.c_str(),crossing.firstSegment,crossing.lastSegment,
|
|
gapLength,shortSpanMaximumMetres,static_cast<int>(crossing.connectedAtStart),
|
|
static_cast<int>(crossing.connectedAtEnd),
|
|
crossing.connectedAtStart&&crossing.connectedAtEnd?"PLANNED":"UNATTACHED");
|
|
}
|
|
if(std::none_of(PassageVoidCrossings.begin(),PassageVoidCrossings.end(),
|
|
[passageIndex](const auto& range){return range.passageIndex==passageIndex;}))
|
|
std::printf("[PassageVoidColumnPlan] id=%s crossings=0 status=NOT_REQUIRED\n",
|
|
passage.stableId.c_str());
|
|
}
|
|
cache.byLayout.emplace(cacheKey,PassageVoidCrossings);
|
|
BuildPassageSpatialIndices();
|
|
}
|
|
|
|
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(ApplyResolvedStrateComposition(Density,FX,FY,FZ),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(ApplyResolvedStrateComposition(
|
|
bRock?-ProxyBandEdge:ProxyBandEdge,FX,FY,FZ),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,bool bApplyComposition=true) const
|
|
{
|
|
if(SharedDensitySource)return SharedDensitySource->AnalyticDensityAt(X,Y,Z,
|
|
bClampNearZero,OutDetailDisplacement,OutCaveInfluence,OutFloorInfluence,
|
|
bApplyEdits,bApplyComposition);
|
|
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))
|
|
{
|
|
const float composed=bApplyComposition
|
|
?ApplyResolvedStrateComposition(IslandDensity,X,Y,Z):IslandDensity;
|
|
return bApplyEdits?ApplyEditBrushes(composed,X,Y,Z):composed;
|
|
}
|
|
if(UsesLegacySurfaceField(X,Y,Z))
|
|
{
|
|
float NativeDensity=0.0f;
|
|
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
|
{
|
|
if(bApplyComposition)
|
|
NativeDensity=ApplyResolvedStrateComposition(NativeDensity,X,Y,Z);
|
|
return bApplyEdits?ApplyEditBrushes(NativeDensity,X,Y,Z):NativeDensity;
|
|
}
|
|
float Density=0.0f;
|
|
if(!LegacyGeneratorDensityAt(X,Y,Z,Density))
|
|
Density=Owner->Generator->GetDensityAt(X,Y,Z);
|
|
const float composed=bApplyComposition
|
|
?ApplyResolvedStrateComposition(Density,X,Y,Z):Density;
|
|
return bApplyEdits?ApplyEditBrushes(composed,X,Y,Z):composed;
|
|
}
|
|
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);
|
|
const float composed=bApplyComposition
|
|
?ApplyResolvedStrateComposition(Density,X,Y,Z):Density;
|
|
return bApplyEdits?ApplyEditBrushes(composed,X,Y,Z):composed;
|
|
}
|
|
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);
|
|
if(bApplyComposition)Density=ApplyResolvedStrateComposition(Density,X,Y,Z);
|
|
return bApplyEdits?ApplyEditBrushes(Density,X,Y,Z):Density;
|
|
}
|
|
|
|
public:
|
|
bool GetNativeSurfaceHeightsAt(float X,float Y,int32 ChunkZ,
|
|
float& OutGroundZ,float& OutCeilingZ) const
|
|
{
|
|
if(SharedDensitySource)
|
|
return SharedDensitySource->GetNativeSurfaceHeightsAt(X,Y,ChunkZ,OutGroundZ,OutCeilingZ);
|
|
if(!WorldAssembly||!ResolvedLayout||!Owner||!Owner->Generator
|
|
||!std::isfinite(X)||!std::isfinite(Y))return false;
|
|
const double ChunkWidth=GetResolvedLayoutChunkWidthVoxels();
|
|
const std::int64_t chunkMinimum=static_cast<std::int64_t>(std::llround(ChunkZ*ChunkWidth));
|
|
const std::int64_t chunkMaximum=static_cast<std::int64_t>(std::llround((ChunkZ+1)*ChunkWidth));
|
|
const FBoundStratumField* Bound=FindBoundStratumForChunk(ChunkZ);
|
|
const auto* Field=Bound?std::get_if<FSurfaceWorldFieldAdapter>(&Bound->Evaluator):nullptr;
|
|
if(bNativeOnly&&(!NativeOnlyContainsColumn(X,Y)
|
|
||chunkMaximum<NativeOnlyMinimum.Z||chunkMinimum>NativeOnlyMaximum.Z
|
|
||!Bound||Bound->Kind!=FBoundStratumField::EKind::SurfaceWorld))
|
|
{RecordNativeOnlyViolation();return false;}
|
|
if(!Bound||Bound->Kind!=FBoundStratumField::EKind::SurfaceWorld
|
|
||!Bound->SurfacePlan||!Field||!Field->Evaluator.isValid())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 auto Column=Field->Evaluator.point(ToFixed(X),ToFixed(Y),
|
|
static_cast<int64>(ChunkZ)*ResolvedLayout->strateLayoutChunkWidthSubvoxels);
|
|
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; }
|
|
|
|
bool IsNativeSurfaceBox(const FIntVector& Origin,int32 Step,int32 Cells) const
|
|
{
|
|
if(SharedDensitySource)return SharedDensitySource->IsNativeSurfaceBox(Origin,Step,Cells);
|
|
if(!WorldAssembly||!ResolvedLayout||!Owner||Step<=0||Cells<=0)return false;
|
|
const int32 Pad=std::max(Step,1);
|
|
const int32 BMin=Origin.Z-Pad;
|
|
const int32 BMax=Origin.Z+(Cells+1)*Step;
|
|
if(!StampSpatialBounds.empty())
|
|
{
|
|
constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
const std::array<std::int64_t,3> minimum={
|
|
static_cast<std::int64_t>(Origin.X-Pad)*Q,
|
|
static_cast<std::int64_t>(Origin.Y-Pad)*Q,
|
|
static_cast<std::int64_t>(BMin)*Q};
|
|
const std::array<std::int64_t,3> maximum={
|
|
static_cast<std::int64_t>(Origin.X+(Cells+1)*Step)*Q,
|
|
static_cast<std::int64_t>(Origin.Y+(Cells+1)*Step)*Q,
|
|
static_cast<std::int64_t>(BMax)*Q};
|
|
if(!BuildStampTileList(minimum,maximum).empty())return false;
|
|
}
|
|
std::vector<std::uint32_t> caveCandidates;
|
|
if(!CavernRuntime.empty())
|
|
{
|
|
constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
const std::array<std::int64_t,3> minimum={
|
|
static_cast<std::int64_t>(Origin.X-Pad)*Q,
|
|
static_cast<std::int64_t>(Origin.Y-Pad)*Q,
|
|
static_cast<std::int64_t>(BMin)*Q};
|
|
const std::array<std::int64_t,3> maximum={
|
|
static_cast<std::int64_t>(Origin.X+(Cells+1)*Step)*Q,
|
|
static_cast<std::int64_t>(Origin.Y+(Cells+1)*Step)*Q,
|
|
static_cast<std::int64_t>(BMax)*Q};
|
|
if(!BuildCavernFeatureTileList(minimum,maximum).empty())return false;
|
|
}
|
|
const float LayoutWidth=static_cast<float>(GetResolvedLayoutChunkWidthVoxels());
|
|
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin)/LayoutWidth);
|
|
const int32 LastChunk=FMath::FloorToInt(static_cast<float>(BMax)/LayoutWidth);
|
|
const FBoundStratumField* BoundPlan=nullptr;
|
|
for(int32 ChunkZ=FirstChunk;ChunkZ<=LastChunk;++ChunkZ)
|
|
{
|
|
const auto* Candidate=FindBoundStratumForChunk(ChunkZ);
|
|
if(!Candidate)return false;
|
|
if(Candidate->Kind==FBoundStratumField::EKind::SurfaceWorld)
|
|
{
|
|
if(!Candidate->SurfacePlan||ChunkZ<Candidate->SurfacePlan->bottomChunkZ
|
|
||ChunkZ>Candidate->SurfacePlan->topChunkZ)return false;
|
|
}
|
|
else if(Candidate->Kind==FBoundStratumField::EKind::Caves)
|
|
{
|
|
if(!Candidate->Resolved||!VoxelForgeCore::HasNativeGraphField(*Candidate->Resolved)
|
|
||(VoxelForgeCore::IsNativeCaveFieldStratum(*Candidate->Resolved)
|
|
?!Candidate->Resolved->passageCapabilities.hasNativeCaveField
|
|
:!Candidate->Resolved->passageCapabilities.hasNativeLatticeField)
|
|
||!std::get_if<FCavesFieldAdapter>(&Candidate->Evaluator))return false;
|
|
}
|
|
else return false;
|
|
if(BoundPlan&&BoundPlan!=Candidate)return false;
|
|
BoundPlan=Candidate;
|
|
}
|
|
if(bNativeOnly)
|
|
{
|
|
const int32 BMinX=Origin.X-Pad,BMinY=Origin.Y-Pad;
|
|
const int32 BMaxX=Origin.X+(Cells+1)*Step;
|
|
const int32 BMaxY=Origin.Y+(Cells+1)*Step;
|
|
if(BMinX<NativeOnlyMinimum.X||BMinY<NativeOnlyMinimum.Y
|
|
||BMin<NativeOnlyMinimum.Z||BMaxX>NativeOnlyMaximum.X
|
|
||BMaxY>NativeOnlyMaximum.Y||BMax>NativeOnlyMaximum.Z)
|
|
{RecordNativeOnlyViolation();return false;}
|
|
}
|
|
if(bNativeOnly&&!BoundPlan)RecordNativeOnlyViolation();
|
|
return BoundPlan!=nullptr;
|
|
}
|
|
|
|
FMetricInterval ClassifyNativeCaveFieldBox(const int32 BMin[3],const int32 BMax[3],
|
|
std::int32_t strateIndex,const std::vector<std::uint32_t>& candidates) const
|
|
{
|
|
const double metresPerVoxel=Owner&&Owner->CoreVoxelSizeMicrons>0
|
|
?Owner->CoreVoxelSizeMicrons/1000000.0:0.0;
|
|
if(!(metresPerVoxel>0.0))return {0.0,0.0,false};
|
|
std::array<double,3> lo{},hi{};
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
lo[axis]=static_cast<double>(BMin[axis])*metresPerVoxel;
|
|
hi[axis]=static_cast<double>(BMax[axis])*metresPerVoxel;
|
|
}
|
|
FMetricInterval caveVoid{std::numeric_limits<double>::infinity(),
|
|
std::numeric_limits<double>::infinity(),true};
|
|
bool haveFeature=false;
|
|
double latticeFloorSupportLowerBound=std::numeric_limits<double>::infinity();
|
|
const auto* resolvedStratum=FindResolvedLayoutStratum(strateIndex);
|
|
const bool nativeLattice=resolvedStratum
|
|
&&VoxelForgeCore::IsNativeLatticeFieldStratum(*resolvedStratum)
|
|
&&resolvedStratum->passageCapabilities.hasNativeLatticeField;
|
|
for(const std::uint32_t index:candidates)
|
|
{
|
|
if(index>=CavernRuntime.size())continue;
|
|
const auto& feature=CavernRuntime[index];
|
|
const std::int32_t featureStrate=feature.room?feature.room->strateIndex
|
|
:feature.edge?feature.edge->strateIndex:-1;
|
|
if(featureStrate!=strateIndex)continue;
|
|
FMetricInterval featureVoid;
|
|
if(feature.room)
|
|
{
|
|
double normalizedMinimumSquared=0.0,normalizedMaximumSquared=0.0;
|
|
double meanRadius=0.0;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const double radius=feature.halfExtentsMetres[axis];
|
|
if(!(radius>0.0))return {0.0,0.0,false};
|
|
const double center=feature.centerMetres[axis];
|
|
const double deltaMinimum=center<lo[axis]?lo[axis]-center
|
|
:center>hi[axis]?center-hi[axis]:0.0;
|
|
const double deltaMaximum=std::max(std::abs(lo[axis]-center),
|
|
std::abs(hi[axis]-center));
|
|
normalizedMinimumSquared+=(deltaMinimum/radius)*(deltaMinimum/radius);
|
|
normalizedMaximumSquared+=(deltaMaximum/radius)*(deltaMaximum/radius);
|
|
meanRadius+=radius/3.0;
|
|
}
|
|
const FMetricInterval ellipsoid{
|
|
(std::sqrt(normalizedMinimumSquared)-1.0)*meanRadius,
|
|
(std::sqrt(normalizedMaximumSquared)-1.0)*meanRadius,true};
|
|
const double floorZ=feature.room->floorMicrometres/1000000.0;
|
|
const FMetricInterval floor={floorZ-hi[2],floorZ-lo[2],true};
|
|
const double width=feature.room->smoothWidthMicrometres/1000000.0;
|
|
featureVoid={SmoothMaxMetres(ellipsoid.minimum,floor.minimum,width),
|
|
SmoothMaxMetres(ellipsoid.maximum,floor.maximum,width),true};
|
|
}
|
|
else if(feature.edge&&!feature.segments.empty())
|
|
{
|
|
const double radius=feature.edge->boreRadiusMicrometres/1000000.0;
|
|
const double height=feature.edge->boreHeightMicrometres/1000000.0;
|
|
if(!(radius>0.0&&height>0.0))return {0.0,0.0,false};
|
|
double tubeMinimum=std::numeric_limits<double>::infinity();
|
|
double tubeMaximum=std::numeric_limits<double>::infinity();
|
|
const std::array<double,3> inverseScale={1.0/radius,1.0/radius,
|
|
1.0/height};
|
|
const std::array<double,3> boxMinimum={lo[0]*inverseScale[0],
|
|
lo[1]*inverseScale[1],lo[2]*inverseScale[2]};
|
|
const std::array<double,3> boxMaximum={hi[0]*inverseScale[0],
|
|
hi[1]*inverseScale[1],hi[2]*inverseScale[2]};
|
|
const double scale=std::min(radius,height);
|
|
const auto nodeLowerBound=[&](const FCavernRuntimeFeature::FSegmentNode& node)
|
|
{
|
|
double gapSquared=0.0;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const double low=node.minimum[axis]*inverseScale[axis];
|
|
const double high=node.maximum[axis]*inverseScale[axis];
|
|
const double gap=high<boxMinimum[axis]?boxMinimum[axis]-high
|
|
:low>boxMaximum[axis]?low-boxMaximum[axis]:0.0;
|
|
gapSquared+=gap*gap;
|
|
}
|
|
return (std::sqrt(gapSquared)-1.0)*scale;
|
|
};
|
|
const auto evaluateSegment=[&](std::uint32_t segmentIndex)
|
|
{
|
|
const auto& segment=feature.segments[segmentIndex];
|
|
std::array<double,3> segmentStart{},segmentEnd{};
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
segmentStart[axis]=segment.start[axis]*inverseScale[axis];
|
|
segmentEnd[axis]=segment.end[axis]*inverseScale[axis];
|
|
}
|
|
double gapSquared=0.0;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const double segmentMinimum=std::min(segmentStart[axis],segmentEnd[axis]);
|
|
const double segmentMaximum=std::max(segmentStart[axis],segmentEnd[axis]);
|
|
const double gap=segmentMaximum<boxMinimum[axis]
|
|
?boxMinimum[axis]-segmentMaximum
|
|
:segmentMinimum>boxMaximum[axis]
|
|
?segmentMinimum-boxMaximum[axis]:0.0;
|
|
gapSquared+=gap*gap;
|
|
}
|
|
const double distanceLower=std::sqrt(gapSquared);
|
|
|
|
const std::array<double,3> boxCenter={
|
|
(boxMinimum[0]+boxMaximum[0])*0.5,
|
|
(boxMinimum[1]+boxMaximum[1])*0.5,
|
|
(boxMinimum[2]+boxMaximum[2])*0.5};
|
|
double lengthSquared=0.0,dot=0.0;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const double delta=segmentEnd[axis]-segmentStart[axis];
|
|
lengthSquared+=delta*delta;
|
|
dot+=(boxCenter[axis]-segmentStart[axis])*delta;
|
|
}
|
|
const double t=lengthSquared>0.0
|
|
?std::clamp(dot/lengthSquared,0.0,1.0):0.0;
|
|
double farthestSquared=0.0;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const double fixedPoint=segmentStart[axis]
|
|
+t*(segmentEnd[axis]-segmentStart[axis]);
|
|
const double farthest=std::max(
|
|
std::abs(boxMinimum[axis]-fixedPoint),
|
|
std::abs(boxMaximum[axis]-fixedPoint));
|
|
farthestSquared+=farthest*farthest;
|
|
}
|
|
const double distanceUpper=std::sqrt(farthestSquared);
|
|
tubeMinimum=std::min(tubeMinimum,(distanceLower-1.0)*scale);
|
|
tubeMaximum=std::min(tubeMaximum,(distanceUpper-1.0)*scale);
|
|
};
|
|
if(feature.segmentNodes.empty())return {0.0,0.0,false};
|
|
const auto visitNode=[&](const auto& self,int nodeIndex)->void
|
|
{
|
|
if(nodeIndex<0||static_cast<std::size_t>(nodeIndex)>=feature.segmentNodes.size())return;
|
|
const auto& node=feature.segmentNodes[static_cast<std::size_t>(nodeIndex)];
|
|
if(nodeLowerBound(node)>tubeMaximum)return;
|
|
if(node.left<0)
|
|
{
|
|
for(std::size_t offset=node.first;offset<node.end;++offset)
|
|
evaluateSegment(feature.segmentOrder[offset]);
|
|
return;
|
|
}
|
|
const double leftBound=nodeLowerBound(feature.segmentNodes[
|
|
static_cast<std::size_t>(node.left)]);
|
|
const double rightBound=nodeLowerBound(feature.segmentNodes[
|
|
static_cast<std::size_t>(node.right)]);
|
|
if(leftBound<=rightBound)
|
|
{self(self,node.left);self(self,node.right);}
|
|
else {self(self,node.right);self(self,node.left);}
|
|
};
|
|
visitNode(visitNode,0);
|
|
const double floorMinimum=feature.minimumFloorMetres;
|
|
const double floorMaximum=feature.maximumFloorMetres;
|
|
const double maximumGrade=feature.maximumSegmentGrade;
|
|
if(!std::isfinite(tubeMinimum)||!std::isfinite(tubeMaximum)
|
|
||!std::isfinite(floorMinimum)||!std::isfinite(floorMaximum))
|
|
return {0.0,0.0,false};
|
|
const double scaleMinimum=1.0/std::sqrt(1.0+maximumGrade*maximumGrade);
|
|
const double numeratorMinimum=floorMinimum-hi[2];
|
|
const double numeratorMaximum=floorMaximum-lo[2];
|
|
const std::array<double,4> products={numeratorMinimum*scaleMinimum,
|
|
numeratorMinimum,numeratorMaximum*scaleMinimum,numeratorMaximum};
|
|
const FMetricInterval floor={
|
|
*std::min_element(products.begin(),products.end()),
|
|
*std::max_element(products.begin(),products.end()),true};
|
|
const FMetricInterval tube={tubeMinimum,tubeMaximum,true};
|
|
const double width=feature.edge->smoothWidthMicrometres/1000000.0;
|
|
featureVoid={SmoothMaxMetres(tube.minimum,floor.minimum,width),
|
|
SmoothMaxMetres(tube.maximum,floor.maximum,width),true};
|
|
}
|
|
else return {0.0,0.0,false};
|
|
if(nativeLattice)
|
|
{
|
|
const double supportDepth=feature.edge
|
|
?feature.edge->floorSupportDepthMicrometres/1000000.0:0.0;
|
|
const double floorMinimum=feature.edge
|
|
?feature.minimumFloorMetres:std::numeric_limits<double>::infinity();
|
|
const double floorMaximum=feature.edge
|
|
?feature.maximumFloorMetres:-std::numeric_limits<double>::infinity();
|
|
const bool overlapsXY=hi[0]>=feature.boundsMinimumMicrometres[0]/1.0e6
|
|
&&lo[0]<=feature.boundsMaximumMicrometres[0]/1.0e6
|
|
&&hi[1]>=feature.boundsMinimumMicrometres[1]/1.0e6
|
|
&&lo[1]<=feature.boundsMaximumMicrometres[1]/1.0e6;
|
|
if(supportDepth>0.0&&overlapsXY
|
|
&&hi[2]>=floorMinimum-supportDepth&&lo[2]<=floorMaximum)
|
|
latticeFloorSupportLowerBound=std::min(latticeFloorSupportLowerBound,
|
|
std::max(-supportDepth,lo[2]-floorMaximum));
|
|
}
|
|
if(!std::isfinite(featureVoid.minimum)||!std::isfinite(featureVoid.maximum))
|
|
return {0.0,0.0,false};
|
|
if(!haveFeature)
|
|
{
|
|
caveVoid=featureVoid;haveFeature=true;
|
|
}
|
|
else
|
|
{
|
|
const double width=feature.room
|
|
?(feature.room->unionWidthMicrometres>0
|
|
?feature.room->unionWidthMicrometres
|
|
:feature.room->smoothWidthMicrometres)/1000000.0
|
|
:(feature.edge->unionWidthMicrometres>0
|
|
?feature.edge->unionWidthMicrometres
|
|
:feature.edge->smoothWidthMicrometres)/1000000.0;
|
|
caveVoid.minimum=SmoothMinMetres(caveVoid.minimum,featureVoid.minimum,width);
|
|
caveVoid.maximum=SmoothMinMetres(caveVoid.maximum,featureVoid.maximum,width);
|
|
}
|
|
}
|
|
constexpr double HostRockMetres=-32.0;
|
|
FMetricInterval result{HostRockMetres,HostRockMetres,true};
|
|
if(haveFeature)
|
|
{
|
|
result.minimum=std::max(HostRockMetres,-caveVoid.maximum);
|
|
result.maximum=std::max(HostRockMetres,-caveVoid.minimum);
|
|
}
|
|
// The final lattice field owns an extra support cap after feature union.
|
|
// Lower only the interval minimum when a box could touch that cap; the
|
|
// untouched maximum remains a sound bound when only part of the box does.
|
|
if(std::isfinite(latticeFloorSupportLowerBound))
|
|
result.minimum=std::min(result.minimum,latticeFloorSupportLowerBound);
|
|
// Cover floating-point rounding in interval endpoints and the field's fixed-point
|
|
// coordinate conversion while keeping the interval tight compared with one micron.
|
|
constexpr double OutwardPaddingMetres=1.0e-6;
|
|
result.minimum=std::nextafter(result.minimum-OutwardPaddingMetres,
|
|
-std::numeric_limits<double>::infinity());
|
|
result.maximum=std::nextafter(result.maximum+OutwardPaddingMetres,
|
|
std::numeric_limits<double>::infinity());
|
|
result.known=std::isfinite(result.minimum)&&std::isfinite(result.maximum)
|
|
&&result.minimum<=result.maximum;
|
|
return result;
|
|
}
|
|
|
|
FBoxClassification ClassifyBoxDetailed(const FIntVector& Origin,int32 Step,int32 Cells,
|
|
bool bUseNativeLatticeProof=false) const
|
|
{
|
|
if(SharedDensitySource)
|
|
return SharedDensitySource->ClassifyBoxDetailed(Origin,Step,Cells,bUseNativeLatticeProof);
|
|
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;
|
|
const FSurfaceWorldFieldAdapter* SurfaceField=nullptr;
|
|
const FFloatingIslandFieldAdapter* IslandField=nullptr;
|
|
const FBoundStratumField* CaveBound=nullptr;
|
|
bool bSurfaceOverlap=false,bIslandOverlap=false,bCavesOverlap=false;
|
|
bool bOtherStrateOverlap=false,bLegacyOverlap=false,bCaveOtherStrateOverlap=false;
|
|
bool bIslandOtherStrateOverlap=false,bBadPlan=false;
|
|
std::vector<std::uint32_t> caveCandidates;
|
|
if(Owner&&ResolvedLayout)
|
|
{
|
|
const float LayoutWidth=static_cast<float>(GetResolvedLayoutChunkWidthVoxels());
|
|
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin[2])/LayoutWidth);
|
|
const int32 LastChunk=FMath::FloorToInt(static_cast<float>(BMax[2])/LayoutWidth);
|
|
for(int32 ChunkZ=FirstChunk;ChunkZ<=LastChunk;++ChunkZ)
|
|
{
|
|
const FBoundStratumField* Bound=FindBoundStratumForChunk(ChunkZ);
|
|
if(Bound&&Bound->Kind==FBoundStratumField::EKind::SurfaceWorld)
|
|
{
|
|
bSurfaceOverlap=true;bIslandOtherStrateOverlap=true;
|
|
const auto* Candidate=Bound->SurfacePlan;
|
|
if(!Candidate||ChunkZ<Candidate->bottomChunkZ
|
|
||ChunkZ>Candidate->topChunkZ
|
|
||(SurfacePlan&&SurfacePlan!=Candidate))bBadPlan=true;
|
|
SurfacePlan=Candidate;
|
|
SurfaceField=std::get_if<FSurfaceWorldFieldAdapter>(
|
|
&Bound->Evaluator);
|
|
}
|
|
else if(Bound&&Bound->Kind==FBoundStratumField::EKind::FloatingIslands)
|
|
{
|
|
bIslandOverlap=true;bOtherStrateOverlap=true;
|
|
const auto* Candidate=Bound->IslandPlan;
|
|
if(!Candidate||ChunkZ<Candidate->bottomChunkZ
|
|
||ChunkZ>Candidate->topChunkZ
|
|
||(IslandPlan&&IslandPlan!=Candidate))bBadPlan=true;
|
|
IslandPlan=Candidate;
|
|
IslandField=std::get_if<FFloatingIslandFieldAdapter>(
|
|
&Bound->Evaluator);
|
|
}
|
|
else if(Bound&&Bound->Kind==FBoundStratumField::EKind::Caves)
|
|
{
|
|
bCavesOverlap=true;bOtherStrateOverlap=true;
|
|
bIslandOtherStrateOverlap=true;
|
|
if(CaveBound&&CaveBound!=Bound)bBadPlan=true;
|
|
CaveBound=Bound;
|
|
if(!Bound->Resolved||!VoxelForgeCore::HasNativeGraphField(*Bound->Resolved)
|
|
||(VoxelForgeCore::IsNativeCaveFieldStratum(*Bound->Resolved)
|
|
?!Bound->Resolved->passageCapabilities.hasNativeCaveField
|
|
:!Bound->Resolved->passageCapabilities.hasNativeLatticeField)
|
|
||!std::get_if<FCavesFieldAdapter>(&Bound->Evaluator))bBadPlan=true;
|
|
}
|
|
else
|
|
{bOtherStrateOverlap=true;bIslandOtherStrateOverlap=true;bLegacyOverlap=true;}
|
|
}
|
|
bCaveOtherStrateOverlap=bCavesOverlap
|
|
&&(bSurfaceOverlap||bIslandOverlap||bLegacyOverlap);
|
|
}
|
|
const auto Finish=[&](EVoxelTileClass Verdict,bool bUnknown=false)
|
|
{
|
|
const bool bNative=(bSurfaceOverlap&&!bOtherStrateOverlap)
|
|
||(bIslandOverlap&&!bIslandOtherStrateOverlap)
|
|
||(bCavesOverlap&&!bCaveOtherStrateOverlap);
|
|
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;
|
|
};
|
|
std::vector<std::uint32_t> stampCandidates;
|
|
if(!StampSpatialBounds.empty())
|
|
{
|
|
constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
const std::array<std::int64_t,3> minimum={
|
|
static_cast<std::int64_t>(BMin[0])*Q,
|
|
static_cast<std::int64_t>(BMin[1])*Q,
|
|
static_cast<std::int64_t>(BMin[2])*Q};
|
|
const std::array<std::int64_t,3> maximum={
|
|
static_cast<std::int64_t>(BMax[0])*Q,
|
|
static_cast<std::int64_t>(BMax[1])*Q,
|
|
static_cast<std::int64_t>(BMax[2])*Q};
|
|
stampCandidates=BuildStampTileList(minimum,maximum);
|
|
for(const std::uint32_t index:stampCandidates)
|
|
{
|
|
if(index>=StampRuntime.size())return Finish(EVoxelTileClass::Mixed,true);
|
|
const auto& stamp=StampRuntime[index];
|
|
if(!stamp.record||(stamp.record->kind
|
|
!=VoxelForgeCore::ResolvedSdfStampKindV1::fillMonolith
|
|
&&stamp.record->kind
|
|
!=VoxelForgeCore::ResolvedSdfStampKindV1::fillPlatform)
|
|
||!SurfacePlan||stamp.record->strateIndex!=SurfacePlan->strateIndex)
|
|
return Finish(EVoxelTileClass::Mixed,true);
|
|
}
|
|
}
|
|
if(!CavernRuntime.empty())
|
|
{
|
|
constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
const std::array<std::int64_t,3> minimum={
|
|
static_cast<std::int64_t>(BMin[0])*Q,
|
|
static_cast<std::int64_t>(BMin[1])*Q,
|
|
static_cast<std::int64_t>(BMin[2])*Q};
|
|
const std::array<std::int64_t,3> maximum={
|
|
static_cast<std::int64_t>(BMax[0])*Q,
|
|
static_cast<std::int64_t>(BMax[1])*Q,
|
|
static_cast<std::int64_t>(BMax[2])*Q};
|
|
caveCandidates=BuildCavernFeatureTileList(minimum,maximum);
|
|
if(!caveCandidates.empty()&&!bCavesOverlap)
|
|
return Finish(EVoxelTileClass::Mixed,true);
|
|
}
|
|
if(ResolvedPassageIntersectsBox(BMin,BMax))
|
|
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);
|
|
if(ResolvedLayout&&Owner)
|
|
{
|
|
const double ZMinMicrometres=static_cast<double>(BMin[2])*Owner->CoreVoxelSizeMicrons;
|
|
const double ZMaxMicrometres=static_cast<double>(BMax[2])*Owner->CoreVoxelSizeMicrons;
|
|
for(const auto& Stratum:ResolvedLayout->strates)
|
|
for(const auto* Band:{&Stratum.ceiling,&Stratum.bedrock})
|
|
if(Band->presentInCurrentField&&ZMaxMicrometres>=Band->bottomMicrometres
|
|
&&ZMinMicrometres<Band->topMicrometres)
|
|
{
|
|
if(ZMinMicrometres>=Band->bottomMicrometres
|
|
&&ZMaxMicrometres<Band->topMicrometres)
|
|
return Finish(EVoxelTileClass::AllSolid);
|
|
return Finish(EVoxelTileClass::Mixed);
|
|
}
|
|
}
|
|
if(bNativeOnly)
|
|
{
|
|
const bool bInsideProofBox=BMin[0]>=NativeOnlyMinimum.X
|
|
&&BMin[1]>=NativeOnlyMinimum.Y&&BMin[2]>=NativeOnlyMinimum.Z
|
|
&&BMax[0]<=NativeOnlyMaximum.X&&BMax[1]<=NativeOnlyMaximum.Y
|
|
&&BMax[2]<=NativeOnlyMaximum.Z;
|
|
const bool bSupportedNative=(bSurfaceOverlap&&!bOtherStrateOverlap)
|
|
||(bIslandOverlap&&!bIslandOtherStrateOverlap)
|
|
||(bCavesOverlap&&!bCaveOtherStrateOverlap);
|
|
if(!bInsideProofBox||!bSupportedNative
|
|
||bBadPlan||!EditBrushes.empty())
|
|
{
|
|
RecordNativeOnlyViolation();
|
|
return Finish(EVoxelTileClass::Mixed,true);
|
|
}
|
|
}
|
|
// 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)
|
|
||(bCavesOverlap&&(bCaveOtherStrateOverlap||bSurfaceOverlap||bIslandOverlap)))
|
|
return Finish(EVoxelTileClass::Mixed,true);
|
|
// The native interval path below is the exact field selected first by DensityAtLattice
|
|
// for SurfaceWorld and FloatingIslands. Legacy analytic primitives/layer bounds cannot
|
|
// modify that field; only the explicit edit transaction above can. Keep their guards on
|
|
// the legacy classifier path, where those overlays do participate in density.
|
|
if(!bNativeOnly&&!bCavesOverlap&&(!bUseNativeLatticeProof
|
|
||(!bSurfaceOverlap&&!bIslandOverlap)))
|
|
{
|
|
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||!SurfaceField)
|
|
return Finish(EVoxelTileClass::Mixed,true);
|
|
if(!stampCandidates.empty())
|
|
{
|
|
constexpr std::int64_t Scale=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
const auto interval=SurfaceField->Evaluator.interval(
|
|
static_cast<std::int64_t>(BMin[0])*Scale,
|
|
static_cast<std::int64_t>(BMin[1])*Scale,
|
|
static_cast<std::int64_t>(BMin[2])*Scale,
|
|
static_cast<std::int64_t>(BMax[0])*Scale,
|
|
static_cast<std::int64_t>(BMax[1])*Scale,
|
|
static_cast<std::int64_t>(BMax[2])*Scale);
|
|
if(!interval.known||interval.minimumDensitySubvoxels
|
|
>interval.maximumDensitySubvoxels)
|
|
return Finish(EVoxelTileClass::Mixed,true);
|
|
FMetricInterval composed{
|
|
static_cast<double>(interval.minimumDensitySubvoxels)/Scale,
|
|
static_cast<double>(interval.maximumDensitySubvoxels)/Scale,true};
|
|
for(const std::uint32_t index:stampCandidates)
|
|
{
|
|
const auto& stamp=StampRuntime[index];
|
|
const auto feature=StampDistanceInterval(stamp,BMin,BMax);
|
|
if(!feature.known)return Finish(EVoxelTileClass::Mixed,true);
|
|
composed.minimum=SmoothMinDistance(static_cast<float>(composed.minimum),
|
|
static_cast<float>(feature.minimum),stamp.smoothWidthVoxels);
|
|
composed.maximum=SmoothMinDistance(static_cast<float>(composed.maximum),
|
|
static_cast<float>(feature.maximum),stamp.smoothWidthVoxels);
|
|
}
|
|
constexpr double outwardPadding=2.0e-5;
|
|
composed.minimum=std::nextafter(composed.minimum-outwardPadding,
|
|
-std::numeric_limits<double>::infinity());
|
|
composed.maximum=std::nextafter(composed.maximum+outwardPadding,
|
|
std::numeric_limits<double>::infinity());
|
|
if(!std::isfinite(composed.minimum)||!std::isfinite(composed.maximum)
|
|
||composed.minimum>composed.maximum)
|
|
return Finish(EVoxelTileClass::Mixed,true);
|
|
if(composed.minimum>=0.0)return Finish(EVoxelTileClass::AllAir);
|
|
if(composed.maximum<0.0)return Finish(EVoxelTileClass::AllSolid);
|
|
return Finish(EVoxelTileClass::Mixed);
|
|
}
|
|
// classifyLatticeBox takes the exact extrema of ground/cap columns at every
|
|
// MC lattice XY in this box. Since density is min(z-ground, cap-z), those extrema
|
|
// bound every lattice sample in Z without a noise amplitude guess.
|
|
const auto Verdict=SurfaceField->Evaluator.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||!IslandField)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 bSharedTopSealEnabled=IslandPlan->manifest.recipe.geometryVersion<3
|
|
||!VoxelDensityAblation::IsBoundarySealOff();
|
|
const bool bTopSealOverlap=bSharedTopSealEnabled
|
|
&&BoxBottom<=Top&&BoxTop>Top-Seal;
|
|
const bool bBottomSealOverlap=IslandPlan->manifest.recipe.geometryVersion<3
|
|
&&BoxBottom<Bottom+Seal&&BoxTop>=Bottom;
|
|
if(bBottomSealOverlap)
|
|
return Finish(BoxBottom>=Bottom&&BoxTop<=Bottom+Seal
|
|
?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed);
|
|
if(bTopSealOverlap)
|
|
return Finish((IslandPlan->manifest.recipe.geometryVersion<3
|
|
?BoxBottom>=Top-Seal:BoxBottom>Top-Seal)&&BoxTop<=Top
|
|
?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed);
|
|
if(!IslandField->Evaluator.isValid())return Finish(EVoxelTileClass::Mixed,true);
|
|
const auto Interval=IslandField->Evaluator.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);
|
|
}
|
|
if(bCavesOverlap)
|
|
{
|
|
if(!CaveBound||!std::get_if<FCavesFieldAdapter>(&CaveBound->Evaluator))
|
|
return Finish(EVoxelTileClass::Mixed,true);
|
|
const auto interval=ClassifyNativeCaveFieldBox(BMin,BMax,
|
|
CaveBound->StrateIndex,caveCandidates);
|
|
if(!interval.known)return Finish(EVoxelTileClass::Mixed,true);
|
|
if(interval.minimum>0.0)return Finish(EVoxelTileClass::AllAir);
|
|
if(interval.maximum<0.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(SharedDensitySource)return SharedDensitySource->IsRock(X,Y,Z);
|
|
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.MaxFloorProfileEndpointValueError<=0.01
|
|
&&Stats.MaxFloorProfileEndpointSlopeError<=0.001
|
|
&&Stats.FloorProfileEndpointStepControlDetected
|
|
&&Stats.FloorProfileEndpointSlopeControlDetected;
|
|
return bDetailPass&&bGridPass&&bThinSheetPass&&bFloorProfilePass;
|
|
}
|
|
|
|
private:
|
|
bool NativeOnlyContainsColumn(float X,float Y) const
|
|
{
|
|
return std::isfinite(X)&&std::isfinite(Y)
|
|
&&X>=NativeOnlyMinimum.X&&X<=NativeOnlyMaximum.X
|
|
&&Y>=NativeOnlyMinimum.Y&&Y<=NativeOnlyMaximum.Y;
|
|
}
|
|
|
|
bool NativeOnlyContains(float X,float Y,float Z) const
|
|
{
|
|
return std::isfinite(X)&&std::isfinite(Y)&&std::isfinite(Z)
|
|
&&X>=NativeOnlyMinimum.X&&X<=NativeOnlyMaximum.X
|
|
&&Y>=NativeOnlyMinimum.Y&&Y<=NativeOnlyMaximum.Y
|
|
&&Z>=NativeOnlyMinimum.Z&&Z<=NativeOnlyMaximum.Z;
|
|
}
|
|
|
|
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(!bUseNativeManifestFields||!WorldAssembly||!ResolvedLayout||!Owner
|
|
||!Owner->Manager||!Owner->Generator||!std::isfinite(X)
|
|
||!std::isfinite(Y)||!std::isfinite(Z))return false;
|
|
const float LayoutWidth=static_cast<float>(GetResolvedLayoutChunkWidthVoxels());
|
|
if(!(LayoutWidth>0.0f))return false;
|
|
const int32 ChunkZ=FMath::FloorToInt(Z/LayoutWidth);
|
|
const FBoundStratumField* Bound=FindBoundStratumForChunk(ChunkZ);
|
|
if(!Bound)return false;
|
|
if(Bound->Kind==FBoundStratumField::EKind::Caves)
|
|
{
|
|
const auto* Field=std::get_if<FCavesFieldAdapter>(&Bound->Evaluator);
|
|
return Field&&Field->densityAt(X,Y,Z,OutDensity);
|
|
}
|
|
if(Bound->Kind!=FBoundStratumField::EKind::SurfaceWorld
|
|
||!Bound->SurfacePlan||ChunkZ<Bound->SurfacePlan->bottomChunkZ
|
|
||ChunkZ>Bound->SurfacePlan->topChunkZ)return false;
|
|
const auto* Field=std::get_if<FSurfaceWorldFieldAdapter>(&Bound->Evaluator);
|
|
return Field&&Field->densityAt(X,Y,Z,OutDensity);
|
|
}
|
|
|
|
bool NativeFloatingIslandDensityAt(float X,float Y,float Z,float& OutDensity) const
|
|
{
|
|
if(!bUseNativeManifestFields||!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
|
|
||!ResolvedLayout||!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 LayoutChunkWidth=GetResolvedLayoutChunkWidthVoxels();
|
|
if(!(LayoutChunkWidth>0.0))return false;
|
|
const int32 ChunkZ=FMath::FloorToInt(Z/static_cast<float>(LayoutChunkWidth));
|
|
const FBoundStratumField* Bound=FindBoundStratumForChunk(ChunkZ);
|
|
if(!Bound||Bound->Kind!=FBoundStratumField::EKind::FloatingIslands
|
|
||!Bound->IslandPlan||ChunkZ<Bound->IslandPlan->bottomChunkZ
|
|
||ChunkZ>Bound->IslandPlan->topChunkZ)return false;
|
|
const int32 StrateIndex=Bound->StrateIndex;
|
|
const auto* Plan=Bound->IslandPlan;
|
|
const auto* FieldAdapter=std::get_if<FFloatingIslandFieldAdapter>(
|
|
&Bound->Evaluator);
|
|
if(!FieldAdapter||!FieldAdapter->Evaluator.isValid())return false;
|
|
VoxelForgeCore::floating_island_detail::FScopedIslandAttributionSample attributionSample;
|
|
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 std::int64_t qx=ToFixed(X),qy=ToFixed(Y),qz=ToFixed(Z);
|
|
std::int64_t Density=FieldAdapter->densitySubvoxelsAt(qx,qy,qz);
|
|
const std::int64_t NativeDensity=Density;
|
|
// The anchor-root field keeps broad lobes, but its shortest authored noise
|
|
// wavelength is below one metre. Filter that detail only around a FloatingIslands
|
|
// passage mouth so Marching Cubes does not expose voxel-sized chips on the crater
|
|
// wall. The broad root profile is unchanged outside this local mouth envelope.
|
|
double mouthFilterWeight=0.0;
|
|
const double qToMetres=static_cast<double>(ResolvedLayout->coreVoxelSizeMicrometres)
|
|
/(static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)*1000000.0);
|
|
const double densityMetres=std::abs(static_cast<double>(Density))*qToMetres;
|
|
if(densityMetres<4.0)
|
|
{
|
|
bool hasResolvedPassageMouth=false;
|
|
for(const auto& passage:ResolvedLayout->passages)
|
|
{
|
|
if(passage.upperStrateIndex!=StrateIndex)continue;
|
|
hasResolvedPassageMouth=true;
|
|
const double dx=(static_cast<double>(qx)-passage.upperMouthSubvoxels[0])*qToMetres;
|
|
const double dy=(static_cast<double>(qy)-passage.upperMouthSubvoxels[1])*qToMetres;
|
|
const double dz=(static_cast<double>(qz)-passage.upperMouthSubvoxels[2])*qToMetres;
|
|
const double distance=std::sqrt(dx*dx+dy*dy+dz*dz);
|
|
constexpr double FilterCoreRadiusMetres=8.5;
|
|
constexpr double FilterOuterRadiusMetres=12.5;
|
|
const double fade=std::clamp((FilterOuterRadiusMetres-distance)
|
|
/(FilterOuterRadiusMetres-FilterCoreRadiusMetres),0.0,1.0);
|
|
const double weight=fade*fade*(3.0-2.0*fade);
|
|
mouthFilterWeight=std::max(mouthFilterWeight,weight);
|
|
}
|
|
if(!hasResolvedPassageMouth)
|
|
{
|
|
// The passage-free native control retains the stratum route anchors.
|
|
// Use its terminal route point as the same local mouth filter origin so
|
|
// both native and composed fields receive the same root smoothing.
|
|
const auto stratum=std::find_if(ResolvedLayout->strates.begin(),
|
|
ResolvedLayout->strates.end(),[StrateIndex](const auto& value)
|
|
{return value.strateIndex==StrateIndex;});
|
|
if(stratum!=ResolvedLayout->strates.end())
|
|
{
|
|
const VoxelForgeCore::ResolvedLayoutAnchorV2* terminal=nullptr;
|
|
for(const auto& anchor:stratum->anchors)
|
|
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
|
terminal=&anchor;
|
|
if(terminal)
|
|
{
|
|
const double dx=static_cast<double>(qx-terminal->values[0])*qToMetres;
|
|
const double dy=static_cast<double>(qy-terminal->values[1])*qToMetres;
|
|
const double dz=static_cast<double>(qz-terminal->values[2])*qToMetres;
|
|
const double distance=std::sqrt(dx*dx+dy*dy+dz*dz);
|
|
constexpr double FilterCoreRadiusMetres=8.5;
|
|
constexpr double FilterOuterRadiusMetres=12.5;
|
|
const double fade=std::clamp((FilterOuterRadiusMetres-distance)
|
|
/(FilterOuterRadiusMetres-FilterCoreRadiusMetres),0.0,1.0);
|
|
mouthFilterWeight=fade*fade*(3.0-2.0*fade);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
double routeNativeBlend=0.0;
|
|
const auto mouthPassage=std::find_if(ResolvedLayout->passages.begin(),
|
|
ResolvedLayout->passages.end(),[StrateIndex](const auto& passage)
|
|
{return passage.upperStrateIndex==StrateIndex;});
|
|
const double queryX=static_cast<double>(qx)*qToMetres;
|
|
const double queryY=static_cast<double>(qy)*qToMetres;
|
|
const double queryZ=static_cast<double>(qz)*qToMetres;
|
|
const bool outsideCraterMouth=mouthPassage==ResolvedLayout->passages.end()
|
|
||std::hypot(queryX-mouthPassage->upperMouthSubvoxels[0]*qToMetres,
|
|
queryY-mouthPassage->upperMouthSubvoxels[1]*qToMetres)>5.5;
|
|
if(mouthFilterWeight>0.0&&outsideCraterMouth)
|
|
{
|
|
// Retain the native route only on the outer approach. The crater-mouth
|
|
// interior stays with the filtered ground field and cannot become a bridge.
|
|
const auto stratum=std::find_if(ResolvedLayout->strates.begin(),
|
|
ResolvedLayout->strates.end(),[StrateIndex](const auto& value)
|
|
{return value.strateIndex==StrateIndex;});
|
|
if(stratum!=ResolvedLayout->strates.end())
|
|
{
|
|
const VoxelForgeCore::ResolvedLayoutAnchorV2* previous=nullptr;
|
|
for(const auto& anchor:stratum->anchors)
|
|
{
|
|
if(anchor.kind!="route_point"||anchor.values.size()<3)continue;
|
|
if(previous)
|
|
{
|
|
const double ax=static_cast<double>(previous->values[0])*qToMetres;
|
|
const double ay=static_cast<double>(previous->values[1])*qToMetres;
|
|
const double az=static_cast<double>(previous->values[2])*qToMetres;
|
|
const double dx=(static_cast<double>(anchor.values[0])
|
|
-previous->values[0])*qToMetres;
|
|
const double dy=(static_cast<double>(anchor.values[1])
|
|
-previous->values[1])*qToMetres;
|
|
const double lengthSquared=dx*dx+dy*dy;
|
|
const double t=lengthSquared>0.0?std::clamp(
|
|
((queryX-ax)*dx+(queryY-ay)*dy)/lengthSquared,0.0,1.0):0.0;
|
|
const double centerX=ax+t*dx,centerY=ay+t*dy;
|
|
const double centerZ=az+t*(static_cast<double>(anchor.values[2])
|
|
-previous->values[2])*qToMetres;
|
|
const double supportRadius=previous->values.size()>=4
|
|
?static_cast<double>(previous->values[3])*qToMetres:0.75;
|
|
const double floorZ=centerZ-1.20;
|
|
constexpr double WalkCoreRadiusMetres=0.45;
|
|
constexpr double WearShoulderMetres=0.80;
|
|
const double protectedRadius=std::min(WalkCoreRadiusMetres,
|
|
supportRadius+0.10);
|
|
const double routeDistance=std::hypot(queryX-centerX,queryY-centerY);
|
|
const double radialT=std::clamp(
|
|
(protectedRadius+WearShoulderMetres-routeDistance)
|
|
/WearShoulderMetres,0.0,1.0);
|
|
const double verticalT=std::clamp(
|
|
(3.25-std::abs(queryZ-floorZ))/2.0,0.0,1.0);
|
|
const double radialSmooth=radialT*radialT*(3.0-2.0*radialT);
|
|
const double verticalSmooth=verticalT*verticalT*(3.0-2.0*verticalT);
|
|
routeNativeBlend=std::max(routeNativeBlend,
|
|
radialSmooth*verticalSmooth);
|
|
}
|
|
previous=&anchor;
|
|
}
|
|
}
|
|
}
|
|
if(mouthFilterWeight>0.0)
|
|
{
|
|
VoxelForgeCore::floating_island_detail::FScopedIslandAttributionTimer mouthFilterTimer(
|
|
VoxelForgeCore::floating_island_detail::EIslandAttributionBucket::MouthFilter);
|
|
constexpr std::int64_t FilterStep=8*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
std::int64_t weightedSum=0;
|
|
VoxelForgeCore::floating_island_detail::FScopedIslandFieldCaller passageFilterCaller(
|
|
VoxelForgeCore::floating_island_detail::EIslandFieldCaller::PassageFilter);
|
|
for(int dz=-1;dz<=1;++dz)
|
|
for(int dy=-1;dy<=1;++dy)
|
|
for(int dx=-1;dx<=1;++dx)
|
|
{
|
|
const std::int64_t weight=(dx==0?2:1)*(dy==0?2:1)*(dz==0?2:1);
|
|
const std::int64_t sample=(dx==0&&dy==0&&dz==0)?Density:
|
|
FieldAdapter->densitySubvoxelsAt(qx+dx*FilterStep,qy+dy*FilterStep,
|
|
qz+dz*FilterStep);
|
|
weightedSum+=sample*weight;
|
|
}
|
|
// A separable [1,2,1]^3 kernel over a 2 m footprint filters sub-voxel
|
|
// root detail while retaining the signed field on both sides of the surface.
|
|
const double filtered=static_cast<double>(weightedSum)/64.0;
|
|
std::int64_t filteredDensity=static_cast<std::int64_t>(std::llround(
|
|
std::lerp(static_cast<double>(Density),filtered,mouthFilterWeight)));
|
|
// Filtering detail at an open mouth may soften the surface, but it cannot
|
|
// turn native air into a new solid shelf over the crater.
|
|
if(NativeDensity>=0&&filteredDensity<0)filteredDensity=0;
|
|
Density=filteredDensity;
|
|
if(routeNativeBlend>0.0&&NativeDensity<0)
|
|
{
|
|
const double routePreserved=std::lerp(static_cast<double>(Density),
|
|
static_cast<double>(std::min(Density,NativeDensity)),
|
|
routeNativeBlend*0.60);
|
|
Density=static_cast<std::int64_t>(std::llround(routePreserved));
|
|
}
|
|
// Marching Cubes can retain an isolated solid with no two-metre axis
|
|
// thickness as a handful of disconnected triangles. Remove only shallow
|
|
// negative samples surrounded by air at a one-metre probe distance; a
|
|
// walk floor or wall has solid material on at least one side of this test.
|
|
constexpr double FragmentProbeMetres=1.0;
|
|
const std::int64_t fragmentStep=static_cast<std::int64_t>(std::llround(
|
|
FragmentProbeMetres/qToMetres));
|
|
if(mouthFilterWeight>=0.25&&Density<0
|
|
&&static_cast<double>(-Density)*qToMetres<FragmentProbeMetres)
|
|
{
|
|
bool surroundedByAir=true;
|
|
std::int64_t minimumAirDensity=std::numeric_limits<std::int64_t>::max();
|
|
VoxelForgeCore::floating_island_detail::FScopedIslandFieldCaller passageFilterCaller(
|
|
VoxelForgeCore::floating_island_detail::EIslandFieldCaller::PassageFilter);
|
|
for(std::size_t axis=0;axis<3&&surroundedByAir;++axis)
|
|
for(const int sign:{-1,1})
|
|
{
|
|
std::array<std::int64_t,3> sample{qx,qy,qz};
|
|
sample[axis]+=sign*fragmentStep;
|
|
const std::int64_t neighbor=FieldAdapter->densitySubvoxelsAt(
|
|
sample[0],sample[1],sample[2]);
|
|
if(neighbor<=0)
|
|
{
|
|
surroundedByAir=false;
|
|
break;
|
|
}
|
|
minimumAirDensity=std::min(minimumAirDensity,neighbor);
|
|
}
|
|
if(surroundedByAir)Density=std::max(Density,minimumAirDensity);
|
|
}
|
|
}
|
|
VoxelForgeCore::floating_island_detail::FScopedIslandAttributionTimer boundaryTimer(
|
|
VoxelForgeCore::floating_island_detail::EIslandAttributionBucket::Boundary);
|
|
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(Plan->manifest.recipe.geometryVersion<3)
|
|
{
|
|
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);
|
|
}
|
|
else if(Plan->manifest.recipe.geometryVersion<4)
|
|
{
|
|
OutDensity=static_cast<float>(static_cast<double>(Density)/Scale);
|
|
FResolvedStratumBounds Bounds;
|
|
if(!MakeResolvedStratumBounds(static_cast<double>(Bottom)/Scale,
|
|
static_cast<double>(Top)/Scale,static_cast<double>(Seal)/Scale,Bounds))
|
|
return false;
|
|
const FFloatingIslandParams* Params=FieldAdapter->legacyBoundaryParamsAt(ChunkZ);
|
|
if(!Params)return false;
|
|
float InternalDensity=-OutDensity;
|
|
VF_ApplyUpperBoundarySeal(InternalDensity,Z,static_cast<float>(Bounds.TopZ),
|
|
static_cast<float>(Bounds.SealThickness),Params->BaseDensity);
|
|
OutDensity=-InternalDensity;
|
|
}
|
|
else OutDensity=static_cast<float>(static_cast<double>(Density)/Scale);
|
|
return std::isfinite(OutDensity);
|
|
}
|
|
|
|
bool UsesLegacySurfaceField(float X,float Y,float Z) const
|
|
{
|
|
const FBoundStratumField* Bound=FindBoundStratumForZ(Z);
|
|
return Bound&&Bound->Kind==FBoundStratumField::EKind::SurfaceWorld;
|
|
}
|
|
|
|
bool LegacyGeneratorDensityAt(float X,float Y,float Z,float& OutDensity) const
|
|
{
|
|
const FBoundStratumField* Bound=FindBoundStratumForZ(Z);
|
|
const auto* Legacy=Bound?std::get_if<FLegacyGeneratorFieldAdapter>(
|
|
&Bound->Evaluator):nullptr;
|
|
return Legacy&&Legacy->densityAt(X,Y,Z,OutDensity);
|
|
}
|
|
|
|
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.
|
|
float density=0.0f;
|
|
if(!LegacyGeneratorDensityAt(X,Y,Z,density))
|
|
density=Owner->Generator->GetDensityAt(X,Y,Z);
|
|
density=ApplyResolvedStrateComposition(density,X,Y,Z);
|
|
return ApplyEditBrushes(density,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 std::uint64_t StampMix64(std::uint64_t value)
|
|
{
|
|
value^=value>>30;value*=0xbf58476d1ce4e5b9ull;
|
|
value^=value>>27;value*=0x94d049bb133111ebull;
|
|
return value^(value>>31);
|
|
}
|
|
static float StampLatticeValue(std::uint64_t seed,std::int64_t x,
|
|
std::int64_t y,std::int64_t z)
|
|
{
|
|
std::uint64_t hash=StampMix64(seed^StampMix64(static_cast<std::uint64_t>(x)));
|
|
hash=StampMix64(hash^StampMix64(static_cast<std::uint64_t>(y)));
|
|
hash=StampMix64(hash^StampMix64(static_cast<std::uint64_t>(z)));
|
|
const double unit=static_cast<double>(hash>>11)
|
|
/static_cast<double>((std::uint64_t{1}<<53)-1);
|
|
return static_cast<float>(unit*2.0-1.0);
|
|
}
|
|
static float StampValueNoise(float x,float y,float z,float wavelength,
|
|
std::uint64_t seed)
|
|
{
|
|
if(!(wavelength>0.0f))return 0.0f;
|
|
const double sx=static_cast<double>(x)/wavelength;
|
|
const double sy=static_cast<double>(y)/wavelength;
|
|
const double sz=static_cast<double>(z)/wavelength;
|
|
const std::int64_t ix=static_cast<std::int64_t>(std::floor(sx));
|
|
const std::int64_t iy=static_cast<std::int64_t>(std::floor(sy));
|
|
const std::int64_t iz=static_cast<std::int64_t>(std::floor(sz));
|
|
const auto smooth=[](float t){return t*t*(3.0f-2.0f*t);};
|
|
const float tx=smooth(static_cast<float>(sx-ix));
|
|
const float ty=smooth(static_cast<float>(sy-iy));
|
|
const float tz=smooth(static_cast<float>(sz-iz));
|
|
const auto lerp=[](float a,float b,float t){return a+(b-a)*t;};
|
|
const float z0=lerp(lerp(StampLatticeValue(seed,ix,iy,iz),
|
|
StampLatticeValue(seed,ix+1,iy,iz),tx),
|
|
lerp(StampLatticeValue(seed,ix,iy+1,iz),
|
|
StampLatticeValue(seed,ix+1,iy+1,iz),tx),ty);
|
|
const float z1=lerp(lerp(StampLatticeValue(seed,ix,iy,iz+1),
|
|
StampLatticeValue(seed,ix+1,iy,iz+1),tx),
|
|
lerp(StampLatticeValue(seed,ix,iy+1,iz+1),
|
|
StampLatticeValue(seed,ix+1,iy+1,iz+1),tx),ty);
|
|
return lerp(z0,z1,tz);
|
|
}
|
|
static float StampEllipsoidSdf(float x,float y,float z,
|
|
const std::array<float,3>& radius)
|
|
{
|
|
const float minimum=std::min({radius[0],radius[1],radius[2]});
|
|
if(!(minimum>0.0f))return std::numeric_limits<float>::infinity();
|
|
const float k0=std::sqrt((x/radius[0])*(x/radius[0])
|
|
+(y/radius[1])*(y/radius[1])+(z/radius[2])*(z/radius[2]));
|
|
const float k1=std::sqrt((x/(radius[0]*radius[0]))*(x/(radius[0]*radius[0]))
|
|
+(y/(radius[1]*radius[1]))*(y/(radius[1]*radius[1]))
|
|
+(z/(radius[2]*radius[2]))*(z/(radius[2]*radius[2])));
|
|
return k1>1.0e-8f?k0*(k0-1.0f)/k1:-minimum;
|
|
}
|
|
static float StampRoundedBoxSdf(float x,float y,float z,
|
|
const std::array<float,3>& halfSize,float rounding)
|
|
{
|
|
const float qx=std::abs(x)-halfSize[0]+rounding;
|
|
const float qy=std::abs(y)-halfSize[1]+rounding;
|
|
const float qz=std::abs(z)-halfSize[2]+rounding;
|
|
const float outside=std::sqrt(std::max(qx,0.0f)*std::max(qx,0.0f)
|
|
+std::max(qy,0.0f)*std::max(qy,0.0f)
|
|
+std::max(qz,0.0f)*std::max(qz,0.0f));
|
|
return outside+std::min(std::max({qx,qy,qz}),0.0f)-rounding;
|
|
}
|
|
static float StampPillarSdf(float x,float y,float z,
|
|
const std::array<float,3>& halfSize)
|
|
{
|
|
const float radial=(std::sqrt((x/halfSize[0])*(x/halfSize[0])
|
|
+(y/halfSize[1])*(y/halfSize[1]))-1.0f)
|
|
*std::min(halfSize[0],halfSize[1]);
|
|
const float vertical=std::abs(z)-halfSize[2];
|
|
const float outside=std::sqrt(std::max(radial,0.0f)*std::max(radial,0.0f)
|
|
+std::max(vertical,0.0f)*std::max(vertical,0.0f));
|
|
return outside+std::min(std::max(radial,vertical),0.0f);
|
|
}
|
|
static float StampRoundedCappedCylinderSdf(float x,float y,float z,
|
|
const std::array<float,3>& halfSize,float rounding)
|
|
{
|
|
const float radius=halfSize[0];
|
|
if(!(radius>0.0f&&halfSize[0]==halfSize[1]&&halfSize[2]>0.0f)
|
|
||rounding<0.0f||rounding>std::min(radius,halfSize[2]))
|
|
return std::numeric_limits<float>::infinity();
|
|
const float radial=std::sqrt(x*x+y*y)-(radius-rounding);
|
|
const float vertical=std::abs(z)-(halfSize[2]-rounding);
|
|
const float outside=std::sqrt(std::max(radial,0.0f)*std::max(radial,0.0f)
|
|
+std::max(vertical,0.0f)*std::max(vertical,0.0f));
|
|
return outside+std::min(std::max(radial,vertical),0.0f)-rounding;
|
|
}
|
|
static float StampArchSdf(float x,float y,float z,
|
|
const std::array<float,3>& halfSize)
|
|
{
|
|
const float round=std::min(0.25f,std::min(halfSize[0],halfSize[1])*0.08f);
|
|
const float lower=StampRoundedBoxSdf(x,y,z+halfSize[2]*0.5f,
|
|
{halfSize[0],halfSize[1],halfSize[2]*0.5f},round);
|
|
const float top=std::max(StampEllipsoidSdf(x,y,z,halfSize),-z);
|
|
return SmoothMinDistance(lower,top,round*2.0f);
|
|
}
|
|
float StampSdf(const FStampRuntime& stamp,float x,float y,float z) const
|
|
{
|
|
float localX=x-stamp.positionVoxels[0];
|
|
float localY=y-stamp.positionVoxels[1];
|
|
const float localZ=z-stamp.positionVoxels[2];
|
|
switch(stamp.quarterTurns&3u)
|
|
{
|
|
case 1:{const float oldX=localX;localX=localY;localY=-oldX;break;}
|
|
case 2:localX=-localX;localY=-localY;break;
|
|
case 3:{const float oldX=localX;localX=-localY;localY=oldX;break;}
|
|
default:break;
|
|
}
|
|
float distance=0.0f;
|
|
switch(stamp.record->kind)
|
|
{
|
|
case VoxelForgeCore::ResolvedSdfStampKindV1::carveEllipsoid:
|
|
distance=StampEllipsoidSdf(localX,localY,localZ,stamp.halfExtentsVoxels);break;
|
|
case VoxelForgeCore::ResolvedSdfStampKindV1::carveArch:
|
|
distance=StampArchSdf(localX,localY,localZ,stamp.halfExtentsVoxels);break;
|
|
case VoxelForgeCore::ResolvedSdfStampKindV1::fillPillar:
|
|
distance=StampPillarSdf(localX,localY,localZ,stamp.halfExtentsVoxels);break;
|
|
case VoxelForgeCore::ResolvedSdfStampKindV1::fillMonolith:
|
|
distance=StampRoundedCappedCylinderSdf(localX,localY,localZ,
|
|
stamp.halfExtentsVoxels,stamp.roundingRadiusVoxels);break;
|
|
case VoxelForgeCore::ResolvedSdfStampKindV1::fillPlatform:
|
|
distance=StampRoundedBoxSdf(localX,localY,localZ,
|
|
stamp.halfExtentsVoxels,stamp.roundingRadiusVoxels);break;
|
|
case VoxelForgeCore::ResolvedSdfStampKindV1::fillDais:
|
|
if(ResolvedLayout&&ResolvedLayout->stampVocabularyVersion
|
|
>=VoxelForgeCore::ResolvedLayoutStampVocabularyVersionV2)
|
|
distance=StampEllipsoidSdf(localX,localY,localZ,stamp.halfExtentsVoxels);
|
|
else
|
|
distance=StampRoundedBoxSdf(localX,localY,localZ,stamp.halfExtentsVoxels,
|
|
std::min(0.35f,std::min({stamp.halfExtentsVoxels[0],
|
|
stamp.halfExtentsVoxels[1],stamp.halfExtentsVoxels[2]})*0.15f));
|
|
break;
|
|
default:return std::numeric_limits<float>::infinity();
|
|
}
|
|
if(stamp.irregularityAmplitudeVoxels>0.0f)
|
|
distance+=StampValueNoise(localX,localY,localZ,
|
|
stamp.irregularityWavelengthVoxels,stamp.irregularitySeed)
|
|
*stamp.irregularityAmplitudeVoxels;
|
|
return distance;
|
|
}
|
|
|
|
static double StampAbsMinimum(double minimum,double maximum)
|
|
{return minimum<=0.0&&maximum>=0.0?0.0:std::min(std::abs(minimum),std::abs(maximum));}
|
|
static double StampRoundedBoxSdfDouble(const std::array<double,3>& absolute,
|
|
const std::array<double,3>& halfSize,double rounding)
|
|
{
|
|
const double qx=absolute[0]-halfSize[0]+rounding;
|
|
const double qy=absolute[1]-halfSize[1]+rounding;
|
|
const double qz=absolute[2]-halfSize[2]+rounding;
|
|
const double outside=std::sqrt(std::max(qx,0.0)*std::max(qx,0.0)
|
|
+std::max(qy,0.0)*std::max(qy,0.0)
|
|
+std::max(qz,0.0)*std::max(qz,0.0));
|
|
return outside+std::min(std::max({qx,qy,qz}),0.0)-rounding;
|
|
}
|
|
FMetricInterval StampDistanceInterval(const FStampRuntime& stamp,
|
|
const int32 bMin[3],const int32 bMax[3]) const
|
|
{
|
|
if(!stamp.record||(stamp.record->kind
|
|
!=VoxelForgeCore::ResolvedSdfStampKindV1::fillMonolith
|
|
&&stamp.record->kind!=VoxelForgeCore::ResolvedSdfStampKindV1::fillPlatform))
|
|
return {0.0,0.0,false};
|
|
std::array<double,3> localMinimum{},localMaximum{};
|
|
const std::array<double,3> center={stamp.positionVoxels[0],
|
|
stamp.positionVoxels[1],stamp.positionVoxels[2]};
|
|
const auto axisRange=[&](std::size_t axis)
|
|
{
|
|
return std::array<double,2>{static_cast<double>(bMin[axis]),
|
|
static_cast<double>(bMax[axis])};
|
|
};
|
|
const auto xRange=axisRange(0),yRange=axisRange(1),zRange=axisRange(2);
|
|
switch(stamp.quarterTurns&3u)
|
|
{
|
|
case 1:
|
|
localMinimum={yRange[0]-center[1],center[0]-xRange[1],zRange[0]-center[2]};
|
|
localMaximum={yRange[1]-center[1],center[0]-xRange[0],zRange[1]-center[2]};
|
|
break;
|
|
case 2:
|
|
localMinimum={center[0]-xRange[1],center[1]-yRange[1],zRange[0]-center[2]};
|
|
localMaximum={center[0]-xRange[0],center[1]-yRange[0],zRange[1]-center[2]};
|
|
break;
|
|
case 3:
|
|
localMinimum={center[1]-yRange[1],xRange[0]-center[0],zRange[0]-center[2]};
|
|
localMaximum={center[1]-yRange[0],xRange[1]-center[0],zRange[1]-center[2]};
|
|
break;
|
|
default:
|
|
localMinimum={xRange[0]-center[0],yRange[0]-center[1],zRange[0]-center[2]};
|
|
localMaximum={xRange[1]-center[0],yRange[1]-center[1],zRange[1]-center[2]};
|
|
break;
|
|
}
|
|
std::array<double,3> absoluteMinimum{},absoluteMaximum{};
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
absoluteMinimum[axis]=StampAbsMinimum(localMinimum[axis],localMaximum[axis]);
|
|
absoluteMaximum[axis]=std::max(std::abs(localMinimum[axis]),
|
|
std::abs(localMaximum[axis]));
|
|
}
|
|
double minimum=0.0,maximum=0.0;
|
|
if(stamp.record->kind==VoxelForgeCore::ResolvedSdfStampKindV1::fillMonolith)
|
|
{
|
|
const double radialMinimum=std::hypot(absoluteMinimum[0],absoluteMinimum[1]);
|
|
const double radialMaximum=std::hypot(absoluteMaximum[0],absoluteMaximum[1]);
|
|
const double round=stamp.roundingRadiusVoxels;
|
|
const double side=stamp.halfExtentsVoxels[0]-round;
|
|
const double cap=stamp.halfExtentsVoxels[2]-round;
|
|
const auto eval=[&](double radial,double vertical)
|
|
{
|
|
const double qr=radial-side,qz=vertical-cap;
|
|
return std::hypot(std::max(qr,0.0),std::max(qz,0.0))
|
|
+std::min(std::max(qr,qz),0.0)-round;
|
|
};
|
|
minimum=eval(radialMinimum,absoluteMinimum[2]);
|
|
maximum=eval(radialMaximum,absoluteMaximum[2]);
|
|
}
|
|
else if(stamp.record->kind==VoxelForgeCore::ResolvedSdfStampKindV1::fillPlatform)
|
|
{
|
|
const std::array<double,3> half={stamp.halfExtentsVoxels[0],
|
|
stamp.halfExtentsVoxels[1],stamp.halfExtentsVoxels[2]};
|
|
minimum=StampRoundedBoxSdfDouble(absoluteMinimum,half,stamp.roundingRadiusVoxels);
|
|
maximum=StampRoundedBoxSdfDouble(absoluteMaximum,half,stamp.roundingRadiusVoxels);
|
|
}
|
|
else return {0.0,0.0,false};
|
|
constexpr double outwardPadding=2.0e-5;
|
|
return {std::nextafter(minimum-outwardPadding,
|
|
-std::numeric_limits<double>::infinity()),
|
|
std::nextafter(maximum+outwardPadding,
|
|
std::numeric_limits<double>::infinity()),true};
|
|
}
|
|
float ApplyResolvedSdfStamps(float density,float X,float Y,float Z) const
|
|
{
|
|
if(StampRuntime.empty()||!Owner||Owner->CoreVoxelSizeMicrons==0)return density;
|
|
const auto* tileQueries=ActivePassageTileQueries;
|
|
const bool scoped=tileQueries&&tileQueries->matches(*this);
|
|
if(scoped&&!tileQueries->hasStampCandidates())return density;
|
|
const auto* bound=FindBoundStratumForManagerZ(Z);
|
|
if(!bound)return density;
|
|
const double micronsPerVoxel=Owner->CoreVoxelSizeMicrons;
|
|
const std::array<double,3> worldMicrometres={X*micronsPerVoxel,
|
|
Y*micronsPerVoxel,Z*micronsPerVoxel};
|
|
float composed=density;
|
|
const auto apply=[&](std::uint32_t index)
|
|
{
|
|
if(index>=StampRuntime.size())return;
|
|
const auto& runtime=StampRuntime[index];const auto& record=*runtime.record;
|
|
if(record.strateIndex!=bound->StrateIndex)return;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
if(worldMicrometres[axis]<record.boundsMinimumMicrometres[axis]
|
|
||worldMicrometres[axis]>record.boundsMaximumMicrometres[axis])return;
|
|
const float distance=StampSdf(runtime,X,Y,Z);
|
|
if(record.kind==VoxelForgeCore::ResolvedSdfStampKindV1::carveEllipsoid
|
|
||record.kind==VoxelForgeCore::ResolvedSdfStampKindV1::carveArch)
|
|
composed=SmoothMaxDistance(composed,-distance,runtime.smoothWidthVoxels);
|
|
else
|
|
composed=SmoothMinDistance(composed,distance,runtime.smoothWidthVoxels);
|
|
};
|
|
if(scoped)
|
|
for(const auto index:tileQueries->stampCandidates())apply(index);
|
|
else
|
|
for(std::size_t index=0;index<StampRuntime.size();++index)
|
|
apply(static_cast<std::uint32_t>(index));
|
|
return composed;
|
|
}
|
|
float ApplyResolvedCavernGraph(float density,float X,float Y,float Z) const
|
|
{
|
|
if(CavernRuntime.empty()||!Owner||Owner->CoreVoxelSizeMicrons==0)return density;
|
|
const auto* tileQueries=ActivePassageTileQueries;
|
|
const bool scoped=tileQueries&&tileQueries->matches(*this);
|
|
if(scoped&&!tileQueries->hasCavernCandidates())return density;
|
|
const auto* bound=FindBoundStratumForManagerZ(Z);
|
|
if(!bound)return density;
|
|
if(bound->Kind==FBoundStratumField::EKind::Caves
|
|
&&bound->Resolved&&VoxelForgeCore::HasNativeGraphField(*bound->Resolved)
|
|
&&(VoxelForgeCore::IsNativeCaveFieldStratum(*bound->Resolved)
|
|
?bound->Resolved->passageCapabilities.hasNativeCaveField
|
|
:bound->Resolved->passageCapabilities.hasNativeLatticeField))
|
|
return ApplyResolvedNativeCavesField(density,X,Y,Z,*bound,tileQueries,scoped);
|
|
const double micronsPerVoxel=Owner->CoreVoxelSizeMicrons;
|
|
const std::array<double,3> worldMicrometres={X*micronsPerVoxel,
|
|
Y*micronsPerVoxel,Z*micronsPerVoxel};
|
|
float composed=density;
|
|
const auto apply=[&](std::uint32_t index)
|
|
{
|
|
if(index>=CavernRuntime.size())return;
|
|
const auto& feature=CavernRuntime[index];
|
|
const std::int32_t strateIndex=feature.room?feature.room->strateIndex
|
|
:feature.edge?feature.edge->strateIndex:-1;
|
|
if(strateIndex!=bound->StrateIndex)return;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
if(worldMicrometres[axis]<feature.boundsMinimumMicrometres[axis]
|
|
||worldMicrometres[axis]>feature.boundsMaximumMicrometres[axis])return;
|
|
if(feature.room)
|
|
{
|
|
const float localX=X-feature.centerVoxels[0];
|
|
const float localY=Y-feature.centerVoxels[1];
|
|
const float localZ=Z-feature.centerVoxels[2];
|
|
if(Z<feature.floorVoxels)return;
|
|
float distance=StampEllipsoidSdf(localX,localY,localZ,
|
|
feature.halfExtentsVoxels);
|
|
if(feature.irregularityAmplitudeVoxels>0.0f)
|
|
{
|
|
const float fade=SmoothUnit((Z-feature.floorVoxels)/0.8f);
|
|
distance+=StampValueNoise(localX,localY,localZ,
|
|
feature.irregularityWavelengthVoxels,feature.irregularitySeed)
|
|
*feature.irregularityAmplitudeVoxels*fade;
|
|
}
|
|
composed=SmoothMaxDistance(composed,-distance,feature.smoothWidthVoxels);
|
|
const float horizontal=std::sqrt(
|
|
(localX*localX)/(feature.halfExtentsVoxels[0]
|
|
*feature.halfExtentsVoxels[0])
|
|
+(localY*localY)/(feature.halfExtentsVoxels[1]
|
|
*feature.halfExtentsVoxels[1]));
|
|
if(horizontal<=1.0f)
|
|
composed=std::min(composed,Z-feature.floorVoxels);
|
|
return;
|
|
}
|
|
if(!feature.edge)return;
|
|
const auto bezier=[&](float t,std::size_t axis)
|
|
{
|
|
const float one=1.0f-t;
|
|
return one*one*one*feature.controlPointsVoxels[0][axis]
|
|
+3.0f*one*one*t*feature.controlPointsVoxels[1][axis]
|
|
+3.0f*one*t*t*feature.controlPointsVoxels[2][axis]
|
|
+t*t*t*feature.controlPointsVoxels[3][axis];
|
|
};
|
|
const auto derivative=[&](float t,std::size_t axis)
|
|
{
|
|
const float one=1.0f-t;
|
|
return 3.0f*one*one*(feature.controlPointsVoxels[1][axis]
|
|
-feature.controlPointsVoxels[0][axis])
|
|
+6.0f*one*t*(feature.controlPointsVoxels[2][axis]
|
|
-feature.controlPointsVoxels[1][axis])
|
|
+3.0f*t*t*(feature.controlPointsVoxels[3][axis]
|
|
-feature.controlPointsVoxels[2][axis]);
|
|
};
|
|
const auto secondDerivative=[&](float t,std::size_t axis)
|
|
{
|
|
return 6.0f*(1.0f-t)*(feature.controlPointsVoxels[2][axis]
|
|
-2.0f*feature.controlPointsVoxels[1][axis]
|
|
+feature.controlPointsVoxels[0][axis])
|
|
+6.0f*t*(feature.controlPointsVoxels[3][axis]
|
|
-2.0f*feature.controlPointsVoxels[2][axis]
|
|
+feature.controlPointsVoxels[1][axis]);
|
|
};
|
|
float t=0.0f,bestDistanceSquared=std::numeric_limits<float>::infinity();
|
|
for(int sample=0;sample<=16;++sample)
|
|
{
|
|
const float candidate=static_cast<float>(sample)/16.0f;
|
|
const float dx=X-bezier(candidate,0),dy=Y-bezier(candidate,1);
|
|
const float distanceSquared=dx*dx+dy*dy;
|
|
if(distanceSquared<bestDistanceSquared)
|
|
{bestDistanceSquared=distanceSquared;t=candidate;}
|
|
}
|
|
for(int iteration=0;iteration<5;++iteration)
|
|
{
|
|
const float dx=bezier(t,0)-X,dy=bezier(t,1)-Y;
|
|
const float vx=derivative(t,0),vy=derivative(t,1);
|
|
const float ax=secondDerivative(t,0),ay=secondDerivative(t,1);
|
|
const float denominator=vx*vx+vy*vy+dx*ax+dy*ay;
|
|
if(std::abs(denominator)<1.0e-5f)break;
|
|
t=std::clamp(t-(dx*vx+dy*vy)/denominator,0.0f,1.0f);
|
|
}
|
|
const float centerX=bezier(t,0),centerY=bezier(t,1);
|
|
const float floorZ=bezier(t,2);
|
|
const float floorHorizontalSpeed=std::hypot(derivative(t,0),derivative(t,1));
|
|
const float floorGrade=floorHorizontalSpeed>1.0e-5f
|
|
?derivative(t,2)/floorHorizontalSpeed:0.0f;
|
|
const float floorDistanceScale=1.0f/std::sqrt(1.0f+floorGrade*floorGrade);
|
|
const float radial=std::hypot(X-centerX,Y-centerY);
|
|
const float radius=feature.boreRadiusVoxels;
|
|
const float vertical=(Z-floorZ)/feature.boreHeightVoxels;
|
|
const float normalized=std::sqrt((radial/radius)*(radial/radius)
|
|
+vertical*vertical);
|
|
float distance=(normalized-1.0f)*std::min(radius,feature.boreHeightVoxels);
|
|
if(feature.irregularityAmplitudeVoxels>0.0f)
|
|
{
|
|
const float fade=SmoothUnit((Z-floorZ)/0.8f);
|
|
distance+=StampValueNoise(X-feature.controlPointsVoxels[0][0],
|
|
Y-feature.controlPointsVoxels[0][1],Z-floorZ,
|
|
feature.irregularityWavelengthVoxels,feature.irregularitySeed)
|
|
*feature.irregularityAmplitudeVoxels*fade;
|
|
}
|
|
if(Z<floorZ)
|
|
{
|
|
if(composed>0.0f&&feature.floorSupportDepthVoxels>0.0f
|
|
&&Z>=floorZ-feature.floorSupportDepthVoxels
|
|
&&radial<radius-0.12f)
|
|
{
|
|
const float supportDistance=std::max(radial-(radius-0.12f),
|
|
std::max(floorZ-feature.floorSupportDepthVoxels-Z,Z-floorZ));
|
|
composed=std::min(composed,supportDistance);
|
|
}
|
|
return;
|
|
}
|
|
composed=SmoothMaxDistance(composed,-distance,feature.smoothWidthVoxels);
|
|
// Apply a signed distance to the sloped floor plane. The old vertical
|
|
// height field made Marching Cubes quantize the shallow ramp into terraces.
|
|
// Below it, native rock remains untouched; the narrow support above fills
|
|
// only baseline air.
|
|
if(radial<=radius)
|
|
composed=std::min(composed,(Z-floorZ)*floorDistanceScale);
|
|
};
|
|
if(scoped)
|
|
for(const auto index:tileQueries->cavernFeatureCandidates())apply(index);
|
|
else
|
|
for(std::size_t index=0;index<CavernRuntime.size();++index)
|
|
apply(static_cast<std::uint32_t>(index));
|
|
return composed;
|
|
}
|
|
|
|
static double SmoothMinMetres(double a,double b,double width)
|
|
{
|
|
if(!(width>0.0))return std::min(a,b);
|
|
const double h=std::max(width-std::abs(a-b),0.0)/width;
|
|
return std::min(a,b)-(h*h*h)*(width/6.0);
|
|
}
|
|
|
|
static double SmoothMaxMetres(double a,double b,double width)
|
|
{return -SmoothMinMetres(-a,-b,width);}
|
|
|
|
float ApplyResolvedNativeCavesField(float density,float X,float Y,float Z,
|
|
const FBoundStratumField& bound,const FScopedPassageTileQueries* tileQueries,
|
|
bool scoped) const
|
|
{
|
|
(void)density;
|
|
const double metresPerVoxel=Owner->CoreVoxelSizeMicrons/1000000.0;
|
|
const std::array<double,3> point={X*metresPerVoxel,Y*metresPerVoxel,
|
|
Z*metresPerVoxel};
|
|
std::vector<std::uint32_t> localCandidates;
|
|
const std::vector<std::uint32_t>* candidates=nullptr;
|
|
if(scoped)candidates=&tileQueries->cavernFeatureCandidates();
|
|
else
|
|
{
|
|
constexpr double Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
const std::array<std::int64_t,3> coordinate={
|
|
static_cast<std::int64_t>(std::llround(X*Q)),
|
|
static_cast<std::int64_t>(std::llround(Y*Q)),
|
|
static_cast<std::int64_t>(std::llround(Z*Q))};
|
|
localCandidates=BuildCavernFeatureTileList(coordinate,coordinate);
|
|
candidates=&localCandidates;
|
|
}
|
|
double caveVoid=std::numeric_limits<double>::infinity();
|
|
double latticeFloorSupportMinimum=std::numeric_limits<double>::infinity();
|
|
const bool nativeLattice=bound.Resolved
|
|
&&VoxelForgeCore::IsNativeLatticeFieldStratum(*bound.Resolved)
|
|
&&bound.Resolved->passageCapabilities.hasNativeLatticeField;
|
|
for(const std::uint32_t index:*candidates)
|
|
{
|
|
if(index>=CavernRuntime.size())continue;
|
|
const auto& feature=CavernRuntime[index];
|
|
const std::int32_t strateIndex=feature.room?feature.room->strateIndex
|
|
:feature.edge?feature.edge->strateIndex:-1;
|
|
if(strateIndex!=bound.StrateIndex)continue;
|
|
double featureVoid=std::numeric_limits<double>::infinity();
|
|
if(feature.room)
|
|
{
|
|
double normalizedSquared=0.0,meanRadius=0.0;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const double radius=feature.halfExtentsMetres[axis];
|
|
if(!(radius>0.0)){normalizedSquared=std::numeric_limits<double>::infinity();break;}
|
|
const double delta=(point[axis]-feature.centerMetres[axis])/radius;
|
|
normalizedSquared+=delta*delta;meanRadius+=radius/3.0;
|
|
}
|
|
const double ellipsoid=(std::sqrt(normalizedSquared)-1.0)*meanRadius;
|
|
const double floor=feature.room->floorMicrometres/1000000.0-point[2];
|
|
const double width=feature.room->smoothWidthMicrometres/1000000.0;
|
|
featureVoid=SmoothMaxMetres(ellipsoid,floor,width);
|
|
}
|
|
else if(feature.edge&&!feature.segments.empty())
|
|
{
|
|
double tube=std::numeric_limits<double>::infinity();
|
|
double nearestFloor=0.0,nearestGrade=0.0,nearestRadial=0.0;
|
|
const double radius=feature.edge->boreRadiusMicrometres/1000000.0;
|
|
const double height=feature.edge->boreHeightMicrometres/1000000.0;
|
|
constexpr double SegmentDistanceTieToleranceMetres=1.0e-12;
|
|
if(!(radius>0.0&&height>0.0))continue;
|
|
const double minScale=std::min(radius,height);
|
|
std::uint32_t nearestSegment=std::numeric_limits<std::uint32_t>::max();
|
|
const auto evaluateSegment=[&](std::uint32_t segmentIndex)
|
|
{
|
|
if(segmentIndex>=feature.segments.size())return;
|
|
const auto& segment=feature.segments[segmentIndex];
|
|
double lengthSquared=0.0,dot=0.0;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const double delta=segment.end[axis]-segment.start[axis];
|
|
lengthSquared+=delta*delta;
|
|
dot+=(point[axis]-segment.start[axis])*delta;
|
|
}
|
|
const double t=lengthSquared>0.0
|
|
?std::clamp(dot/lengthSquared,0.0,1.0):0.0;
|
|
double radialSquared=0.0,vertical=0.0;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const double delta=point[axis]-(segment.start[axis]
|
|
+t*(segment.end[axis]-segment.start[axis]));
|
|
if(axis<2)radialSquared+=delta*delta;
|
|
else vertical=delta;
|
|
}
|
|
const double normalized=std::sqrt(radialSquared/(radius*radius)
|
|
+vertical*vertical/(height*height));
|
|
const double distance=(normalized-1.0)*std::min(radius,height);
|
|
const bool clearlyCloser=distance<tube-SegmentDistanceTieToleranceMetres;
|
|
const bool stableTie=std::abs(distance-tube)
|
|
<=SegmentDistanceTieToleranceMetres&&segmentIndex<nearestSegment;
|
|
if(clearlyCloser||stableTie)
|
|
{
|
|
tube=distance;nearestSegment=segmentIndex;
|
|
nearestFloor=segment.start[2]+t*(segment.end[2]-segment.start[2]);
|
|
nearestRadial=std::sqrt(radialSquared);
|
|
const double horizontal=std::hypot(
|
|
segment.end[0]-segment.start[0],segment.end[1]-segment.start[1]);
|
|
nearestGrade=horizontal>1.0e-9
|
|
?(segment.end[2]-segment.start[2])/horizontal:0.0;
|
|
}
|
|
};
|
|
const auto nodeLowerBound=[&](const FCavernRuntimeFeature::FSegmentNode& node)
|
|
{
|
|
double gapSquared=0.0;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const double scale=axis==2?height:radius;
|
|
const double q=point[axis]/scale;
|
|
const double low=node.minimum[axis]/scale;
|
|
const double high=node.maximum[axis]/scale;
|
|
const double gap=q<low?low-q:q>high?q-high:0.0;
|
|
gapSquared+=gap*gap;
|
|
}
|
|
return (std::sqrt(gapSquared)-1.0)*minScale;
|
|
};
|
|
if(!feature.segmentNodes.empty())
|
|
{
|
|
const auto visitNode=[&](const auto& self,int nodeIndex)->void
|
|
{
|
|
if(nodeIndex<0||static_cast<std::size_t>(nodeIndex)>=feature.segmentNodes.size())return;
|
|
const auto& node=feature.segmentNodes[static_cast<std::size_t>(nodeIndex)];
|
|
if(nodeLowerBound(node)>tube+SegmentDistanceTieToleranceMetres)return;
|
|
if(node.left<0)
|
|
{
|
|
for(std::size_t offset=node.first;offset<node.end;++offset)
|
|
evaluateSegment(feature.segmentOrder[offset]);
|
|
return;
|
|
}
|
|
const double leftBound=nodeLowerBound(feature.segmentNodes[
|
|
static_cast<std::size_t>(node.left)]);
|
|
const double rightBound=nodeLowerBound(feature.segmentNodes[
|
|
static_cast<std::size_t>(node.right)]);
|
|
if(leftBound<=rightBound)
|
|
{self(self,node.left);self(self,node.right);}
|
|
else {self(self,node.right);self(self,node.left);}
|
|
};
|
|
visitNode(visitNode,0);
|
|
}
|
|
else for(std::uint32_t segment=0;segment<feature.segments.size();++segment)
|
|
evaluateSegment(segment);
|
|
const double supportDepth=feature.edge->floorSupportDepthMicrometres/1000000.0;
|
|
if(nativeLattice&&supportDepth>0.0&&nearestRadial<radius-0.12
|
|
&&point[2]<nearestFloor&&nearestFloor-point[2]<=supportDepth)
|
|
latticeFloorSupportMinimum=std::min(latticeFloorSupportMinimum,
|
|
(point[2]-nearestFloor)/std::sqrt(1.0+nearestGrade*nearestGrade));
|
|
const double floor=(nearestFloor-point[2])
|
|
/std::sqrt(1.0+nearestGrade*nearestGrade);
|
|
const double width=feature.edge->smoothWidthMicrometres/1000000.0;
|
|
featureVoid=SmoothMaxMetres(tube,floor,width);
|
|
}
|
|
if(!std::isfinite(featureVoid))continue;
|
|
if(!std::isfinite(caveVoid))caveVoid=featureVoid;
|
|
else caveVoid=SmoothMinMetres(caveVoid,featureVoid,
|
|
feature.room
|
|
?(feature.room->unionWidthMicrometres>0
|
|
?feature.room->unionWidthMicrometres
|
|
:feature.room->smoothWidthMicrometres)/1000000.0
|
|
:(feature.edge->unionWidthMicrometres>0
|
|
?feature.edge->unionWidthMicrometres
|
|
:feature.edge->smoothWidthMicrometres)/1000000.0);
|
|
}
|
|
// A versioned native caves recipe owns a solid host-rock field. The
|
|
// resolved interval oracle uses the same finite -32 m solid value; using
|
|
// the underlying legacy generator density here would leak its unrelated
|
|
// terrain detail into the cave floor and invalidate those proofs.
|
|
constexpr double HostRockMetres=-32.0;
|
|
const double hostDistance=HostRockMetres;
|
|
double composed=std::isfinite(caveVoid)
|
|
?SmoothMaxMetres(hostDistance,-caveVoid,0.0):hostDistance;
|
|
// Keep the floor support owned by the lattice route after smooth-unioning
|
|
// neighboring room and corridor voids. This cap is limited to each record's
|
|
// explicit support depth so separated helix turns remain independent.
|
|
if(std::isfinite(latticeFloorSupportMinimum))
|
|
composed=std::min(composed,latticeFloorSupportMinimum);
|
|
return static_cast<float>(composed/metresPerVoxel);
|
|
}
|
|
|
|
float ApplyResolvedCavernFloorCaps(float density,float X,float Y,float Z) const
|
|
{
|
|
if(CavernRuntime.empty()||!Owner||Owner->CoreVoxelSizeMicrons==0)return density;
|
|
const auto* tileQueries=ActivePassageTileQueries;
|
|
const bool scoped=tileQueries&&tileQueries->matches(*this);
|
|
if(scoped&&!tileQueries->hasCavernCandidates())return density;
|
|
const auto* bound=FindBoundStratumForManagerZ(Z);
|
|
if(!bound)return density;
|
|
const double micronsPerVoxel=Owner->CoreVoxelSizeMicrons;
|
|
const std::array<double,3> worldMicrometres={X*micronsPerVoxel,
|
|
Y*micronsPerVoxel,Z*micronsPerVoxel};
|
|
float capped=density;
|
|
const auto apply=[&](std::uint32_t index)
|
|
{
|
|
if(index>=CavernRuntime.size())return;
|
|
const auto& feature=CavernRuntime[index];
|
|
const std::int32_t strateIndex=feature.room?feature.room->strateIndex
|
|
:feature.edge?feature.edge->strateIndex:-1;
|
|
if(strateIndex!=bound->StrateIndex)return;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
if(worldMicrometres[axis]<feature.boundsMinimumMicrometres[axis]
|
|
||worldMicrometres[axis]>feature.boundsMaximumMicrometres[axis])return;
|
|
if(feature.room)
|
|
{
|
|
if(Z<feature.floorVoxels)return;
|
|
const float localX=X-feature.centerVoxels[0];
|
|
const float localY=Y-feature.centerVoxels[1];
|
|
const float horizontal=std::sqrt(
|
|
(localX*localX)/(feature.halfExtentsVoxels[0]
|
|
*feature.halfExtentsVoxels[0])
|
|
+(localY*localY)/(feature.halfExtentsVoxels[1]
|
|
*feature.halfExtentsVoxels[1]));
|
|
if(horizontal<=1.0f)
|
|
capped=std::min(capped,Z-feature.floorVoxels);
|
|
return;
|
|
}
|
|
if(!feature.edge)return;
|
|
const auto bezier=[&](float t,std::size_t axis)
|
|
{
|
|
const float one=1.0f-t;
|
|
return one*one*one*feature.controlPointsVoxels[0][axis]
|
|
+3.0f*one*one*t*feature.controlPointsVoxels[1][axis]
|
|
+3.0f*one*t*t*feature.controlPointsVoxels[2][axis]
|
|
+t*t*t*feature.controlPointsVoxels[3][axis];
|
|
};
|
|
const auto derivative=[&](float t,std::size_t axis)
|
|
{
|
|
const float one=1.0f-t;
|
|
return 3.0f*one*one*(feature.controlPointsVoxels[1][axis]
|
|
-feature.controlPointsVoxels[0][axis])
|
|
+6.0f*one*t*(feature.controlPointsVoxels[2][axis]
|
|
-feature.controlPointsVoxels[1][axis])
|
|
+3.0f*t*t*(feature.controlPointsVoxels[3][axis]
|
|
-feature.controlPointsVoxels[2][axis]);
|
|
};
|
|
const auto secondDerivative=[&](float t,std::size_t axis)
|
|
{
|
|
return 6.0f*(1.0f-t)*(feature.controlPointsVoxels[2][axis]
|
|
-2.0f*feature.controlPointsVoxels[1][axis]
|
|
+feature.controlPointsVoxels[0][axis])
|
|
+6.0f*t*(feature.controlPointsVoxels[3][axis]
|
|
-2.0f*feature.controlPointsVoxels[2][axis]
|
|
+feature.controlPointsVoxels[1][axis]);
|
|
};
|
|
float t=0.0f,bestDistanceSquared=std::numeric_limits<float>::infinity();
|
|
for(int sample=0;sample<=16;++sample)
|
|
{
|
|
const float candidate=static_cast<float>(sample)/16.0f;
|
|
const float dx=X-bezier(candidate,0),dy=Y-bezier(candidate,1);
|
|
const float distanceSquared=dx*dx+dy*dy;
|
|
if(distanceSquared<bestDistanceSquared)
|
|
{bestDistanceSquared=distanceSquared;t=candidate;}
|
|
}
|
|
for(int iteration=0;iteration<5;++iteration)
|
|
{
|
|
const float dx=bezier(t,0)-X,dy=bezier(t,1)-Y;
|
|
const float vx=derivative(t,0),vy=derivative(t,1);
|
|
const float ax=secondDerivative(t,0),ay=secondDerivative(t,1);
|
|
const float denominator=vx*vx+vy*vy+dx*ax+dy*ay;
|
|
if(std::abs(denominator)<1.0e-5f)break;
|
|
t=std::clamp(t-(dx*vx+dy*vy)/denominator,0.0f,1.0f);
|
|
}
|
|
const float floorZ=bezier(t,2);
|
|
const float floorHorizontalSpeed=std::hypot(derivative(t,0),derivative(t,1));
|
|
const float floorGrade=floorHorizontalSpeed>1.0e-5f
|
|
?derivative(t,2)/floorHorizontalSpeed:0.0f;
|
|
const float floorDistanceScale=1.0f/std::sqrt(1.0f+floorGrade*floorGrade);
|
|
const float radial=std::hypot(X-bezier(t,0),Y-bezier(t,1));
|
|
const float floorDistance=(Z-floorZ)*floorDistanceScale;
|
|
const float floorSdfBand=std::max(1.0f,feature.floorSupportDepthVoxels);
|
|
if(Z<floorZ-floorSdfBand)
|
|
{
|
|
// Preserve the existing narrow support outside the signed-distance
|
|
// band beneath the floor.
|
|
if(capped>0.0f&&feature.floorSupportDepthVoxels>0.0f
|
|
&&Z>=floorZ-feature.floorSupportDepthVoxels
|
|
&&radial<feature.boreRadiusVoxels-0.12f)
|
|
{
|
|
const float supportDistance=std::max(radial-
|
|
(feature.boreRadiusVoxels-0.12f),
|
|
std::max(floorZ-feature.floorSupportDepthVoxels-Z,Z-floorZ));
|
|
capped=std::min(capped,supportDistance);
|
|
}
|
|
return;
|
|
}
|
|
if(radial<=feature.boreRadiusVoxels&&Z<=floorZ+0.8f)
|
|
{
|
|
const float vertical=(Z-floorZ)/feature.boreHeightVoxels;
|
|
const float normalized=std::sqrt((radial/feature.boreRadiusVoxels)
|
|
*(radial/feature.boreRadiusVoxels)+vertical*vertical);
|
|
float tunnelDistance=(normalized-1.0f)*std::min(
|
|
feature.boreRadiusVoxels,feature.boreHeightVoxels);
|
|
if(feature.irregularityAmplitudeVoxels>0.0f)
|
|
{
|
|
const float fade=SmoothUnit((Z-floorZ)/0.8f);
|
|
tunnelDistance+=StampValueNoise(
|
|
X-feature.controlPointsVoxels[0][0],
|
|
Y-feature.controlPointsVoxels[0][1],Z-floorZ,
|
|
feature.irregularityWavelengthVoxels,feature.irregularitySeed)
|
|
*feature.irregularityAmplitudeVoxels*fade;
|
|
}
|
|
// Reapply the tunnel SDF in the floor-adjacent strip. Keep a narrow
|
|
// correction along the ramp, with a wider fade only at the stamped mouth.
|
|
const float heightAboveFloor=Z-floorZ;
|
|
const float mouthFade=1.0f-SmoothUnit(t/0.10f);
|
|
const float recarveWeight=heightAboveFloor<=0.25f?1.0f:mouthFade;
|
|
if(recarveWeight>0.0f)
|
|
{
|
|
const float recarved=SmoothMaxDistance(capped,-tunnelDistance,
|
|
feature.smoothWidthVoxels);
|
|
capped=std::lerp(capped,recarved,recarveWeight);
|
|
}
|
|
}
|
|
if(radial<=feature.boreRadiusVoxels)
|
|
capped=std::min(capped,floorDistance);
|
|
};
|
|
if(scoped)
|
|
for(const auto index:tileQueries->cavernFeatureCandidates())apply(index);
|
|
else
|
|
for(std::size_t index=0;index<CavernRuntime.size();++index)
|
|
apply(static_cast<std::uint32_t>(index));
|
|
return capped;
|
|
}
|
|
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;
|
|
// The shared passage sweep supplies its own irregular relief. Keep only a
|
|
// small trace of the terrain value-noise bands so their cell edges cannot
|
|
// dominate the passage wall and read as repeated rings.
|
|
const float PassageRelief=0.10f*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)
|
|
{
|
|
FResolvedStratumBounds Bounds;
|
|
if(!GetResolvedStratumBounds(Slot,Bounds))continue;
|
|
const int32 ResolvedBottom=FMath::FloorToInt(static_cast<float>(Bounds.BottomZ));
|
|
const int32 ResolvedTop=FMath::CeilToInt(static_cast<float>(Bounds.TopZ));
|
|
Top=std::max(Top,ResolvedTop);Bottom=std::min(Bottom,ResolvedBottom);
|
|
if(!Slot.Definition)continue;
|
|
const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2;
|
|
FLayerField Field;
|
|
Field.Lo=ResolvedBottom;
|
|
Field.Hi=ResolvedTop;
|
|
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,Top});
|
|
}
|
|
}
|
|
|
|
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 EvaluateFloorProfileSlope(double A,double B,double TangentA,double TangentB,
|
|
double Length,double StartBlend,double EndBlend,double S)
|
|
{
|
|
if(S<0.0)return TangentA;
|
|
if(S>Length)return TangentB;
|
|
const double Secant=(B-A)/Length;
|
|
double Result=Secant;
|
|
const auto CorrectionSlope=[](double DeltaSlope,double T)
|
|
{return DeltaSlope*(1.0-4.0*T+3.0*T*T);};
|
|
if(S<StartBlend)
|
|
Result+=CorrectionSlope(TangentA-Secant,S/StartBlend);
|
|
if(Length-S<EndBlend)
|
|
Result+=CorrectionSlope(TangentB-Secant,(Length-S)/EndBlend);
|
|
return Result;
|
|
}
|
|
|
|
void AuditFloorProfileBlendNegativeControls()
|
|
{
|
|
constexpr double A=2.0,B=11.0,TangentA=0.35,TangentB=-0.20;
|
|
constexpr double Length=20.0,StartBlend=7.0,EndBlend=7.0;
|
|
constexpr double Step=3.0,SlopeStep=0.25;
|
|
constexpr double ValueThreshold=0.01,SlopeThreshold=0.001;
|
|
const double endValue=EvaluateFloorProfile(A,B,TangentA,TangentB,
|
|
Length,StartBlend,EndBlend,Length);
|
|
const double startSlope=EvaluateFloorProfileSlope(A,B,TangentA,TangentB,
|
|
Length,StartBlend,EndBlend,0.0);
|
|
Stats.FloorProfileEndpointStepControlDetected=
|
|
std::abs((endValue+Step)-B)>ValueThreshold;
|
|
Stats.FloorProfileEndpointSlopeControlDetected=
|
|
std::abs((startSlope+SlopeStep)-TangentA)>SlopeThreshold;
|
|
std::printf("[FloorProfileBlendNegativeControls] endpoint_step=%.3f_vox tangent_step=%.3f_vox_per_m detected=%s/%s status=%s\n",
|
|
Step,SlopeStep,
|
|
Stats.FloorProfileEndpointStepControlDetected?"true":"false",
|
|
Stats.FloorProfileEndpointSlopeControlDetected?"true":"false",
|
|
Stats.FloorProfileEndpointStepControlDetected
|
|
&&Stats.FloorProfileEndpointSlopeControlDetected?"PASS":"FAIL");
|
|
}
|
|
|
|
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));
|
|
}
|
|
P.FloorCurveEndpointValueError=std::max(
|
|
std::abs(EvaluateFloorProfile(FloorA,FloorB,TangentA,TangentB,Length,
|
|
P.FloorCurveStartBlend,P.FloorCurveEndBlend,0.0)-FloorA),
|
|
std::abs(EvaluateFloorProfile(FloorA,FloorB,TangentA,TangentB,Length,
|
|
P.FloorCurveStartBlend,P.FloorCurveEndBlend,Length)-FloorB));
|
|
P.FloorCurveEndpointSlopeError=std::max(
|
|
std::abs(EvaluateFloorProfileSlope(FloorA,FloorB,TangentA,TangentB,Length,
|
|
P.FloorCurveStartBlend,P.FloorCurveEndBlend,0.0)-TangentA),
|
|
std::abs(EvaluateFloorProfileSlope(FloorA,FloorB,TangentA,TangentB,Length,
|
|
P.FloorCurveStartBlend,P.FloorCurveEndBlend,Length)-TangentB));
|
|
}
|
|
|
|
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);
|
|
Stats.MaxFloorProfileEndpointValueError=std::max(
|
|
Stats.MaxFloorProfileEndpointValueError,
|
|
Primitives.back().FloorCurveEndpointValueError);
|
|
Stats.MaxFloorProfileEndpointSlopeError=std::max(
|
|
Stats.MaxFloorProfileEndpointSlopeError,
|
|
Primitives.back().FloorCurveEndpointSlopeError);
|
|
}
|
|
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);
|
|
Stats.MaxFloorProfileEndpointValueError=std::max(
|
|
Stats.MaxFloorProfileEndpointValueError,
|
|
Primitives.back().FloorCurveEndpointValueError);
|
|
Stats.MaxFloorProfileEndpointSlopeError=std::max(
|
|
Stats.MaxFloorProfileEndpointSlopeError,
|
|
Primitives.back().FloorCurveEndpointSlopeError);
|
|
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 LayoutChunkWidth=GetResolvedLayoutChunkWidthVoxels();
|
|
const double ZBottom=static_cast<double>(Slot.BottomChunkZ)*LayoutChunkWidth;
|
|
const double ZTop=static_cast<double>(Slot.TopChunkZ+1)*LayoutChunkWidth;
|
|
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()
|
|
{
|
|
if(bParallelSignEdgeRepair&&Scheduler)
|
|
return CompleteSignEdgeDistancesParallel();
|
|
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;
|
|
}
|
|
|
|
uint64 CompleteSignEdgeDistancesParallel()
|
|
{
|
|
uint64 AddedTotal=0,SignCorrections=0;
|
|
const size_t Columns=static_cast<size_t>(Size[0])*Size[1];
|
|
for(int32 Pass=0;Pass<4;++Pass)
|
|
{
|
|
std::vector<size_t> MissingNodes;
|
|
const auto AddMissing=[&](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;
|
|
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)==0)
|
|
MissingNodes.push_back((Zi*static_cast<size_t>(Size[1])
|
|
+static_cast<size_t>(Y-Min[1]))*Size[0]
|
|
+static_cast<size_t>(X-Min[0]));
|
|
};
|
|
|
|
// Discover against a stable pass snapshot. Density work is then independent per
|
|
// lattice node; packed candidates and occupancy corrections publish in node order.
|
|
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);
|
|
AddMissing(X,Y,Z);AddMissing(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;
|
|
AddMissing(X,Y,Min[2]+static_cast<int32>(W*64+Bit));
|
|
AddMissing(X+1,Y,Min[2]+static_cast<int32>(W*64+Bit));
|
|
}
|
|
}
|
|
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;
|
|
AddMissing(X,Y,Min[2]+static_cast<int32>(W*64+Bit));
|
|
AddMissing(X,Y+1,Min[2]+static_cast<int32>(W*64+Bit));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
std::sort(MissingNodes.begin(),MissingNodes.end());
|
|
MissingNodes.erase(std::unique(MissingNodes.begin(),MissingNodes.end()),
|
|
MissingNodes.end());
|
|
std::vector<float> Densities(MissingNodes.size());
|
|
struct FParallelContext
|
|
{
|
|
FStandaloneShapeWorld* Shape;
|
|
const std::vector<size_t>* Nodes;
|
|
std::vector<float>* Values;
|
|
int32 Minimum[3];
|
|
int32 Extent[3];
|
|
} Context{this,&MissingNodes,&Densities,{Min[0],Min[1],Min[2]},
|
|
{Size[0],Size[1],Size[2]}};
|
|
const auto EvaluateNode=[](void* Raw,size_t Index,size_t)
|
|
{
|
|
auto& C=*static_cast<FParallelContext*>(Raw);
|
|
const size_t Node=(*C.Nodes)[Index];
|
|
const int32 X=static_cast<int32>(Node%static_cast<size_t>(C.Extent[0]));
|
|
const size_t YZ=Node/static_cast<size_t>(C.Extent[0]);
|
|
const int32 Y=static_cast<int32>(YZ%static_cast<size_t>(C.Extent[1]));
|
|
const int32 Z=static_cast<int32>(YZ/static_cast<size_t>(C.Extent[1]));
|
|
(*C.Values)[Index]=C.Shape->AnalyticDensityAt(
|
|
static_cast<float>(C.Minimum[0]+X),static_cast<float>(C.Minimum[1]+Y),
|
|
static_cast<float>(C.Minimum[2]+Z));
|
|
};
|
|
Scheduler->parallel_for(MissingNodes.size(),64,
|
|
static_cast<size_t>(std::max(1,WorkerCount)),&Context,EvaluateNode,nullptr,0);
|
|
|
|
uint64 AddedThisPass=0;
|
|
bool bSignChanged=false;
|
|
for(size_t Index=0;Index<MissingNodes.size();++Index)
|
|
{
|
|
const size_t Node=MissingNodes[Index];
|
|
const int32 X=Min[0]+static_cast<int32>(Node%static_cast<size_t>(Size[0]));
|
|
const size_t YZ=Node/static_cast<size_t>(Size[0]);
|
|
const int32 Y=Min[1]+static_cast<int32>(YZ%static_cast<size_t>(Size[1]));
|
|
const int32 Z=Min[2]+static_cast<int32>(YZ/static_cast<size_t>(Size[1]));
|
|
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)continue;
|
|
const float Density=Densities[Index];
|
|
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;
|
|
}
|
|
}
|
|
AddedTotal+=AddedThisPass;
|
|
if(!bSignChanged)break;
|
|
}
|
|
std::printf("[GridStairDistanceRepair] exact_endpoint_proxies=%llu occupancy_corrections=%llu mode=parallel_deterministic\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(SharedDensitySource)
|
|
{
|
|
SharedDensitySource->DensityAtLatticeZRun(X,Y,ZStart,ZStep,Count,OutDensity);
|
|
return;
|
|
}
|
|
if(Count<=0||OutDensity==nullptr)return;
|
|
if(bNativeOnly)
|
|
{
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
OutDensity[Lane]=DensityAtLattice(X,Y,ZStart+Lane*ZStep);
|
|
return;
|
|
}
|
|
bool bHasResolvedBoundaryBand=false;
|
|
for(int32 Lane=0;Lane<Count&&!bHasResolvedBoundaryBand;++Lane)
|
|
bHasResolvedBoundaryBand=IsInsideResolvedStrateBoundaryBand(
|
|
static_cast<float>(ZStart+Lane*ZStep));
|
|
if(bHasResolvedBoundaryBand)
|
|
{
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
OutDensity[Lane]=DensityAtLattice(X,Y,ZStart+Lane*ZStep);
|
|
return;
|
|
}
|
|
bool bHasLegacySurfaceSample=false;
|
|
bool bHasNativeGraphSample=false;
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
{
|
|
const float SampleZ=static_cast<float>(ZStart+Lane*ZStep);
|
|
bHasLegacySurfaceSample|=UsesLegacySurfaceField(static_cast<float>(X),
|
|
static_cast<float>(Y),SampleZ);
|
|
const auto* managerBound=FindBoundStratumForManagerZ(SampleZ);
|
|
const auto* resolvedBound=FindBoundStratumForZ(SampleZ);
|
|
bHasNativeGraphSample|=(managerBound
|
|
&&managerBound->Kind==FBoundStratumField::EKind::Caves)
|
|
||(resolvedBound&&resolvedBound->Kind==FBoundStratumField::EKind::Caves);
|
|
}
|
|
if(bHasLegacySurfaceSample||bHasNativeGraphSample)
|
|
{
|
|
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;
|
|
const FStandaloneShapeWorld* SharedDensitySource=nullptr;
|
|
int32 WorkerCount=6;
|
|
const VoxelForgeCore::JobScheduler* Scheduler=nullptr;
|
|
bool RoundTunnelsEnabled=false;
|
|
bool bProfileDetailBand=false;
|
|
bool bParallelSignEdgeRepair=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 bNativeOnly=false;
|
|
bool bUseNativeManifestFields=false;
|
|
float BoundManagerLayoutChunkWidthVoxels=0.0f;
|
|
FIntVector NativeOnlyMinimum=FIntVector::ZeroValue;
|
|
FIntVector NativeOnlyMaximum=FIntVector::ZeroValue;
|
|
mutable std::atomic<uint32> NativeOnlyViolations{0};
|
|
bool FarBuilt=false;
|
|
std::vector<std::array<double,2>> ResolvedBoundaryZIntervalsMicrometres;
|
|
std::vector<FBoundStratumField> BoundStratumFields;
|
|
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::vector<FStandalonePassageVoidCrossing> PassageVoidCrossings;
|
|
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;
|
|
inline static thread_local const FScopedPassageTileQueries* ActivePassageTileQueries=nullptr;
|
|
std::vector<FStampSpatialBound> StampSpatialBounds;
|
|
std::vector<FStampSpatialNode> StampSpatialNodes;
|
|
std::vector<std::uint32_t> StampSpatialOrder;
|
|
std::vector<FStampRuntime> StampRuntime;
|
|
std::vector<FOreVeinSpatialBound> OreVeinSpatialBounds;
|
|
std::vector<FOreVeinSpatialNode> OreVeinSpatialNodes;
|
|
std::vector<std::uint32_t> OreVeinSpatialOrder;
|
|
std::vector<FOreVeinRuntime> OreVeinRuntime;
|
|
std::vector<FCavernRuntimeFeature> CavernRuntime;
|
|
std::vector<FCavernSpatialNode> CavernSpatialNodes;
|
|
std::vector<std::uint32_t> CavernSpatialOrder;
|
|
std::vector<VoxelForgeCore::WorldAssemblyFeatureBoundsV1> PassageFeatureBounds;
|
|
std::vector<FPassageTileFeature> PassageTileFeatures;
|
|
std::vector<std::uint32_t> PassageFeatureOrder;
|
|
std::vector<FPassageFeatureNode> PassageFeatureNodes;
|
|
bool PassageFeatureBoundsFailOpen=false;
|
|
std::vector<std::vector<FPassageNearestNode>> PassageNearestNodes;
|
|
std::vector<FPassageProofBound> PassageProofBounds;
|
|
std::vector<FPassageProofNode> PassageProofNodes;
|
|
std::vector<std::vector<std::uint8_t>> PassageColumnSegmentFlags,PassageShortSpanSegmentFlags;
|
|
std::vector<std::uint8_t> PassageHasShortSpanFlags;
|
|
};
|