Versioned capsule vein records (stable ID, resource ID, endpoints, radius, seeded irregularity) resolved into the snapshot only when requested, placed beside the route and away from passages, landings and landmarks (forced-passage control rejected). The bound field table gains materialAt (host + two resource IDs + Q16 weights) behind a per-tile BVH; mesh export v2 carries per-vertex materials; a measurement-only yield computes removed resource volume (0 on already-air space). With veins on, every density, mesh, payload and topology hash is unchanged; demo off: owner hashes identical to L47. fast/medium/full PASS. Research R18 (cavern-graph planner) and R19 (why FloatingIslands costs ~10x SurfaceWorld). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
9724 lines
518 KiB
C++
9724 lines
518 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 <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);
|
|
}
|
|
};
|
|
|
|
using FGeneratorFieldAdapterV1=std::variant<std::monostate,
|
|
FSurfaceWorldFieldAdapter,FFloatingIslandFieldAdapter,
|
|
FLegacyGeneratorFieldAdapter>;
|
|
|
|
struct FBoundStratumField
|
|
{
|
|
enum class EKind : uint8 { Legacy, SurfaceWorld, FloatingIslands };
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
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));
|
|
}
|
|
|
|
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 float composed=ApplyResolvedStratePassageCarves(
|
|
ApplyResolvedStrateBoundaryBand(graphDensity,X,Y,Z),X,Y,Z);
|
|
const float stamped=StampRuntime.empty()?composed:ApplyResolvedSdfStamps(composed,X,Y,Z);
|
|
// Keep the explicit graph floor cap after smooth unions, relief, passages,
|
|
// and decorative stamps so nothing can reopen the tunnel floor below profile.
|
|
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)
|
|
{
|
|
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;
|
|
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";
|
|
if(NativeSurfaceDensityAt(X,Y,Z,Density))return "SurfaceWorld-native";
|
|
if(UsesLegacySurfaceField(X,Y,Z))return "SurfaceWorld-legacy";
|
|
const FBoundStratumField* Bound=FindBoundStratumForZ(Z);
|
|
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;
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
{
|
|
const float SampleX=static_cast<float>(XStart+Lane*XStep);
|
|
const float SampleY=static_cast<float>(Y);
|
|
const float SampleZ=static_cast<float>(Z);
|
|
bHasLegacySurfaceSample|=UsesLegacySurfaceField(SampleX,SampleY,SampleZ);
|
|
float IslandDensity=0.0f;
|
|
bHasNativeIslandSample|=NativeFloatingIslandDensityAt(
|
|
SampleX,SampleY,SampleZ,IslandDensity);
|
|
}
|
|
if(bHasLegacySurfaceSample||bHasNativeIslandSample)
|
|
{
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
OutDensity[Lane]=DensityAtLattice(XStart+Lane*XStep,Y,Z);
|
|
return;
|
|
}
|
|
if(bAnalyticOnly)
|
|
{
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
OutDensity[Lane]=DensityAtLattice(XStart+Lane*XStep,Y,Z);
|
|
return;
|
|
}
|
|
bool bFast=EditBrushes.empty()&&Y>=Min[1]&&Y<=Max[1]
|
|
&&Z>=Min[2]&&Z<=Max[2]
|
|
&&XStart>=Min[0]
|
|
&&XStart+static_cast<int64>(Count-1)*XStep<=Max[0];
|
|
if(!bFast)
|
|
{
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
OutDensity[Lane]=DensityAtLattice(XStart+Lane*XStep,Y,Z);
|
|
return;
|
|
}
|
|
|
|
const size_t ColumnStart=static_cast<size_t>(Y-Min[1])*Size[0]
|
|
+static_cast<size_t>(XStart-Min[0]);
|
|
const size_t Zi=static_cast<size_t>(Z-Min[2]);
|
|
const size_t WordOffset=Zi>>6;
|
|
const uint64 Mask=uint64{1}<<(Zi&63u);
|
|
const size_t NodeBase=Zi*static_cast<size_t>(Size[1])*Size[0]+ColumnStart;
|
|
constexpr float ProxyBandEdge=DetailSupremum+0.125f;
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
{
|
|
const size_t Column=ColumnStart+static_cast<size_t>(Lane*XStep);
|
|
const size_t Word=Column*WordsPerZ+WordOffset;
|
|
const bool bRock=(RockWords[Word]&Mask)!=0;
|
|
float Density;
|
|
if(CandidateWords[Word]&Mask)
|
|
{
|
|
const int16 Proxy=SurfaceDistanceQ[NodeBase
|
|
+static_cast<size_t>(Lane*XStep)];
|
|
Density=static_cast<float>(Proxy)*(1.0f/256.0f);
|
|
if((Density<0.0f)!=bRock)
|
|
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),1.0f/256.0f);
|
|
}
|
|
else
|
|
Density=bRock?-ProxyBandEdge:ProxyBandEdge;
|
|
OutDensity[Lane]=Density;
|
|
}
|
|
}
|
|
|
|
private:
|
|
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;
|
|
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;
|
|
std::array<std::int64_t,3> boundsMinimumMicrometres{},boundsMaximumMicrometres{};
|
|
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;
|
|
};
|
|
|
|
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.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();
|
|
if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0
|
|
||ResolvedLayout->cavernGraphVocabularyVersion
|
|
!=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV1)return;
|
|
const float micronsPerVoxel=static_cast<float>(Owner->CoreVoxelSizeMicrons);
|
|
for(const auto& room:ResolvedLayout->cavernRooms)
|
|
{
|
|
FCavernRuntimeFeature runtime;runtime.room=&room;
|
|
runtime.boundsMinimumMicrometres=room.boundsMinimumMicrometres;
|
|
runtime.boundsMaximumMicrometres=room.boundsMaximumMicrometres;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
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.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;
|
|
CavernRuntime.push_back(runtime);
|
|
}
|
|
}
|
|
|
|
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())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<long double,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<long double>(minimumSubvoxels[axis])
|
|
*Owner->CoreVoxelSizeMicrons/Q;
|
|
maximumMicrometres[axis]=static_cast<long double>(maximumSubvoxels[axis])
|
|
*Owner->CoreVoxelSizeMicrons/Q;
|
|
}
|
|
for(std::size_t index=0;index<CavernRuntime.size();++index)
|
|
{
|
|
const auto& feature=CavernRuntime[index];bool overlap=true;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
overlap=overlap&&feature.boundsMaximumMicrometres[axis]
|
|
>=minimumMicrometres[axis]
|
|
&&feature.boundsMinimumMicrometres[axis]
|
|
<=maximumMicrometres[axis];
|
|
if(overlap)result.push_back(static_cast<std::uint32_t>(index));
|
|
}
|
|
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;
|
|
}
|
|
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||Candidate->Kind!=FBoundStratumField::EKind::SurfaceWorld
|
|
||!Candidate->SurfacePlan||ChunkZ<Candidate->SurfacePlan->bottomChunkZ
|
|
||ChunkZ>Candidate->SurfacePlan->topChunkZ)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;
|
|
}
|
|
|
|
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;
|
|
bool bSurfaceOverlap=false,bIslandOverlap=false,bOtherStrateOverlap=false;
|
|
bool bIslandOtherStrateOverlap=false,bBadPlan=false;
|
|
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
|
|
{bOtherStrateOverlap=true;bIslandOtherStrateOverlap=true;}
|
|
}
|
|
}
|
|
const auto Finish=[&](EVoxelTileClass Verdict,bool bUnknown=false)
|
|
{
|
|
const bool bNative=(bSurfaceOverlap&&!bOtherStrateOverlap)
|
|
||(bIslandOverlap&&!bIslandOtherStrateOverlap);
|
|
FBoxClassification Result{Verdict,bNative,bUnknown};
|
|
auto& Counts=ThreadClassificationStats();
|
|
Counts.Milliseconds+=std::chrono::duration<double,std::milli>(Clock::now()-Start).count();
|
|
if(!Result.bNativeSurface||bUnknown)++Counts.UnknownBoxes;
|
|
else if(Verdict==EVoxelTileClass::AllSolid)++Counts.SolidBoxes;
|
|
else if(Verdict==EVoxelTileClass::AllAir)++Counts.AirBoxes;
|
|
else ++Counts.MixedBoxes;
|
|
return Result;
|
|
};
|
|
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};
|
|
if(!BuildStampTileList(minimum,maximum).empty())
|
|
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};
|
|
if(!BuildCavernFeatureTileList(minimum,maximum).empty())
|
|
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;
|
|
if(!bInsideProofBox||!bSurfaceOverlap||bIslandOverlap||bOtherStrateOverlap
|
|
||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
|
|
||(!bSurfaceOverlap&&bOtherStrateOverlap))))
|
|
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&&(!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);
|
|
// 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);
|
|
}
|
|
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||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;
|
|
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)
|
|
{
|
|
constexpr std::int64_t FilterStep=8*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
std::int64_t weightedSum=0;
|
|
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();
|
|
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);
|
|
}
|
|
}
|
|
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 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::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;
|
|
}
|
|
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;
|
|
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;
|
|
}
|
|
|
|
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()
|
|
{
|
|
uint64 AddedTotal=0,SignCorrections=0;
|
|
const size_t Columns=static_cast<size_t>(Size[0])*Size[1];
|
|
for(int32 Pass=0;Pass<4;++Pass)
|
|
{
|
|
uint64 AddedThisPass=0;
|
|
bool bSignChanged=false;
|
|
const auto EnsureDistance=[&](int32 X,int32 Y,int32 Z)
|
|
{
|
|
const size_t Column=static_cast<size_t>(Y-Min[1])*Size[0]
|
|
+static_cast<size_t>(X-Min[0]);
|
|
const size_t Zi=static_cast<size_t>(Z-Min[2]);
|
|
const size_t Word=Column*WordsPerZ+(Zi>>6);
|
|
const uint64 Mask=uint64{1}<<(Zi&63u);
|
|
if(CandidateWords[Word]&Mask)return;
|
|
|
|
const float Density=AnalyticDensityAt(static_cast<float>(X),
|
|
static_cast<float>(Y),static_cast<float>(Z));
|
|
const size_t Node=(Zi*static_cast<size_t>(Size[1])
|
|
+static_cast<size_t>(Y-Min[1]))*Size[0]
|
|
+static_cast<size_t>(X-Min[0]);
|
|
StoreSurfaceDistance(Node,Density);
|
|
CandidateWords[Word]|=Mask;
|
|
++AddedThisPass;
|
|
|
|
const bool bExactRock=Density<0.0f;
|
|
const bool bPackedRock=(RockWords[Word]&Mask)!=0;
|
|
if(bExactRock!=bPackedRock)
|
|
{
|
|
if(bExactRock)RockWords[Word]|=Mask;
|
|
else RockWords[Word]&=~Mask;
|
|
++SignCorrections;
|
|
bSignChanged=true;
|
|
}
|
|
};
|
|
|
|
for(size_t Line=0;Line<Columns;++Line)
|
|
{
|
|
const int32 X=Min[0]+static_cast<int32>(Line%Size[0]);
|
|
const int32 Y=Min[1]+static_cast<int32>(Line/Size[0]);
|
|
const size_t Base=Line*WordsPerZ;
|
|
for(size_t W=0;W<WordsPerZ;++W)
|
|
{
|
|
const size_t Word=Base+W;
|
|
const uint64 Rock=RockWords[Word],Candidates=CandidateWords[Word];
|
|
const uint64 PreviousRock=W?RockWords[Word-1]>>63:0;
|
|
const uint64 PreviousCandidates=W?CandidateWords[Word-1]>>63:0;
|
|
uint64 Valid=(Size[2]-W*64)>=64?~uint64{0}
|
|
:((uint64{1}<<(Size[2]-W*64))-1);
|
|
if(W==0)Valid&=~uint64{1};
|
|
|
|
const uint64 ShiftedRock=(Rock<<1)|PreviousRock;
|
|
const uint64 ShiftedCandidates=(Candidates<<1)|PreviousCandidates;
|
|
uint64 Missing=((Rock^ShiftedRock)&Valid)
|
|
&~(Candidates&ShiftedCandidates);
|
|
while(Missing)
|
|
{
|
|
const uint32 Bit=std::countr_zero(Missing);Missing&=Missing-1;
|
|
const int32 Z=Min[2]+static_cast<int32>(W*64+Bit);
|
|
EnsureDistance(X,Y,Z);
|
|
EnsureDistance(X,Y,Z-1);
|
|
}
|
|
|
|
if(X<Max[0])
|
|
{
|
|
const size_t Other=Word+WordsPerZ;
|
|
Missing=(Rock^RockWords[Other])&Valid
|
|
&~(Candidates&CandidateWords[Other]);
|
|
while(Missing)
|
|
{
|
|
const uint32 Bit=std::countr_zero(Missing);Missing&=Missing-1;
|
|
const int32 Z=Min[2]+static_cast<int32>(W*64+Bit);
|
|
EnsureDistance(X,Y,Z);
|
|
EnsureDistance(X+1,Y,Z);
|
|
}
|
|
}
|
|
if(Y<Max[1])
|
|
{
|
|
const size_t Other=Word+static_cast<size_t>(Size[0])*WordsPerZ;
|
|
Missing=(Rock^RockWords[Other])&Valid
|
|
&~(Candidates&CandidateWords[Other]);
|
|
while(Missing)
|
|
{
|
|
const uint32 Bit=std::countr_zero(Missing);Missing&=Missing-1;
|
|
const int32 Z=Min[2]+static_cast<int32>(W*64+Bit);
|
|
EnsureDistance(X,Y,Z);
|
|
EnsureDistance(X,Y+1,Z);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
AddedTotal+=AddedThisPass;
|
|
if(!bSignChanged)break;
|
|
}
|
|
std::printf("[GridStairDistanceRepair] exact_endpoint_proxies=%llu occupancy_corrections=%llu\n",
|
|
static_cast<unsigned long long>(AddedTotal),
|
|
static_cast<unsigned long long>(SignCorrections));
|
|
return AddedTotal;
|
|
}
|
|
|
|
void StoreSurfaceDistance(size_t Node,float Density)
|
|
{
|
|
int32 Q=static_cast<int32>(std::lround(Density*256.0f));
|
|
Q=std::clamp(Q,-32767,32767);
|
|
if(Q==0)Q=Density<0.0f?-1:1;
|
|
SurfaceDistanceQ[Node]=static_cast<int16>(Q);
|
|
}
|
|
|
|
bool IsInsideSolidOverlay(int32 X,int32 Y,int32 Z) const
|
|
{
|
|
const FBinKey K=PointBin(X,Y,Z);const auto It=SpatialBins.find(K);
|
|
if(It==SpatialBins.end())return false;
|
|
for(uint32 Id:It->second)
|
|
{
|
|
const FPrimitive& P=Primitives[Id];if(P.bCarve)continue;
|
|
if(X<P.Min[0]||X>P.Max[0]||Y<P.Min[1]||Y>P.Max[1]||Z<P.Min[2]||Z>P.Max[2])continue;
|
|
if(ShapeContains(P,X,Y,Z))return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool IsInsideCriticalAir(int32 X,int32 Y,int32 Z) const
|
|
{
|
|
const auto It=CriticalAirBins.find(PointBin(X,Y,Z));
|
|
if(It==CriticalAirBins.end())return false;
|
|
for(uint32 Id:It->second)
|
|
{
|
|
const FPrimitive& P=Primitives[Id];if(!P.bCarve||!P.bCritical)continue;
|
|
if(X<P.Min[0]||X>P.Max[0]||Y<P.Min[1]||Y>P.Max[1]||Z<P.Min[2]||Z>P.Max[2])continue;
|
|
if(ShapeContains(P,X,Y,Z))return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool IsNearCriticalAirBoundary(int32 X,int32 Y,int32 Z,double Margin) const
|
|
{
|
|
const auto It=CriticalAirBins.find(PointBin(X,Y,Z));
|
|
if(It==CriticalAirBins.end())return false;
|
|
for(uint32 Id:It->second)
|
|
{
|
|
const FPrimitive& P=Primitives[Id];if(!P.bCarve||!P.bCritical)continue;
|
|
if(X<P.Min[0]-Margin||X>P.Max[0]+Margin
|
|
||Y<P.Min[1]-Margin||Y>P.Max[1]+Margin
|
|
||Z<P.Min[2]-Margin||Z>P.Max[2]+Margin)continue;
|
|
float Distance=ShapeDistance(P,X,Y,Z);
|
|
if(P.bHasFloorClip)
|
|
{
|
|
const float FloorDistance=static_cast<float>(FloorAt(P,X,Y)-Z);
|
|
Distance=SmoothMaxDistance(Distance,FloorDistance,static_cast<float>(P.FloorBlendRadius));
|
|
}
|
|
if(std::abs(Distance)<=Margin)return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool IsInsideSolidOverlay(double X,double Y,double Z) const
|
|
{
|
|
const auto It=SpatialBins.find(PointBin(X,Y,Z));
|
|
if(It==SpatialBins.end())return false;
|
|
for(uint32 Id:It->second)
|
|
{
|
|
const FPrimitive& P=Primitives[Id];if(P.bCarve)continue;
|
|
if(X<P.Min[0]||X>P.Max[0]||Y<P.Min[1]||Y>P.Max[1]||Z<P.Min[2]||Z>P.Max[2])continue;
|
|
if(ShapeDistance(P,X,Y,Z)<=0.0f)return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
float SolidOverlayDistance(double X,double Y,double Z) const
|
|
{
|
|
const auto It=SpatialBins.find(PointBin(X,Y,Z));
|
|
if(It==SpatialBins.end())return -100000.0f;
|
|
float Best=-100000.0f;
|
|
for(uint32 Id:It->second)
|
|
{
|
|
const FPrimitive& P=Primitives[Id];if(P.bCarve)continue;
|
|
if(X<P.Min[0]||X>P.Max[0]||Y<P.Min[1]||Y>P.Max[1]||Z<P.Min[2]||Z>P.Max[2])continue;
|
|
Best=std::max(Best,-ShapeDistance(P,X,Y,Z));
|
|
}
|
|
return Best;
|
|
}
|
|
|
|
bool IsInsideCriticalAir(double X,double Y,double Z) const
|
|
{
|
|
const auto It=CriticalAirBins.find(PointBin(X,Y,Z));
|
|
if(It==CriticalAirBins.end())return false;
|
|
for(uint32 Id:It->second)
|
|
{
|
|
const FPrimitive& P=Primitives[Id];if(!P.bCarve||!P.bCritical)continue;
|
|
if(X<P.Min[0]||X>P.Max[0]||Y<P.Min[1]||Y>P.Max[1]||Z<P.Min[2]||Z>P.Max[2])continue;
|
|
if(ShapeContains(P,X,Y,Z))return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
float CaveDistance(double X,double Y,double Z,bool* OutFloorSurface=nullptr,
|
|
float* OutFloorInfluence=nullptr) const
|
|
{
|
|
const auto It=SpatialBins.find(PointBin(X,Y,Z));
|
|
if(It==SpatialBins.end())
|
|
{
|
|
if(OutFloorSurface)*OutFloorSurface=false;
|
|
if(OutFloorInfluence)*OutFloorInfluence=0.0f;
|
|
return 100000.0f;
|
|
}
|
|
float Best=100000.0f;
|
|
float BestFloorInfluence=0.0f;
|
|
for(uint32 Id:It->second)
|
|
{
|
|
const FPrimitive& P=Primitives[Id];if(!P.bCarve)continue;
|
|
if(X<P.Min[0]-DetailBand||X>P.Max[0]+DetailBand||Y<P.Min[1]-DetailBand||Y>P.Max[1]+DetailBand
|
|
||Z<P.Min[2]-DetailBand||Z>P.Max[2]+DetailBand)continue;
|
|
float Distance=ShapeDistance(P,X,Y,Z);
|
|
float PrimitiveFloorInfluence=0.0f;
|
|
if(P.bHasFloorClip)
|
|
{
|
|
const double Floor=FloorAt(P,X,Y);
|
|
const float FloorT=(static_cast<float>(std::abs(Z-Floor))-static_cast<float>(P.FloorBlendRadius))
|
|
/static_cast<float>(std::max(DetailBand*2,1));
|
|
PrimitiveFloorInfluence=1.0f-SmoothUnit(FloorT);
|
|
Distance=SmoothMaxDistance(Distance,static_cast<float>(Floor-Z),
|
|
static_cast<float>(P.FloorBlendRadius));
|
|
}
|
|
const float MetadataWidth=static_cast<float>(std::max(P.BlendRadius,6.0));
|
|
const float PrimitiveWeight=SmoothUnit(0.5f+(Best-Distance)/(2.0f*MetadataWidth));
|
|
const float Combined=SmoothMinDistance(Best,Distance,static_cast<float>(P.BlendRadius));
|
|
BestFloorInfluence=std::lerp(BestFloorInfluence,PrimitiveFloorInfluence,PrimitiveWeight);
|
|
Best=Combined;
|
|
}
|
|
if(OutFloorInfluence)*OutFloorInfluence=BestFloorInfluence;
|
|
if(OutFloorSurface)*OutFloorSurface=BestFloorInfluence>=0.5f;
|
|
return Best;
|
|
}
|
|
|
|
float StrataDistance(double Z) const
|
|
{
|
|
if(LayerRanges.empty())return -100000.0f;
|
|
return static_cast<float>(std::min(Z-LayerRanges[0].Lo,LayerRanges[0].Hi-Z));
|
|
}
|
|
|
|
const FLayerField* FindLayerField(double Z) const
|
|
{
|
|
for(const FLayerField& Field:LayerFields)
|
|
if(Z>=Field.Lo&&Z<=Field.Hi)return &Field;
|
|
return nullptr;
|
|
}
|
|
|
|
float EffectiveRockDistance(double X,double Y,double Z,
|
|
ESurfaceFamily* OutFamily=nullptr,
|
|
float* OutSurfaceSDF=nullptr,
|
|
bool* OutFloorSurface=nullptr,
|
|
float* OutCaveInfluence=nullptr,
|
|
float* OutFloorInfluence=nullptr,
|
|
bool* OutCriticalAir=nullptr,
|
|
const FBinItems* KnownBinItems=nullptr,
|
|
bool bKnownBin=false,
|
|
float* OutPassageInfluence=nullptr) const
|
|
{
|
|
const float Base=StrataDistance(Z);
|
|
float Cave=100000.0f;
|
|
float Overlay=-100000.0f;
|
|
float CaveFloorInfluence=0.0f;
|
|
float CavePassageInfluence=0.0f;
|
|
bool bCriticalAir=false;
|
|
const FBinItems* BinItems=KnownBinItems;
|
|
if(!bKnownBin)
|
|
{
|
|
const auto Bin=SpatialBins.find(PointBin(X,Y,Z));
|
|
if(Bin!=SpatialBins.end())BinItems=&Bin->second;
|
|
}
|
|
if(BinItems)
|
|
{
|
|
for(uint32 Id:*BinItems)
|
|
{
|
|
const FPrimitive& P=Primitives[Id];
|
|
if(P.bCarve)
|
|
{
|
|
if(P.bCritical&&X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1]
|
|
&&Z>=P.Min[2]&&Z<=P.Max[2]&&ShapeContains(P,
|
|
static_cast<int32>(X),static_cast<int32>(Y),static_cast<int32>(Z)))
|
|
bCriticalAir=true;
|
|
if(X<P.Min[0]-DetailBand||X>P.Max[0]+DetailBand
|
|
||Y<P.Min[1]-DetailBand||Y>P.Max[1]+DetailBand
|
|
||Z<P.Min[2]-DetailBand||Z>P.Max[2]+DetailBand)continue;
|
|
float Distance=ShapeDistance(P,X,Y,Z);
|
|
float PrimitiveFloorInfluence=0.0f;
|
|
if(P.bHasFloorClip)
|
|
{
|
|
const double Floor=FloorAt(P,X,Y);
|
|
const float FloorT=(static_cast<float>(std::abs(Z-Floor))
|
|
-static_cast<float>(P.FloorBlendRadius))
|
|
/static_cast<float>(std::max(DetailBand*2,1));
|
|
PrimitiveFloorInfluence=1.0f-SmoothUnit(FloorT);
|
|
Distance=SmoothMaxDistance(Distance,static_cast<float>(Floor-Z),
|
|
static_cast<float>(P.FloorBlendRadius));
|
|
}
|
|
const float MetadataWidth=static_cast<float>(std::max(P.BlendRadius,6.0));
|
|
const float PrimitiveWeight=SmoothUnit(0.5f+(Cave-Distance)/(2.0f*MetadataWidth));
|
|
const float Combined=SmoothMinDistance(Cave,Distance,static_cast<float>(P.BlendRadius));
|
|
CaveFloorInfluence=std::lerp(CaveFloorInfluence,PrimitiveFloorInfluence,PrimitiveWeight);
|
|
if(OutPassageInfluence&&(CavePassageInfluence>0.0f||P.bPassageSurface))
|
|
CavePassageInfluence=std::lerp(CavePassageInfluence,
|
|
P.bPassageSurface?1.0f:0.0f,PrimitiveWeight);
|
|
Cave=Combined;
|
|
}
|
|
else if(X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1]
|
|
&&Z>=P.Min[2]&&Z<=P.Max[2])
|
|
Overlay=std::max(Overlay,-ShapeDistance(P,X,Y,Z));
|
|
}
|
|
}
|
|
if(OutCriticalAir)*OutCriticalAir=bCriticalAir;
|
|
const float CarveOrOverlay=std::max(Cave,Overlay);
|
|
const ESurfaceFamily Family=Base<=CarveOrOverlay?ESurfaceFamily::Strata:
|
|
(Cave>=Overlay?ESurfaceFamily::Cave:ESurfaceFamily::SolidOverlay);
|
|
constexpr float FamilyBlendWidth=6.0f;
|
|
const float CaveVsOverlay=SmoothUnit(0.5f+(Cave-Overlay)/(2.0f*FamilyBlendWidth));
|
|
const float CarveInfluence=SmoothUnit(0.5f+(Base-CarveOrOverlay)/(2.0f*FamilyBlendWidth));
|
|
const float CaveInfluence=CarveInfluence*CaveVsOverlay;
|
|
const float FloorInfluence=CaveInfluence*CaveFloorInfluence;
|
|
const float PassageInfluence=CaveInfluence*CavePassageInfluence;
|
|
const float RockDistance=SmoothMinDistance(Base,CarveOrOverlay,StrataCaveBlendRadius);
|
|
if(OutFamily)
|
|
*OutFamily=Family;
|
|
if(OutSurfaceSDF)
|
|
*OutSurfaceSDF=Family==ESurfaceFamily::Strata?Base:
|
|
(Family==ESurfaceFamily::Cave?Cave:Overlay);
|
|
if(OutCaveInfluence)*OutCaveInfluence=CaveInfluence;
|
|
if(OutFloorInfluence)*OutFloorInfluence=FloorInfluence;
|
|
if(OutPassageInfluence)*OutPassageInfluence=PassageInfluence;
|
|
if(OutFloorSurface)*OutFloorSurface=FloorInfluence>=0.5f;
|
|
return RockDistance;
|
|
}
|
|
|
|
struct FEffectiveRockSample
|
|
{
|
|
float Distance=0.0f;
|
|
float SurfaceSDF=0.0f;
|
|
float CaveInfluence=0.0f;
|
|
float FloorInfluence=0.0f;
|
|
float PassageInfluence=0.0f;
|
|
ESurfaceFamily Family=ESurfaceFamily::Strata;
|
|
bool bFloorSurface=false;
|
|
bool bCriticalAir=false;
|
|
};
|
|
|
|
void ShapeDistanceZ8(const FPrimitive& P,int32 X,int32 Y,const int32 Z[8],int32 Count,
|
|
float OutDistance[8]) const
|
|
{
|
|
const __m256d Zero=_mm256_setzero_pd();
|
|
const __m256d One=_mm256_set1_pd(1.0);
|
|
const __m256d SignMask=_mm256_set1_pd(-0.0);
|
|
for(int32 Base=0;Base<Count;Base+=4)
|
|
{
|
|
double ZLane[4];
|
|
const int32 Lanes=std::min(4,Count-Base);
|
|
for(int32 Lane=0;Lane<4;++Lane)
|
|
ZLane[Lane]=static_cast<double>(Z[Base+std::min(Lane,Lanes-1)]);
|
|
const __m256d VZ=_mm256_setr_pd(ZLane[0],ZLane[1],ZLane[2],ZLane[3]);
|
|
__m256d Distance;
|
|
if(P.bCapsule)
|
|
{
|
|
const double VX=static_cast<double>(X)-P.CapsuleStartWorld[0];
|
|
const double VY=static_cast<double>(Y)-P.CapsuleStartWorld[1];
|
|
const __m256d VXv=_mm256_set1_pd(VX);
|
|
const __m256d VYv=_mm256_set1_pd(VY);
|
|
const __m256d VZv=_mm256_sub_pd(VZ,_mm256_set1_pd(P.CapsuleStartWorld[2]));
|
|
__m256d VD=_mm256_mul_pd(VXv,_mm256_set1_pd(P.CapsuleAxisDelta[0]));
|
|
VD=_mm256_add_pd(VD,_mm256_mul_pd(VYv,_mm256_set1_pd(P.CapsuleAxisDelta[1])));
|
|
VD=_mm256_add_pd(VD,_mm256_mul_pd(VZv,_mm256_set1_pd(P.CapsuleAxisDelta[2])));
|
|
const __m256d T=P.CapsuleAxisLengthSquared>1.0e-12
|
|
?_mm256_max_pd(Zero,_mm256_min_pd(One,
|
|
_mm256_div_pd(VD,_mm256_set1_pd(P.CapsuleAxisLengthSquared))))
|
|
:Zero;
|
|
const __m256d EX=_mm256_sub_pd(VXv,
|
|
_mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[0])));
|
|
const __m256d EY=_mm256_sub_pd(VYv,
|
|
_mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[1])));
|
|
const __m256d EZ=_mm256_sub_pd(VZv,
|
|
_mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[2])));
|
|
__m256d DistanceSquared=_mm256_mul_pd(EX,EX);
|
|
DistanceSquared=_mm256_add_pd(DistanceSquared,_mm256_mul_pd(EY,EY));
|
|
DistanceSquared=_mm256_add_pd(DistanceSquared,_mm256_mul_pd(EZ,EZ));
|
|
const __m256d Root=_mm256_sqrt_pd(DistanceSquared);
|
|
double TValues[4],RootValues[4];
|
|
_mm256_storeu_pd(TValues,T);
|
|
_mm256_storeu_pd(RootValues,Root);
|
|
for(int32 Lane=0;Lane<Lanes;++Lane)
|
|
OutDistance[Base+Lane]=static_cast<float>(RootValues[Lane]
|
|
-CapsuleRadiusAtT(P,TValues[Lane],P.CapsuleAxisLength));
|
|
continue;
|
|
}
|
|
if(P.bRoundedBox)
|
|
{
|
|
const double DX=std::abs(static_cast<double>(X)-P.CenterSub[0]/16.0)
|
|
-P.RoundedHalfExtent[0];
|
|
const double DY=std::abs(static_cast<double>(Y)-P.CenterSub[1]/16.0)
|
|
-P.RoundedHalfExtent[1];
|
|
const double DZCenter=P.CenterSub[2]/16.0;
|
|
const __m256d DZv=_mm256_sub_pd(_mm256_andnot_pd(SignMask,
|
|
_mm256_sub_pd(VZ,_mm256_set1_pd(DZCenter))),
|
|
_mm256_set1_pd(P.RoundedHalfExtent[2]));
|
|
const __m256d DXv=_mm256_set1_pd(DX),DYv=_mm256_set1_pd(DY);
|
|
const __m256d Ox=_mm256_max_pd(DXv,Zero),Oy=_mm256_max_pd(DYv,Zero);
|
|
const __m256d Oz=_mm256_max_pd(DZv,Zero);
|
|
__m256d OutsideSquared=_mm256_mul_pd(Ox,Ox);
|
|
OutsideSquared=_mm256_add_pd(OutsideSquared,_mm256_mul_pd(Oy,Oy));
|
|
OutsideSquared=_mm256_add_pd(OutsideSquared,_mm256_mul_pd(Oz,Oz));
|
|
const __m256d MaxXY=_mm256_max_pd(DXv,DYv);
|
|
const __m256d MaxQ=_mm256_max_pd(MaxXY,DZv);
|
|
const __m256d Inside=_mm256_min_pd(MaxQ,Zero);
|
|
Distance=_mm256_sub_pd(_mm256_add_pd(_mm256_sqrt_pd(OutsideSquared),Inside),
|
|
_mm256_set1_pd(P.Rounding));
|
|
}
|
|
else
|
|
{
|
|
const __m256d D[3]={
|
|
_mm256_set1_pd(static_cast<double>(X)-P.CenterSub[0]/16.0),
|
|
_mm256_set1_pd(static_cast<double>(Y)-P.CenterSub[1]/16.0),
|
|
_mm256_sub_pd(VZ,_mm256_set1_pd(P.CenterSub[2]/16.0))};
|
|
__m256d Q=_mm256_setzero_pd();
|
|
for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J)
|
|
{
|
|
const double Coefficient=static_cast<double>(P.Q[I][J])/QScale;
|
|
__m256d Term=_mm256_mul_pd(_mm256_set1_pd(Coefficient),D[I]);
|
|
Term=_mm256_mul_pd(Term,D[J]);
|
|
Term=_mm256_mul_pd(Term,_mm256_set1_pd(256.0));
|
|
Q=_mm256_add_pd(Q,Term);
|
|
}
|
|
Distance=_mm256_mul_pd(_mm256_sub_pd(
|
|
_mm256_sqrt_pd(_mm256_max_pd(Q,Zero)),_mm256_set1_pd(1.0)),
|
|
_mm256_set1_pd(P.MinRadius));
|
|
}
|
|
double Values[4];
|
|
_mm256_storeu_pd(Values,Distance);
|
|
for(int32 Lane=0;Lane<Lanes;++Lane)
|
|
OutDistance[Base+Lane]=static_cast<float>(Values[Lane]);
|
|
}
|
|
}
|
|
|
|
void EffectiveRockDistanceZ8(int32 X,int32 Y,const int32 Z[8],int32 Count,
|
|
const FBinItems* BinItems,FEffectiveRockSample Out[8]) const
|
|
{
|
|
float Base[8],Cave[8],Overlay[8],CaveFloorInfluence[8],CavePassageInfluence[8];
|
|
bool CriticalAir[8]{};
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
{
|
|
Base[Lane]=StrataDistance(Z[Lane]);
|
|
Cave[Lane]=100000.0f;
|
|
Overlay[Lane]=-100000.0f;
|
|
CaveFloorInfluence[Lane]=0.0f;
|
|
CavePassageInfluence[Lane]=0.0f;
|
|
}
|
|
if(BinItems)
|
|
{
|
|
for(uint32 Id:*BinItems)
|
|
{
|
|
const FPrimitive& P=Primitives[Id];
|
|
bool Eligible[8]{};
|
|
bool AnyEligible=false;
|
|
if(P.bCarve)
|
|
{
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
{
|
|
const int32 ZValue=Z[Lane];
|
|
if(P.bCritical&&X>=P.Min[0]&&X<=P.Max[0]
|
|
&&Y>=P.Min[1]&&Y<=P.Max[1]
|
|
&&ZValue>=P.Min[2]&&ZValue<=P.Max[2]
|
|
&&ShapeContains(P,X,Y,ZValue))
|
|
CriticalAir[Lane]=true;
|
|
if(X<P.Min[0]-DetailBand||X>P.Max[0]+DetailBand
|
|
||Y<P.Min[1]-DetailBand||Y>P.Max[1]+DetailBand
|
|
||ZValue<P.Min[2]-DetailBand||ZValue>P.Max[2]+DetailBand)
|
|
continue;
|
|
Eligible[Lane]=true;
|
|
AnyEligible=true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
{
|
|
const int32 ZValue=Z[Lane];
|
|
Eligible[Lane]=X>=P.Min[0]&&X<=P.Max[0]
|
|
&&Y>=P.Min[1]&&Y<=P.Max[1]
|
|
&&ZValue>=P.Min[2]&&ZValue<=P.Max[2];
|
|
AnyEligible=AnyEligible||Eligible[Lane];
|
|
}
|
|
}
|
|
if(!AnyEligible)continue;
|
|
|
|
float Distances[8];
|
|
ShapeDistanceZ8(P,X,Y,Z,Count,Distances);
|
|
if(P.bCarve)
|
|
{
|
|
const double Floor=P.bHasFloorClip?FloorAt(P,X,Y):0.0;
|
|
const float MetadataWidth=static_cast<float>(std::max(P.BlendRadius,6.0));
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
{
|
|
if(!Eligible[Lane])continue;
|
|
float Distance=Distances[Lane];
|
|
float PrimitiveFloorInfluence=0.0f;
|
|
if(P.bHasFloorClip)
|
|
{
|
|
const float FloorT=(static_cast<float>(std::abs(Z[Lane]-Floor))
|
|
-static_cast<float>(P.FloorBlendRadius))
|
|
/static_cast<float>(std::max(DetailBand*2,1));
|
|
PrimitiveFloorInfluence=1.0f-SmoothUnit(FloorT);
|
|
Distance=SmoothMaxDistance(Distance,static_cast<float>(Floor-Z[Lane]),
|
|
static_cast<float>(P.FloorBlendRadius));
|
|
}
|
|
const float PrimitiveWeight=SmoothUnit(0.5f
|
|
+(Cave[Lane]-Distance)/(2.0f*MetadataWidth));
|
|
const float Combined=SmoothMinDistance(Cave[Lane],Distance,
|
|
static_cast<float>(P.BlendRadius));
|
|
CaveFloorInfluence[Lane]=std::lerp(CaveFloorInfluence[Lane],
|
|
PrimitiveFloorInfluence,PrimitiveWeight);
|
|
if(CavePassageInfluence[Lane]>0.0f||P.bPassageSurface)
|
|
CavePassageInfluence[Lane]=std::lerp(CavePassageInfluence[Lane],
|
|
P.bPassageSurface?1.0f:0.0f,PrimitiveWeight);
|
|
Cave[Lane]=Combined;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
if(Eligible[Lane])Overlay[Lane]=std::max(Overlay[Lane],-Distances[Lane]);
|
|
}
|
|
}
|
|
}
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
{
|
|
const float CarveOrOverlay=std::max(Cave[Lane],Overlay[Lane]);
|
|
const ESurfaceFamily Family=Base[Lane]<=CarveOrOverlay?ESurfaceFamily::Strata:
|
|
(Cave[Lane]>=Overlay[Lane]?ESurfaceFamily::Cave:ESurfaceFamily::SolidOverlay);
|
|
constexpr float FamilyBlendWidth=6.0f;
|
|
const float CaveVsOverlay=SmoothUnit(0.5f
|
|
+(Cave[Lane]-Overlay[Lane])/(2.0f*FamilyBlendWidth));
|
|
const float CarveInfluence=SmoothUnit(0.5f
|
|
+(Base[Lane]-CarveOrOverlay)/(2.0f*FamilyBlendWidth));
|
|
const float CaveInfluence=CarveInfluence*CaveVsOverlay;
|
|
const float FloorInfluence=CaveInfluence*CaveFloorInfluence[Lane];
|
|
const float PassageInfluence=CaveInfluence*CavePassageInfluence[Lane];
|
|
Out[Lane].Distance=SmoothMinDistance(Base[Lane],CarveOrOverlay,StrataCaveBlendRadius);
|
|
Out[Lane].Family=Family;
|
|
Out[Lane].SurfaceSDF=Family==ESurfaceFamily::Strata?Base[Lane]:
|
|
(Family==ESurfaceFamily::Cave?Cave[Lane]:Overlay[Lane]);
|
|
Out[Lane].CaveInfluence=CaveInfluence;
|
|
Out[Lane].FloorInfluence=FloorInfluence;
|
|
Out[Lane].PassageInfluence=PassageInfluence;
|
|
Out[Lane].bFloorSurface=FloorInfluence>=0.5f;
|
|
Out[Lane].bCriticalAir=CriticalAir[Lane];
|
|
}
|
|
}
|
|
|
|
public:
|
|
void DensityAtLatticeZRun(int32 X,int32 Y,int32 ZStart,int32 ZStep,
|
|
int32 Count,float* OutDensity) const
|
|
{
|
|
if(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;
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
bHasLegacySurfaceSample|=UsesLegacySurfaceField(static_cast<float>(X),
|
|
static_cast<float>(Y),static_cast<float>(ZStart+Lane*ZStep));
|
|
if(bHasLegacySurfaceSample)
|
|
{
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
OutDensity[Lane]=DensityAtLattice(X,Y,ZStart+Lane*ZStep);
|
|
return;
|
|
}
|
|
if(!bAnalyticOnly)
|
|
{
|
|
for(int32 Lane=0;Lane<Count;++Lane)
|
|
OutDensity[Lane]=DensityAtLattice(X,Y,ZStart+Lane*ZStep);
|
|
return;
|
|
}
|
|
|
|
FDetailNoiseLineContext DetailNoiseLine(
|
|
static_cast<float>(X),static_cast<float>(Y),static_cast<uint32>(Owner->Seed));
|
|
for(int32 Offset=0;Offset<Count;)
|
|
{
|
|
int32 Z[8];
|
|
const int32 Remaining=std::min(8,Count-Offset);
|
|
for(int32 Lane=0;Lane<Remaining;++Lane)
|
|
Z[Lane]=ZStart+(Offset+Lane)*ZStep;
|
|
const int32 BinZ=FloorDiv(Z[0],ActiveSpatialBinSize);
|
|
const auto Found=SpatialBins.find({FloorDiv(X,ActiveSpatialBinSize),
|
|
FloorDiv(Y,ActiveSpatialBinSize),BinZ});
|
|
const FBinItems* BinItems=Found==SpatialBins.end()?nullptr:&Found->second;
|
|
int32 BatchCount=1;
|
|
while(BatchCount<Remaining
|
|
&&FloorDiv(Z[BatchCount],ActiveSpatialBinSize)==BinZ)++BatchCount;
|
|
|
|
FEffectiveRockSample Samples[8]{};
|
|
EffectiveRockDistanceZ8(X,Y,Z,BatchCount,BinItems,Samples);
|
|
for(int32 Lane=0;Lane<BatchCount;++Lane)
|
|
{
|
|
const int32 SampleZ=Z[Lane];
|
|
const bool bInsideFine=X>=Min[0]&&X<=Max[0]
|
|
&&Y>=Min[1]&&Y<=Max[1]&&SampleZ>=Min[2]&&SampleZ<=Max[2];
|
|
float RockDistance=Samples[Lane].Distance;
|
|
if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth
|
|
&&std::abs(RockDistance)<=DetailSupremum+0.125f)
|
|
{
|
|
const float Fade=DetailBandFadeAt(static_cast<float>(X),
|
|
static_cast<float>(Y),static_cast<float>(SampleZ),RockDistance,
|
|
Samples[Lane].SurfaceSDF,Samples[Lane].CaveInfluence,
|
|
Samples[Lane].FloorInfluence);
|
|
if(Fade>0.0f)RockDistance+=DetailNoiseOnLine(static_cast<float>(X),
|
|
static_cast<float>(Y),static_cast<float>(SampleZ),
|
|
Samples[Lane].SurfaceSDF,Samples[Lane].CaveInfluence,
|
|
Samples[Lane].FloorInfluence,Samples[Lane].PassageInfluence,
|
|
DetailNoiseLine)*Fade;
|
|
}
|
|
if(Samples[Lane].bCriticalAir)
|
|
RockDistance=std::min(RockDistance,-1.0f/16.0f);
|
|
const bool bRock=RockDistance>=0.0f;
|
|
float Density=-RockDistance;
|
|
if((Density<0.0f)!=bRock||std::abs(Density)<0.05f)
|
|
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),0.05f);
|
|
OutDensity[Offset+Lane]=ApplyEditBrushes(Density,X,Y,SampleZ);
|
|
}
|
|
Offset+=BatchCount;
|
|
}
|
|
}
|
|
|
|
void DensityAtLatticeZ8(int32 X,int32 Y,int32 ZStart,int32 ZStep,
|
|
int32 Count,float OutDensity[8]) const
|
|
{
|
|
DensityAtLatticeZRun(X,Y,ZStart,ZStep,std::clamp(Count,0,8),OutDensity);
|
|
}
|
|
|
|
private:
|
|
|
|
float CoarseRockDistance(double X,double Y,double Z,bool* OutCriticalAir) const
|
|
{
|
|
const float Base=StrataDistance(Z);
|
|
float Cave=100000.0f,Overlay=-100000.0f;
|
|
bool bCriticalAir=false;
|
|
const FBinKey QueryKey=PointBin(X,Y,Z);
|
|
const auto QueryBin=SpatialBins.find(QueryKey);
|
|
const FBinItems* QueryItems=QueryBin==SpatialBins.end()?nullptr:&QueryBin->second;
|
|
if(QueryItems)
|
|
{
|
|
for(uint32 Id:*QueryItems)
|
|
{
|
|
const FPrimitive& P=Primitives[Id];
|
|
const VF_FieldPrimitive& Shared=SharedFieldPrimitives[Id];
|
|
if(P.bCarve)
|
|
{
|
|
if(P.bCritical&&X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1]
|
|
&&Z>=P.Min[2]&&Z<=P.Max[2]&&ShapeContains(P,
|
|
static_cast<int32>(X),static_cast<int32>(Y),static_cast<int32>(Z)))
|
|
bCriticalAir=true;
|
|
if(X<P.Min[0]-DetailBand||X>P.Max[0]+DetailBand
|
|
||Y<P.Min[1]-DetailBand||Y>P.Max[1]+DetailBand
|
|
||Z<P.Min[2]-DetailBand||Z>P.Max[2]+DetailBand)continue;
|
|
float Distance=VF_FieldPrimitiveDistance(Shared,X,Y,Z);
|
|
if(P.bHasFloorClip)
|
|
{
|
|
const double Floor=VF_FieldPrimitiveFloorAt(Shared,X,Y);
|
|
Distance=VF_FieldSmoothMax(Distance,static_cast<float>(Floor-Z),
|
|
static_cast<float>(P.FloorBlendRadius));
|
|
}
|
|
Cave=VF_FieldSmoothMin(Cave,Distance,static_cast<float>(P.BlendRadius));
|
|
}
|
|
else if(X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1]
|
|
&&Z>=P.Min[2]&&Z<=P.Max[2])
|
|
Overlay=std::max(Overlay,-VF_FieldPrimitiveDistance(Shared,X,Y,Z));
|
|
}
|
|
}
|
|
if(OutCriticalAir)*OutCriticalAir=bCriticalAir;
|
|
return VF_FieldSmoothMin(Base,std::max(Cave,Overlay),StrataCaveBlendRadius);
|
|
}
|
|
|
|
struct FOwnerNoiseLineContext
|
|
{
|
|
struct FAnchorCache
|
|
{
|
|
const FLayerField* Field=nullptr;
|
|
int32 GridZ0=MIN_int32;
|
|
int32 SampleZ0=MIN_int32,SampleZ1=MIN_int32;
|
|
float LowerCore=0.0f,UpperCore=0.0f;
|
|
} Step8,Step2;
|
|
};
|
|
|
|
float OwnerCaveRoughnessCore(double X,double Y,double Z,
|
|
const FStrateGenerationParams& Params) const
|
|
{
|
|
// Keep CPU prediction and the near-field HLSL on one scalar source. The helper
|
|
// preserves the legacy seed offsets, octave order, and roughness families.
|
|
return VF_FieldOwnerRoughnessCore(static_cast<float>(X),static_cast<float>(Y),
|
|
static_cast<float>(Z),static_cast<uint32>(Owner->Seed),Params.SurfaceRoughness,
|
|
Params.RoughnessFrequency,Params.VerticalScale,Params.DomainWarpStrength,
|
|
Params.DomainWarpFrequency,static_cast<uint32>(Params.RoughnessNoiseType));
|
|
}
|
|
|
|
float OwnerCaveRoughness(double X,double Y,double Z,float SurfaceSDF,
|
|
const FLayerField& Field,float CaveInfluence,
|
|
FOwnerNoiseLineContext& LineContext,
|
|
FDetailWorkerProfile* Profile=nullptr,
|
|
int32 AnchorStep=8) const
|
|
{
|
|
const FStrateGenerationParams& Params=Field.Params;
|
|
const float Depth=Params.SurfaceRoughness*2.0f;
|
|
if(!(Depth>0.0f)||std::abs(SurfaceSDF)>=Depth||!(Params.RoughnessFrequency>0.0f))return 0.0f;
|
|
if(Profile)++Profile->OwnerRoughnessActiveCalls;
|
|
const int32 ZStep=std::max(1,AnchorStep);
|
|
const int32 GridZ0=static_cast<int32>(std::floor(Z/static_cast<double>(ZStep)))*ZStep;
|
|
const int32 SampleZ0=std::max(GridZ0,Field.Lo);
|
|
const int32 SampleZ1=std::min(GridZ0+ZStep,Field.Hi);
|
|
float Total=0.0f;
|
|
if(ZStep==1)
|
|
Total=OwnerCaveRoughnessCore(X,Y,Z,Params);
|
|
else if(SampleZ1>SampleZ0)
|
|
{
|
|
FOwnerNoiseLineContext::FAnchorCache& Cache=ZStep==2
|
|
?LineContext.Step2:LineContext.Step8;
|
|
const bool bCacheHit=Cache.Field==&Field&&Cache.GridZ0==GridZ0
|
|
&&Cache.SampleZ0==SampleZ0&&Cache.SampleZ1==SampleZ1;
|
|
if(Profile)
|
|
{
|
|
if(bCacheHit)++Profile->OwnerNoiseCacheHits;
|
|
else ++Profile->OwnerNoiseCacheMisses;
|
|
}
|
|
if(!bCacheHit)
|
|
{
|
|
const bool bReusesPreviousUpper=Cache.Field==&Field
|
|
&&Cache.SampleZ1==SampleZ0;
|
|
Cache.Field=&Field;
|
|
Cache.GridZ0=GridZ0;
|
|
Cache.SampleZ0=SampleZ0;
|
|
Cache.SampleZ1=SampleZ1;
|
|
Cache.LowerCore=bReusesPreviousUpper?Cache.UpperCore
|
|
:OwnerCaveRoughnessCore(X,Y,SampleZ0,Params);
|
|
Cache.UpperCore=OwnerCaveRoughnessCore(X,Y,SampleZ1,Params);
|
|
}
|
|
const float Alpha=static_cast<float>((Z-SampleZ0)/(SampleZ1-SampleZ0));
|
|
Total=FMath::Lerp(Cache.LowerCore,Cache.UpperCore,Alpha);
|
|
}
|
|
else
|
|
Total=OwnerCaveRoughnessCore(X,Y,Z,Params);
|
|
if(SurfaceSDF<0.0f)
|
|
Total=std::lerp(Total,std::min(Total,0.0f),std::clamp(CaveInfluence,0.0f,1.0f));
|
|
const float Fade=1.0f-std::abs(SurfaceSDF)/Depth;
|
|
return Total*Fade*Fade;
|
|
}
|
|
|
|
float OwnerCaveRoughnessForDetail(double X,double Y,double Z,float SurfaceSDF,
|
|
const FLayerField& Field,float CaveInfluence,
|
|
float PassageInfluence,
|
|
FOwnerNoiseLineContext& LineContext,
|
|
FDetailWorkerProfile* Profile=nullptr) const
|
|
{
|
|
constexpr float DirectStart=0.40f,DirectEnd=0.60f;
|
|
constexpr int32 PassageAnchorStep=2;
|
|
const float PassageWeight=std::clamp(PassageInfluence,0.0f,1.0f);
|
|
if(PassageWeight>=DirectEnd)
|
|
return OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field,CaveInfluence,
|
|
LineContext,Profile,PassageAnchorStep);
|
|
|
|
const float Anchored=OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field,
|
|
CaveInfluence,LineContext,Profile);
|
|
if(PassageWeight<=DirectStart)return Anchored;
|
|
|
|
const float Direct=OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field,
|
|
CaveInfluence,LineContext,Profile,PassageAnchorStep);
|
|
const float DirectWeight=SmoothUnit((PassageWeight-DirectStart)
|
|
/(DirectEnd-DirectStart));
|
|
const float AnchoredWeight=1.0f-DirectWeight;
|
|
const float Normalization=1.0f/std::sqrt(
|
|
AnchoredWeight*AnchoredWeight+DirectWeight*DirectWeight);
|
|
return (AnchoredWeight*Anchored+DirectWeight*Direct)*Normalization;
|
|
}
|
|
|
|
static float SurfaceRelief(float X,float Y,float Z,uint32 Seed,
|
|
float* OutPassageBandRelief=nullptr)
|
|
{
|
|
// Keep all active bands at four voxels or wider: the full-detail lattice can
|
|
// resolve them without turning the surface into per-voxel steps. The broad
|
|
// owner comparison showed that the shortest octave needed more usable relief.
|
|
const float Macro=FStandaloneResearchTunnels::ValueNoise(X,Y,Z,32,Seed^0xB5297A4Du);
|
|
const float Mid=FStandaloneResearchTunnels::ValueNoise(X,Y,Z,16,Seed^0x63D83595u);
|
|
const float Fine=FStandaloneResearchTunnels::ValueNoise(X,Y,Z,4,Seed^0xA511E9B3u);
|
|
if(OutPassageBandRelief)
|
|
*OutPassageBandRelief=1.20f*Macro+(0.80f+0.25f*Macro)*(0.40f*Mid);
|
|
return 1.20f*Macro+(0.80f+0.25f*Macro)*(0.40f*Mid+0.60f*Fine);
|
|
}
|
|
|
|
struct FValueNoiseLineContext
|
|
{
|
|
int32 X0=0,Y0=0,Z0=std::numeric_limits<int32>::min();
|
|
int32 Wavelength=1;
|
|
uint32 Seed=0;
|
|
float Inv=1.0f,TX=0.0f,TY=0.0f;
|
|
std::array<float,8> Corners{};
|
|
|
|
FValueNoiseLineContext(float X,float Y,int32 InWavelength,uint32 InSeed)
|
|
:Wavelength(InWavelength),Seed(InSeed),Inv(1.0f/static_cast<float>(InWavelength))
|
|
{
|
|
const float FX=X*Inv,FY=Y*Inv;
|
|
X0=FMath::FloorToInt(FX);Y0=FMath::FloorToInt(FY);
|
|
TX=FStandaloneResearchTunnels::Fade(FX-static_cast<float>(X0));
|
|
TY=FStandaloneResearchTunnels::Fade(FY-static_cast<float>(Y0));
|
|
}
|
|
|
|
void SetPlane(int32 Z,bool bUpper)
|
|
{
|
|
const size_t I=bUpper?4u:0u;
|
|
Corners[I+0]=FStandaloneResearchTunnels::NoiseValue(X0,Y0,Z,Seed);
|
|
Corners[I+1]=FStandaloneResearchTunnels::NoiseValue(X0+1,Y0,Z,Seed);
|
|
Corners[I+2]=FStandaloneResearchTunnels::NoiseValue(X0,Y0+1,Z,Seed);
|
|
Corners[I+3]=FStandaloneResearchTunnels::NoiseValue(X0+1,Y0+1,Z,Seed);
|
|
}
|
|
|
|
float Sample(float Z)
|
|
{
|
|
const float FZ=Z*Inv;
|
|
const int32 NewZ0=FMath::FloorToInt(FZ);
|
|
const float TZ=FStandaloneResearchTunnels::Fade(FZ-static_cast<float>(NewZ0));
|
|
if(NewZ0!=Z0)
|
|
{
|
|
if(NewZ0==Z0+1)
|
|
{
|
|
Corners[0]=Corners[4];Corners[2]=Corners[6];
|
|
Corners[1]=Corners[5];Corners[3]=Corners[7];
|
|
SetPlane(NewZ0+1,true);
|
|
}
|
|
else
|
|
{
|
|
SetPlane(NewZ0,false);SetPlane(NewZ0+1,true);
|
|
}
|
|
Z0=NewZ0;
|
|
}
|
|
const float Z00=FMath::Lerp(Corners[0],Corners[1],TX);
|
|
const float Z10=FMath::Lerp(Corners[2],Corners[3],TX);
|
|
const float Z01=FMath::Lerp(Corners[4],Corners[5],TX);
|
|
const float Z11=FMath::Lerp(Corners[6],Corners[7],TX);
|
|
return FMath::Lerp(FMath::Lerp(Z00,Z10,TY),FMath::Lerp(Z01,Z11,TY),TZ);
|
|
}
|
|
};
|
|
|
|
struct FDetailNoiseLineContext
|
|
{
|
|
FValueNoiseLineContext Macro,Mid,Fine;
|
|
FOwnerNoiseLineContext OwnerNoise;
|
|
FDetailNoiseLineContext(float X,float Y,uint32 Seed)
|
|
:Macro(X,Y,32,Seed^0xB5297A4Du),Mid(X,Y,16,Seed^0x63D83595u),
|
|
Fine(X,Y,4,Seed^0xA511E9B3u)
|
|
{}
|
|
|
|
float SurfaceReliefAt(float Z,bool bMacroKnown=false,float KnownMacro=0.0f,
|
|
float* OutPassageBandRelief=nullptr)
|
|
{
|
|
const float MacroValue=bMacroKnown?KnownMacro:Macro.Sample(Z);
|
|
const float MidValue=Mid.Sample(Z);
|
|
const float FineValue=Fine.Sample(Z);
|
|
if(OutPassageBandRelief)
|
|
*OutPassageBandRelief=1.20f*MacroValue
|
|
+(0.80f+0.25f*MacroValue)*(0.40f*MidValue);
|
|
return 1.20f*MacroValue+(0.80f+0.25f*MacroValue)
|
|
*(0.40f*MidValue+0.60f*FineValue);
|
|
}
|
|
|
|
};
|
|
|
|
float DetailNoise(double X,double Y,double Z,float SurfaceSDF,
|
|
float CaveInfluence,float FloorInfluence,float PassageInfluence) const
|
|
{
|
|
if(DetailVariant==EDetailVariant::Smooth)return 0.0f;
|
|
if(DetailVariant==EDetailVariant::PerVoxel)
|
|
{
|
|
const int32 IX=static_cast<int32>(std::floor(X)),IY=static_cast<int32>(std::floor(Y)),IZ=static_cast<int32>(std::floor(Z));
|
|
return FStandaloneResearchTunnels::NoiseValue(IX,IY,IZ,static_cast<uint32>(Owner->Seed))
|
|
*DetailSupremum;
|
|
}
|
|
float PassageBandRelief=0.0f;
|
|
const float Relief=SurfaceRelief(static_cast<float>(X),static_cast<float>(Y),
|
|
static_cast<float>(Z),static_cast<uint32>(Owner->Seed),&PassageBandRelief);
|
|
const float PassageWeight=std::clamp(PassageInfluence,0.0f,1.0f);
|
|
const FLayerField* Field=FindLayerField(Z);
|
|
FOwnerNoiseLineContext OwnerNoise;
|
|
float OwnerRelief=0.0f;
|
|
if(Field)
|
|
OwnerRelief=OwnerCaveRoughnessForDetail(X,Y,Z,SurfaceSDF,*Field,
|
|
CaveInfluence,PassageWeight,OwnerNoise);
|
|
const float Detail=std::clamp(BlendDetailFamilies(Relief,PassageBandRelief,
|
|
OwnerRelief,CaveInfluence,FloorInfluence,PassageWeight),-3.9f,3.9f);
|
|
return Detail;
|
|
}
|
|
|
|
float DetailNoiseOnLine(double X,double Y,double Z,float SurfaceSDF,
|
|
float CaveInfluence,float FloorInfluence,float PassageInfluence,
|
|
FDetailNoiseLineContext& NoiseLine,
|
|
FDetailWorkerProfile* Profile=nullptr,
|
|
const FLayerField* KnownField=nullptr,
|
|
bool bFieldKnown=false,bool bMacroKnown=false,
|
|
float KnownMacro=0.0f) const
|
|
{
|
|
if(DetailVariant!=EDetailVariant::Coherent)
|
|
return DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence,PassageInfluence);
|
|
using Clock=std::chrono::steady_clock;
|
|
const auto ReliefStart=Profile?Clock::now():Clock::time_point{};
|
|
float PassageBandRelief=0.0f;
|
|
const float Relief=NoiseLine.SurfaceReliefAt(static_cast<float>(Z),
|
|
bMacroKnown,KnownMacro,&PassageBandRelief);
|
|
if(Profile)
|
|
Profile->ReliefSampleNanoseconds+=static_cast<uint64>(
|
|
std::chrono::duration_cast<std::chrono::nanoseconds>(Clock::now()-ReliefStart).count());
|
|
const auto LookupStart=Profile?Clock::now():Clock::time_point{};
|
|
const FLayerField* Field=bFieldKnown?KnownField:FindLayerField(Z);
|
|
if(Profile)
|
|
Profile->LayerLookupSampleNanoseconds+=static_cast<uint64>(
|
|
std::chrono::duration_cast<std::chrono::nanoseconds>(Clock::now()-LookupStart).count());
|
|
const auto OwnerStart=Profile?Clock::now():Clock::time_point{};
|
|
if(Profile&&Field)++Profile->OwnerRoughnessFieldCalls;
|
|
const float PassageWeight=std::clamp(PassageInfluence,0.0f,1.0f);
|
|
float OwnerRelief=0.0f;
|
|
if(Field)
|
|
OwnerRelief=OwnerCaveRoughnessForDetail(X,Y,Z,SurfaceSDF,*Field,
|
|
CaveInfluence,PassageWeight,NoiseLine.OwnerNoise,Profile);
|
|
if(Profile)
|
|
Profile->OwnerRoughnessSampleNanoseconds+=static_cast<uint64>(
|
|
std::chrono::duration_cast<std::chrono::nanoseconds>(Clock::now()-OwnerStart).count());
|
|
const float Detail=std::clamp(BlendDetailFamilies(Relief,PassageBandRelief,
|
|
OwnerRelief,CaveInfluence,FloorInfluence,PassageWeight),-3.9f,3.9f);
|
|
return Detail;
|
|
}
|
|
|
|
bool EvaluateFast(double X,double Y,double Z) const
|
|
{
|
|
ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
|
|
bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
|
|
float PassageInfluence=0.0f;
|
|
float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,
|
|
&bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir,nullptr,false,
|
|
&PassageInfluence);
|
|
const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]&&Z>=Min[2]&&Z<=Max[2];
|
|
if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth
|
|
&&std::abs(RockDistance)<=DetailSupremum+0.125f)
|
|
{
|
|
const float Fade=DetailBandFadeAt(X,Y,Z,RockDistance,SurfaceSDF,
|
|
CaveInfluence,FloorInfluence);
|
|
if(Fade>0.0f)RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,
|
|
FloorInfluence,PassageInfluence)*Fade;
|
|
}
|
|
if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f);
|
|
return RockDistance>=0.0f;
|
|
}
|
|
|
|
bool ValidatePassageRoutes()
|
|
{
|
|
constexpr double SampleStep=0.5;
|
|
uint32 PassageIndex=0;
|
|
for(const FVoxelPassage& P:Owner->Manager->GetPassages())
|
|
{
|
|
const auto& Points=P.ControlPoints;
|
|
if(Points.Num()<2){++PassageIndex;continue;}
|
|
uint64 RouteSamples=0,RouteFailures=0,RouteCritical=0;
|
|
int32 FirstFailure[3]={0,0,0};bool bFirstFailureCritical=false;
|
|
for(int32 I=0;I+1<Points.Num();++I)
|
|
{
|
|
const FVector A=Points[I],B=Points[I+1];
|
|
const double L=std::sqrt((B.X-A.X)*(B.X-A.X)+(B.Y-A.Y)*(B.Y-A.Y)+(B.Z-A.Z)*(B.Z-A.Z));
|
|
const int32 Count=std::max(1,static_cast<int32>(std::ceil(L/SampleStep)));
|
|
uint64 SegmentSamples=0,SegmentFailures=0,SegmentCritical=0;
|
|
for(int32 J=0;J<=Count;++J)
|
|
{
|
|
const double T=static_cast<double>(J)/Count;
|
|
const double X=A.X+(B.X-A.X)*T,Y=A.Y+(B.Y-A.Y)*T,Z=A.Z+(B.Z-A.Z)*T;
|
|
++Stats.WalkSamples;
|
|
++RouteSamples;++SegmentSamples;
|
|
const int32 VX=static_cast<int32>(std::lround(X));
|
|
const int32 VY=static_cast<int32>(std::lround(Y));
|
|
const int32 VZ=static_cast<int32>(std::lround(Z));
|
|
const bool bCritical=IsInsideCriticalAir(VX,VY,VZ);
|
|
if(bCritical){++RouteCritical;++SegmentCritical;}
|
|
if(IsRock(VX,VY,VZ))
|
|
{
|
|
++Stats.WalkFailures;++RouteFailures;++SegmentFailures;
|
|
if(RouteFailures==1)
|
|
{FirstFailure[0]=VX;FirstFailure[1]=VY;FirstFailure[2]=VZ;bFirstFailureCritical=bCritical;}
|
|
}
|
|
}
|
|
if(SegmentFailures)
|
|
std::fprintf(stderr,"[WalkabilitySegment] passage=%u segment=%d failures=%llu/%llu critical=%llu length=%.2f start=(%.2f,%.2f,%.2f) end=(%.2f,%.2f,%.2f)\n",
|
|
PassageIndex,I,static_cast<unsigned long long>(SegmentFailures),
|
|
static_cast<unsigned long long>(SegmentSamples),
|
|
static_cast<unsigned long long>(SegmentCritical),L,
|
|
A.X,A.Y,A.Z,B.X,B.Y,B.Z);
|
|
}
|
|
if(RouteFailures)
|
|
std::fprintf(stderr,"[WalkabilityRoute] passage=%u type=%u points=%d failures=%llu/%llu critical_centerline=%llu first_solid=(%d,%d,%d) critical=%u\n",
|
|
PassageIndex,static_cast<uint32>(P.PassageType),Points.Num(),
|
|
static_cast<unsigned long long>(RouteFailures),
|
|
static_cast<unsigned long long>(RouteSamples),
|
|
static_cast<unsigned long long>(RouteCritical),FirstFailure[0],
|
|
FirstFailure[1],FirstFailure[2],bFirstFailureCritical?1u:0u);
|
|
++PassageIndex;
|
|
}
|
|
if(Stats.WalkFailures)
|
|
std::fprintf(stderr,"[Walkability] FAIL %llu/%llu passage centerline samples are solid\n",
|
|
static_cast<unsigned long long>(Stats.WalkFailures),static_cast<unsigned long long>(Stats.WalkSamples));
|
|
else
|
|
std::printf("[Walkability] PASS %llu/%llu passage centerline samples remain open; native landing floors retained\n",
|
|
static_cast<unsigned long long>(Stats.WalkSamples),static_cast<unsigned long long>(Stats.WalkSamples));
|
|
return Stats.WalkFailures==0;
|
|
}
|
|
|
|
static double DetailAutocorrelation(EDetailVariant Variant)
|
|
{
|
|
constexpr int32 N=24;double Sum=0.0,SumSq=0.0,PairA=0.0,PairB=0.0,PairAA=0.0,PairBB=0.0,PairAB=0.0;uint64 Count=0;
|
|
auto Value=[&](int32 X,int32 Y,int32 Z)->double
|
|
{
|
|
if(Variant==EDetailVariant::PerVoxel)return FStandaloneResearchTunnels::NoiseValue(X,Y,Z,11);
|
|
if(Variant==EDetailVariant::Smooth)return 0.0;
|
|
return SurfaceRelief(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z),11);
|
|
};
|
|
for(int32 Z=0;Z<N;++Z)for(int32 Y=0;Y<N;++Y)for(int32 X=0;X<N;++X)
|
|
{const double V=Value(X,Y,Z);Sum+=V;SumSq+=V*V;}
|
|
const double Mean=Sum/(N*N*N),Variance=SumSq/(N*N*N)-Mean*Mean;
|
|
if(Variance<1.0e-9)return 1.0;
|
|
for(int32 Z=0;Z<N;++Z)for(int32 Y=0;Y<N;++Y)for(int32 X=0;X+1<N;++X)
|
|
{
|
|
const double A=Value(X,Y,Z)-Mean,B=Value(X+1,Y,Z)-Mean;
|
|
PairAA+=A*A;PairBB+=B*B;PairAB+=A*B;++Count;
|
|
}
|
|
return Count?PairAB/std::sqrt(std::max(1.0e-12,PairAA*PairBB)):0.0;
|
|
}
|
|
|
|
template<class Fn> void ParallelLines(size_t Count,Fn&& Work,FParallelTiming* Timing=nullptr,
|
|
std::array<double,16>* WorkerActiveSeconds=nullptr,size_t WorkBatch=1) const
|
|
{
|
|
const auto WallStart=std::chrono::steady_clock::now();
|
|
if(WorkerCount<=1||Count<512||!Scheduler)
|
|
{
|
|
const auto WorkerStart=std::chrono::steady_clock::now();
|
|
for(size_t I=0;I<Count;++I)
|
|
{
|
|
if constexpr(std::is_invocable_v<Fn&,size_t,int32>)Work(I,0);
|
|
else Work(I);
|
|
}
|
|
if(WorkerActiveSeconds)
|
|
(*WorkerActiveSeconds)[0]=std::chrono::duration<double>(
|
|
std::chrono::steady_clock::now()-WorkerStart).count();
|
|
if(Timing)
|
|
{
|
|
const double WorkerSeconds=std::chrono::duration<double>(std::chrono::steady_clock::now()-WorkerStart).count();
|
|
Timing->WallSeconds+=std::chrono::duration<double>(std::chrono::steady_clock::now()-WallStart).count();
|
|
Timing->WorkerSeconds+=WorkerSeconds;
|
|
}
|
|
return;
|
|
}
|
|
WorkBatch=std::max<size_t>(WorkBatch,1);
|
|
using WorkType=std::remove_reference_t<Fn>;
|
|
struct FDispatchContext { WorkType* Work; } Dispatch{&Work};
|
|
const auto Invoke=[](void* Opaque,size_t Index,size_t Worker)
|
|
{
|
|
auto& Context=*static_cast<FDispatchContext*>(Opaque);
|
|
if constexpr(std::is_invocable_v<WorkType&,size_t,int32>)
|
|
(*Context.Work)(Index,static_cast<int32>(Worker));
|
|
else
|
|
(*Context.Work)(Index);
|
|
};
|
|
std::array<double,16> ActiveSeconds{};
|
|
const double WorkerSeconds=Scheduler->parallel_for(Count,WorkBatch,
|
|
static_cast<size_t>(WorkerCount),&Dispatch,Invoke,ActiveSeconds.data(),ActiveSeconds.size());
|
|
if(WorkerActiveSeconds)*WorkerActiveSeconds=ActiveSeconds;
|
|
if(Timing)
|
|
{
|
|
Timing->WallSeconds+=std::chrono::duration<double>(std::chrono::steady_clock::now()-WallStart).count();
|
|
Timing->WorkerSeconds+=WorkerSeconds;
|
|
}
|
|
}
|
|
|
|
void InvalidateFarFieldGpuInput()
|
|
{
|
|
std::lock_guard<std::mutex> Lock(FarFieldGpuInputMutex);
|
|
CachedFarFieldGpuInput.reset();
|
|
}
|
|
|
|
FStandaloneOwnerWorld* Owner=nullptr;
|
|
const VoxelForgeCore::WorldAssemblyManifest* WorldAssembly=nullptr;
|
|
const FStandaloneShapeWorld* SharedDensitySource=nullptr;
|
|
int32 WorkerCount=6;
|
|
const VoxelForgeCore::JobScheduler* Scheduler=nullptr;
|
|
bool RoundTunnelsEnabled=false;
|
|
bool bProfileDetailBand=false;
|
|
int32 Min[3]{},Max[3]{},ClipMin[3]{},ClipMax[3]{},Size[3]{};
|
|
FIntVector EditBoundsMin=FIntVector(MAX_int32,MAX_int32,MAX_int32);
|
|
FIntVector EditBoundsMax=FIntVector(MIN_int32,MIN_int32,MIN_int32);
|
|
int32 ActiveSpatialBinSize=LineBinSize;
|
|
double ActiveBlendRadius=0.0;
|
|
double ActiveFloorBlendRadius=0.0;
|
|
size_t WordsPerZ=0;
|
|
EDetailVariant DetailVariant=EDetailVariant::Coherent;
|
|
bool Built=false;
|
|
bool 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<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;
|
|
};
|