standalone: the owner's world outside Unreal -- bit-identical density, D3D11 fly-through viewer

Standalone/ (C++20, MSVC /O2 /arch:AVX2 /fp:precise, Build.ps1): copied density pipeline, op stack,
cave morphology, passages, strate manager/composer, noise, DetSinCos, mesher, clipmap selection,
with a UE compatibility layer (Compat/). Owner data exported once to Standalone/data/owner_world.json
by a default-off explore-commandlet exporter (the only plugin-source change). 25 cm voxels.
Parity: seed 0, 3,436 samples (strate edges, negative/far coords, rooms, tunnels, passages,
landings): bit-identical. Viewer: full 7-level clipmap, per-level toggles 1-7, T = round winding
tunnels (standalone option), N/P seed, F wireframe.
Timing NOT yet a baseline: 515 s for 12,054 tiles (L0 70 s, L6 296 s); the compat TMap/TSet are
vector-backed with linear lookup, which the plugin's caches do not expect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-02 12:14:04 +02:00
1 parent a443eb8546
commit 05f62cff36
114 files changed
+68206 -3

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
$Root = [IO.Path]::GetFullPath($PSScriptRoot)
$BuildDir = Join-Path $Root 'Build'
$BinDir = Join-Path $Root 'bin'
[IO.Directory]::CreateDirectory($BuildDir) | Out-Null
[IO.Directory]::CreateDirectory($BinDir) | Out-Null
$VcVars = 'E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat'
if (-not (Test-Path -LiteralPath $VcVars -PathType Leaf)) { throw "MSVC environment script not found: $VcVars" }
$Sources = @(
(Join-Path $Root 'Source\StandaloneViewer.cpp'),
(Join-Path $Root 'Compat\UnrealPerlin.cpp'),
(Join-Path $Root 'Compat\StandaloneDataAssetGlue.cpp'),
(Join-Path $Root 'Source\VoxelForge\Private\VoxelBiomeDefinition.cpp'),
(Join-Path $Root 'Source\VoxelForge\Private\VoxelCaveMorphology.cpp'),
(Join-Path $Root 'Source\VoxelForge\Private\VoxelDensityAblation.cpp'),
(Join-Path $Root 'Source\VoxelForge\Private\VoxelDensityOpStack.cpp'),
(Join-Path $Root 'Source\VoxelForge\Private\VoxelDensityProfile.cpp'),
(Join-Path $Root 'Source\VoxelForge\Private\VoxelDiffLayer.cpp'),
(Join-Path $Root 'Source\VoxelForge\Private\VoxelGenerator.cpp'),
(Join-Path $Root 'Source\VoxelForge\Private\VoxelHeightOpStack.cpp'),
(Join-Path $Root 'Source\VoxelForge\Private\VoxelMarchingCubesMesher.cpp'),
(Join-Path $Root 'Source\VoxelForge\Private\VoxelSettings.cpp'),
(Join-Path $Root 'Source\VoxelForge\Private\VoxelStrateComposer.cpp'),
(Join-Path $Root 'Source\VoxelForge\Private\VoxelStrateManager.cpp'),
(Join-Path $Root 'Source\VoxelForge\Private\VoxelStrateMeasure.cpp'),
(Join-Path $Root 'Source\VoxelForge\Private\VoxelTerrainOpDefinition.cpp'),
(Join-Path $Root 'Source\VoxelForge\Private\VoxelWormField.cpp')
)
foreach ($Source in $Sources) {
if (-not (Test-Path -LiteralPath $Source -PathType Leaf)) { throw "Required source is missing: $Source" }
}
$ResponsePath = Join-Path $BuildDir 'standalone.rsp'
$RspLines = @(
'/nologo', '/std:c++20', '/O2', '/arch:AVX2', '/fp:precise', '/EHsc',
'/D_CRT_SECURE_NO_WARNINGS',
('/I"{0}"' -f (Join-Path $Root 'Compat')),
('/I"{0}"' -f (Join-Path $Root 'Source')),
('/I"{0}"' -f (Join-Path $Root 'Source\VoxelForge\Public')),
('/I"{0}"' -f (Join-Path $Root 'Source\VoxelForge\Private')),
('/Fe"{0}"' -f (Join-Path $BinDir 'VoxelForgeStandalone.exe'))
)
foreach ($Source in $Sources) { $RspLines += ('"{0}"' -f $Source) }
$RspLines += @('/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib','gdi32.lib')
[IO.File]::WriteAllLines($ResponsePath,$RspLines,[Text.UTF8Encoding]::new($false))
Push-Location $BuildDir
try {
$CommandLine = 'call "{0}" >nul && cl.exe @"{1}"' -f $VcVars,$ResponsePath
& $env:ComSpec /d /s /c $CommandLine
if ($LASTEXITCODE -ne 0) { throw "Standalone MSVC build failed with exit code $LASTEXITCODE" }
} finally {
Pop-Location
}
Write-Host "Built $((Join-Path $BinDir 'VoxelForgeStandalone.exe'))"
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+362
View File
@@ -0,0 +1,362 @@
#pragma once
// Deliberately small UE-style surface used by the copied VoxelForge CPU sources.
// This is a value/container compatibility layer; it does not start or link Unreal.
#include <algorithm>
#include <array>
#include <atomic>
#include <bit>
#include <cassert>
#include <chrono>
#include <cctype>
#include <cstdarg>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <functional>
#include <immintrin.h>
#include <initializer_list>
#include <limits>
#include <memory>
#include <mutex>
#include <optional>
#include <shared_mutex>
#include <string>
#include <string_view>
#include <thread>
#include <type_traits>
#include <unordered_map>
#include <utility>
#include <vector>
#define UCLASS(...)
#define USTRUCT(...)
#define UENUM(...)
#define UPROPERTY(...)
#define UFUNCTION(...)
#define UMETA(...)
#define GENERATED_BODY()
#define GENERATED_UCLASS_BODY()
#define VOXELFORGE_API
#define FORCEINLINE inline
#define WITH_EDITOR 0
#define TRACE_CPUPROFILER_EVENT_SCOPE(...)
#define PRAGMA_DISABLE_SHADOW_VARIABLE_WARNINGS
#define PRAGMA_ENABLE_SHADOW_VARIABLE_WARNINGS
#define UE_ARRAY_COUNT(Array) (sizeof(Array) / sizeof((Array)[0]))
#define TEXT(x) x
#define UE_LOG(...)
#define LogTemp 0
#define check(x) assert(x)
#define checkf(x, ...) assert(x)
#define ensure(x) (x)
#define ensureMsgf(x, ...) (x)
#define verify(x) (x)
#define INDEX_NONE -1
#define MIN_int32 (std::numeric_limits<int32>::min())
#define MAX_int32 (std::numeric_limits<int32>::max())
#define MAX_uint32 (std::numeric_limits<uint32>::max())
#define MIN_flt (-std::numeric_limits<float>::max())
#define MAX_flt (std::numeric_limits<float>::max())
#define PI 3.14159265358979323846f
#define HALF_PI 1.57079632679489661923f
#define SMALL_NUMBER 1.0e-8f
#define KINDA_SMALL_NUMBER 1.0e-4f
#define MoveTemp(x) std::move(x)
using int8 = std::int8_t;
using uint8 = std::uint8_t;
using int16 = std::int16_t;
using uint16 = std::uint16_t;
using int32 = std::int32_t;
using uint32 = std::uint32_t;
using int64 = std::int64_t;
using uint64 = std::uint64_t;
using UPTRINT = std::uintptr_t;
using TCHAR = char;
using SIZE_T = std::size_t;
constexpr uint64 MAX_uint64 = std::numeric_limits<uint64>::max();
enum EForceInit { ForceInit };
enum class ESearchCase : uint8 { CaseSensitive, IgnoreCase };
struct FDefaultAllocator {};
template<int32 N> struct TInlineAllocator {};
enum class EAllowShrinking : uint8 { No, Yes };
template<class Signature> using TFunction = std::function<Signature>;
template<class Signature> using TFunctionRef = std::function<Signature>;
template<class T> constexpr T&& Forward(std::remove_reference_t<T>& Value){return std::forward<T>(Value);}
template<class T> constexpr T&& Forward(std::remove_reference_t<T>&& Value){return std::forward<T>(Value);}
template<class T, class Allocator = FDefaultAllocator>
class TArray
{
public:
using value_type = T;
using FStored = std::conditional_t<std::is_same_v<T,bool>,uint8,T>;
using FStorage = std::vector<FStored>;
using iterator = typename FStorage::iterator;
using const_iterator = typename FStorage::const_iterator;
using reference = typename FStorage::reference;
using const_reference = typename FStorage::const_reference;
TArray() = default;
TArray(std::initializer_list<T> Values) : Values(Values) {}
int32 Num() const { return static_cast<int32>(Values.size()); }
bool IsEmpty() const { return Values.empty(); }
bool IsValidIndex(int32 Index) const { return Index >= 0 && Index < Num(); }
void Reserve(int32 Count) { Values.reserve(static_cast<std::size_t>(std::max(0, Count))); }
void Reset(int32 Slack = 0) { Values.clear(); if (Slack > 0) Reserve(Slack); }
void Empty(int32 Slack = 0) { Reset(Slack); }
void SetNum(int32 Count) { Values.resize(static_cast<std::size_t>(std::max(0, Count))); }
void Init(const T& Value,int32 Count) { Values.assign(static_cast<std::size_t>(std::max(0,Count)),static_cast<FStored>(Value)); }
void SetNumZeroed(int32 Count) { SetNum(Count); if constexpr (std::is_trivially_copyable_v<T>) if (!Values.empty()) std::memset(Values.data(), 0, Values.size() * sizeof(T)); }
void SetNumUninitialized(int32 Count) { SetNum(Count); }
int32 Add(const T& Value) { Values.push_back(Value); return Num() - 1; }
int32 Add(T&& Value) { Values.push_back(std::move(Value)); return Num() - 1; }
template<class... Args> int32 Emplace(Args&&... ArgsIn) { Values.emplace_back(std::forward<Args>(ArgsIn)...); return Num() - 1; }
reference AddDefaulted_GetRef() { Values.emplace_back(); return Values.back(); }
int32 AddDefaulted() { Values.emplace_back(); return Num() - 1; }
template<class... Args> reference Emplace_GetRef(Args&&... ArgsIn) { Values.emplace_back(std::forward<Args>(ArgsIn)...); return Values.back(); }
template<class... Args> reference Add_GetRef(Args&&... ArgsIn) { Values.emplace_back(std::forward<Args>(ArgsIn)...); return Values.back(); }
int32 AddUnique(const T& Value) { if (Contains(Value)) return IndexOfByKey(Value); return Add(Value); }
void Append(const TArray& Other) { Values.insert(Values.end(), Other.Values.begin(), Other.Values.end()); }
void Append(TArray&& Other) { Values.insert(Values.end(), std::make_move_iterator(Other.Values.begin()), std::make_move_iterator(Other.Values.end())); }
void RemoveAt(int32 Index, int32 Count = 1, bool = true) { if (Index >= 0 && Count > 0 && Index < Num()) Values.erase(Values.begin()+Index, Values.begin()+std::min(Num(),Index+Count)); }
void RemoveAt(int32 Index, int32 Count, EAllowShrinking) { RemoveAt(Index, Count, true); }
void RemoveAtSwap(int32 Index, int32 Count = 1, bool = true) { if (Index < 0 || Count <= 0 || Index >= Num()) return; const int32 End = std::min(Num(), Index + Count); for (int32 I = Index; I < End; ++I) Values[I] = std::move(Values.back()); Values.resize(Values.size() - static_cast<std::size_t>(End - Index)); }
void RemoveAtSwap(int32 Index, int32 Count, EAllowShrinking) { RemoveAtSwap(Index, Count, true); }
template<class Pred> int32 RemoveAll(Pred P) { const auto Before=Values.size(); Values.erase(std::remove_if(Values.begin(),Values.end(),P),Values.end()); return static_cast<int32>(Before-Values.size()); }
bool Contains(const T& Value) const { return std::find(Values.begin(), Values.end(), Value) != Values.end(); }
int32 IndexOfByKey(const T& Value) const { auto I=std::find(Values.begin(),Values.end(),Value); return I==Values.end()?INDEX_NONE:static_cast<int32>(I-Values.begin()); }
T* Find(const T& Value) { const int32 I=IndexOfByKey(Value); return I==INDEX_NONE?nullptr:&Values[I]; }
const T* Find(const T& Value) const { const int32 I=IndexOfByKey(Value); return I==INDEX_NONE?nullptr:&Values[I]; }
T* GetData() { return reinterpret_cast<T*>(Values.data()); }
const T* GetData() const { return reinterpret_cast<const T*>(Values.data()); }
std::size_t GetAllocatedSize() const { return Values.capacity() * sizeof(T); }
T& Last() { return Values.back(); }
const T& Last() const { return Values.back(); }
T Pop(bool = false) { T Out=std::move(Values.back()); Values.pop_back(); return Out; }
void Swap(int32 A,int32 B) { std::swap(Values[A],Values[B]); }
template<class Pred> void Sort(Pred P) { std::sort(Values.begin(),Values.end(),P); }
void Sort() { std::sort(Values.begin(),Values.end()); }
reference operator[](int32 I) { return Values[static_cast<std::size_t>(I)]; }
const_reference operator[](int32 I) const { return Values[static_cast<std::size_t>(I)]; }
iterator begin() { return Values.begin(); } iterator end() { return Values.end(); }
const_iterator begin() const { return Values.begin(); } const_iterator end() const { return Values.end(); }
const_iterator cbegin() const { return Values.cbegin(); } const_iterator cend() const { return Values.cend(); }
private:
FStorage Values;
};
template<class K, class V> struct TPair { K Key{}; V Value{}; TPair()=default; TPair(const K& K0,const V& V0):Key(K0),Value(V0){}; };
template<class K, class V, class Allocator = FDefaultAllocator>
class TMap
{
public:
int32 Num() const { return static_cast<int32>(Pairs.size()); }
bool IsEmpty() const { return Pairs.empty(); }
void Reserve(int32 N) { Pairs.reserve(static_cast<std::size_t>(std::max(0,N))); }
void Reset() { Pairs.clear(); }
void Empty(int32 = 0) { Reset(); }
bool Contains(const K& Key) const { return FindPair(Key) != Pairs.end(); }
V* Find(const K& Key) { auto I=FindPair(Key); return I==Pairs.end()?nullptr:&I->Value; }
const V* Find(const K& Key) const { auto I=FindPair(Key); return I==Pairs.end()?nullptr:&I->Value; }
V& FindOrAdd(const K& Key) { if (V* P=Find(Key)) return *P; Pairs.emplace_back(Key,V{}); return Pairs.back().Value; }
V& Add(const K& Key,const V& Value) { if (V* P=Find(Key)) { *P=Value; return *P; } Pairs.emplace_back(Key,Value); return Pairs.back().Value; }
V& Add(const K& Key,V&& Value) { if (V* P=Find(Key)) { *P=std::move(Value); return *P; } Pairs.emplace_back(Key,std::move(Value)); return Pairs.back().Value; }
int32 Remove(const K& Key) { auto I=FindPair(Key); if(I==Pairs.end())return 0; Pairs.erase(I);return 1; }
V& operator[](const K& Key) { return FindOrAdd(Key); }
const V& operator[](const K& Key) const { const V* P=Find(Key); assert(P); return *P; }
auto begin(){return Pairs.begin();} auto end(){return Pairs.end();}
auto begin()const{return Pairs.begin();} auto end()const{return Pairs.end();}
private:
using It=typename std::vector<TPair<K,V>>::iterator;
using CIt=typename std::vector<TPair<K,V>>::const_iterator;
It FindPair(const K& Key){return std::find_if(Pairs.begin(),Pairs.end(),[&](auto& P){return P.Key==Key;});}
CIt FindPair(const K& Key)const{return std::find_if(Pairs.begin(),Pairs.end(),[&](auto& P){return P.Key==Key;});}
std::vector<TPair<K,V>> Pairs;
};
template<class T, class Allocator = FDefaultAllocator>
class TSet
{
public:
int32 Num()const{return Values.Num();} void Reset(){Values.Reset();} void Empty(){Reset();}
bool Contains(const T& V)const{return Values.Contains(V);} void Add(const T& V){Values.AddUnique(V);} void Add(T&& V){if(!Contains(V))Values.Add(std::move(V));}
auto begin(){return Values.begin();}auto end(){return Values.end();}auto begin()const{return Values.begin();}auto end()const{return Values.end();}
private:TArray<T,Allocator> Values;
};
template<class T> class TUniquePtr
{
public:
TUniquePtr()=default; TUniquePtr(std::nullptr_t){} explicit TUniquePtr(std::unique_ptr<T>&&P):Ptr(std::move(P)){}
template<class U> requires std::is_convertible_v<U*,T*> TUniquePtr(TUniquePtr<U>&& P):Ptr(P.Release()){}
TUniquePtr(TUniquePtr&&)=default; TUniquePtr& operator=(TUniquePtr&&)=default; TUniquePtr(const TUniquePtr&)=delete; TUniquePtr& operator=(const TUniquePtr&)=delete;
template<class U> requires std::is_convertible_v<U*,T*> TUniquePtr& operator=(TUniquePtr<U>&& P){Ptr.reset(P.Release());return *this;}
T* Get()const{return Ptr.get();} T* get()const{return Ptr.get();} T* operator->()const{return Ptr.get();} T& operator*()const{return *Ptr;} explicit operator bool()const{return !!Ptr;} bool IsValid()const{return !!Ptr;}
void Reset(T*P=nullptr){Ptr.reset(P);} void reset(T*P=nullptr){Ptr.reset(P);} T* Release(){return Ptr.release();} void Swap(TUniquePtr&O){Ptr.swap(O.Ptr);}
private:std::unique_ptr<T> Ptr;
};
enum class ESPMode : uint8 { NotThreadSafe, ThreadSafe };
template<class T, ESPMode Mode=ESPMode::NotThreadSafe> class TSharedPtr { public:TSharedPtr()=default;TSharedPtr(std::nullptr_t){}TSharedPtr(std::shared_ptr<T>P):Ptr(std::move(P)){}template<class U,ESPMode Other> requires std::is_convertible_v<U*,T*> TSharedPtr(const TSharedPtr<U,Other>&P):Ptr(P.Std()){}T*Get()const{return Ptr.get();}T*operator->()const{return Ptr.get();}T&operator*()const{return *Ptr;}explicit operator bool()const{return !!Ptr;}bool IsValid()const{return !!Ptr;}void Reset(){Ptr.reset();}void Reset(T*P){Ptr.reset(P);}std::shared_ptr<T>&Std(){return Ptr;}const std::shared_ptr<T>&Std()const{return Ptr;}friend bool operator==(const TSharedPtr&P,std::nullptr_t){return !P.Ptr;}friend bool operator!=(const TSharedPtr&P,std::nullptr_t){return !!P.Ptr;}private:std::shared_ptr<T>Ptr; };
template<class T,ESPMode Mode=ESPMode::NotThreadSafe> using TSharedRef = TSharedPtr<T,Mode>;
template<class T> class TOptional { public:TOptional()=default;TOptional(const T&V):Value(V){}TOptional(T&&V):Value(std::move(V)){}bool IsSet()const{return Value.has_value();}explicit operator bool()const{return Value.has_value();}T&GetValue(){return Value.value();}const T&GetValue()const{return Value.value();}T&Get(){return Value.value();}const T&Get()const{return Value.value();}void Reset(){Value.reset();}private:std::optional<T>Value;};
template<class T, class... A> TUniquePtr<T> MakeUnique(A&&... Args) { return TUniquePtr<T>(std::make_unique<T>(std::forward<A>(Args)...)); }
template<class T, ESPMode Mode=ESPMode::NotThreadSafe, class... A> TSharedRef<T,Mode> MakeShared(A&&... Args) { return TSharedRef<T,Mode>(std::make_shared<T>(std::forward<A>(Args)...)); }
class FString : public std::string
{
public:
using std::string::string;
FString()=default; FString(const std::string& S):std::string(S){} FString(std::string&& S):std::string(std::move(S)){}
bool IsEmpty()const{return empty();} void Reset(){clear();}
int32 Len()const{return static_cast<int32>(size());}
bool Contains(std::string_view S)const{return find(S)!=npos;}
bool StartsWith(std::string_view S)const{return rfind(S,0)==0;}
bool EndsWith(std::string_view S)const{return size()>=S.size()&&compare(size()-S.size(),S.size(),S)==0;}
bool Equals(std::string_view S, ESearchCase SearchCase = ESearchCase::CaseSensitive)const{if(SearchCase==ESearchCase::CaseSensitive)return *this==S;if(size()!=S.size())return false;for(size_t I=0;I<size();++I)if(std::tolower(static_cast<unsigned char>((*this)[I]))!=std::tolower(static_cast<unsigned char>(S[I])))return false;return true;}
void ToLowerInline(){for(char& C:*this)C=static_cast<char>(std::tolower(static_cast<unsigned char>(C)));}
void TrimStartAndEndInline(){auto B=find_first_not_of(" \t\r\n"),E=find_last_not_of(" \t\r\n");*this=(B==npos)?FString{}:substr(B,E-B+1);}
void LeftChopInline(std::size_t N){if(N>=size())clear();else resize(size()-N);}
int32 ReplaceInline(std::string_view From,std::string_view To){if(From.empty())return 0;int32 C=0;std::size_t P=0;while((P=find(From,P))!=npos){replace(P,From.size(),To);P+=To.size();++C;}return C;}
static FString Join(const TArray<FString>& Parts, std::string_view Delimiter){FString Result;for(int32 I=0;I<Parts.Num();++I){if(I)Result+=Delimiter;Result+=Parts[I];}return Result;}
static FString Printf(const char* Format,...){char Buf[4096];va_list A;va_start(A,Format);int N=std::vsnprintf(Buf,sizeof(Buf),Format,A);va_end(A);return N<0?FString{}:FString(Buf,static_cast<std::size_t>(std::min<int>(N,sizeof(Buf)-1)));}
};
inline const char* operator*(const FString& S){return S.c_str();}
using FName = FString;
constexpr const char* NAME_None = "";
class UObject;
template<class T> T* FindStandaloneAsset(const FString& Path);
void RegisterStandaloneAsset(const FString& Path, UObject* Asset);
class FText { FString Value; public: FText()=default; FText(FString S):Value(std::move(S)){} static FText FromString(const FString& S){return FText(S);} static FText FromName(const FName& S){return FText(S);} FString ToString()const{return Value;} bool IsEmpty()const{return Value.IsEmpty();} };
class UEnum { public: FString GetNameStringByValue(int64 V)const{return FString::Printf("Value_%lld",static_cast<long long>(V));} };
template<class T> const UEnum* StaticEnum(){static const UEnum Value;return &Value;}
constexpr uint32 RF_Transient = 1;
template<class T> T* NewObject(UObject* = nullptr,FName = NAME_None,uint32 = 0){return new T;}
template<class T> T* DuplicateObject(const T* Source,UObject*){return Source?new T(*Source):nullptr;}
template<class T> class TGuardValue { public:TGuardValue(T& Target,const T& NewValue):Ref(Target),Old(Target){Ref=NewValue;}~TGuardValue(){Ref=Old;}TGuardValue(const TGuardValue&)=delete;private:T&Ref;T Old;};
struct FIntPoint { int32 X=0,Y=0; constexpr FIntPoint()=default; constexpr FIntPoint(int32 A,int32 B):X(A),Y(B){} static const FIntPoint ZeroValue; bool operator==(const FIntPoint&O)const{return X==O.X&&Y==O.Y;} bool operator!=(const FIntPoint&O)const{return !(*this==O);} FIntPoint operator+(const FIntPoint&O)const{return {X+O.X,Y+O.Y};} FIntPoint operator-(const FIntPoint&O)const{return {X-O.X,Y-O.Y};} };
inline const FIntPoint FIntPoint::ZeroValue{0,0};
struct FIntVector { int32 X=0,Y=0,Z=0; constexpr FIntVector()=default; constexpr FIntVector(int32 A,int32 B,int32 C):X(A),Y(B),Z(C){} static const FIntVector ZeroValue; bool operator==(const FIntVector&O)const{return X==O.X&&Y==O.Y&&Z==O.Z;} bool operator!=(const FIntVector&O)const{return !(*this==O);} FIntVector operator+(const FIntVector&O)const{return {X+O.X,Y+O.Y,Z+O.Z};} FIntVector operator-(const FIntVector&O)const{return {X-O.X,Y-O.Y,Z-O.Z};} FIntVector operator*(int32 S)const{return {X*S,Y*S,Z*S};} FIntVector operator/(int32 S)const{return {X/S,Y/S,Z/S};} FIntVector& operator+=(const FIntVector&O){X+=O.X;Y+=O.Y;Z+=O.Z;return *this;} };
inline const FIntVector FIntVector::ZeroValue{0,0,0};
inline FIntVector operator*(int32 S,const FIntVector& V){return V*S;}
struct FVector2D { double X=0,Y=0; constexpr FVector2D()=default; constexpr FVector2D(double A,double B):X(A),Y(B){} static const FVector2D ZeroVector; double SizeSquared()const{return X*X+Y*Y;} double Size()const{return std::sqrt(SizeSquared());} bool Normalize(double Tol=1.e-8){double L=Size();if(L<=Tol)return false;X/=L;Y/=L;return true;} FVector2D GetSafeNormal(double Tol=1.e-8)const{FVector2D V=*this;V.Normalize(Tol);return V;} double DistSquared(const FVector2D&O)const{return (*this-O).SizeSquared();} static double DistSquared(const FVector2D&A,const FVector2D&B){return (A-B).SizeSquared();} FVector2D operator-()const{return {-X,-Y};} FVector2D operator+(const FVector2D&O)const{return {X+O.X,Y+O.Y};} FVector2D operator-(const FVector2D&O)const{return {X-O.X,Y-O.Y};} FVector2D operator*(double S)const{return {X*S,Y*S};} FVector2D operator/(double S)const{return {X/S,Y/S};} FVector2D&operator+=(const FVector2D&O){X+=O.X;Y+=O.Y;return *this;} FVector2D&operator*=(double S){X*=S;Y*=S;return *this;} static double DotProduct(const FVector2D&A,const FVector2D&B){return A.X*B.X+A.Y*B.Y;} };
inline const FVector2D FVector2D::ZeroVector{0,0};
struct FVector2f { float X=0,Y=0; constexpr FVector2f()=default; constexpr FVector2f(float A,float B):X(A),Y(B){} };
struct FVector3f { float X=0,Y=0,Z=0; constexpr FVector3f()=default; constexpr FVector3f(float A,float B,float C):X(A),Y(B),Z(C){} FVector3f operator+(const FVector3f&O)const{return {X+O.X,Y+O.Y,Z+O.Z};} FVector3f operator-(const FVector3f&O)const{return {X-O.X,Y-O.Y,Z-O.Z};} FVector3f operator*(float S)const{return {X*S,Y*S,Z*S};} FVector3f&operator+=(const FVector3f&O){X+=O.X;Y+=O.Y;Z+=O.Z;return *this;} bool operator==(const FVector3f&O)const{return X==O.X&&Y==O.Y&&Z==O.Z;} };
struct FVector
{
using FReal = double;
double X=0,Y=0,Z=0; constexpr FVector()=default; constexpr explicit FVector(double S):X(S),Y(S),Z(S){} constexpr FVector(double A,double B,double C):X(A),Y(B),Z(C){} constexpr explicit FVector(const FIntVector& V):X(V.X),Y(V.Y),Z(V.Z){}
static const FVector ZeroVector, OneVector, UpVector, DownVector, ForwardVector, RightVector;
FVector operator+(const FVector&O)const{return {X+O.X,Y+O.Y,Z+O.Z};} FVector operator-(const FVector&O)const{return {X-O.X,Y-O.Y,Z-O.Z};}
FVector operator-()const{return {-X,-Y,-Z};} FVector operator*(double S)const{return {X*S,Y*S,Z*S};} FVector operator/(double S)const{return {X/S,Y/S,Z/S};} FVector operator/(const FVector&O)const{return {X/O.X,Y/O.Y,Z/O.Z};}
FVector& operator+=(const FVector&O){X+=O.X;Y+=O.Y;Z+=O.Z;return *this;} FVector& operator-=(const FVector&O){X-=O.X;Y-=O.Y;Z-=O.Z;return *this;} FVector& operator*=(double S){X*=S;Y*=S;Z*=S;return *this;} FVector& operator/=(double S){X/=S;Y/=S;Z/=S;return *this;}
bool operator==(const FVector&O)const{return X==O.X&&Y==O.Y&&Z==O.Z;} bool operator!=(const FVector&O)const{return !(*this==O);}
double SizeSquared()const{return X*X+Y*Y+Z*Z;} double Size()const{return std::sqrt(SizeSquared());}
double Dist(const FVector&O)const{return (*this-O).Size();} double DistSquared(const FVector&O)const{return (*this-O).SizeSquared();}
static double Dist(const FVector&A,const FVector&B){return (A-B).Size();} static double DistSquared(const FVector&A,const FVector&B){return (A-B).SizeSquared();}
bool Normalize(double Tolerance=1.e-8){double L=Size();if(L<=Tolerance)return false;*this=*this/L;return true;}
FVector GetAbs()const{return {std::abs(X),std::abs(Y),std::abs(Z)};}
FVector GetSafeNormal(double Tolerance=1.e-8)const{FVector V=*this;V.Normalize(Tolerance);return V;}
bool IsNearlyZero(double Tolerance=1.e-8)const{return SizeSquared()<=Tolerance*Tolerance;}
bool ContainsNaN()const{return !std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z);}
bool Equals(const FVector&O,double Tolerance=1.e-8)const{return (*this-O).IsNearlyZero(Tolerance);}
double Dot(const FVector&O)const{return X*O.X+Y*O.Y+Z*O.Z;} FVector Cross(const FVector&O)const{return {Y*O.Z-Z*O.Y,Z*O.X-X*O.Z,X*O.Y-Y*O.X};}
static double DotProduct(const FVector&A,const FVector&B){return A.Dot(B);} static FVector CrossProduct(const FVector&A,const FVector&B){return A.Cross(B);}
};
inline const FVector FVector::ZeroVector{0,0,0}; inline const FVector FVector::OneVector{1,1,1}; inline const FVector FVector::UpVector{0,0,1}; inline const FVector FVector::DownVector{0,0,-1}; inline const FVector FVector::ForwardVector{1,0,0}; inline const FVector FVector::RightVector{0,1,0};
inline FVector operator*(double S,const FVector&V){return V*S;}
inline FVector operator*(float S,const FVector&V){return V*static_cast<double>(S);}
struct FBox { FVector Min{MAX_flt,MAX_flt,MAX_flt},Max{MIN_flt,MIN_flt,MIN_flt}; bool IsValid=false; FBox()=default; explicit FBox(EForceInit):FBox(){} FBox(const FVector&A,const FVector&B):Min(A),Max(B),IsValid(A.X<=B.X&&A.Y<=B.Y&&A.Z<=B.Z){} FVector GetCenter()const{return (Min+Max)*0.5;} FVector GetExtent()const{return (Max-Min)*0.5;} bool IsInside(const FVector&P)const{return P.X>=Min.X&&P.X<=Max.X&&P.Y>=Min.Y&&P.Y<=Max.Y&&P.Z>=Min.Z&&P.Z<=Max.Z;} bool operator==(const FBox&O)const{return Min==O.Min&&Max==O.Max&&IsValid==O.IsValid;} bool operator!=(const FBox&O)const{return !(*this==O);} };
struct FColor { uint8 R=0,G=0,B=0,A=255; constexpr FColor()=default; constexpr FColor(uint8 r,uint8 g,uint8 b,uint8 a=255):R(r),G(g),B(b),A(a){} static const FColor White, Black, Red, Green, Blue, Transparent; };
inline const FColor FColor::White{255,255,255,255}; inline const FColor FColor::Black{0,0,0,255}; inline const FColor FColor::Red{255,0,0,255}; inline const FColor FColor::Green{0,255,0,255}; inline const FColor FColor::Blue{0,0,255,255}; inline const FColor FColor::Transparent{0,0,0,0};
struct FLinearColor { float R=0,G=0,B=0,A=1; constexpr FLinearColor()=default; constexpr FLinearColor(float r,float g,float b,float a=1):R(r),G(g),B(b),A(a){} static const FLinearColor White,Black,Red,Green,Blue; FColor ToFColor(bool bSRGB)const{auto cv=[](float V,bool S){V=std::clamp(V,0.0f,1.0f);if(S)V=V<=0.0031308f?12.92f*V:1.055f*std::pow(V,1.0f/2.4f)-0.055f;return static_cast<uint8>(std::clamp(static_cast<int>(std::floor(V*255.0f+0.5f)),0,255));};return {cv(R,bSRGB),cv(G,bSRGB),cv(B,bSRGB),cv(A,false)};} };
inline const FLinearColor FLinearColor::White{1,1,1,1}; inline const FLinearColor FLinearColor::Black{0,0,0,1}; inline const FLinearColor FLinearColor::Red{1,0,0,1}; inline const FLinearColor FLinearColor::Green{0,1,0,1}; inline const FLinearColor FLinearColor::Blue{0,0,1,1};
struct FRotator { double Pitch=0,Yaw=0,Roll=0; constexpr FRotator()=default; constexpr FRotator(double P,double Y,double R):Pitch(P),Yaw(Y),Roll(R){} static const FRotator ZeroRotator; };
inline const FRotator FRotator::ZeroRotator{0,0,0};
namespace FMath
{
template<class A,class B> constexpr auto Min(A A0,B B0){using R=std::common_type_t<A,B>;R A1=static_cast<R>(A0),B1=static_cast<R>(B0);return A1<B1?A1:B1;} template<class A,class B> constexpr auto Max(A A0,B B0){using R=std::common_type_t<A,B>;R A1=static_cast<R>(A0),B1=static_cast<R>(B0);return A1>B1?A1:B1;}
template<class T,class A,class B> constexpr T Clamp(T V,A A0,B B0){T Lo=static_cast<T>(A0),Hi=static_cast<T>(B0);return V<Lo?Lo:(V>Hi?Hi:V);} template<class T> constexpr T Abs(T V){return V<T(0)?-V:V;}
inline int32 FloorToInt(double V){return static_cast<int32>(std::floor(V));} inline int32 CeilToInt(double V){return static_cast<int32>(std::ceil(V));} inline int32 RoundToInt(double V){return static_cast<int32>(std::floor(V+0.5));}
inline double FloorToDouble(double V){return std::floor(V);} inline double CeilToDouble(double V){return std::ceil(V);} inline double RoundToDouble(double V){return std::floor(V+0.5);}
inline float FloorToFloat(float V){return std::floor(V);} inline float CeilToFloat(float V){return std::ceil(V);} inline float RoundToFloat(float V){return std::floor(V+0.5f);}
inline bool IsFinite(double V){return std::isfinite(V);} inline bool IsNearlyZero(double V,double T=1.e-8){return std::abs(V)<=T;} inline bool IsNearlyEqual(double A,double B,double T=1.e-8){return std::abs(A-B)<=T;}
inline float Sqrt(float V){return std::sqrt(V);} inline double Sqrt(double V){return std::sqrt(V);} inline float InvSqrt(float V){return 1.0f/std::sqrt(V);} inline double InvSqrt(double V){return 1.0/std::sqrt(V);}
inline float Sin(float V){return std::sin(V);} inline double Sin(double V){return std::sin(V);} inline float Cos(float V){return std::cos(V);} inline double Cos(double V){return std::cos(V);}
inline float Tan(float V){return std::tan(V);} inline double Tan(double V){return std::tan(V);} inline float Asin(float V){return std::asin(V);} inline double Asin(double V){return std::asin(V);} inline float Acos(float V){return std::acos(V);} inline double Acos(double V){return std::acos(V);}
inline float Atan2(float Y,float X){return std::atan2(Y,X);} inline double Atan2(double Y,double X){return std::atan2(Y,X);} inline float Pow(float A,float B){return std::pow(A,B);} inline double Pow(double A,double B){return std::pow(A,B);}
inline float Exp(float V){return std::exp(V);} inline double Exp(double V){return std::exp(V);} inline float Loge(float V){return std::log(V);} inline float Fmod(float A,float B){return std::fmod(A,B);} inline double Fmod(double A,double B){return std::fmod(A,B);}
template<class T> constexpr T Square(T V){return V*V;} template<class T> constexpr T Sign(T V){return V<T(0)?T(-1):(V>T(0)?T(1):T(0));}
template<class T> constexpr T Max3(T A,T B,T C){return Max(Max(A,B),C);} template<class T> constexpr T Min3(T A,T B,T C){return Min(Min(A,B),C);}
inline double DegreesToRadians(double D){return D*(3.1415926535897932384626433832795/180.0);} inline double RadiansToDegrees(double R){return R*(180.0/3.1415926535897932384626433832795);}
inline float Frac(float V){return V-std::floor(V);} inline double Frac(double V){return V-std::floor(V);}
inline int32 DivideAndRoundUp(int32 A,int32 B){return (A+B-1)/B;}
template<class T> bool IsPowerOfTwo(T V){return V>0&&(V&(V-1))==0;}
inline int32 FloorLog2(uint32 V){return V?31-std::countl_zero(V):0;}
float PerlinNoise2D(const FVector2D& Location);
float PerlinNoise3D(const FVector& Location);
template<class A,class B,class C> inline auto Lerp(const A& A0,const B& B0,C Alpha)->decltype(A0+(B0-A0)*Alpha){return A0+(B0-A0)*Alpha;}
inline float SmoothStep(float A,float B,float X){float T=Clamp((X-A)/(B-A),0.0f,1.0f);return T*T*(3-2*T);}
}
inline uint32 HashCombine(uint32 A,uint32 B){return A^(B+0x9e3779b9u+(A<<6)+(A>>2));}
inline uint32 GetTypeHash(int32 V){return static_cast<uint32>(V)*0x9e3779b9u;}
inline uint32 GetTypeHash(uint32 V){return V*0x9e3779b9u;}
inline uint32 GetTypeHash(float V){return std::bit_cast<uint32>(V);}
inline uint32 GetTypeHash(double V){return static_cast<uint32>(std::bit_cast<uint64>(V)^ (std::bit_cast<uint64>(V)>>32));}
inline uint32 GetTypeHash(int64 V){return static_cast<uint32>(static_cast<uint64>(V)^(static_cast<uint64>(V)>>32));}
inline uint32 GetTypeHash(uint64 V){return static_cast<uint32>(V^(V>>32));}
inline uint32 GetTypeHash(const FIntVector& V){return HashCombine(HashCombine(GetTypeHash(V.X),GetTypeHash(V.Y)),GetTypeHash(V.Z));}
inline uint32 GetTypeHash(const FIntPoint& V){return HashCombine(GetTypeHash(V.X),GetTypeHash(V.Y));}
struct FMemory { static void Memcpy(void*D,const void*S,std::size_t N){std::memcpy(D,S,N);} static void Memzero(void*D,std::size_t N){std::memset(D,0,N);} static int32 Memcmp(const void*A,const void*B,std::size_t N){return std::memcmp(A,B,N);} static SIZE_T GetAllocSize(const void* P){return P?sizeof(void*):0;} };
struct FPrimaryAssetId { FString Type,Name; FPrimaryAssetId()=default; FPrimaryAssetId(FString T,FName N):Type(std::move(T)),Name(std::move(N)){} };
class UObject { public: virtual ~UObject()=default; virtual void PostLoad(){} virtual FName GetFName()const{return FName{};} virtual FString GetPathName()const{return FString{};} virtual bool Modify(){return true;} virtual void MarkPackageDirty(){} };
inline std::unordered_map<std::string, UObject*>& StandaloneAssetRegistry()
{
static std::unordered_map<std::string, UObject*> Assets;
return Assets;
}
inline void RegisterStandaloneAsset(const FString& Path, UObject* Asset)
{
if (!Path.IsEmpty() && Asset) StandaloneAssetRegistry()[Path] = Asset;
}
template<class T> inline T* FindStandaloneAsset(const FString& Path)
{
auto It = StandaloneAssetRegistry().find(Path);
return It == StandaloneAssetRegistry().end() ? nullptr : dynamic_cast<T*>(It->second);
}
class UDataAsset : public UObject {};
class UPrimaryDataAsset : public UDataAsset { public: virtual FPrimaryAssetId GetPrimaryAssetId()const{return {};} };
using FSoftObjectPath = FString;
template<class T> class TSoftObjectPtr { public: TSoftObjectPtr()=default; TSoftObjectPtr(T*P):Ptr(P){} TSoftObjectPtr(const FSoftObjectPath&P):Path(P){} bool IsNull()const{return Ptr==nullptr&&Path.IsEmpty();} T* Get()const{return Ptr?Ptr:FindStandaloneAsset<T>(Path);} T* LoadSynchronous()const{return Get();} FString ToString()const{return Path;} void Set(T*P,FString S={}){Ptr=P;Path=std::move(S);} T* operator->()const{return Get();} explicit operator bool()const{return Get()!=nullptr;} private:T*Ptr=nullptr;FString Path; };
template<class T> class TWeakObjectPtr { public:TWeakObjectPtr()=default;TWeakObjectPtr(T*P):Ptr(P){}T*Get()const{return Ptr;}T*operator->()const{return Ptr;}private:T*Ptr=nullptr;};
template<class T> class TSubclassOf { public:TSubclassOf()=default; };
class FGameplayTag {}; class FGameplayTagContainer {};
struct FRandomStream { explicit FRandomStream(int32 S=0):Seed(S){} int32 RandRange(int32 Min,int32 Max){Seed=Seed*196314165+907633515;uint32 R=(Seed>>16)&0x7fff;return Min+static_cast<int32>((uint64(R)*(uint64(Max-Min)+1))>>15);} int32 GetCurrentSeed()const{return Seed;} private:int32 Seed; };
struct FCString { static int32 Strcmp(const char*A,const char*B){return std::strcmp(A,B);} static float Atof(const char*S){return static_cast<float>(std::atof(S));} static int32 Atoi(const char*S){return std::atoi(S);} };
struct FPlatformTime { using Clock=std::chrono::steady_clock; static uint64 Cycles64(){return static_cast<uint64>(std::chrono::duration_cast<std::chrono::nanoseconds>(Clock::now().time_since_epoch()).count());} static double Seconds(){return Cycles64()*1e-9;} static double GetSecondsPerCycle64(){return 1e-9;} };
struct FEvent { void Trigger(){} bool Wait(uint32=0,bool=false){return true;} };
struct FPlatformProcess { static void Sleep(float S){std::this_thread::sleep_for(std::chrono::duration<double>(S));} static void YieldThread(){std::this_thread::yield();} static void Yield(){std::this_thread::yield();} static FEvent* GetSynchEventFromPool(bool=false){return new FEvent;} static void ReturnSynchEventToPool(FEvent*E){delete E;} };
using FCriticalSection = std::mutex;
struct FScopeLock { explicit FScopeLock(std::mutex&M):Lock(M){} explicit FScopeLock(std::mutex* M):Lock(*M){} std::lock_guard<std::mutex> Lock; };
struct FRWLock { mutable std::shared_mutex Mutex; };
struct FReadScopeLock { explicit FReadScopeLock(FRWLock&L):Lock(L.Mutex){} std::shared_lock<std::shared_mutex> Lock; };
struct FWriteScopeLock { explicit FWriteScopeLock(FRWLock&L):Lock(L.Mutex){} std::unique_lock<std::shared_mutex> Lock; };
enum EConsoleVariableFlags { ECVF_Default=0, ECVF_SetByCommandline=1, ECVF_SetByCode=2 };
class FAutoConsoleVariableRef { public:FAutoConsoleVariableRef(const char*,int32&,const char*,uint32=0){} FAutoConsoleVariableRef(const char*,float&,const char*,uint32=0){} FAutoConsoleVariableRef(const char*,bool&,const char*,uint32=0){} };
template<class T> class TAutoConsoleVariable { public:TAutoConsoleVariable(const char*,T Default,const char*,uint32=0):Value(Default){}T GetValueOnAnyThread()const{return Value.load();}T GetValueOnGameThread()const{return Value.load();}void Set(T V,uint32=0){Value.store(V);} TAutoConsoleVariable*operator->(){return this;}private:std::atomic<T>Value;};
struct FCommandLine { static const char* Get(){return "";} };
struct FParse { static bool Value(const char*,const char*,int32&){return false;} static bool Value(const char*,const char*,float&){return false;} static bool Value(const char*,const char*,FString&){return false;} static bool Param(const char*,const char*){return false;} };
struct FCrc { static uint32 MemCrc32(const void*Data,int32 Length,uint32 Crc=0){const auto*P=static_cast<const uint8*>(Data);uint32 V=~Crc;for(int32 I=0;I<Length;++I){V^=P[I];for(int J=0;J<8;++J)V=(V>>1)^(0xEDB88320u&static_cast<uint32>(-static_cast<int32>(V&1)));}return ~V;} static uint32 StrCrc32(const char*S,uint32 Crc=0){return MemCrc32(S,static_cast<int32>(std::strlen(S)),Crc);} };
template<class T> using TAtomic = std::atomic<T>;
class FThreadSafeCounter { public:int32 Increment(){return ++Value;}int32 Decrement(){return --Value;}int32 GetValue()const{return Value.load();}void Reset(){Value.store(0);}private:std::atomic<int32>Value{0};};
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+154
View File
@@ -0,0 +1,154 @@
#pragma once
#include <charconv>
#include <fstream>
#include <variant>
namespace VFJson
{
struct Value
{
using Array = std::vector<Value>;
using Object = std::unordered_map<std::string, Value>;
using Storage = std::variant<std::nullptr_t, bool, double, std::string, Array, Object>;
Storage Data = nullptr;
bool IsObject() const { return std::holds_alternative<Object>(Data); }
bool IsArray() const { return std::holds_alternative<Array>(Data); }
bool IsString() const { return std::holds_alternative<std::string>(Data); }
bool IsNumber() const { return std::holds_alternative<double>(Data); }
const Object& AsObject() const { static const Object Empty; return IsObject() ? std::get<Object>(Data) : Empty; }
const Array& AsArray() const { static const Array Empty; return IsArray() ? std::get<Array>(Data) : Empty; }
const Value& Get(std::string_view Key) const
{
static const Value Null;
const Object& O = AsObject();
auto It = O.find(std::string(Key));
return It == O.end() ? Null : It->second;
}
double Number(double Fallback = 0.0) const { return IsNumber() ? std::get<double>(Data) : Fallback; }
int64 Integer(int64 Fallback = 0) const { return IsNumber() ? static_cast<int64>(std::get<double>(Data)) : Fallback; }
bool Boolean(bool Fallback = false) const { return std::holds_alternative<bool>(Data) ? std::get<bool>(Data) : Fallback; }
const std::string& String() const { static const std::string Empty; return IsString() ? std::get<std::string>(Data) : Empty; }
};
class Parser
{
public:
explicit Parser(std::string Source) : Text(std::move(Source)) {}
bool Parse(Value& Out, std::string& Error)
{
SkipSpace();
if (!ReadValue(Out, Error)) return false;
SkipSpace();
if (Position != Text.size()) { Error = "trailing characters after JSON value"; return false; }
return true;
}
private:
std::string Text;
size_t Position = 0;
void SkipSpace() { while (Position < Text.size() && static_cast<unsigned char>(Text[Position]) <= 0x20) ++Position; }
bool Consume(char C) { SkipSpace(); if (Position < Text.size() && Text[Position] == C) { ++Position; return true; } return false; }
static void AppendUtf8(std::string& Out, uint32 Code)
{
if (Code <= 0x7f) Out.push_back(static_cast<char>(Code));
else if (Code <= 0x7ff) { Out.push_back(static_cast<char>(0xc0 | (Code >> 6))); Out.push_back(static_cast<char>(0x80 | (Code & 0x3f))); }
else if (Code <= 0xffff) { Out.push_back(static_cast<char>(0xe0 | (Code >> 12))); Out.push_back(static_cast<char>(0x80 | ((Code >> 6) & 0x3f))); Out.push_back(static_cast<char>(0x80 | (Code & 0x3f))); }
else { Out.push_back(static_cast<char>(0xf0 | (Code >> 18))); Out.push_back(static_cast<char>(0x80 | ((Code >> 12) & 0x3f))); Out.push_back(static_cast<char>(0x80 | ((Code >> 6) & 0x3f))); Out.push_back(static_cast<char>(0x80 | (Code & 0x3f))); }
}
bool Hex4(uint32& Out)
{
if (Position + 4 > Text.size()) return false;
Out = 0;
for (int32 I = 0; I < 4; ++I)
{
const char C = Text[Position++];
const int32 N = (C >= '0' && C <= '9') ? C - '0' : (C >= 'a' && C <= 'f') ? C - 'a' + 10 : (C >= 'A' && C <= 'F') ? C - 'A' + 10 : -1;
if (N < 0) return false;
Out = (Out << 4) | static_cast<uint32>(N);
}
return true;
}
bool ReadString(std::string& Out, std::string& Error)
{
SkipSpace();
if (Position >= Text.size() || Text[Position++] != '"') { Error = "expected JSON string"; return false; }
while (Position < Text.size())
{
const unsigned char C = static_cast<unsigned char>(Text[Position++]);
if (C == '"') return true;
if (C < 0x20) { Error = "control character in JSON string"; return false; }
if (C != '\\') { Out.push_back(static_cast<char>(C)); continue; }
if (Position >= Text.size()) break;
const char Escape = Text[Position++];
switch (Escape)
{
case '"': Out.push_back('"'); break; case '\\': Out.push_back('\\'); break;
case '/': Out.push_back('/'); break; case 'b': Out.push_back('\b'); break;
case 'f': Out.push_back('\f'); break; case 'n': Out.push_back('\n'); break;
case 'r': Out.push_back('\r'); break; case 't': Out.push_back('\t'); break;
case 'u':
{
uint32 Code = 0; if (!Hex4(Code)) { Error = "invalid JSON unicode escape"; return false; }
if (Code >= 0xd800 && Code <= 0xdbff)
{
if (Position + 2 > Text.size() || Text[Position++] != '\\' || Text[Position++] != 'u') { Error = "missing JSON low surrogate"; return false; }
uint32 Low = 0; if (!Hex4(Low) || Low < 0xdc00 || Low > 0xdfff) { Error = "invalid JSON low surrogate"; return false; }
Code = 0x10000 + ((Code - 0xd800) << 10) + (Low - 0xdc00);
}
AppendUtf8(Out, Code); break;
}
default: Error = "invalid JSON escape"; return false;
}
}
Error = "unterminated JSON string"; return false;
}
bool ReadValue(Value& Out, std::string& Error)
{
SkipSpace();
if (Position >= Text.size()) { Error = "unexpected end of JSON"; return false; }
const char C = Text[Position];
if (C == '"') { std::string S; if (!ReadString(S, Error)) return false; Out.Data = std::move(S); return true; }
if (C == '{')
{
++Position; Value::Object O; SkipSpace();
if (Consume('}')) { Out.Data = std::move(O); return true; }
for (;;)
{
std::string Key; if (!ReadString(Key, Error) || !Consume(':')) { if (Error.empty()) Error = "expected colon in JSON object"; return false; }
Value V; if (!ReadValue(V, Error)) return false; O[std::move(Key)] = std::move(V);
if (Consume('}')) break; if (!Consume(',')) { Error = "expected comma in JSON object"; return false; }
}
Out.Data = std::move(O); return true;
}
if (C == '[')
{
++Position; Value::Array A; SkipSpace();
if (Consume(']')) { Out.Data = std::move(A); return true; }
for (;;) { Value V; if (!ReadValue(V, Error)) return false; A.push_back(std::move(V)); if (Consume(']')) break; if (!Consume(',')) { Error = "expected comma in JSON array"; return false; } }
Out.Data = std::move(A); return true;
}
auto Match = [&](std::string_view S) { if (Text.compare(Position, S.size(), S) == 0) { Position += S.size(); return true; } return false; };
if (Match("true")) { Out.Data = true; return true; }
if (Match("false")) { Out.Data = false; return true; }
if (Match("null")) { Out.Data = nullptr; return true; }
const size_t Start = Position;
if (C == '-') ++Position;
while (Position < Text.size() && Text[Position] >= '0' && Text[Position] <= '9') ++Position;
if (Position < Text.size() && Text[Position] == '.') { ++Position; while (Position < Text.size() && Text[Position] >= '0' && Text[Position] <= '9') ++Position; }
if (Position < Text.size() && (Text[Position] == 'e' || Text[Position] == 'E')) { ++Position; if (Position < Text.size() && (Text[Position] == '+' || Text[Position] == '-')) ++Position; while (Position < Text.size() && Text[Position] >= '0' && Text[Position] <= '9') ++Position; }
if (Start == Position) { Error = "invalid JSON value"; return false; }
char* End = nullptr; const double Number = std::strtod(Text.c_str() + Start, &End);
if (End != Text.c_str() + Position || !std::isfinite(Number)) { Error = "invalid JSON number"; return false; }
Out.Data = Number; return true;
}
};
inline bool Load(const std::string& Path, Value& Out, std::string& Error)
{
std::ifstream Input(Path, std::ios::binary);
if (!Input) { Error = "cannot open " + Path; return false; }
std::string Text((std::istreambuf_iterator<char>(Input)), std::istreambuf_iterator<char>());
return Parser(std::move(Text)).Parse(Out, Error);
}
}
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1,32 @@
#include "VoxelSeasonAsset.h"
#include "VoxelSeasonManifest.h"
#include "VoxelStrateDefinition.h"
#include "VoxelStartupTrace.h"
// The owner's exported settings have no active Season reference. Engine deserialization already
// ran the version migration before export, so the standalone data holder only reapplies the
// current normalized compatibility field.
void UVoxelStrateDefinition::PostLoad()
{
GenerationParams.bTunnelFloorEnabled = true;
}
bool UVoxelSeasonAsset::LoadManifest(FVoxelSeasonManifest& OutManifest, FString& OutReport) const
{
OutManifest = FVoxelSeasonManifest();
OutReport = TEXT("Standalone owner snapshot has no active season manifest.");
return false;
}
namespace VoxelForgeStartupTrace
{
void BeginFromCommandLine() {}
bool IsActive() { return false; }
void RecordEvent(const TCHAR*, const FString&) {}
void StageBegin(const TCHAR*) {}
void StageEnd(const TCHAR*) {}
void RecordTile(const FTileSample&) {}
void Finish(const TCHAR*) {}
FStageScope::FStageScope(const TCHAR*) {}
FStageScope::~FStageScope() = default;
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#define STATCAT_Advanced 0
#define DECLARE_STATS_GROUP(Name,Group,Category) constexpr int32 Group=0
#define DECLARE_DWORD_COUNTER_STAT_EXTERN(Name,Stat,Group,Api) constexpr int32 Stat=0
#define DEFINE_STAT(Stat)
#define INC_DWORD_STAT(Stat) ((void)0)
#define DEC_DWORD_STAT(Stat) ((void)0)
#define SET_DWORD_STAT(Stat,Value) ((void)0)
#define SCOPE_CYCLE_COUNTER(Stat) ((void)0)
#define QUICK_SCOPE_CYCLE_COUNTER(Stat) ((void)0)
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
+1
View File
@@ -0,0 +1 @@
#pragma once
+64
View File
@@ -0,0 +1,64 @@
#include "CoreMinimal.h"
#include "UnrealPerlinHelpers.inl"
float FMath::PerlinNoise2D(const FVector2D& Location)
{
using namespace FMathPerlinHelpers;
const float Xfl = FMath::FloorToFloat(static_cast<float>(Location.X));
const float Yfl = FMath::FloorToFloat(static_cast<float>(Location.Y));
const int32 Xi = static_cast<int32>(Xfl) & 255;
const int32 Yi = static_cast<int32>(Yfl) & 255;
const float X = static_cast<float>(Location.X) - Xfl;
const float Y = static_cast<float>(Location.Y) - Yfl;
const float Xm1 = X - 1.0f;
const float Ym1 = Y - 1.0f;
const int32* P = Permutation;
const int32 AA = P[Xi] + Yi;
const int32 AB = AA + 1;
const int32 BA = P[Xi + 1] + Yi;
const int32 BB = BA + 1;
const float U = SmoothCurve(X);
const float V = SmoothCurve(Y);
return FMath::Lerp(
FMath::Lerp(Grad2(P[AA], X, Y), Grad2(P[BA], Xm1, Y), U),
FMath::Lerp(Grad2(P[AB], X, Ym1), Grad2(P[BB], Xm1, Ym1), U),
V);
}
float FMath::PerlinNoise3D(const FVector& Location)
{
using namespace FMathPerlinHelpers;
const float Xfl = FMath::FloorToFloat(static_cast<float>(Location.X));
const float Yfl = FMath::FloorToFloat(static_cast<float>(Location.Y));
const float Zfl = FMath::FloorToFloat(static_cast<float>(Location.Z));
const int32 Xi = static_cast<int32>(Xfl) & 255;
const int32 Yi = static_cast<int32>(Yfl) & 255;
const int32 Zi = static_cast<int32>(Zfl) & 255;
const float X = static_cast<float>(Location.X) - Xfl;
const float Y = static_cast<float>(Location.Y) - Yfl;
const float Z = static_cast<float>(Location.Z) - Zfl;
const float Xm1 = X - 1.0f;
const float Ym1 = Y - 1.0f;
const float Zm1 = Z - 1.0f;
const int32* P = Permutation;
const int32 A = P[Xi] + Yi;
const int32 AA = P[A] + Zi;
const int32 AB = P[A + 1] + Zi;
const int32 B = P[Xi + 1] + Yi;
const int32 BA = P[B] + Zi;
const int32 BB = P[B + 1] + Zi;
const float U = SmoothCurve(X);
const float V = SmoothCurve(Y);
const float W = SmoothCurve(Z);
const float Value = 0.97f * FMath::Lerp(
FMath::Lerp(
FMath::Lerp(Grad3(P[AA], X, Y, Z), Grad3(P[BA], Xm1, Y, Z), U),
FMath::Lerp(Grad3(P[AB], X, Ym1, Z), Grad3(P[BB], Xm1, Ym1, Z), U),
V),
FMath::Lerp(
FMath::Lerp(Grad3(P[AA + 1], X, Y, Zm1), Grad3(P[BA + 1], Xm1, Y, Zm1), U),
FMath::Lerp(Grad3(P[AB + 1], X, Ym1, Zm1), Grad3(P[BB + 1], Xm1, Ym1, Zm1), U),
V),
W);
return FMath::Clamp(Value, -1.0f, 1.0f);
}
+82
View File
@@ -0,0 +1,82 @@
// Implementation of 1D, 2D and 3D Perlin noise based on Ken Perlin's improved version https://mrl.nyu.edu/~perlin/noise/
// (See Random3.tps for additional third party software info.)
namespace FMathPerlinHelpers
{
// random permutation of 256 numbers, repeated 2x
static const int32 Permutation[512] = {
63, 9, 212, 205, 31, 128, 72, 59, 137, 203, 195, 170, 181, 115, 165, 40, 116, 139, 175, 225, 132, 99, 222, 2, 41, 15, 197, 93, 169, 90, 228, 43, 221, 38, 206, 204, 73, 17, 97, 10, 96, 47, 32, 138, 136, 30, 219,
78, 224, 13, 193, 88, 134, 211, 7, 112, 176, 19, 106, 83, 75, 217, 85, 0, 98, 140, 229, 80, 118, 151, 117, 251, 103, 242, 81, 238, 172, 82, 110, 4, 227, 77, 243, 46, 12, 189, 34, 188, 200, 161, 68, 76, 171, 194,
57, 48, 247, 233, 51, 105, 5, 23, 42, 50, 216, 45, 239, 148, 249, 84, 70, 125, 108, 241, 62, 66, 64, 240, 173, 185, 250, 49, 6, 37, 26, 21, 244, 60, 223, 255, 16, 145, 27, 109, 58, 102, 142, 253, 120, 149, 160,
124, 156, 79, 186, 135, 127, 14, 121, 22, 65, 54, 153, 91, 213, 174, 24, 252, 131, 192, 190, 202, 208, 35, 94, 231, 56, 95, 183, 163, 111, 147, 25, 67, 36, 92, 236, 71, 166, 1, 187, 100, 130, 143, 237, 178, 158,
104, 184, 159, 177, 52, 214, 230, 119, 87, 114, 201, 179, 198, 3, 248, 182, 39, 11, 152, 196, 113, 20, 232, 69, 141, 207, 234, 53, 86, 180, 226, 74, 150, 218, 29, 133, 8, 44, 123, 28, 146, 89, 101, 154, 220, 126,
155, 122, 210, 168, 254, 162, 129, 33, 18, 209, 61, 191, 199, 157, 245, 55, 164, 167, 215, 246, 144, 107, 235,
63, 9, 212, 205, 31, 128, 72, 59, 137, 203, 195, 170, 181, 115, 165, 40, 116, 139, 175, 225, 132, 99, 222, 2, 41, 15, 197, 93, 169, 90, 228, 43, 221, 38, 206, 204, 73, 17, 97, 10, 96, 47, 32, 138, 136, 30, 219,
78, 224, 13, 193, 88, 134, 211, 7, 112, 176, 19, 106, 83, 75, 217, 85, 0, 98, 140, 229, 80, 118, 151, 117, 251, 103, 242, 81, 238, 172, 82, 110, 4, 227, 77, 243, 46, 12, 189, 34, 188, 200, 161, 68, 76, 171, 194,
57, 48, 247, 233, 51, 105, 5, 23, 42, 50, 216, 45, 239, 148, 249, 84, 70, 125, 108, 241, 62, 66, 64, 240, 173, 185, 250, 49, 6, 37, 26, 21, 244, 60, 223, 255, 16, 145, 27, 109, 58, 102, 142, 253, 120, 149, 160,
124, 156, 79, 186, 135, 127, 14, 121, 22, 65, 54, 153, 91, 213, 174, 24, 252, 131, 192, 190, 202, 208, 35, 94, 231, 56, 95, 183, 163, 111, 147, 25, 67, 36, 92, 236, 71, 166, 1, 187, 100, 130, 143, 237, 178, 158,
104, 184, 159, 177, 52, 214, 230, 119, 87, 114, 201, 179, 198, 3, 248, 182, 39, 11, 152, 196, 113, 20, 232, 69, 141, 207, 234, 53, 86, 180, 226, 74, 150, 218, 29, 133, 8, 44, 123, 28, 146, 89, 101, 154, 220, 126,
155, 122, 210, 168, 254, 162, 129, 33, 18, 209, 61, 191, 199, 157, 245, 55, 164, 167, 215, 246, 144, 107, 235
};
// Gradient functions for 1D, 2D and 3D Perlin noise
FORCEINLINE float Grad1(int32 Hash, float X)
{
// Slicing Perlin's 3D improved noise would give us only scales of -1, 0 and 1; this looks pretty bad so let's use a different sampling
static const float Grad1Scales[16] = {-8/8, -7/8., -6/8., -5/8., -4/8., -3/8., -2/8., -1/8., 1/8., 2/8., 3/8., 4/8., 5/8., 6/8., 7/8., 8/8};
return Grad1Scales[Hash & 15] * X;
}
// Note: If you change the Grad2 or Grad3 functions, check that you don't change the range of the resulting noise as well; it should be (within floating point error) in the range of (-1, 1)
FORCEINLINE float Grad2(int32 Hash, float X, float Y)
{
// corners and major axes (similar to the z=0 projection of the cube-edge-midpoint sampling from improved Perlin noise)
switch (Hash & 7)
{
case 0: return X;
case 1: return X + Y;
case 2: return Y;
case 3: return -X + Y;
case 4: return -X;
case 5: return -X - Y;
case 6: return -Y;
case 7: return X - Y;
// can't happen
default: return 0;
}
}
FORCEINLINE float Grad3(int32 Hash, float X, float Y, float Z)
{
switch (Hash & 15)
{
// 12 cube midpoints
case 0: return X + Z;
case 1: return X + Y;
case 2: return Y + Z;
case 3: return -X + Y;
case 4: return -X + Z;
case 5: return -X - Y;
case 6: return -Y + Z;
case 7: return X - Y;
case 8: return X - Z;
case 9: return Y - Z;
case 10: return -X - Z;
case 11: return -Y - Z;
// 4 vertices of regular tetrahedron
case 12: return X + Y;
case 13: return -X + Y;
case 14: return -Y + Z;
case 15: return -Y - Z;
// can't happen
default: return 0;
}
}
// Curve w/ second derivative vanishing at 0 and 1, from Perlin's improved noise paper
FORCEINLINE float SmoothCurve(float X)
{
return X * X * X * (X * (X * 6.0f - 15.0f) + 10.0f);
}
};
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
+100
View File
@@ -0,0 +1,100 @@
# VoxelForge standalone world
This C++20 program loads the owner snapshot from `data/owner_world.json`, runs the copied
VoxelForge density pipeline and marching-cubes mesher, builds all clipmap levels with six worker
threads, and displays them through Direct3D 11. Unreal is not linked or needed at run time.
## Build and launch
From the plugin root, run:
```powershell
.\Standalone\Build.ps1
.\Standalone\bin\VoxelForgeStandalone.exe
```
The build uses the configured VS 2022 `vcvars64.bat`, MSVC `/std:c++20 /O2 /arch:AVX2 /fp:precise`.
The executable finds the JSON beside itself under `Standalone\data\`.
## Controls
- Hold right mouse button and move to look.
- WASD moves; Space/Ctrl or E/Q moves vertically; Shift moves faster; the wheel changes speed.
- F toggles wireframe.
- 1 through 7 toggle clipmap levels 0 through 6 individually; 0 toggles every level.
- N/P changes the seed. T switches between the owner's base tunnels and the research-shaped round,
winding tunnel option. Either action rebuilds the clipmap. Moving three chunks from its center
also recenters and rebuilds it.
- Esc exits.
The console reports density parity at startup, each level's generation time, and the total time.
## Copied code and compatibility layer
`Source\VoxelForge\` contains the plugin's density evaluator and operator stack, cave morphology,
passage geometry, strate definitions/layout/manager/composer, noise, voxel math and types,
marching-cubes tables/mesher, and clipmap tile selection. These are copies. Plugin runtime source
is untouched apart from the allowed editor commandlet export addition.
`Compat\CoreMinimal.h` supplies the UE-style arrays/maps/sets, vectors and boxes, math helpers,
strings, smart pointers, locks, logging/stat macros, UObject-like data holders, and platform helpers
needed by the copied CPU code. The `Compat` subfolders cover the UE include paths used by those
copies. Perlin 2D/3D uses the copied UE 5.7 implementation in `UnrealPerlin.cpp` and
`UnrealPerlinHelpers.inl`. `OwnerWorldLoader.h` maps the exported settings, resolved strate
definitions, biomes, and terrain operations into copied holders; soft references resolve through a
small local asset registry. The copied strate manager reconstructs runtime passages from that data
and seed. The export also records their geometry in JSON for reference.
## Behavior differences
- Standalone `VOXEL_SIZE` is 25 cm; the plugin's checked-out value remains 12.5 cm. No other voxel
conversion constant needed changing. Mesh vertices stay in centimetres in the copied mesher and
the viewer converts them to metres at upload.
- The compatibility `TMap` and `TSet` are vector-backed containers, so lookup is linear and their
iteration follows insertion order rather than Unreal hash-table order. Owner strate slots and
passages are read from ordered JSON arrays. Perlin noise and deterministic `DetSinCos` are copied
unchanged; MSVC uses `/fp:precise` without fast-math/FMA contraction.
- Unreal reflection and UObject construction are replaced by explicit JSON-to-holder mapping.
Density-relevant settings, resolved strate layouts, passages, biome parameters, and terrain-op
assets are loaded. Runtime actor/material assets are not instantiated; the viewer uses one lit
rock material for all generated geometry.
- The owner export has no active season. Standalone season loading is not implemented; an active
season would need to be exported and mapped before it could affect this viewer.
- `WITH_EDITOR` research/debug branches, Unreal logging, startup trace, profiling counters, and
reflection metadata do not run in the standalone. The base generator path is the default.
- T selects a standalone-only tunnel-shape port. It disables the owner's first-strate tunnel
connections and adds the research field's cubic winding centerline and round elliptical tunnel
profile (144-voxel pitch, 28-voxel radius, 26-voxel half-height, deterministic variable radius
and vertical wave), anchored around the camera's current height. It reuses the base rooms and
passages; it does not port the research field's room planner or full parameterized density field.
## Owner data and parity
`data/owner_world.json` came from one default-off `VoxelForgeExploreCommandlet` export using the
guarded `Tools\VoxelForgeTest.ps1 -Scenario standalone-export -Build -Assets owner` path. The
snapshot contains the resolved settings/strates/passages and 3,436 plugin density samples. For that
commandlet build, `VOXEL_SIZE` was temporarily set to 25 cm in the worktree and restored afterward;
the exported JSON records 25 cm and the checked-out plugin source still records 12.5 cm.
Standalone comparison: **3,436/3,436 bit-identical, zero mismatches**. Samples covered strate
boundaries (2,420), varied world coordinates including negative and far-from-origin points (768),
tunnel interiors (222), room interiors (12), inter-strate passages (10), and passage landings (4).
## Baseline clipmap timing
Owner seed 0, base world, 25 cm voxels, six workers; the final viewer run generated the full
seven-level clipmap (12,054 tiles, 6,605,189 vertices, 36,307,137 indices) in 515.117 seconds.
These level timers cover CPU meshing and queue handoff; D3D11 buffer upload is separate. An earlier
headless pass over the same tiles took 408.719 seconds, so use one run mode consistently when
comparing later speed changes.
| Level | Seconds | Tiles | Vertices | Density samples |
|---:|---:|---:|---:|---:|
| 0 | 70.201 | 2,744 | 957,655 | 117.65 M |
| 1 | 65.376 | 2,009 | 657,423 | 86.14 M |
| 2 | 9.042 | 1,274 | 292,267 | 8.74 M |
| 3 | 25.689 | 588 | 235,755 | 4.03 M |
| 4 | 24.248 | 441 | 229,720 | 3.02 M |
| 5 | 23.809 | 294 | 199,503 | 2.02 M |
| 6 | 296.304 | 4,704 | 4,032,866 | 32.26 M |
| **Total** | **515.117** | **12,054** | **6,605,189** | **253.86 M** |
+323
View File
@@ -0,0 +1,323 @@
#pragma once
#include "CoreMinimal.h"
#include "Json.h"
#include "VoxelBiomeDefinition.h"
#include "VoxelGenerator.h"
#include "VoxelSettings.h"
#include "VoxelStrateDefinition.h"
#include "VoxelStrateManager.h"
#include "VoxelTerrainOpDefinition.h"
struct FStandaloneParitySample
{
FVector PositionVoxels;
float PluginDensity = 0.0f;
std::string Kind;
int32 StrateIndex = INDEX_NONE;
};
struct FStandaloneOwnerWorld
{
std::unique_ptr<UVoxelSettings> Settings;
std::unique_ptr<UVoxelStrateManager> Manager;
std::unique_ptr<UVoxelGenerator> Generator;
std::vector<std::unique_ptr<UVoxelStrateDefinition>> DefinitionStorage;
std::vector<std::unique_ptr<UVoxelBiomeDefinition>> BiomeStorage;
std::vector<std::unique_ptr<UVoxelTerrainOpDefinition>> OperationStorage;
std::vector<FStandaloneParitySample> DensitySamples;
FVector ViewerStartVoxels = FVector::ZeroVector;
bool ViewerStartFound = false;
int32 Seed = 0;
int32 ClipRadius = 6;
int32 MaxClipLevel = 6;
int32 FullResClipLevels = 2;
int32 CoarseTileCells = 16;
int32 RenderDistanceChunks = 1536;
int32 StrateContentCutMinLevel = 9;
bool bGenerateSkirts = true;
int32 SkirtCells = 2;
int32 LODOctaveDrop = 0;
};
namespace StandaloneOwnerLoader
{
template<class T>
inline void ReadField(const VFJson::Value& Object, const char* Name, T& Out)
{
const VFJson::Value& Value = Object.Get(Name);
if constexpr (std::is_same_v<T, bool>)
{
if (std::holds_alternative<bool>(Value.Data)) Out = Value.Boolean(Out);
else if (Value.IsNumber()) Out = Value.Number() != 0.0;
}
else if constexpr (std::is_enum_v<T>)
{
if (Value.IsNumber()) Out = static_cast<T>(Value.Integer());
}
else if constexpr (std::is_arithmetic_v<T>)
{
if (Value.IsNumber()) Out = static_cast<T>(Value.Number());
}
}
inline void ReadText(const VFJson::Value& Object, const char* Name, FText& Out)
{
const VFJson::Value& Value = Object.Get(Name);
if (Value.IsString()) Out = FText::FromString(FString(Value.String()));
}
inline void ReadLinearColor(const VFJson::Value& Object, FLinearColor& Out)
{
ReadField(Object, "R", Out.R); ReadField(Object, "G", Out.G);
ReadField(Object, "B", Out.B); ReadField(Object, "A", Out.A);
}
inline void ReadVector(const VFJson::Value& Object, FVector& Out)
{
ReadField(Object, "x", Out.X); ReadField(Object, "y", Out.Y); ReadField(Object, "z", Out.Z);
}
inline void ReadGenerationParams(const VFJson::Value& J, FStrateGenerationParams& P)
{
#define VF_LOAD_STRATE_FIELD(Name) ReadField(J, #Name, P.Name);
VF_STRATE_PARAM_FIELDS(VF_LOAD_STRATE_FIELD, VF_LOAD_STRATE_FIELD)
#undef VF_LOAD_STRATE_FIELD
ReadField(J, "bTunnelFloorEnabled", P.bTunnelFloorEnabled);
}
inline void ReadDisturbances(const VFJson::Value& J, FStrateDisturbanceParams& P)
{
ReadField(J,"ChasmDensity",P.ChasmDensity); ReadField(J,"ChasmSpacing",P.ChasmSpacing);
ReadField(J,"ChasmRadius",P.ChasmRadius); ReadField(J,"BridgeDensity",P.BridgeDensity);
ReadField(J,"BridgeSpacing",P.BridgeSpacing); ReadField(J,"BridgeRadius",P.BridgeRadius);
ReadField(J,"RidgeDensity",P.RidgeDensity); ReadField(J,"RidgeSpacing",P.RidgeSpacing);
ReadField(J,"RidgeHeight",P.RidgeHeight); ReadField(J,"RidgeThickness",P.RidgeThickness);
ReadField(J,"BaseDensity",P.BaseDensity); ReadField(J,"BoundarySealThickness",P.BoundarySealThickness);
}
inline void ReadPassageConfig(const VFJson::Value& J, FStratePassageConfig& P)
{
ReadField(J,"Connections",P.Connections); ReadField(J,"Style",P.Style);
ReadField(J,"MouthRadius",P.MouthRadius); ReadField(J,"MidRadius",P.MidRadius);
ReadField(J,"ReachMin",P.ReachMin); ReadField(J,"ReachMax",P.ReachMax);
ReadField(J,"DistanceMin",P.DistanceMin); ReadField(J,"DistanceMax",P.DistanceMax);
ReadField(J,"Wander",P.Wander); ReadField(J,"Segments",P.Segments);
ReadField(J,"VerticalWobble",P.VerticalWobble); ReadField(J,"SpiralRadius",P.SpiralRadius);
ReadField(J,"SpiralTurns",P.SpiralTurns); ReadField(J,"CascadeSteps",P.CascadeSteps);
ReadField(J,"CascadeLedge",P.CascadeLedge);
}
inline void ReadSurfaceParams(const VFJson::Value& J, FSurfaceGenerationParams& P)
{
#define VF_SURFACE_FIELD(Name) ReadField(J, #Name, P.Name);
VF_SURFACE_FIELD(BaseGroundRelative) VF_SURFACE_FIELD(ElevationRange)
VF_SURFACE_FIELD(ContinentFrequency) VF_SURFACE_FIELD(MountainStrength)
VF_SURFACE_FIELD(MountainFrequency) VF_SURFACE_FIELD(DetailFrequency)
VF_SURFACE_FIELD(SurfaceRoughness) VF_SURFACE_FIELD(HeightWarpStrength)
VF_SURFACE_FIELD(HeightWarpFrequency) VF_SURFACE_FIELD(ReliefFrequency)
VF_SURFACE_FIELD(ReliefStrength) VF_SURFACE_FIELD(ReliefContrast)
VF_SURFACE_FIELD(TerraceStrength) VF_SURFACE_FIELD(TerraceHeight)
VF_SURFACE_FIELD(TerraceHardness) VF_SURFACE_FIELD(LayerLineDepth)
VF_SURFACE_FIELD(LayerLineSpacing) VF_SURFACE_FIELD(CliffStrength)
VF_SURFACE_FIELD(CliffSlopeThreshold) VF_SURFACE_FIELD(CliffSharpness)
VF_SURFACE_FIELD(CliffSampleDist) VF_SURFACE_FIELD(OverhangStrength)
VF_SURFACE_FIELD(OverhangReach) VF_SURFACE_FIELD(OverhangHeight)
VF_SURFACE_FIELD(OverhangFrequency) VF_SURFACE_FIELD(OverhangZScale)
VF_SURFACE_FIELD(OverhangSlopeThreshold) VF_SURFACE_FIELD(WaterLevelRelative)
VF_SURFACE_FIELD(BeachWidth) VF_SURFACE_FIELD(CeilingRelative)
VF_SURFACE_FIELD(CeilingRoughness) VF_SURFACE_FIELD(CeilingRoughnessFrequency)
VF_SURFACE_FIELD(CeilingUndulation) VF_SURFACE_FIELD(CeilingUndulationFrequency)
VF_SURFACE_FIELD(CeilingRidgeStrength) VF_SURFACE_FIELD(CeilingRidgeFrequency)
VF_SURFACE_FIELD(CeilingWarpStrength) VF_SURFACE_FIELD(CeilingWarpFrequency)
VF_SURFACE_FIELD(BoundarySealThickness) VF_SURFACE_FIELD(BaseDensity)
#undef VF_SURFACE_FIELD
}
inline void ReadBiomeMap(const VFJson::Value& J, FBiomeMapParams& P)
{
ReadField(J,"CellSize",P.CellSize); ReadField(J,"WarpStrength",P.WarpStrength);
ReadField(J,"WarpFrequency",P.WarpFrequency); ReadField(J,"BorderBlend",P.BorderBlend);
ReadField(J,"ReliefFrequency",P.ReliefFrequency); ReadField(J,"ReliefContrast",P.ReliefContrast);
ReadField(J,"MoistureFrequency",P.MoistureFrequency);
}
inline void ReadOperation(const VFJson::Value& J, UVoxelTerrainOpDefinition& O)
{
ReadField(J,"Type",O.Type);
#define VF_OPERATION_FIELD(Name) ReadField(J, #Name, O.Name);
VF_OPERATION_FIELD(TerraceStepHeight) VF_OPERATION_FIELD(TerraceHardness)
VF_OPERATION_FIELD(TerraceNoiseDisplacement) VF_OPERATION_FIELD(LayerLineSpacing)
VF_OPERATION_FIELD(LayerLineDepth) VF_OPERATION_FIELD(RibbingSpacing)
VF_OPERATION_FIELD(RibbingDepth) VF_OPERATION_FIELD(CliffStrength)
VF_OPERATION_FIELD(ScallopStrength) VF_OPERATION_FIELD(ScallopFrequency)
VF_OPERATION_FIELD(OverhangStrength) VF_OPERATION_FIELD(OverhangDepth)
VF_OPERATION_FIELD(OverhangFrequency) VF_OPERATION_FIELD(ArchDensity)
VF_OPERATION_FIELD(ArchMinRadius) VF_OPERATION_FIELD(ArchMaxRadius)
VF_OPERATION_FIELD(ColumnDensity) VF_OPERATION_FIELD(ColumnMinRadius)
VF_OPERATION_FIELD(ColumnMaxRadius) VF_OPERATION_FIELD(PitDensity)
VF_OPERATION_FIELD(PitMinRadius) VF_OPERATION_FIELD(PitMaxRadius)
VF_OPERATION_FIELD(PitDepth) VF_OPERATION_FIELD(ChimneyDensity)
VF_OPERATION_FIELD(ChimneyMinRadius) VF_OPERATION_FIELD(ChimneyMaxRadius)
VF_OPERATION_FIELD(ChimneyHeight) VF_OPERATION_FIELD(DomeDensity)
VF_OPERATION_FIELD(DomeMinRadius) VF_OPERATION_FIELD(DomeMaxRadius)
VF_OPERATION_FIELD(DomeHeightRatio) VF_OPERATION_FIELD(PinchDensity)
VF_OPERATION_FIELD(PinchStrength) VF_OPERATION_FIELD(PinchLength)
#undef VF_OPERATION_FIELD
}
inline void ReadBiome(const VFJson::Value& J, UVoxelBiomeDefinition& B)
{
ReadText(J,"BiomeName",B.BiomeName); ReadText(J,"BiomeDescription",B.BiomeDescription);
ReadLinearColor(J.Get("DebugColor"),B.DebugColor);
ReadField(J,"ReliefMin",B.ReliefMin); ReadField(J,"ReliefMax",B.ReliefMax);
ReadField(J,"MoistureMin",B.MoistureMin); ReadField(J,"MoistureMax",B.MoistureMax);
ReadField(J,"bOverrideTerrain",B.bOverrideTerrain); ReadField(J,"GeneratorType",B.GeneratorType);
ReadSurfaceParams(J.Get("SurfaceParams"),B.SurfaceParams);
ReadField(J,"bOverrideAtmosphere",B.bOverrideAtmosphere);
ReadLinearColor(J.Get("FogColor"),B.FogColor); ReadField(J,"FogDensity",B.FogDensity);
ReadLinearColor(J.Get("AmbientLightColor"),B.AmbientLightColor);
ReadField(J,"AmbientLightIntensity",B.AmbientLightIntensity);
ReadField(J,"MaterialPaletteIndex",B.MaterialPaletteIndex);
}
inline UVoxelStrateDefinition* ReadDefinition(
FStandaloneOwnerWorld& World, const VFJson::Value& Slot, int32 SlotIndex)
{
const VFJson::Value& J = Slot.Get("asset");
auto Definition = std::make_unique<UVoxelStrateDefinition>();
ReadText(J,"StrateName",Definition->StrateName);
ReadText(J,"StrateDescription",Definition->StrateDescription);
ReadField(J,"StrateHeightInChunks",Definition->StrateHeightInChunks);
ReadField(J,"TransitionType",Definition->TransitionType);
ReadField(J,"TransitionBlendChunks",Definition->TransitionBlendChunks);
ReadField(J,"GeneratorType",Definition->GeneratorType);
ReadField(J,"bEnableWorms",Definition->bEnableWorms);
ReadGenerationParams(Slot.Get("generation_params"),Definition->GenerationParams);
ReadDisturbances(Slot.Get("disturbances"),Definition->Disturbances);
ReadPassageConfig(Slot.Get("passage_config"),Definition->PassageConfig);
ReadSurfaceParams(J.Get("SurfaceParams"),Definition->SurfaceParams);
ReadBiomeMap(J.Get("BiomeMapParams"),Definition->BiomeMapParams);
ReadField(J,"bHasWater",Definition->bHasWater);
const auto& Biomes = J.Get("Biomes").AsArray();
Definition->Biomes.Reserve(static_cast<int32>(Biomes.size()));
for (const VFJson::Value& BiomeJson : Biomes)
{
auto Biome = std::make_unique<UVoxelBiomeDefinition>();
ReadBiome(BiomeJson,*Biome);
Definition->Biomes.Add(Biome.get());
World.BiomeStorage.push_back(std::move(Biome));
}
const auto& OperationEntries = Slot.Get("terrain_operation_assets").AsArray();
Definition->TerrainOperations.Reserve(static_cast<int32>(OperationEntries.size()));
for (int32 I = 0; I < static_cast<int32>(OperationEntries.size()); ++I)
{
const VFJson::Value& EntryJson = OperationEntries[static_cast<size_t>(I)];
const VFJson::Value& AssetJson = EntryJson.Get("asset");
const VFJson::Value& PathJson = EntryJson.Get("asset_path");
const FString Path = PathJson.IsString() ? FString(PathJson.String()) : FString{};
FStrateTerrainOpEntry Entry;
ReadField(EntryJson,"weight",Entry.Weight);
ReadField(EntryJson,"probability",Entry.Probability);
if (AssetJson.IsObject())
{
auto Operation = std::make_unique<UVoxelTerrainOpDefinition>();
ReadOperation(AssetJson,*Operation);
UVoxelTerrainOpDefinition* Raw = Operation.get();
World.OperationStorage.push_back(std::move(Operation));
Entry.Operation.Set(Raw,Path);
RegisterStandaloneAsset(Path,Raw);
}
else if (!Path.IsEmpty()) Entry.Operation = TSoftObjectPtr<UVoxelTerrainOpDefinition>(Path);
Definition->TerrainOperations.Add(std::move(Entry));
}
const FString SyntheticPath = FString::Printf("/Standalone/Strate_%d",SlotIndex);
Definition->PostLoad();
UVoxelStrateDefinition* Raw = Definition.get();
World.DefinitionStorage.push_back(std::move(Definition));
RegisterStandaloneAsset(SyntheticPath,Raw);
return Raw;
}
inline bool Load(const std::string& Path, FStandaloneOwnerWorld& Out, std::string& Error)
{
VFJson::Value Root;
if (!VFJson::Load(Path,Root,Error)) return false;
if (Root.Get("voxel_size_cm").Number() != VOXEL_SIZE)
{
Error = "export voxel_size_cm does not match the standalone VOXEL_SIZE";
return false;
}
Out.Seed = static_cast<int32>(Root.Get("seed").Integer());
Out.ClipRadius = static_cast<int32>(Root.Get("clip_radius").Integer(6));
Out.MaxClipLevel = static_cast<int32>(Root.Get("max_clip_level").Integer(6));
Out.FullResClipLevels = static_cast<int32>(Root.Get("full_res_clip_levels").Integer(2));
Out.CoarseTileCells = static_cast<int32>(Root.Get("coarse_tile_cells").Integer(16));
Out.RenderDistanceChunks = static_cast<int32>(Root.Get("render_distance_chunks").Integer(1536));
Out.StrateContentCutMinLevel = static_cast<int32>(Root.Get("strate_content_cut_min_level").Integer(9));
Out.bGenerateSkirts = Root.Get("generate_skirts").Boolean(true);
Out.SkirtCells = static_cast<int32>(Root.Get("skirt_cells").Integer(2));
Out.LODOctaveDrop = static_cast<int32>(Root.Get("lod_octave_drop").Integer(0));
ReadVector(Root.Get("viewer_start"),Out.ViewerStartVoxels);
Out.ViewerStartFound = Root.Get("viewer_start").Get("found_open_space").Boolean();
for (const VFJson::Value& SampleJson : Root.Get("density_samples").AsArray())
{
FStandaloneParitySample Sample;
ReadField(SampleJson,"x",Sample.PositionVoxels.X);
ReadField(SampleJson,"y",Sample.PositionVoxels.Y);
ReadField(SampleJson,"z",Sample.PositionVoxels.Z);
ReadField(SampleJson,"density",Sample.PluginDensity);
ReadField(SampleJson,"strate_index",Sample.StrateIndex);
Sample.Kind = SampleJson.Get("kind").String();
Out.DensitySamples.push_back(std::move(Sample));
}
Out.Settings = std::make_unique<UVoxelSettings>();
const VFJson::Value& SettingsJson = Root.Get("settings_asset");
Out.Settings->Seed = Out.Seed;
ReadField(Root,"origin_spine_radius",Out.Settings->OriginSpineRadius);
ReadField(Root,"world_radius_voxels",Out.Settings->WorldRadiusVoxels);
ReadField(Root,"edge_seal_thickness",Out.Settings->EdgeSealThickness);
ReadField(Root,"open_surface_entry",Out.Settings->bOpenSurfaceEntry);
ReadField(Root,"inter_strate_gap_chunks",Out.Settings->InterStrateGapChunks);
ReadField(SettingsJson,"total_strates",Out.Settings->TotalStrates);
ReadField(Root,"clip_radius",Out.Settings->ClipRadius);
ReadField(Root,"max_clip_level",Out.Settings->MaxClipLevel);
ReadField(Root,"full_res_clip_levels",Out.Settings->FullResClipLevels);
ReadField(Root,"coarse_tile_cells",Out.Settings->CoarseTileCells);
ReadField(Root,"render_distance_chunks",Out.Settings->RenderDistanceChunks);
ReadField(Root,"generate_skirts",Out.Settings->bGenerateSkirts);
ReadField(Root,"skirt_cells",Out.Settings->SkirtCells);
ReadField(Root,"lod_octave_drop",Out.Settings->LODOctaveDrop);
ReadField(Root,"strate_content_cut_min_level",Out.Settings->StrateContentCutMinLevel);
ReadField(SettingsJson,"TotalStrates",Out.Settings->TotalStrates);
ReadField(SettingsJson,"InterStrateGapChunks",Out.Settings->InterStrateGapChunks);
Out.Settings->StratePool.Reset();
Out.Settings->FixedStrates.Empty();
const auto& Strates = Root.Get("strates").AsArray();
Out.Settings->TotalStrates = static_cast<int32>(Strates.size());
for (int32 I = 0; I < static_cast<int32>(Strates.size()); ++I)
{
UVoxelStrateDefinition* Definition = ReadDefinition(Out,Strates[static_cast<size_t>(I)],I);
const FString SyntheticPath = FString::Printf("/Standalone/Strate_%d",I);
TSoftObjectPtr<UVoxelStrateDefinition> SoftDefinition;
SoftDefinition.Set(Definition,SyntheticPath);
Out.Settings->FixedStrates.Add(I,std::move(SoftDefinition));
}
if (Strates.empty()) { Error = "owner JSON contains no resolved strates"; return false; }
Out.Manager = std::make_unique<UVoxelStrateManager>();
if (!Out.Manager->Initialize(Out.Settings.get(),Out.Seed))
{
Error = "copied strate manager rejected the owner settings snapshot";
return false;
}
Out.Generator = std::make_unique<UVoxelGenerator>();
Out.Generator->InitializeSettings(Out.Settings.get());
Out.Generator->SetStrateManager(Out.Manager.get());
return true;
}
}
@@ -0,0 +1,128 @@
#pragma once
#include <atomic>
// Standalone-only port of the research field's round, winding tunnel profile.
// The cubic centerline, variable radius and elliptical vertical section follow
// GeneratedTunnelEdgeFromColumnProfile in VoxelRedefinedDensityField.cpp.
namespace FStandaloneResearchTunnels
{
inline std::atomic<bool> Enabled{false};
inline std::atomic<float> CenterZVoxels{0.0f};
inline uint32 Mix(uint32 X)
{
X ^= X >> 16;
X *= 0x7FEB352Du;
X ^= X >> 15;
X *= 0x846CA68Bu;
X ^= X >> 16;
return X;
}
inline uint32 EdgeHash(int32 X, int32 Y, int32 Axis, uint32 Seed)
{
uint32 H = Seed ^ 0x9E3779B9u;
H = Mix(H ^ static_cast<uint32>(X) * 0x85EBCA6Bu);
H = Mix(H ^ static_cast<uint32>(Y) * 0xC2B2AE35u);
return Mix(H ^ static_cast<uint32>(Axis) * 0x27D4EB2Fu);
}
inline float Unit(uint32 X)
{
return static_cast<float>(Mix(X) >> 8) * (1.0f / 16777216.0f);
}
inline void Set(bool bEnabled, float CenterZ)
{
CenterZVoxels.store(CenterZ,std::memory_order_relaxed);
Enabled.store(bEnabled,std::memory_order_release);
}
inline float Apply(float BaseDensity, float X, float Y, float Z, uint32 Seed)
{
if (!Enabled.load(std::memory_order_acquire)) return BaseDensity;
const float CenterZ = CenterZVoxels.load(std::memory_order_relaxed);
if (FMath::Abs(Z-CenterZ) > 36.0f) return BaseDensity;
constexpr float Pitch = 144.0f;
constexpr float RadiusBase = 28.0f;
constexpr float HalfHeight = 26.0f;
const int32 BaseCellX = FMath::FloorToInt(X/Pitch);
const int32 BaseCellY = FMath::FloorToInt(Y/Pitch);
float Best = -FLT_MAX;
bool bHasEdge = false;
const auto CellCenter = [&](int32 CX, int32 CY, float& OutX, float& OutY)
{
const uint32 H = EdgeHash(CX,CY,3,Seed);
OutX = (static_cast<float>(CX)+0.5f)*Pitch
+ (Unit(H^0xA511E9B3u)-0.5f)*Pitch*0.18f;
OutY = (static_cast<float>(CY)+0.5f)*Pitch
+ (Unit(H^0x63D83595u)-0.5f)*Pitch*0.18f;
};
const auto EvaluateEdge = [&](int32 CX, int32 CY, int32 Axis)
{
const uint32 H = EdgeHash(CX,CY,Axis,Seed);
if (Unit(H^0x68E31DA4u) > 0.56f) return;
bHasEdge = true;
const int32 NX = CX + (Axis == 0 ? 1 : 0);
const int32 NY = CY + (Axis == 1 ? 1 : 0);
float AX,AY,BX,BY;
CellCenter(CX,CY,AX,AY);
CellCenter(NX,NY,BX,BY);
const float ABX = BX-AX, ABY = BY-AY;
const float LengthSquared = ABX*ABX+ABY*ABY;
float T = LengthSquared > 1.0e-8f
? FMath::Clamp(((X-AX)*ABX+(Y-AY)*ABY)/LengthSquared,0.0f,1.0f)
: 0.0f;
const float Length = FMath::Sqrt(LengthSquared);
const float NXrm = Length > 1.0e-4f ? -ABY/Length : 0.0f;
const float NYrm = Length > 1.0e-4f ? ABX/Length : 0.0f;
const float BendA = (Unit(H^0xB5297A4Du)*2.0f-1.0f)*Pitch*0.16f;
const float BendB = (Unit(H^0x1B56C4E9u)*2.0f-1.0f)*Pitch*0.16f;
const float C1X=AX+ABX/3.0f+NXrm*BendA, C1Y=AY+ABY/3.0f+NYrm*BendA;
const float C2X=AX+ABX*(2.0f/3.0f)+NXrm*BendB;
const float C2Y=AY+ABY*(2.0f/3.0f)+NYrm*BendB;
for (int32 Iteration=0;Iteration<4;++Iteration)
{
const float U=T, O=1.0f-U;
const float PX=O*O*O*AX+3.0f*O*O*U*C1X+3.0f*O*U*U*C2X+U*U*U*BX;
const float PY=O*O*O*AY+3.0f*O*O*U*C1Y+3.0f*O*U*U*C2Y+U*U*U*BY;
const float DX=3.0f*O*O*(C1X-AX)+6.0f*O*U*(C2X-C1X)+3.0f*U*U*(BX-C2X);
const float DY=3.0f*O*O*(C1Y-AY)+6.0f*O*U*(C2Y-C1Y)+3.0f*U*U*(BY-C2Y);
const float DDX=6.0f*O*(C2X-2.0f*C1X+AX)+6.0f*U*(BX-2.0f*C2X+C1X);
const float DDY=6.0f*O*(C2Y-2.0f*C1Y+AY)+6.0f*U*(BY-2.0f*C2Y+C1Y);
const float Den=DX*DX+DY*DY+(PX-X)*DDX+(PY-Y)*DDY;
if (FMath::Abs(Den)>1.0e-4f)
T=FMath::Clamp(U-((PX-X)*DX+(PY-Y)*DY)/Den,0.0f,1.0f);
}
const float O=1.0f-T;
const float PX=O*O*O*AX+3.0f*O*O*T*C1X+3.0f*O*T*T*C2X+T*T*T*BX;
const float PY=O*O*O*AY+3.0f*O*O*T*C1Y+3.0f*O*T*T*C2Y+T*T*T*BY;
const float DX=X-PX,DY=Y-PY;
const float Distance=FMath::Sqrt(DX*DX+DY*DY);
const float Phase=Unit(H^0xA511E9B3u)*6.2831853071795864769f;
const float Radius=RadiusBase*(0.82f+0.36f*Unit(H^0x63D83595u))
*(1.0f+0.22f*FMath::Sin(6.2831853071795864769f*T+Phase));
const float VerticalWave=4.0f*FMath::Sin(3.14159265358979323846f*T)
*FMath::Sin(6.2831853071795864769f*T+Phase);
const float VerticalOffset=Z-(CenterZ+VerticalWave);
const float HorizontalDistance=FMath::Max(0.0f,Distance);
const float Normalized=FMath::Sqrt(
FMath::Square(HorizontalDistance/FMath::Max(Radius,1.0f))
+FMath::Square(VerticalOffset/FMath::Max(HalfHeight,1.0f)));
Best=FMath::Max(Best,(1.0f-Normalized)*FMath::Min(Radius,HalfHeight));
};
for (int32 CY=BaseCellY-1;CY<=BaseCellY+1;++CY)
for (int32 CX=BaseCellX-1;CX<=BaseCellX+1;++CX)
{
EvaluateEdge(CX,CY,0);
EvaluateEdge(CX,CY,1);
}
return bHasEdge ? FMath::Max(BaseDensity,Best) : BaseDensity;
}
}
+492
View File
@@ -0,0 +1,492 @@
#include "OwnerWorldLoader.h"
#include "WorldMesher.h"
#include "StandaloneResearchTunnels.h"
#ifdef TEXT
#undef TEXT
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <d3d11.h>
#include <d3dcompiler.h>
#include <dxgi.h>
#include <windowsx.h>
#include <bit>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <map>
#include <sstream>
#pragma comment(lib,"d3d11.lib")
#pragma comment(lib,"dxgi.lib")
#pragma comment(lib,"d3dcompiler.lib")
#pragma comment(lib,"user32.lib")
#pragma comment(lib,"gdi32.lib")
namespace
{
struct V3 { float X=0.0f,Y=0.0f,Z=0.0f; };
V3 Add(V3 A,V3 B){return {A.X+B.X,A.Y+B.Y,A.Z+B.Z};}
V3 Sub(V3 A,V3 B){return {A.X-B.X,A.Y-B.Y,A.Z-B.Z};}
V3 Mul(V3 A,float S){return {A.X*S,A.Y*S,A.Z*S};}
float Dot(V3 A,V3 B){return A.X*B.X+A.Y*B.Y+A.Z*B.Z;}
V3 Cross(V3 A,V3 B){return {A.Y*B.Z-A.Z*B.Y,A.Z*B.X-A.X*B.Z,A.X*B.Y-A.Y*B.X};}
V3 Normalize(V3 A){return Mul(A,1.0f/std::sqrt(std::max(1.0e-20f,Dot(A,A))));}
struct M4 { float M[4][4]{}; };
M4 Identity(){M4 R{};for(int I=0;I<4;++I)R.M[I][I]=1.0f;return R;}
M4 Transpose(M4 A){M4 B{};for(int I=0;I<4;++I)for(int J=0;J<4;++J)B.M[I][J]=A.M[J][I];return B;}
M4 Perspective(float Fov,float Aspect,float NearZ,float FarZ)
{
M4 R{};const float Y=1.0f/std::tan(Fov*0.5f);
R.M[0][0]=Y/Aspect;R.M[1][1]=Y;R.M[2][2]=FarZ/(FarZ-NearZ);
R.M[2][3]=1.0f;R.M[3][2]=-NearZ*FarZ/(FarZ-NearZ);return R;
}
M4 View(V3 Position,V3 Forward,V3 Right,V3 Up)
{
M4 R=Identity();
R.M[0][0]=Right.X;R.M[1][0]=Right.Y;R.M[2][0]=Right.Z;R.M[3][0]=-Dot(Right,Position);
R.M[0][1]=Up.X;R.M[1][1]=Up.Y;R.M[2][1]=Up.Z;R.M[3][1]=-Dot(Up,Position);
R.M[0][2]=Forward.X;R.M[1][2]=Forward.Y;R.M[2][2]=Forward.Z;R.M[3][2]=-Dot(Forward,Position);
return R;
}
struct FGpuVertex { float Position[3]; float Normal[3]; };
struct FConstants
{
M4 ViewProjection;
float Camera[3]; float FogStart;
float Forward[3]; float FogEnd;
float LightDirection[3]; float Ambient;
float Padding[4];
};
constexpr int32 MaxLevels=8;
struct FLevelGpu
{
ID3D11Buffer* VertexBuffer=nullptr;
ID3D11Buffer* IndexBuffer=nullptr;
UINT IndexCount=0;
uint64 VertexCount=0;
double Seconds=0.0;
uint64 Tiles=0;
uint64 Samples=0;
bool Ready=false;
};
struct FReadyLevel { int32 Level=0; FStandaloneCpuLevel Mesh; };
struct FApp
{
HWND Window=nullptr;
ID3D11Device* Device=nullptr;
ID3D11DeviceContext* Context=nullptr;
IDXGISwapChain* SwapChain=nullptr;
ID3D11RenderTargetView* RenderTarget=nullptr;
ID3D11DepthStencilView* DepthView=nullptr;
ID3D11VertexShader* VertexShader=nullptr;
ID3D11PixelShader* PixelShader=nullptr;
ID3D11InputLayout* InputLayout=nullptr;
ID3D11Buffer* Constants=nullptr;
ID3D11RasterizerState* SolidRasterizer=nullptr;
ID3D11RasterizerState* WireRasterizer=nullptr;
FLevelGpu Levels[MaxLevels+1];
std::unique_ptr<FStandaloneOwnerWorld> World;
std::string DataPath;
std::mutex ReadyMutex;
std::vector<FReadyLevel> ReadyLevels;
std::thread GenerationThread;
std::atomic<bool> GenerationDone{false};
std::atomic<bool> CancelGeneration{false};
FStandaloneCpuClipmap LastClipmap;
bool bGenerationRunning=false;
bool bRebuildPending=false;
bool bParityChecked=false;
bool bRoundTunnels=false;
bool bWireframe=false;
bool bCaptured=false;
bool bShowLevel[MaxLevels+1]={true,true,true,true,true,true,true,true,true};
bool Keys[256]{};
int32 Seed=0;
int32 Width=1280,Height=800;
float Yaw=0.0f,Pitch=0.0f,MoveSpeed=8.0f;
V3 Position{};
POINT LastMouse{};
std::chrono::steady_clock::time_point PreviousFrame=std::chrono::steady_clock::now();
double FrameMs=0.0;
uint64 TotalVertices=0;
uint64 TotalIndices=0;
FIntVector BuildTargetChunk=FIntVector::ZeroValue;
~FApp(){
StopGeneration(true);ReleaseMeshes();
if(SolidRasterizer)SolidRasterizer->Release();if(WireRasterizer)WireRasterizer->Release();
if(Constants)Constants->Release();if(InputLayout)InputLayout->Release();
if(VertexShader)VertexShader->Release();if(PixelShader)PixelShader->Release();
if(RenderTarget)RenderTarget->Release();if(DepthView)DepthView->Release();
if(SwapChain)SwapChain->Release();if(Context)Context->Release();if(Device)Device->Release();
}
void ReleaseMeshes()
{
for(FLevelGpu& L:Levels){if(L.VertexBuffer)L.VertexBuffer->Release();if(L.IndexBuffer)L.IndexBuffer->Release();L={};}
TotalVertices=0;TotalIndices=0;
}
void StopGeneration(bool bCancel=false)
{
if(bCancel)CancelGeneration.store(true,std::memory_order_relaxed);
if(GenerationThread.joinable())GenerationThread.join();
bGenerationRunning=false;
}
V3 Forward() const
{return Normalize({std::sin(Yaw)*std::cos(Pitch),std::cos(Yaw)*std::cos(Pitch),std::sin(Pitch)});}
FIntVector CameraChunk() const
{return StandaloneWorldMesher::CameraChunk(FVector(Position.X*4.0,Position.Y*4.0,Position.Z*4.0));}
bool NeedsRecenter(const FIntVector& Center) const
{
const FIntVector Current=CameraChunk();
const int32 Threshold=FMath::Max(1,(World?World->ClipRadius:6)/2);
return FMath::Abs(Current.X-Center.X)>=Threshold
||FMath::Abs(Current.Y-Center.Y)>=Threshold
||FMath::Abs(Current.Z-Center.Z)>=Threshold;
}
bool MakeBuffer(ID3D11Buffer** Out,const void* Data,size_t Bytes,UINT Bind)
{
if(Bytes==0||Bytes>std::numeric_limits<UINT>::max())return false;
D3D11_BUFFER_DESC Desc{};Desc.ByteWidth=static_cast<UINT>(Bytes);
Desc.Usage=D3D11_USAGE_DEFAULT;Desc.BindFlags=Bind;
D3D11_SUBRESOURCE_DATA Initial{};Initial.pSysMem=Data;
return SUCCEEDED(Device->CreateBuffer(&Desc,&Initial,Out));
}
bool Resize(UINT NewWidth,UINT NewHeight)
{
if(!Device||NewWidth==0||NewHeight==0)return false;
Width=static_cast<int32>(NewWidth);Height=static_cast<int32>(NewHeight);
if(RenderTarget){RenderTarget->Release();RenderTarget=nullptr;}
if(DepthView){DepthView->Release();DepthView=nullptr;}
if(SwapChain->ResizeBuffers(0,NewWidth,NewHeight,DXGI_FORMAT_UNKNOWN,0)!=S_OK)return false;
ID3D11Texture2D* Back=nullptr;
if(FAILED(SwapChain->GetBuffer(0,IID_PPV_ARGS(&Back))))return false;
const HRESULT RtvResult=Device->CreateRenderTargetView(Back,nullptr,&RenderTarget);Back->Release();
if(FAILED(RtvResult))return false;
D3D11_TEXTURE2D_DESC DepthDesc{};DepthDesc.Width=NewWidth;DepthDesc.Height=NewHeight;
DepthDesc.MipLevels=1;DepthDesc.ArraySize=1;DepthDesc.Format=DXGI_FORMAT_D24_UNORM_S8_UINT;
DepthDesc.SampleDesc.Count=1;DepthDesc.Usage=D3D11_USAGE_DEFAULT;DepthDesc.BindFlags=D3D11_BIND_DEPTH_STENCIL;
ID3D11Texture2D* Depth=nullptr;
if(FAILED(Device->CreateTexture2D(&DepthDesc,nullptr,&Depth)))return false;
const HRESULT DsvResult=Device->CreateDepthStencilView(Depth,nullptr,&DepthView);Depth->Release();
return SUCCEEDED(DsvResult);
}
bool InitializeDevice()
{
DXGI_SWAP_CHAIN_DESC Desc{};Desc.BufferCount=1;Desc.BufferDesc.Format=DXGI_FORMAT_R8G8B8A8_UNORM;
Desc.BufferUsage=DXGI_USAGE_RENDER_TARGET_OUTPUT;Desc.OutputWindow=Window;Desc.SampleDesc.Count=1;
Desc.Windowed=TRUE;Desc.SwapEffect=DXGI_SWAP_EFFECT_DISCARD;
D3D_FEATURE_LEVEL Feature{};
if(FAILED(D3D11CreateDeviceAndSwapChain(nullptr,D3D_DRIVER_TYPE_HARDWARE,nullptr,0,nullptr,0,
D3D11_SDK_VERSION,&Desc,&SwapChain,&Device,&Feature,&Context)))return false;
const char* ShaderSource=R"(
cbuffer C:register(b0){float4x4 VP;float3 Camera;float FogStart;float3 Forward;float FogEnd;float3 LightDir;float Ambient;float4 Pad;}
struct I{float3 p:POSITION;float3 n:NORMAL;};
struct O{float4 p:SV_POSITION;float3 world:TEXCOORD0;float3 n:NORMAL;};
O VS(I i){O o;o.p=mul(float4(i.p,1),VP);o.world=i.p;o.n=normalize(i.n);return o;}
float4 PS(O i):SV_TARGET{
float3 N=normalize(i.n);float3 V=normalize(Camera-i.world);float3 L=normalize(-LightDir);
float Diffuse=saturate(dot(N,L));float Head=saturate(dot(N,V));
float Dist=length(Camera-i.world);float Atten=1.0-saturate(Dist/95.0);Head=Head*Head*Atten;
float3 Rock=lerp(float3(0.28,0.34,0.38),float3(0.43,0.47,0.48),saturate(N.z*0.5+0.5));
float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.62,0.76,0.9)*Head*1.25;
float Fog=saturate((Dist-FogStart)/(FogEnd-FogStart));
Lit=lerp(Lit,float3(0.10,0.14,0.19),Fog);return float4(Lit,1);
})";
ID3DBlob* VsCode=nullptr;ID3DBlob* PsCode=nullptr;ID3DBlob* Errors=nullptr;
HRESULT Hr=D3DCompile(ShaderSource,std::strlen(ShaderSource),nullptr,nullptr,nullptr,"VS","vs_5_0",0,0,&VsCode,&Errors);
if(FAILED(Hr)){if(Errors)std::fprintf(stderr,"Vertex shader compile: %s\n",static_cast<const char*>(Errors->GetBufferPointer()));if(Errors)Errors->Release();return false;}
Hr=D3DCompile(ShaderSource,std::strlen(ShaderSource),nullptr,nullptr,nullptr,"PS","ps_5_0",0,0,&PsCode,&Errors);
if(FAILED(Hr)){if(Errors)std::fprintf(stderr,"Pixel shader compile: %s\n",static_cast<const char*>(Errors->GetBufferPointer()));if(Errors)Errors->Release();VsCode->Release();return false;}
Hr=Device->CreateVertexShader(VsCode->GetBufferPointer(),VsCode->GetBufferSize(),nullptr,&VertexShader);
if(SUCCEEDED(Hr))Hr=Device->CreatePixelShader(PsCode->GetBufferPointer(),PsCode->GetBufferSize(),nullptr,&PixelShader);
D3D11_INPUT_ELEMENT_DESC Elements[]={{"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0},
{"NORMAL",0,DXGI_FORMAT_R32G32B32_FLOAT,0,12,D3D11_INPUT_PER_VERTEX_DATA,0}};
if(SUCCEEDED(Hr))Hr=Device->CreateInputLayout(Elements,2,VsCode->GetBufferPointer(),VsCode->GetBufferSize(),&InputLayout);
VsCode->Release();PsCode->Release();if(FAILED(Hr))return false;
D3D11_BUFFER_DESC ConstantDesc{};ConstantDesc.ByteWidth=sizeof(FConstants);ConstantDesc.Usage=D3D11_USAGE_DEFAULT;
ConstantDesc.BindFlags=D3D11_BIND_CONSTANT_BUFFER;
if(FAILED(Device->CreateBuffer(&ConstantDesc,nullptr,&Constants)))return false;
D3D11_RASTERIZER_DESC Raster{};Raster.FillMode=D3D11_FILL_SOLID;Raster.CullMode=D3D11_CULL_NONE;
Raster.DepthClipEnable=TRUE;if(FAILED(Device->CreateRasterizerState(&Raster,&SolidRasterizer)))return false;
Raster.FillMode=D3D11_FILL_WIREFRAME;if(FAILED(Device->CreateRasterizerState(&Raster,&WireRasterizer)))return false;
return Resize(Width,Height);
}
void PrintParity()
{
if(bParityChecked||!World)return;
bParityChecked=true;size_t MismatchCount=0;
std::map<std::string,size_t> Kinds;
for(size_t I=0;I<World->DensitySamples.size();++I)
{
const FStandaloneParitySample& S=World->DensitySamples[I];
const float Actual=World->Generator->GetDensityAt(static_cast<float>(S.PositionVoxels.X),
static_cast<float>(S.PositionVoxels.Y),static_cast<float>(S.PositionVoxels.Z));
++Kinds[S.Kind];
if(std::bit_cast<uint32>(Actual)!=std::bit_cast<uint32>(S.PluginDensity))
{
++MismatchCount;
std::fprintf(stderr,"PARITY MISMATCH %s strate=%d voxels=(%.6f,%.6f,%.6f) plugin=%.9g standalone=%.9g\n",
S.Kind.c_str(),S.StrateIndex,S.PositionVoxels.X,S.PositionVoxels.Y,S.PositionVoxels.Z,S.PluginDensity,Actual);
}
}
std::printf("Density parity: %zu/%zu bit-identical; mismatches=%zu\n",
World->DensitySamples.size()-MismatchCount,World->DensitySamples.size(),MismatchCount);
for(const auto& [Kind,Count]:Kinds)std::printf(" %-24s %zu\n",Kind.c_str(),Count);
std::fflush(stdout);
std::fflush(stdout);
}
bool ConfigureWorld(std::string& Error)
{
World=std::make_unique<FStandaloneOwnerWorld>();
if(!StandaloneOwnerLoader::Load(DataPath,*World,Error))return false;
if(!bParityChecked&&Seed==World->Seed&&!bRoundTunnels)PrintParity();
World->Generator.reset();World->Manager.reset();
World->Settings->Seed=Seed;World->Seed=Seed;
if(bRoundTunnels)
{
// The research tunnel graph replaces the owner's first-strate corridor graph.
for(auto& Pair:World->Settings->FixedStrates)
{
UVoxelStrateDefinition* Definition=Pair.Value.Get();
if(Pair.Key==0&&Definition&&Definition->GeneratorType==ECaveGeneratorType::TunnelNetwork)
{
Definition->GenerationParams.TunnelDensity=0.0f;
Definition->GenerationParams.TunnelWarpStrength=0.0f;
}
}
FStandaloneResearchTunnels::Set(true,Position.Z*4.0f);
}
else FStandaloneResearchTunnels::Set(false,0.0f);
World->Manager=std::make_unique<UVoxelStrateManager>();
if(!World->Manager->Initialize(World->Settings.get(),Seed))
{Error="standalone copied strate manager failed while applying viewer seed";return false;}
World->Generator=std::make_unique<UVoxelGenerator>();
World->Generator->InitializeSettings(World->Settings.get());
World->Generator->SetStrateManager(World->Manager.get());
return true;
}
void StartGeneration()
{
StopGeneration();
ReleaseMeshes();
std::string Error;
if(!ConfigureWorld(Error))
{std::fprintf(stderr,"World load failed: %s\n",Error.c_str());MessageBoxA(Window,Error.c_str(),"VoxelForge viewer",MB_ICONERROR);return;}
const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
BuildTargetChunk=StandaloneWorldMesher::CameraChunk(CameraVoxels);
ReadyLevels.clear();CancelGeneration.store(false,std::memory_order_relaxed);
GenerationDone.store(false,std::memory_order_relaxed);bGenerationRunning=true;bRebuildPending=false;
std::printf("Building seed %d%s full clipmap (six workers)...\n",Seed,bRoundTunnels?" with research round/winding tunnels":"");
GenerationThread=std::thread([this,CameraVoxels]()
{
LastClipmap=StandaloneWorldMesher::Build(*World,CameraVoxels,6,
[this](int32 Level,FStandaloneCpuLevel&& Mesh)
{
std::lock_guard<std::mutex> Lock(ReadyMutex);
ReadyLevels.push_back({Level,std::move(Mesh)});
},false,&CancelGeneration);
GenerationDone.store(true,std::memory_order_release);
});
}
void RequestSeed(int32 Delta)
{
const uint32 Next=static_cast<uint32>(Seed)+static_cast<uint32>(Delta);
Seed=static_cast<int32>(Next);bRebuildPending=true;
CancelGeneration.store(true,std::memory_order_relaxed);
std::printf("Seed changed to %d; rebuilding after the active clipmap finishes.\n",Seed);
}
bool UploadLevel(int32 Index,FStandaloneCpuLevel&& Mesh)
{
FLevelGpu& L=Levels[Index];
if(L.VertexBuffer)L.VertexBuffer->Release();if(L.IndexBuffer)L.IndexBuffer->Release();L={};
L.Seconds=Mesh.Seconds;L.Tiles=Mesh.TileCount;L.Samples=Mesh.DensitySamples;
L.VertexCount=Mesh.VertexCount;L.IndexCount=static_cast<uint64>(Mesh.Indices.size());
if(Mesh.Vertices.empty()||Mesh.Indices.empty()){L.Ready=true;return true;}
if(Mesh.Indices.size()>std::numeric_limits<UINT>::max())
{std::fprintf(stderr,"LOD %d has too many indices for one D3D11 draw.\n",Index);return false;}
const bool bVertex=MakeBuffer(&L.VertexBuffer,Mesh.Vertices.data(),Mesh.Vertices.size()*sizeof(FStandaloneCpuVertex),D3D11_BIND_VERTEX_BUFFER);
const bool bIndex=bVertex&&MakeBuffer(&L.IndexBuffer,Mesh.Indices.data(),Mesh.Indices.size()*sizeof(uint32),D3D11_BIND_INDEX_BUFFER);
if(!bIndex){if(L.VertexBuffer){L.VertexBuffer->Release();L.VertexBuffer=nullptr;}std::fprintf(stderr,"D3D11 buffer upload failed for LOD %d.\n",Index);return false;}
L.IndexCount=static_cast<UINT>(Mesh.Indices.size());L.Ready=true;
TotalVertices+=L.VertexCount;TotalIndices+=L.IndexCount;
std::printf("LOD %d uploaded: %llu vertices, %.3f s, %llu tiles.\n",Index,
static_cast<unsigned long long>(L.VertexCount),L.Seconds,static_cast<unsigned long long>(L.Tiles));
return true;
}
void PumpGeneration()
{
std::vector<FReadyLevel> Ready;
{std::lock_guard<std::mutex> Lock(ReadyMutex);Ready.swap(ReadyLevels);}
for(FReadyLevel& R:Ready)UploadLevel(R.Level,std::move(R.Mesh));
if(bGenerationRunning&&GenerationDone.load(std::memory_order_acquire))
{
StopGeneration();
if(!LastClipmap.bCancelled)
std::printf("Full clipmap ready: %.3f s on %d workers; %llu vertices, %llu indices.\n",
LastClipmap.TotalSeconds,LastClipmap.WorkerCount,
static_cast<unsigned long long>(TotalVertices),static_cast<unsigned long long>(TotalIndices));
for(int32 L=0;L<=World->MaxClipLevel;++L)
std::printf(" LOD %d: %.3f s, %llu tiles, %llu vertices, %.2f M density samples\n",L,
LastClipmap.Levels[L].Seconds,static_cast<unsigned long long>(LastClipmap.Levels[L].TileCount),
static_cast<unsigned long long>(LastClipmap.Levels[L].VertexCount),
static_cast<double>(LastClipmap.Levels[L].DensitySamples)/1.0e6);
std::fflush(stdout);
if(bRebuildPending)StartGeneration();
}
else if(!bGenerationRunning&&bRebuildPending)StartGeneration();
}
void Capture(bool bOn)
{
bCaptured=bOn;
if(bOn){SetCapture(Window);ShowCursor(FALSE);GetCursorPos(&LastMouse);POINT C{Width/2,Height/2};ClientToScreen(Window,&C);SetCursorPos(C.x,C.y);}
else{ReleaseCapture();ShowCursor(TRUE);}
}
void UpdateTitle()
{
uint64 VisibleIndices=0;for(int32 I=0;I<=MaxLevels;++I)if(bShowLevel[I])VisibleIndices+=Levels[I].IndexCount;
std::ostringstream S;S<<"VoxelForge standalone | seed "<<Seed<<" | "<<(bRoundTunnels?"round/winding research tunnels":"base world")
<<" | "<<VisibleIndices/3<<" tris | "<<FrameMs<<" ms | "<<(bGenerationRunning?"generating":"ready")
<<" | pos "<<std::fixed<<std::setprecision(1)<<Position.X<<", "<<Position.Y<<", "<<Position.Z<<" m";
SetWindowTextA(Window,S.str().c_str());
}
void Draw()
{
PumpGeneration();
const auto Now=std::chrono::steady_clock::now();
const float Dt=std::clamp(std::chrono::duration<float>(Now-PreviousFrame).count(),0.0f,0.1f);
PreviousFrame=Now;FrameMs=Dt*1000.0;
const V3 F=Forward(),R=Normalize(Cross(F,{0,0,1})),U=Normalize(Cross(R,F));
const float Step=MoveSpeed*Dt*(Keys[VK_SHIFT]?4.0f:1.0f);
if(Keys['W'])Position=Add(Position,Mul(F,Step));if(Keys['S'])Position=Sub(Position,Mul(F,Step));
if(Keys['A'])Position=Sub(Position,Mul(R,Step));if(Keys['D'])Position=Add(Position,Mul(R,Step));
if(Keys[VK_SPACE]||Keys['E'])Position.Z+=Step;if(Keys[VK_CONTROL]||Keys['Q'])Position.Z-=Step;
if(bGenerationRunning&&NeedsRecenter(BuildTargetChunk))
{bRebuildPending=true;CancelGeneration.store(true,std::memory_order_relaxed);}
else if(!bGenerationRunning&&World&&NeedsRecenter(LastClipmap.CenterChunk))
{bRebuildPending=true;}
const float Clear[4]={0.055f,0.075f,0.105f,1.0f};
Context->OMSetRenderTargets(1,&RenderTarget,DepthView);Context->ClearRenderTargetView(RenderTarget,Clear);
Context->ClearDepthStencilView(DepthView,D3D11_CLEAR_DEPTH,1.0f,0);
D3D11_VIEWPORT Viewport{0,0,static_cast<float>(Width),static_cast<float>(Height),0,1};Context->RSSetViewports(1,&Viewport);
Context->RSSetState(bWireframe?WireRasterizer:SolidRasterizer);Context->IASetInputLayout(InputLayout);
Context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
FConstants C{};const M4 V=View(Position,F,R,U);const M4 P=Perspective(1.04719755f,static_cast<float>(Width)/std::max(1,Height),0.05f,5000.0f);
M4 Combined{};for(int I=0;I<4;++I)for(int J=0;J<4;++J)for(int K=0;K<4;++K)Combined.M[I][J]+=V.M[I][K]*P.M[K][J];
C.ViewProjection=Transpose(Combined);C.Camera[0]=Position.X;C.Camera[1]=Position.Y;C.Camera[2]=Position.Z;
C.Forward[0]=F.X;C.Forward[1]=F.Y;C.Forward[2]=F.Z;C.LightDirection[0]=-0.5f;C.LightDirection[1]=-0.72f;C.LightDirection[2]=-0.48f;
C.FogStart=260.0f;C.FogEnd=2600.0f;C.Ambient=0.36f;
Context->UpdateSubresource(Constants,0,nullptr,&C,0,0);Context->VSSetConstantBuffers(0,1,&Constants);Context->PSSetConstantBuffers(0,1,&Constants);
Context->VSSetShader(VertexShader,nullptr,0);Context->PSSetShader(PixelShader,nullptr,0);
for(int32 I=0;I<=MaxLevels;++I)
{
const FLevelGpu& L=Levels[I];if(!bShowLevel[I]||!L.VertexBuffer||!L.IndexBuffer||!L.IndexCount)continue;
const UINT Stride=sizeof(FGpuVertex),Offset=0;Context->IASetVertexBuffers(0,1,&L.VertexBuffer,&Stride,&Offset);
Context->IASetIndexBuffer(L.IndexBuffer,DXGI_FORMAT_R32_UINT,0);Context->DrawIndexed(L.IndexCount,0,0);
}
SwapChain->Present(1,0);UpdateTitle();
}
};
FApp* GApp=nullptr;
LRESULT CALLBACK WindowProc(HWND H,UINT Message,WPARAM W,LPARAM L)
{
if(!GApp)return DefWindowProc(H,Message,W,L);
switch(Message)
{
case WM_SIZE:if(W!=SIZE_MINIMIZED)GApp->Resize(LOWORD(L),HIWORD(L));return 0;
case WM_KEYDOWN:
{
const bool bWasDown=(L&(1ll<<30))!=0;if(W<256)GApp->Keys[W]=true;
if(W==VK_ESCAPE){if(GApp->bCaptured)GApp->Capture(false);else DestroyWindow(H);}
if(!bWasDown&&W=='F')GApp->bWireframe=!GApp->bWireframe;
if(!bWasDown&&W=='T'){GApp->bRoundTunnels=!GApp->bRoundTunnels;GApp->bRebuildPending=true;GApp->CancelGeneration.store(true,std::memory_order_relaxed);}
if(!bWasDown&&(W=='N'||W=='P'))GApp->RequestSeed(W=='N'?1:-1);
if(!bWasDown&&W>='1'&&W<='8'){const int32 I=static_cast<int32>(W-'1');GApp->bShowLevel[I]=!GApp->bShowLevel[I];}
if(!bWasDown&&W=='9')GApp->bShowLevel[8]=!GApp->bShowLevel[8];
if(!bWasDown&&W=='0')for(bool& B:GApp->bShowLevel)B=!B;
return 0;
}
case WM_KEYUP:if(W<256)GApp->Keys[W]=false;return 0;
case WM_RBUTTONDOWN:GApp->Capture(true);return 0;
case WM_RBUTTONUP:GApp->Capture(false);return 0;
case WM_MOUSEMOVE:
if(GApp->bCaptured){POINT C{GApp->Width/2,GApp->Height/2};ClientToScreen(H,&C);POINT P{GET_X_LPARAM(L),GET_Y_LPARAM(L)},Screen=P;ClientToScreen(H,&Screen);const int32 DX=Screen.x-C.x,DY=Screen.y-C.y;if(DX||DY){GApp->Yaw+=DX*0.0035f;GApp->Pitch=std::clamp(GApp->Pitch-DY*0.0035f,-1.55f,1.55f);SetCursorPos(C.x,C.y);}}return 0;
case WM_MOUSEWHEEL:GApp->MoveSpeed=std::clamp(GApp->MoveSpeed*(GET_WHEEL_DELTA_WPARAM(W)>0?1.25f:0.8f),0.25f,1000.0f);return 0;
case WM_DESTROY:GApp->CancelGeneration.store(true,std::memory_order_relaxed);PostQuitMessage(0);return 0;
}
return DefWindowProc(H,Message,W,L);
}
std::filesystem::path DefaultDataPath()
{
wchar_t Module[MAX_PATH]{};GetModuleFileNameW(nullptr,Module,MAX_PATH);
const std::filesystem::path Exe(Module);
return Exe.parent_path().parent_path()/L"data"/L"owner_world.json";
}
}
int main(int Argc,char** Argv)
{
FApp App;App.DataPath=Argc>1?Argv[1]:DefaultDataPath().string();
std::string Error;
App.World=std::make_unique<FStandaloneOwnerWorld>();
if(!StandaloneOwnerLoader::Load(App.DataPath,*App.World,Error)){std::fprintf(stderr,"Owner data load failed: %s\n",Error.c_str());return 2;}
App.Seed=App.World->Seed;
App.Position={static_cast<float>(App.World->ViewerStartVoxels.X*0.25),
static_cast<float>(App.World->ViewerStartVoxels.Y*0.25),static_cast<float>(App.World->ViewerStartVoxels.Z*0.25)};
App.World.reset();
WNDCLASSA Class{};Class.lpfnWndProc=WindowProc;Class.hInstance=GetModuleHandleA(nullptr);Class.lpszClassName="VoxelForgeStandaloneWorld";
Class.hCursor=LoadCursor(nullptr,IDC_ARROW);RegisterClassA(&Class);
RECT Rect{0,0,App.Width,App.Height};AdjustWindowRect(&Rect,WS_OVERLAPPEDWINDOW,FALSE);
App.Window=CreateWindowA(Class.lpszClassName,"VoxelForge standalone world",WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,
Rect.right-Rect.left,Rect.bottom-Rect.top,nullptr,nullptr,Class.hInstance,nullptr);
GApp=&App;
if(!App.Window||!App.InitializeDevice()){MessageBoxA(nullptr,"Direct3D 11 initialization failed.","VoxelForge standalone",MB_ICONERROR);return 3;}
ShowWindow(App.Window,SW_SHOW);UpdateWindow(App.Window);
std::printf("Controls: hold right mouse to look; WASD move; Space/Ctrl (E/Q) up/down; Shift faster; wheel changes speed; F wireframe; 1-9 toggle LOD; 0 toggle all levels; N/P change seed; T swap to round/winding research tunnels; Esc quit.\n");
std::printf("Owner data: %s; starting at first-strate open-space sample (%.2f, %.2f, %.2f) m.\n",
App.DataPath.c_str(),App.Position.X,App.Position.Y,App.Position.Z);
std::fflush(stdout);
App.StartGeneration();
MSG Message{};
while(Message.message!=WM_QUIT)
{
if(PeekMessage(&Message,nullptr,0,0,PM_REMOVE)){TranslateMessage(&Message);DispatchMessage(&Message);}
else App.Draw();
}
GApp=nullptr;
return 0;
}
@@ -0,0 +1,5 @@
// VoxelBiomeDefinition.cpp
// All data lives in the header; this TU exists so UHT generates the asset's body and
// the module has an object file for it.
#include "VoxelBiomeDefinition.h"
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,152 @@
#pragma once
#include "VoxelTypes.h"
namespace VoxelClipmapDesiredTiles
{
struct FParameters
{
int32 ClipRadius = 3;
int32 MaxClipLevel = 4;
int32 RenderDistanceChunks = 0;
};
FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor)
{
return Value >= 0 ? (Value / Divisor) : -(((-Value) + Divisor - 1) / Divisor);
}
FORCEINLINE FIntVector FloorDiv(const FIntVector& Value, int32 Divisor)
{
return FIntVector(FloorDiv(Value.X, Divisor), FloorDiv(Value.Y, Divisor),
FloorDiv(Value.Z, Divisor));
}
FORCEINLINE int32 CeilDiv(int32 Value, int32 Divisor)
{
return -FloorDiv(-Value, Divisor);
}
FORCEINLINE void AlignedRingBounds(const FIntVector& Center, int32 Radius, int32 Alignment,
FIntVector& OutMin, FIntVector& OutMax)
{
const auto AlignAxis = [Radius, Alignment](int32 CenterAxis,
int32& OutAxisMin, int32& OutAxisMax)
{
const int32 BaseMin = CenterAxis - Radius;
const int32 BaseEnd = CenterAxis + Radius + 1;
OutAxisMin = FloorDiv(BaseMin, Alignment) * Alignment;
OutAxisMax = CeilDiv(BaseEnd, Alignment) * Alignment - 1;
};
AlignAxis(Center.X, OutMin.X, OutMax.X);
AlignAxis(Center.Y, OutMin.Y, OutMax.Y);
AlignAxis(Center.Z, OutMin.Z, OutMax.Z);
}
FORCEINLINE int32 OuterShellRadius(const FParameters& Parameters, int32 Radius, int32 MaxLevel)
{
if (Parameters.RenderDistanceChunks <= 0)
{
return Radius;
}
return FMath::Max(Radius,
(Parameters.RenderDistanceChunks + (1 << MaxLevel) - 1) >> MaxLevel);
}
FORCEINLINE void OuterShell(const FParameters& Parameters, int32 Radius, int32 MaxLevel,
int32& OutLevel, int32& OutRadius)
{
OutLevel = MaxLevel;
OutRadius = OuterShellRadius(Parameters, Radius, MaxLevel);
}
FORCEINLINE void ResolveVerticalBounds(
int32 CenterChunkZ, int32 ViewDistanceUp, int32 ViewDistanceDown,
int32 StrateViewMarginChunks, bool bClampToStrate, bool bHasStrate,
int32 StrateTopChunkZ, int32 StrateBottomChunkZ,
int32& OutMinChunkZ, int32& OutMaxChunkZ)
{
OutMinChunkZ = MIN_int32;
OutMaxChunkZ = MAX_int32;
if (!bClampToStrate)
{
return;
}
OutMinChunkZ = CenterChunkZ - ViewDistanceDown;
OutMaxChunkZ = CenterChunkZ + ViewDistanceUp;
if (bHasStrate)
{
OutMinChunkZ = StrateBottomChunkZ - StrateViewMarginChunks;
OutMaxChunkZ = StrateTopChunkZ + StrateViewMarginChunks;
}
}
FORCEINLINE void Build(const FIntVector& Center, int32 MinChunkZ, int32 MaxChunkZ,
const FParameters& Parameters, TArray<FVoxelTileKey>& OutTiles)
{
OutTiles.Reset();
const int32 Radius = FMath::Max(1, Parameters.ClipRadius);
const int32 MaxLevel = FMath::Clamp(Parameters.MaxClipLevel, 0, 8);
const int32 OuterRadius = OuterShellRadius(Parameters, Radius, MaxLevel);
FIntVector PreviousRingMin = FIntVector::ZeroValue;
FIntVector PreviousRingMax = FIntVector::ZeroValue;
for (int32 Level = 0; Level <= MaxLevel; ++Level)
{
const int32 Pow = 1 << Level;
const FIntVector CenterLevel = FloorDiv(Center, Pow);
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
// Every finer shell must end on a complete child-pair boundary so a coarser tile is
// either wholly inside it or wholly outside it. The outermost ring has no coarser
// neighbour and needs no alignment.
const int32 RingAlignment = Level < MaxLevel ? 2 : 1;
FIntVector RingMin, RingMax;
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
int32 DeltaZMin = RingMin.Z - CenterLevel.Z;
int32 DeltaZMax = RingMax.Z - CenterLevel.Z;
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
{
DeltaZMin = FMath::Max(DeltaZMin, FloorDiv(MinChunkZ, Pow) - CenterLevel.Z);
DeltaZMax = FMath::Min(DeltaZMax, FloorDiv(MaxChunkZ, Pow) - CenterLevel.Z);
}
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
for (int32 DeltaY = RingMin.Y - CenterLevel.Y;
DeltaY <= RingMax.Y - CenterLevel.Y; ++DeltaY)
for (int32 DeltaX = RingMin.X - CenterLevel.X;
DeltaX <= RingMax.X - CenterLevel.X; ++DeltaX)
{
const FIntVector Tile = CenterLevel + FIntVector(DeltaX, DeltaY, DeltaZ);
const int32 TileMinZ = Tile.Z << Level;
const int32 TileMaxZ = ((Tile.Z + 1) << Level) - 1;
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
{
continue;
}
if (Level > 0)
{
const bool bCovered =
(2 * Tile.X >= PreviousRingMin.X)
&& (2 * Tile.X + 1 <= PreviousRingMax.X)
&& (2 * Tile.Y >= PreviousRingMin.Y)
&& (2 * Tile.Y + 1 <= PreviousRingMax.Y)
&& (2 * Tile.Z >= PreviousRingMin.Z)
&& (2 * Tile.Z + 1 <= PreviousRingMax.Z);
if (bCovered)
{
continue;
}
}
OutTiles.Emplace(Tile, Level);
}
PreviousRingMin = RingMin;
PreviousRingMax = RingMax;
}
}
}
@@ -0,0 +1,131 @@
// VoxelDensityAblation.cpp
// Development-only TunnelNetwork ablation controls.
#include "VoxelDensityAblation.h"
#include "HAL/IConsoleManager.h"
#include "Misc/CommandLine.h"
#include "Misc/Parse.h"
#include <atomic>
namespace
{
constexpr uint32 UnresolvedMask = 0xFFFFFFFFu;
std::atomic<uint32> GResolvedMask(UnresolvedMask);
#if !UE_BUILD_SHIPPING
int32 GCaveWarp = 0;
int32 GDetailOps = 0;
int32 GRoomSDF = 0;
int32 GTunnelSDF = 0;
int32 GTunnelCore = 0;
int32 GPassageCarving = 0;
int32 GPassageStructuralPosts = 0;
int32 GNativeFloor = 0;
int32 GDisturbances = 0;
int32 GOriginSpine = 0;
int32 GBoundarySeal = 0;
int32 GLandingPosts = 0;
int32 GXYEdgeSeal = 0;
int32 GPitChimneySDF = 0;
#define VF_ABLATION_CVAR(Name, Storage) \
FAutoConsoleVariableRef CVar##Storage( \
Name, Storage, \
TEXT("WORLD-CHANGING DEVELOPMENT-ONLY measurement override. Forces this " \
"TunnelNetwork density stage off; never use as a gameplay switch. " \
"All peers and regenerated worlds must use the same value. Resolved once " \
"per process. 0=on, 1=off."), ECVF_Default)
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateCaveWarp"), GCaveWarp);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateDetailOps"), GDetailOps);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateRoomSDF"), GRoomSDF);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateTunnelSDF"), GTunnelSDF);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateTunnelCore"), GTunnelCore);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblatePassageCarving"), GPassageCarving);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblatePassageStructuralPosts"), GPassageStructuralPosts);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateNativeFloor"), GNativeFloor);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateDisturbances"), GDisturbances);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateOriginSpine"), GOriginSpine);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateBoundarySeal"), GBoundarySeal);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateLandingPosts"), GLandingPosts);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateXYEdgeSeal"), GXYEdgeSeal);
VF_ABLATION_CVAR(TEXT("voxel.TunnelAblatePitChimneySDF"), GPitChimneySDF);
#undef VF_ABLATION_CVAR
uint32 ResolveValue(const TCHAR* Name, int32& Storage)
{
int32 Value = Storage;
FParse::Value(FCommandLine::Get(), Name, Value);
Storage = Value != 0 ? 1 : 0;
return Storage != 0 ? 1u : 0u;
}
uint32 ResolveMask()
{
uint32 Mask = 0;
Mask |= ResolveValue(TEXT("voxel.TunnelAblateCaveWarp="), GCaveWarp)
* static_cast<uint32>(VoxelDensityAblation::EStage::CaveWarp);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateDetailOps="), GDetailOps)
* static_cast<uint32>(VoxelDensityAblation::EStage::DetailOps);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateRoomSDF="), GRoomSDF)
* static_cast<uint32>(VoxelDensityAblation::EStage::RoomSDF);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateTunnelSDF="), GTunnelSDF)
* static_cast<uint32>(VoxelDensityAblation::EStage::TunnelSDF);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateTunnelCore="), GTunnelCore)
* static_cast<uint32>(VoxelDensityAblation::EStage::TunnelCore);
Mask |= ResolveValue(TEXT("voxel.TunnelAblatePassageCarving="), GPassageCarving)
* static_cast<uint32>(VoxelDensityAblation::EStage::PassageCarving);
Mask |= ResolveValue(
TEXT("voxel.TunnelAblatePassageStructuralPosts="), GPassageStructuralPosts)
* static_cast<uint32>(VoxelDensityAblation::EStage::PassageStructuralPosts);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateNativeFloor="), GNativeFloor)
* static_cast<uint32>(VoxelDensityAblation::EStage::NativeFloor);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateDisturbances="), GDisturbances)
* static_cast<uint32>(VoxelDensityAblation::EStage::Disturbances);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateOriginSpine="), GOriginSpine)
* static_cast<uint32>(VoxelDensityAblation::EStage::OriginSpine);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateBoundarySeal="), GBoundarySeal)
* static_cast<uint32>(VoxelDensityAblation::EStage::BoundarySeal);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateLandingPosts="), GLandingPosts)
* static_cast<uint32>(VoxelDensityAblation::EStage::LandingPosts);
Mask |= ResolveValue(TEXT("voxel.TunnelAblateXYEdgeSeal="), GXYEdgeSeal)
* static_cast<uint32>(VoxelDensityAblation::EStage::XYEdgeSeal);
Mask |= ResolveValue(TEXT("voxel.TunnelAblatePitChimneySDF="), GPitChimneySDF)
* static_cast<uint32>(VoxelDensityAblation::EStage::PitChimneySDF);
return Mask;
}
#endif
}
namespace VoxelDensityAblation
{
uint32 GetResolvedMask()
{
uint32 Existing = GResolvedMask.load(std::memory_order_acquire);
if (Existing != UnresolvedMask)
{
return Existing;
}
#if UE_BUILD_SHIPPING
constexpr uint32 Resolved = 0;
#else
const uint32 Resolved = ResolveMask();
#endif
uint32 Expected = UnresolvedMask;
if (!GResolvedMask.compare_exchange_strong(
Expected, Resolved, std::memory_order_release, std::memory_order_acquire))
{
return Expected;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeTunnelAblation] resolved mask=0x%08x "
"(world-changing development measurement overrides; 0=all on)"),
Resolved);
return Resolved;
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,994 @@
#include "VoxelDensityProfile.h"
#include "Misc/Crc.h"
namespace VoxelDensityProfile
{
std::atomic<bool> GEnabled(false);
std::atomic<EMode> GMode(EMode::Disabled);
namespace
{
std::atomic<uint32> GSampleInterval(DefaultSampleInterval);
std::atomic<uint64> GTimerPairCycles(0);
// The old implementation performed a contended atomic fetch_add for every timer and
// counter. A TunnelNetwork sample can close twenty-plus ranges and add several
// candidate counters, so profiling changed the workload it was supposed to measure.
// Keep one accumulator per worker and only aggregate at Snapshot(). Commandlet resets
// happen between completed jobs, so the deliberately light-weight list is sufficient.
struct FThreadState
{
uint64 Calls[BucketCount]{};
uint64 Cycles[BucketCount]{};
uint64 Samples[BucketCount]{};
uint64 Counters[CounterCount]{};
uint32 SampleSequence[BucketCount]{};
uint32 BlockCallCount[BucketCount]{};
uint64 BlockStartCycles[BucketCount]{};
bool BlockActive[BucketCount]{};
FSnapshot::FClassifierStats Classifier;
uint64 TunnelCacheCapacityEntries = 0;
uint64 TunnelCacheValidEntries = 0;
uint64 TunnelCacheStaticBytes = 0;
uint64 TunnelCacheDynamicBytes = 0;
uint64 TunnelCacheEntryBytes = 0;
uint64 TunnelCacheLargestEntryBytes = 0;
uint64 TunnelCacheLargestWorkerValidEntries = 0;
FCacheMemoryBreakdown TunnelCacheBreakdown;
uint64 RoomGraphCacheCapacityEntries = 0;
uint64 RoomGraphCacheValidEntries = 0;
uint64 RoomGraphCacheStaticBytes = 0;
uint64 RoomGraphCacheDynamicBytes = 0;
uint64 RoomGraphCacheEntryBytes = 0;
uint64 RoomGraphCacheLargestEntryBytes = 0;
uint64 RoomGraphCacheLargestWorkerValidEntries = 0;
FCacheMemoryBreakdown RoomGraphCacheBreakdown;
std::atomic<FThreadState*> Next{nullptr};
};
std::atomic<FThreadState*> GThreadStates{nullptr};
thread_local FThreadState* GThreadState = nullptr;
thread_local uint32 GScopeDepth = 0;
thread_local bool GScopeSampled = false;
FThreadState& GetThreadState()
{
if (GThreadState == nullptr)
{
GThreadState = new FThreadState();
FThreadState* Head = GThreadStates.load(std::memory_order_relaxed);
do
{
GThreadState->Next.store(Head, std::memory_order_relaxed);
}
while (!GThreadStates.compare_exchange_weak(
Head, GThreadState,
std::memory_order_release,
std::memory_order_relaxed));
}
return *GThreadState;
}
int32 ToIndex(EBucket Bucket)
{
const int32 Index = static_cast<int32>(Bucket);
return (Index >= 0 && Index < BucketCount)
? Index
: static_cast<int32>(EBucket::OtherOp);
}
bool IsSampledScopeEnabled(EBucket Bucket)
{
// Sampled mode pays for the trustworthy parent and mesh containers only. Attribution
// additionally exposes the component scopes, but still samples them; Full remains the
// deliberately invasive mode for per-call scope timing.
switch (Bucket)
{
case EBucket::GetDensityAt:
case EBucket::MesherGenerateMesh:
case EBucket::MesherDensityGrid:
case EBucket::MesherOther:
return true;
default:
return false;
}
}
bool IsAttributionScope(EBucket Bucket)
{
switch (Bucket)
{
case EBucket::GetDensityAt:
case EBucket::ApplyDisturbances:
case EBucket::PassageModifier:
case EBucket::PassageLandingAir:
case EBucket::PassageLandingFloor:
case EBucket::PassageLandingRoomFloor:
case EBucket::PassageTunnelAir:
case EBucket::PassageStructuralPosts:
case EBucket::TunnelCoreSupport:
case EBucket::TunnelCoreSdf:
case EBucket::TunnelCorePosts:
case EBucket::StructuralTail:
case EBucket::RoomGraphSource:
case EBucket::SdfCarve:
case EBucket::FusedEvaluator:
case EBucket::InterpretedOpStack:
case EBucket::OperatorBlock:
case EBucket::RoomGraphBuild:
case EBucket::RoomGraphSdf:
case EBucket::TunnelCoreWorld:
case EBucket::FusedDetail:
case EBucket::FusedWorm:
case EBucket::FusedStructural:
case EBucket::ClassifierTotal:
case EBucket::ClassifierIntervalProof:
case EBucket::ClassifierExactCore:
case EBucket::ClassifierExactFinal:
case EBucket::ClassifierRoomTail:
case EBucket::RuntimeStreamBuilding:
case EBucket::DensityPrologue:
case EBucket::DensityCore:
case EBucket::DensityDisturbances:
case EBucket::DensityStructuralPosts:
case EBucket::DensityBoundarySeal:
case EBucket::DensityDiffLayer:
case EBucket::DensityTail:
case EBucket::MesherGenerateMesh:
case EBucket::MesherDensityGrid:
case EBucket::MesherCellClassification:
case EBucket::MesherGradientNormals:
case EBucket::MesherVertexInterpolation:
case EBucket::MesherStreamBuilding:
case EBucket::MesherOther:
return true;
default:
return false;
}
}
bool IsAttributionAlwaysTimed(EBucket Bucket)
{
switch (Bucket)
{
case EBucket::MesherGenerateMesh:
case EBucket::MesherDensityGrid:
case EBucket::MesherOther:
case EBucket::RoomGraphBuild:
return true;
default:
return false;
}
}
uint64 ScaleSampledValue(uint64 Value, uint64 Calls, uint64 Samples)
{
if (Samples == 0 || Calls <= Samples)
{
return Value;
}
const double Scaled = static_cast<double>(Value)
* static_cast<double>(Calls) / static_cast<double>(Samples);
return Scaled > static_cast<double>(MAX_uint64)
? MAX_uint64
: static_cast<uint64>(Scaled + 0.5);
}
void AddClassifierStatsScaled(
FSnapshot::FClassifierStats& Destination,
const FSnapshot::FClassifierStats& Source,
uint64 Calls,
uint64 Samples)
{
Destination.RefineNodes += ScaleSampledValue(Source.RefineNodes, Calls, Samples);
Destination.StackBoxCalls += ScaleSampledValue(Source.StackBoxCalls, Calls, Samples);
Destination.WholeMixedNodes += ScaleSampledValue(Source.WholeMixedNodes, Calls, Samples);
Destination.WholeSolidNodes += ScaleSampledValue(Source.WholeSolidNodes, Calls, Samples);
Destination.WholeAirNodes += ScaleSampledValue(Source.WholeAirNodes, Calls, Samples);
Destination.NeedsFinalFieldNodes += ScaleSampledValue(Source.NeedsFinalFieldNodes, Calls, Samples);
Destination.SplitNodes += ScaleSampledValue(Source.SplitNodes, Calls, Samples);
Destination.MaxRefinementDepth = FMath::Max(
Destination.MaxRefinementDepth, Source.MaxRefinementDepth);
Destination.ExactCoreSamples += ScaleSampledValue(Source.ExactCoreSamples, Calls, Samples);
Destination.ExactFinalSamples += ScaleSampledValue(Source.ExactFinalSamples, Calls, Samples);
Destination.ExactCoreCacheHits += ScaleSampledValue(Source.ExactCoreCacheHits, Calls, Samples);
Destination.ExactFinalCacheHits += ScaleSampledValue(Source.ExactFinalCacheHits, Calls, Samples);
Destination.ExactCoreLeaves += ScaleSampledValue(Source.ExactCoreLeaves, Calls, Samples);
Destination.ExactFinalLeaves += ScaleSampledValue(Source.ExactFinalLeaves, Calls, Samples);
Destination.StackBoxCycles += ScaleSampledValue(Source.StackBoxCycles, Calls, Samples);
Destination.ExactCoreCycles += ScaleSampledValue(Source.ExactCoreCycles, Calls, Samples);
Destination.ExactFinalCycles += ScaleSampledValue(Source.ExactFinalCycles, Calls, Samples);
Destination.RoomTailQueries += ScaleSampledValue(Source.RoomTailQueries, Calls, Samples);
Destination.RoomTailEvaluated += ScaleSampledValue(Source.RoomTailEvaluated, Calls, Samples);
Destination.RoomTailCycles += ScaleSampledValue(Source.RoomTailCycles, Calls, Samples);
Destination.RoomPropagateCycles += ScaleSampledValue(Source.RoomPropagateCycles, Calls, Samples);
Destination.RoomExactPrimitiveCycles += ScaleSampledValue(Source.RoomExactPrimitiveCycles, Calls, Samples);
Destination.RoomCacheWindowCycles += ScaleSampledValue(Source.RoomCacheWindowCycles, Calls, Samples);
// These are topology descriptors, not per-call work counts. Keep the largest observed
// descriptor instead of multiplying one sampled graph description by the classifier
// call expansion factor.
Destination.RoomNumRooms = FMath::Max(Destination.RoomNumRooms, Source.RoomNumRooms);
Destination.RoomNumTunnels = FMath::Max(Destination.RoomNumTunnels, Source.RoomNumTunnels);
Destination.RoomNumRoomFloorJoins = FMath::Max(
Destination.RoomNumRoomFloorJoins, Source.RoomNumRoomFloorJoins);
Destination.RoomNumPits = FMath::Max(Destination.RoomNumPits, Source.RoomNumPits);
Destination.RoomNumChimneys = FMath::Max(Destination.RoomNumChimneys, Source.RoomNumChimneys);
}
}
void SetMode(EMode Mode, uint32 SampleInterval)
{
if (SampleInterval == 0)
{
SampleInterval = DefaultSampleInterval;
}
GSampleInterval.store(SampleInterval, std::memory_order_relaxed);
GMode.store(Mode, std::memory_order_relaxed);
GEnabled.store(Mode != EMode::Disabled, std::memory_order_relaxed);
if (Mode != EMode::Disabled && GTimerPairCycles.load(std::memory_order_relaxed) == 0)
{
CalibrateTimer();
}
}
bool IsEnabled()
{
return GEnabled.load(std::memory_order_relaxed);
}
bool IsCycleTimingEnabled()
{
return GMode.load(std::memory_order_relaxed) != EMode::Disabled;
}
EMode GetMode()
{
return GMode.load(std::memory_order_relaxed);
}
uint32 GetSampleInterval()
{
return GSampleInterval.load(std::memory_order_relaxed);
}
uint64 GetTimerPairCycles()
{
return GTimerPairCycles.load(std::memory_order_relaxed);
}
void CalibrateTimer()
{
constexpr uint32 Iterations = 100000;
volatile uint64 Sink = 0;
const uint64 Start = FPlatformTime::Cycles64();
for (uint32 Index = 0; Index < Iterations; ++Index)
{
const uint64 A = FPlatformTime::Cycles64();
const uint64 B = FPlatformTime::Cycles64();
Sink ^= (B - A);
}
const uint64 End = FPlatformTime::Cycles64();
const uint64 Total = End - Start;
const uint64 PairCycles = Total > 0
? FMath::Max<uint64>(1, Total / Iterations)
: 1;
GTimerPairCycles.store(PairCycles, std::memory_order_relaxed);
(void)Sink;
}
void Reset()
{
for (FThreadState* State = GThreadStates.load(std::memory_order_acquire);
State != nullptr;
State = State->Next.load(std::memory_order_acquire))
{
for (int32 Index = 0; Index < BucketCount; ++Index)
{
State->Calls[Index] = 0;
State->Cycles[Index] = 0;
State->Samples[Index] = 0;
}
for (int32 Index = 0; Index < CounterCount; ++Index)
{
State->Counters[Index] = 0;
}
for (int32 Index = 0; Index < BucketCount; ++Index)
{
State->SampleSequence[Index] = 0;
State->BlockCallCount[Index] = 0;
State->BlockStartCycles[Index] = 0;
State->BlockActive[Index] = false;
}
State->Classifier = FSnapshot::FClassifierStats();
State->TunnelCacheCapacityEntries = 0;
State->TunnelCacheValidEntries = 0;
State->TunnelCacheStaticBytes = 0;
State->TunnelCacheDynamicBytes = 0;
State->TunnelCacheEntryBytes = 0;
State->TunnelCacheLargestEntryBytes = 0;
State->TunnelCacheBreakdown = FCacheMemoryBreakdown();
State->RoomGraphCacheCapacityEntries = 0;
State->RoomGraphCacheValidEntries = 0;
State->RoomGraphCacheStaticBytes = 0;
State->RoomGraphCacheDynamicBytes = 0;
State->RoomGraphCacheEntryBytes = 0;
State->RoomGraphCacheLargestEntryBytes = 0;
State->RoomGraphCacheBreakdown = FCacheMemoryBreakdown();
}
GScopeDepth = 0;
GScopeSampled = false;
GSampledScopeActive = false;
}
FSnapshot Snapshot()
{
FSnapshot Result;
for (FThreadState* State = GThreadStates.load(std::memory_order_acquire);
State != nullptr;
State = State->Next.load(std::memory_order_acquire))
{
for (int32 Index = 0; Index < BucketCount; ++Index)
{
Result.Calls[Index] += State->Calls[Index];
Result.Samples[Index] += State->Samples[Index];
if (State->Samples[Index] == 0)
{
Result.Cycles[Index] += State->Cycles[Index];
}
else
{
// Store raw sampled cycles in the worker-local state. Estimate the bucket
// from its observed call/sample ratio at aggregation time. This matters for
// small buckets: multiplying every sample by 64 would overstate a bucket with
// only eight calls where all eight were sampled.
const double Scale = static_cast<double>(State->Calls[Index])
/ static_cast<double>(State->Samples[Index]);
const double EstimatedCycles = static_cast<double>(State->Cycles[Index]) * Scale;
Result.Cycles[Index] += EstimatedCycles > static_cast<double>(MAX_uint64)
? MAX_uint64
: static_cast<uint64>(EstimatedCycles + 0.5);
}
if (State->Calls[Index] > 0)
{
++Result.ActiveWorkers[Index];
const double EstimatedWorkerCycles = State->Samples[Index] == 0
? static_cast<double>(State->Cycles[Index])
: static_cast<double>(State->Cycles[Index])
* static_cast<double>(State->Calls[Index])
/ static_cast<double>(State->Samples[Index]);
Result.WallCycles[Index] = FMath::Max(
Result.WallCycles[Index],
EstimatedWorkerCycles > static_cast<double>(MAX_uint64)
? MAX_uint64
: static_cast<uint64>(EstimatedWorkerCycles + 0.5));
}
}
for (int32 Index = 0; Index < CounterCount; ++Index)
{
Result.Counters[Index] += State->Counters[Index];
}
Result.Classifier.Calls += State->Classifier.Calls;
Result.Classifier.SampledCalls += State->Classifier.SampledCalls;
AddClassifierStatsScaled(
Result.Classifier,
State->Classifier,
State->Classifier.Calls,
State->Classifier.SampledCalls);
const uint64 TunnelWorkerBytes = State->TunnelCacheStaticBytes
+ State->TunnelCacheDynamicBytes;
if (State->TunnelCacheCapacityEntries > 0)
{
++Result.TunnelCacheWorkers;
Result.TunnelCacheCapacityEntries += State->TunnelCacheCapacityEntries;
Result.TunnelCacheValidEntries += State->TunnelCacheValidEntries;
Result.TunnelCacheStaticBytes += State->TunnelCacheStaticBytes;
Result.TunnelCacheDynamicBytes += State->TunnelCacheDynamicBytes;
Result.TunnelCacheEntryBytes += State->TunnelCacheEntryBytes;
Result.TunnelCacheLargestEntryBytes = FMath::Max(
Result.TunnelCacheLargestEntryBytes,
State->TunnelCacheLargestEntryBytes);
Result.TunnelCacheLargestWorkerValidEntries = FMath::Max(
Result.TunnelCacheLargestWorkerValidEntries,
State->TunnelCacheValidEntries);
Result.TunnelCacheLargestWorkerBytes = FMath::Max(
Result.TunnelCacheLargestWorkerBytes, TunnelWorkerBytes);
Result.TunnelCacheBreakdown += State->TunnelCacheBreakdown;
}
const uint64 RoomGraphWorkerBytes = State->RoomGraphCacheStaticBytes
+ State->RoomGraphCacheDynamicBytes;
if (State->RoomGraphCacheCapacityEntries > 0)
{
++Result.RoomGraphCacheWorkers;
Result.RoomGraphCacheCapacityEntries += State->RoomGraphCacheCapacityEntries;
Result.RoomGraphCacheValidEntries += State->RoomGraphCacheValidEntries;
Result.RoomGraphCacheStaticBytes += State->RoomGraphCacheStaticBytes;
Result.RoomGraphCacheDynamicBytes += State->RoomGraphCacheDynamicBytes;
Result.RoomGraphCacheEntryBytes += State->RoomGraphCacheEntryBytes;
Result.RoomGraphCacheLargestEntryBytes = FMath::Max(
Result.RoomGraphCacheLargestEntryBytes,
State->RoomGraphCacheLargestEntryBytes);
Result.RoomGraphCacheLargestWorkerValidEntries = FMath::Max(
Result.RoomGraphCacheLargestWorkerValidEntries,
State->RoomGraphCacheValidEntries);
Result.RoomGraphCacheLargestWorkerBytes = FMath::Max(
Result.RoomGraphCacheLargestWorkerBytes, RoomGraphWorkerBytes);
Result.RoomGraphCacheBreakdown += State->RoomGraphCacheBreakdown;
}
}
Result.TimerPairCycles = GetTimerPairCycles();
Result.SampleInterval = GetSampleInterval();
Result.Mode = GetMode();
return Result;
}
FSnapshot SnapshotCurrentThread()
{
FSnapshot Result;
FThreadState* State = GThreadState;
if (State == nullptr)
{
Result.TimerPairCycles = GetTimerPairCycles();
Result.SampleInterval = GetSampleInterval();
Result.Mode = GetMode();
return Result;
}
for (int32 Index = 0; Index < BucketCount; ++Index)
{
Result.Calls[Index] = State->Calls[Index];
Result.Samples[Index] = State->Samples[Index];
if (State->Samples[Index] == 0)
{
Result.Cycles[Index] = State->Cycles[Index];
}
else
{
const double Scale = static_cast<double>(State->Calls[Index])
/ static_cast<double>(State->Samples[Index]);
const double EstimatedCycles = static_cast<double>(State->Cycles[Index]) * Scale;
Result.Cycles[Index] = EstimatedCycles > static_cast<double>(MAX_uint64)
? MAX_uint64
: static_cast<uint64>(EstimatedCycles + 0.5);
}
Result.WallCycles[Index] = Result.Cycles[Index];
Result.ActiveWorkers[Index] = State->Calls[Index] > 0 ? 1 : 0;
}
for (int32 Index = 0; Index < CounterCount; ++Index)
{
Result.Counters[Index] = State->Counters[Index];
}
Result.Classifier = State->Classifier;
if (State->Classifier.SampledCalls > 0)
{
FSnapshot::FClassifierStats Raw = State->Classifier;
Result.Classifier = FSnapshot::FClassifierStats();
Result.Classifier.Calls = State->Classifier.Calls;
Result.Classifier.SampledCalls = State->Classifier.SampledCalls;
AddClassifierStatsScaled(
Result.Classifier,
Raw,
State->Classifier.Calls,
State->Classifier.SampledCalls);
}
Result.TimerPairCycles = GetTimerPairCycles();
Result.SampleInterval = GetSampleInterval();
Result.Mode = GetMode();
return Result;
}
void AddCounter(ECounter Counter, uint64 Amount)
{
if (!IsEnabledFast())
{
return;
}
const int32 Index = static_cast<int32>(Counter);
if (Index >= 0 && Index < CounterCount)
{
GetThreadState().Counters[Index] += Amount;
}
}
void AddMeasurement(EBucket Bucket, uint64 Cycles, uint64 Calls)
{
const int32 Index = ToIndex(Bucket);
FThreadState& State = GetThreadState();
State.Calls[Index] += Calls;
State.Cycles[Index] += Cycles;
}
void RecordCall(EBucket Bucket)
{
const int32 Index = ToIndex(Bucket);
GetThreadState().Calls[Index] += 1;
}
void RecordClassifierCall(bool bSampled)
{
FThreadState& State = GetThreadState();
++State.Classifier.Calls;
if (bSampled)
{
++State.Classifier.SampledCalls;
}
++State.Calls[ToIndex(EBucket::ClassifierTotal)];
}
void AddClassifierStats(const FSnapshot::FClassifierStats& Stats)
{
FThreadState& State = GetThreadState();
State.Classifier.RefineNodes += Stats.RefineNodes;
State.Classifier.StackBoxCalls += Stats.StackBoxCalls;
State.Classifier.WholeMixedNodes += Stats.WholeMixedNodes;
State.Classifier.WholeSolidNodes += Stats.WholeSolidNodes;
State.Classifier.WholeAirNodes += Stats.WholeAirNodes;
State.Classifier.NeedsFinalFieldNodes += Stats.NeedsFinalFieldNodes;
State.Classifier.SplitNodes += Stats.SplitNodes;
State.Classifier.MaxRefinementDepth = FMath::Max(
State.Classifier.MaxRefinementDepth, Stats.MaxRefinementDepth);
State.Classifier.ExactCoreSamples += Stats.ExactCoreSamples;
State.Classifier.ExactFinalSamples += Stats.ExactFinalSamples;
State.Classifier.ExactCoreCacheHits += Stats.ExactCoreCacheHits;
State.Classifier.ExactFinalCacheHits += Stats.ExactFinalCacheHits;
State.Classifier.ExactCoreLeaves += Stats.ExactCoreLeaves;
State.Classifier.ExactFinalLeaves += Stats.ExactFinalLeaves;
State.Classifier.StackBoxCycles += Stats.StackBoxCycles;
State.Classifier.ExactCoreCycles += Stats.ExactCoreCycles;
State.Classifier.ExactFinalCycles += Stats.ExactFinalCycles;
State.Classifier.RoomTailQueries += Stats.RoomTailQueries;
State.Classifier.RoomTailEvaluated += Stats.RoomTailEvaluated;
State.Classifier.RoomTailCycles += Stats.RoomTailCycles;
State.Classifier.RoomPropagateCycles += Stats.RoomPropagateCycles;
State.Classifier.RoomExactPrimitiveCycles += Stats.RoomExactPrimitiveCycles;
State.Classifier.RoomCacheWindowCycles += Stats.RoomCacheWindowCycles;
State.Classifier.RoomNumRooms = FMath::Max(
State.Classifier.RoomNumRooms, Stats.RoomNumRooms);
State.Classifier.RoomNumTunnels = FMath::Max(
State.Classifier.RoomNumTunnels, Stats.RoomNumTunnels);
State.Classifier.RoomNumRoomFloorJoins = FMath::Max(
State.Classifier.RoomNumRoomFloorJoins, Stats.RoomNumRoomFloorJoins);
State.Classifier.RoomNumPits = FMath::Max(
State.Classifier.RoomNumPits, Stats.RoomNumPits);
State.Classifier.RoomNumChimneys = FMath::Max(
State.Classifier.RoomNumChimneys, Stats.RoomNumChimneys);
}
bool ShouldSample(EBucket Bucket)
{
if (!IsCycleTimingEnabled())
{
return false;
}
if (GetMode() == EMode::Full)
{
return true;
}
FThreadState& State = GetThreadState();
const int32 Index = ToIndex(Bucket);
const uint32 Sequence = State.SampleSequence[Index]++;
const uint32 Interval = GetSampleInterval();
if ((Interval & (Interval - 1u)) == 0u)
{
return (Sequence & (Interval - 1u)) == 0u;
}
return (Sequence % Interval) == 0u;
}
void AddSampledMeasurement(EBucket Bucket, uint64 RawCycles)
{
const int32 Index = ToIndex(Bucket);
FThreadState& State = GetThreadState();
const uint64 TimerCost = GetTimerPairCycles();
const uint64 AdjustedCycles = RawCycles > TimerCost
? RawCycles - TimerCost
: 0;
State.Cycles[Index] += AdjustedCycles;
State.Samples[Index] += 1;
}
void SetWorkerTunnelCacheFootprint(
uint64 CapacityEntries, uint64 ValidEntries,
uint64 StaticBytes, uint64 DynamicBytes,
uint64 EntryBytes, uint64 LargestEntryBytes,
uint64 LargestWorkerValidEntries,
const FCacheMemoryBreakdown& Breakdown)
{
if (!IsEnabled()) { return; }
FThreadState& State = GetThreadState();
State.TunnelCacheCapacityEntries = CapacityEntries;
State.TunnelCacheValidEntries = ValidEntries;
State.TunnelCacheStaticBytes = StaticBytes;
State.TunnelCacheDynamicBytes = DynamicBytes;
State.TunnelCacheEntryBytes = EntryBytes;
State.TunnelCacheLargestEntryBytes = LargestEntryBytes;
State.TunnelCacheLargestWorkerValidEntries = LargestWorkerValidEntries;
State.TunnelCacheBreakdown = Breakdown;
}
void SetWorkerRoomGraphCacheFootprint(
uint64 CapacityEntries, uint64 ValidEntries,
uint64 StaticBytes, uint64 DynamicBytes,
uint64 EntryBytes, uint64 LargestEntryBytes,
uint64 LargestWorkerValidEntries,
const FCacheMemoryBreakdown& Breakdown)
{
if (!IsEnabled()) { return; }
FThreadState& State = GetThreadState();
State.RoomGraphCacheCapacityEntries = CapacityEntries;
State.RoomGraphCacheValidEntries = ValidEntries;
State.RoomGraphCacheStaticBytes = StaticBytes;
State.RoomGraphCacheDynamicBytes = DynamicBytes;
State.RoomGraphCacheEntryBytes = EntryBytes;
State.RoomGraphCacheLargestEntryBytes = LargestEntryBytes;
State.RoomGraphCacheLargestWorkerValidEntries = LargestWorkerValidEntries;
State.RoomGraphCacheBreakdown = Breakdown;
}
const TCHAR* CounterName(ECounter Counter)
{
switch (Counter)
{
case ECounter::TunnelCacheLookup: return TEXT("TunnelCacheLookup");
case ECounter::TunnelCacheHit: return TEXT("TunnelCacheHit");
case ECounter::TunnelCacheMiss: return TEXT("TunnelCacheMiss");
case ECounter::SdfCacheBuild: return TEXT("SdfCacheBuild");
case ECounter::SdfSpatialIndexBuild: return TEXT("SdfSpatialIndexBuild");
case ECounter::SdfBrickIndexBuild: return TEXT("SdfBrickIndexBuild");
case ECounter::RoomGraphBuildGeneratorTile: return TEXT("RoomGraphBuildGeneratorTile");
case ECounter::RoomGraphBuildGeneratorTunnelCore: return TEXT("RoomGraphBuildGeneratorTunnelCore");
case ECounter::RoomGraphBuildOpShared: return TEXT("RoomGraphBuildOpShared");
case ECounter::RoomGraphBuildOpLocal: return TEXT("RoomGraphBuildOpLocal");
case ECounter::RoomGraphBuildClassifierShared: return TEXT("RoomGraphBuildClassifierShared");
case ECounter::RoomGraphBuildClassifierLocal: return TEXT("RoomGraphBuildClassifierLocal");
case ECounter::RoomGraphBuildUnknown: return TEXT("RoomGraphBuildUnknown");
case ECounter::CaveRoomCandidates:return TEXT("CaveRoomCandidates");
case ECounter::CaveRoomEvaluated: return TEXT("CaveRoomEvaluated");
case ECounter::CaveTunnelCandidates:return TEXT("CaveTunnelCandidates");
case ECounter::CaveTunnelEvaluated:return TEXT("CaveTunnelEvaluated");
case ECounter::TunnelCoreCandidates:return TEXT("TunnelCoreCandidates");
case ECounter::TunnelCoreEvaluated:return TEXT("TunnelCoreEvaluated");
case ECounter::TunnelSupportColumnBuilds:return TEXT("TunnelSupportColumnBuilds");
case ECounter::TunnelSupportColumnCandidates:return TEXT("TunnelSupportColumnCandidates");
case ECounter::TunnelSupportFloorQueries:return TEXT("TunnelSupportFloorQueries");
case ECounter::TunnelSupportFloorChecks:return TEXT("TunnelSupportFloorChecks");
case ECounter::TunnelSupportFloorBackstopFires:return TEXT("TunnelSupportFloorBackstopFires");
case ECounter::TunnelRoomFloorBackstopFires:return TEXT("TunnelRoomFloorBackstopFires");
case ECounter::TunnelAuthoredFloorSamples:return TEXT("TunnelAuthoredFloorSamples");
case ECounter::TunnelAuthoredFloorProtectionClamps:return TEXT("TunnelAuthoredFloorProtectionClamps");
case ECounter::TunnelAuthoredFloorDisturbanceSkips:return TEXT("TunnelAuthoredFloorDisturbanceSkips");
case ECounter::TunnelFloorProfileFallbacks:return TEXT("TunnelFloorProfileFallbacks");
case ECounter::TunnelFloorProfileBuilds:return TEXT("TunnelFloorProfileBuilds");
case ECounter::TunnelFloorProfileSegments:return TEXT("TunnelFloorProfileSegments");
case ECounter::TunnelFloorProfileLedges:return TEXT("TunnelFloorProfileLedges");
case ECounter::TunnelFloorProfileLedgesBelow2Voxels:return TEXT("TunnelFloorProfileLedgesBelow2Voxels");
case ECounter::TunnelFloorProfileLedges2To4Voxels:return TEXT("TunnelFloorProfileLedges2To4Voxels");
case ECounter::TunnelFloorProfileLedges4To8Voxels:return TEXT("TunnelFloorProfileLedges4To8Voxels");
case ECounter::TunnelFloorProfileLedges8PlusVoxels:return TEXT("TunnelFloorProfileLedges8PlusVoxels");
case ECounter::TunnelFloorSteepEdges:return TEXT("TunnelFloorSteepEdges");
case ECounter::TunnelFloorWindingEdges:return TEXT("TunnelFloorWindingEdges");
case ECounter::TunnelFloorDropEdges:return TEXT("TunnelFloorDropEdges");
case ECounter::TunnelFloorMultiStepEdges:return TEXT("TunnelFloorMultiStepEdges");
case ECounter::PassageSupportFloorBackstopFires:return TEXT("PassageSupportFloorBackstopFires");
case ECounter::PassageNativeFloorCompositions:return TEXT("PassageNativeFloorCompositions");
case ECounter::PassageCandidates: return TEXT("PassageCandidates");
case ECounter::PassageEvaluated: return TEXT("PassageEvaluated");
case ECounter::MesherBlockTests: return TEXT("MesherBlockTests");
case ECounter::MesherBlockAllSolid:return TEXT("MesherBlockAllSolid");
case ECounter::MesherBlockAllAir: return TEXT("MesherBlockAllAir");
case ECounter::TileClassifyCalls: return TEXT("TileClassifyCalls");
case ECounter::TileClassifyAllSolid: return TEXT("TileClassifyAllSolid");
case ECounter::TileClassifyAllAir: return TEXT("TileClassifyAllAir");
case ECounter::TileClassifyMixed: return TEXT("TileClassifyMixed");
case ECounter::FusedTunnelSamples: return TEXT("FusedTunnelSamples");
case ECounter::FusedTunnelDetailSamples: return TEXT("FusedTunnelDetailSamples");
case ECounter::FusedTunnelDetailSkipped: return TEXT("FusedTunnelDetailSkipped");
case ECounter::WormEligibleSamples: return TEXT("WormEligibleSamples");
case ECounter::WormBlockProofs: return TEXT("WormBlockProofs");
case ECounter::WormBlockSkippedSamples: return TEXT("WormBlockSkippedSamples");
case ECounter::OpBlockBuilds: return TEXT("OpBlockBuilds");
case ECounter::OpBlockSamples: return TEXT("OpBlockSamples");
case ECounter::OpBlockOperators: return TEXT("OpBlockOperators");
case ECounter::OpBlockActiveOperators: return TEXT("OpBlockActiveOperators");
case ECounter::OpBlockPrunedOperators: return TEXT("OpBlockPrunedOperators");
case ECounter::MesherDensityGridBytes: return TEXT("MesherDensityGridBytes");
case ECounter::MesherSharedGridReadBytes: return TEXT("MesherSharedGridReadBytes");
case ECounter::MesherOutputArrayBytes: return TEXT("MesherOutputArrayBytes");
case ECounter::MesherOutputAllocatedBytes: return TEXT("MesherOutputAllocatedBytes");
case ECounter::OpBlockScratchBytes: return TEXT("OpBlockScratchBytes");
case ECounter::OpBlockCopyBytes: return TEXT("OpBlockCopyBytes");
case ECounter::Count: break;
}
return TEXT("Unknown");
}
EBucket BucketFromName(const TCHAR* Name)
{
if (Name == nullptr) { return EBucket::OtherOp; }
struct FNameBucket
{
const TCHAR* Name;
EBucket Bucket;
};
static const FNameBucket Names[] =
{
{ TEXT("ApplyDisturbances"), EBucket::ApplyDisturbances },
{ TEXT("PassageModifier"), EBucket::PassageModifier },
{ TEXT("PassageLandingAir"), EBucket::PassageLandingAir },
{ TEXT("PassageLandingFloor"), EBucket::PassageLandingFloor },
{ TEXT("PassageLandingRoomFloor"), EBucket::PassageLandingRoomFloor },
{ TEXT("PassageTunnelAir"), EBucket::PassageTunnelAir },
{ TEXT("PassageStructuralPosts"), EBucket::PassageStructuralPosts },
{ TEXT("TunnelCoreSupport"), EBucket::TunnelCoreSupport },
{ TEXT("TunnelCoreSdf"), EBucket::TunnelCoreSdf },
{ TEXT("TunnelCorePosts"), EBucket::TunnelCorePosts },
{ TEXT("StructuralTail"), EBucket::StructuralTail },
{ TEXT("RoomGraphSource"), EBucket::RoomGraphSource },
{ TEXT("SdfCarve"), EBucket::SdfCarve },
{ TEXT("SdfConvertOp"), EBucket::SdfCarve },
{ TEXT("CaveRoughnessMod"), EBucket::CaveRoughnessMod },
{ TEXT("CaveTerraceMod"), EBucket::CaveTerraceMod },
{ TEXT("LayerLineMod"), EBucket::LayerLineMod },
{ TEXT("RibbingMod"), EBucket::RibbingMod },
{ TEXT("CaveOverhangMod"), EBucket::CaveOverhangMod },
{ TEXT("CaveCliffMod"), EBucket::CaveCliffMod },
{ TEXT("ScallopMod"), EBucket::ScallopMod },
{ TEXT("CaveArchMod"), EBucket::CaveArchMod },
{ TEXT("RoomColumnMod"), EBucket::RoomColumnMod },
{ TEXT("DomeMod"), EBucket::DomeMod },
{ TEXT("PinchMod"), EBucket::PinchMod },
{ TEXT("FloorBiasMod"), EBucket::FloorBiasMod },
{ TEXT("WormFieldSource"), EBucket::WormFieldSource },
{ TEXT("OriginSpineOp"), EBucket::OriginSpineOp },
{ TEXT("BoundarySealOp"), EBucket::BoundarySealOp },
{ TEXT("PassageCarveOp"), EBucket::PassageCarveOp },
{ TEXT("XYEdgeSealOp"), EBucket::XYEdgeSealOp },
{ TEXT("FusedEvaluator"), EBucket::FusedEvaluator },
{ TEXT("InterpretedOpStack"),EBucket::InterpretedOpStack },
{ TEXT("OperatorBlock"), EBucket::OperatorBlock },
{ TEXT("RoomGraphBuild"), EBucket::RoomGraphBuild },
{ TEXT("RoomGraphSdf"), EBucket::RoomGraphSdf },
{ TEXT("TunnelCoreWorld"), EBucket::TunnelCoreWorld },
{ TEXT("FusedDetail"), EBucket::FusedDetail },
{ TEXT("FusedWorm"), EBucket::FusedWorm },
{ TEXT("FusedStructural"), EBucket::FusedStructural },
{ TEXT("ClassifierTotal"), EBucket::ClassifierTotal },
{ TEXT("ClassifierIntervalProof"), EBucket::ClassifierIntervalProof },
{ TEXT("ClassifierExactCore"), EBucket::ClassifierExactCore },
{ TEXT("ClassifierExactFinal"), EBucket::ClassifierExactFinal },
{ TEXT("ClassifierRoomTail"), EBucket::ClassifierRoomTail },
{ TEXT("RuntimeStreamBuilding"), EBucket::RuntimeStreamBuilding },
{ TEXT("DensityPrologue"), EBucket::DensityPrologue },
{ TEXT("DensityCore"), EBucket::DensityCore },
{ TEXT("DensityDisturbances"), EBucket::DensityDisturbances },
{ TEXT("DensityStructuralPosts"), EBucket::DensityStructuralPosts },
{ TEXT("DensityBoundarySeal"), EBucket::DensityBoundarySeal },
{ TEXT("DensityDiffLayer"), EBucket::DensityDiffLayer },
{ TEXT("DensityTail"), EBucket::DensityTail },
{ TEXT("MesherGenerateMesh"), EBucket::MesherGenerateMesh },
{ TEXT("MesherDensityGrid"), EBucket::MesherDensityGrid },
{ TEXT("MesherCellClassification"), EBucket::MesherCellClassification },
{ TEXT("MesherGradientNormals"), EBucket::MesherGradientNormals },
{ TEXT("MesherVertexInterpolation"), EBucket::MesherVertexInterpolation },
{ TEXT("MesherStreamBuilding"), EBucket::MesherStreamBuilding },
{ TEXT("MesherOther"), EBucket::MesherOther },
};
static const TMap<uint32, TArray<const FNameBucket*>> NamesByHash = []()
{
TMap<uint32, TArray<const FNameBucket*>> Lookup;
for (const FNameBucket& Entry : Names)
{
Lookup.FindOrAdd(FCrc::StrCrc32(Entry.Name)).Add(&Entry);
}
return Lookup;
}();
const TArray<const FNameBucket*>* Candidates = NamesByHash.Find(FCrc::StrCrc32(Name));
if (Candidates != nullptr)
{
for (const FNameBucket* Entry : *Candidates)
{
if (Entry != nullptr && FCString::Strcmp(Name, Entry->Name) == 0)
{
return Entry->Bucket;
}
}
}
return EBucket::OtherOp;
}
const TCHAR* BucketName(EBucket Bucket)
{
switch (Bucket)
{
case EBucket::GetDensityAt: return TEXT("GetDensityAt");
case EBucket::ApplyDisturbances:return TEXT("ApplyDisturbances");
case EBucket::PassageModifier: return TEXT("PassageModifier");
case EBucket::PassageLandingAir:return TEXT("PassageLandingAir");
case EBucket::PassageLandingFloor:return TEXT("PassageLandingFloor");
case EBucket::PassageLandingRoomFloor:return TEXT("PassageLandingRoomFloor");
case EBucket::PassageTunnelAir:return TEXT("PassageTunnelAir");
case EBucket::PassageStructuralPosts:return TEXT("PassageStructuralPosts");
case EBucket::TunnelCoreSupport:return TEXT("TunnelCoreSupport");
case EBucket::TunnelCoreSdf: return TEXT("TunnelCoreSdf");
case EBucket::TunnelCorePosts: return TEXT("TunnelCorePosts");
case EBucket::StructuralTail: return TEXT("StructuralTail");
case EBucket::RoomGraphSource: return TEXT("RoomGraphSource");
case EBucket::SdfCarve: return TEXT("SdfCarve");
case EBucket::CaveRoughnessMod: return TEXT("CaveRoughnessMod");
case EBucket::CaveTerraceMod: return TEXT("CaveTerraceMod");
case EBucket::LayerLineMod: return TEXT("LayerLineMod");
case EBucket::RibbingMod: return TEXT("RibbingMod");
case EBucket::CaveOverhangMod: return TEXT("CaveOverhangMod");
case EBucket::CaveCliffMod: return TEXT("CaveCliffMod");
case EBucket::ScallopMod: return TEXT("ScallopMod");
case EBucket::CaveArchMod: return TEXT("CaveArchMod");
case EBucket::RoomColumnMod: return TEXT("RoomColumnMod");
case EBucket::DomeMod: return TEXT("DomeMod");
case EBucket::PinchMod: return TEXT("PinchMod");
case EBucket::FloorBiasMod: return TEXT("FloorBiasMod");
case EBucket::WormFieldSource: return TEXT("WormFieldSource");
case EBucket::OriginSpineOp: return TEXT("OriginSpineOp");
case EBucket::BoundarySealOp: return TEXT("BoundarySealOp");
case EBucket::PassageCarveOp: return TEXT("PassageCarveOp");
case EBucket::XYEdgeSealOp: return TEXT("XYEdgeSealOp");
case EBucket::OtherOp: return TEXT("OtherOp");
case EBucket::FusedEvaluator: return TEXT("FusedEvaluator");
case EBucket::InterpretedOpStack:return TEXT("InterpretedOpStack");
case EBucket::OperatorBlock: return TEXT("OperatorBlock");
case EBucket::RoomGraphBuild: return TEXT("RoomGraphBuild");
case EBucket::RoomGraphSdf: return TEXT("RoomGraphSdf");
case EBucket::TunnelCoreWorld: return TEXT("TunnelCoreWorld");
case EBucket::FusedDetail: return TEXT("FusedDetail");
case EBucket::FusedWorm: return TEXT("FusedWorm");
case EBucket::FusedStructural: return TEXT("FusedStructural");
case EBucket::ClassifierTotal: return TEXT("ClassifierTotal");
case EBucket::ClassifierIntervalProof:return TEXT("ClassifierIntervalProof");
case EBucket::ClassifierExactCore:return TEXT("ClassifierExactCore");
case EBucket::ClassifierExactFinal:return TEXT("ClassifierExactFinal");
case EBucket::ClassifierRoomTail:return TEXT("ClassifierRoomTail");
case EBucket::RuntimeStreamBuilding:return TEXT("RuntimeStreamBuilding");
case EBucket::DensityPrologue: return TEXT("DensityPrologue");
case EBucket::DensityCore: return TEXT("DensityCore");
case EBucket::DensityDisturbances:return TEXT("DensityDisturbances");
case EBucket::DensityStructuralPosts:return TEXT("DensityStructuralPosts");
case EBucket::DensityBoundarySeal:return TEXT("DensityBoundarySeal");
case EBucket::DensityDiffLayer: return TEXT("DensityDiffLayer");
case EBucket::DensityTail: return TEXT("DensityTail");
case EBucket::MesherGenerateMesh:return TEXT("MesherGenerateMesh");
case EBucket::MesherDensityGrid:return TEXT("MesherDensityGrid");
case EBucket::MesherCellClassification:return TEXT("MesherCellClassification");
case EBucket::MesherGradientNormals:return TEXT("MesherGradientNormals");
case EBucket::MesherVertexInterpolation:return TEXT("MesherVertexInterpolation");
case EBucket::MesherStreamBuilding:return TEXT("MesherStreamBuilding");
case EBucket::MesherOther: return TEXT("MesherOther");
case EBucket::Count: break;
}
return TEXT("Unknown");
}
FScopeToken BeginScope(EBucket Bucket)
{
FScopeToken Token{};
if (!IsEnabledFast())
{
return Token;
}
const EMode Mode = GetMode();
const bool bSampledMode = Mode == EMode::Sampled || Mode == EMode::Attribution;
if ((Mode == EMode::Sampled && !IsSampledScopeEnabled(Bucket))
|| (Mode == EMode::Attribution && !IsAttributionScope(Bucket)))
{
return Token;
}
Token.bEntered = true;
Token.bPreviousSampled = GScopeSampled;
Token.bPreviousSampledScopeActive = GSampledScopeActive;
++GScopeDepth;
// For the hot parent, amortise the timer over a deterministic block of calls. A timer
// pair around one ~100 ns operation is mostly the timer; a pair around 64 calls is cheap
// and estimates the parent from real elapsed work rather than instrumentation latency.
if (bSampledMode && Bucket == EBucket::GetDensityAt)
{
FThreadState& State = GetThreadState();
const int32 Index = ToIndex(Bucket);
const uint32 Sequence = State.SampleSequence[Index]++;
Token.bBlockTiming = true;
const uint32 Interval = GetSampleInterval();
const bool bBlockBoundary = (Interval & (Interval - 1u)) == 0u
? (Sequence & (Interval - 1u)) == 0u
: (Sequence % Interval) == 0u;
if (bBlockBoundary && !State.BlockActive[Index])
{
State.BlockActive[Index] = true;
State.BlockCallCount[Index] = 0;
State.BlockStartCycles[Index] = FPlatformTime::Cycles64();
}
// The selected call admits all nested component scopes. Every other call exits the
// inline FScopedTimer fast path before crossing into this translation unit.
GScopeSampled = bBlockBoundary;
GSampledScopeActive = bBlockBoundary;
Token.bSampled = bBlockBoundary;
return Token;
}
// Every bucket samples independently, but only one nested non-container scope owns a
// timer at a time. Otherwise a sampled GetDensityAt would also start timers for every
// phase and operation inside that same call, measuring instrumentation instead of work.
// Mesher container ranges deliberately remain transparent so their grid can still expose
// density samples underneath them.
const bool bContainerScope = Bucket == EBucket::MesherGenerateMesh
|| Bucket == EBucket::MesherDensityGrid;
const bool bInheritedAttributionSample = Mode == EMode::Attribution
&& GSampledScopeActive;
Token.bSampled = Mode == EMode::Full
? true
: ((Mode == EMode::Attribution && IsAttributionAlwaysTimed(Bucket))
? true
: (bInheritedAttributionSample
? true
: (Mode == EMode::Attribution && IsAttributionScope(Bucket)
? ShouldSample(Bucket)
: (!GSampledScopeActive && ShouldSample(Bucket)))));
GScopeSampled = Token.bSampled;
if (Token.bSampled)
{
if ((Mode == EMode::Sampled || Mode == EMode::Attribution) && !bContainerScope)
{
GSampledScopeActive = true;
}
Token.StartCycles = FPlatformTime::Cycles64();
}
return Token;
}
void EndScope(EBucket Bucket, const FScopeToken& Token)
{
if (!Token.bEntered)
{
return;
}
RecordCall(Bucket);
if (Token.bBlockTiming)
{
FThreadState& State = GetThreadState();
const int32 Index = ToIndex(Bucket);
++State.BlockCallCount[Index];
const uint32 Interval = GetSampleInterval();
if (State.BlockActive[Index] && State.BlockCallCount[Index] >= Interval)
{
const uint64 RawCycles = FPlatformTime::Cycles64() - State.BlockStartCycles[Index];
const uint64 TimerCost = GetTimerPairCycles();
const uint64 AdjustedCycles = RawCycles > TimerCost
? RawCycles - TimerCost
: 0;
State.Cycles[Index] += AdjustedCycles / FMath::Max<uint32>(1, Interval);
State.Samples[Index] += 1;
State.BlockActive[Index] = false;
State.BlockCallCount[Index] = 0;
State.BlockStartCycles[Index] = 0;
}
}
else if (Token.bSampled)
{
AddSampledMeasurement(Bucket, FPlatformTime::Cycles64() - Token.StartCycles);
}
check(GScopeDepth > 0);
--GScopeDepth;
GScopeSampled = Token.bPreviousSampled;
GSampledScopeActive = Token.bPreviousSampledScopeActive;
}
}
@@ -0,0 +1,469 @@
// VoxelDiffLayer.cpp
// Runtime terrain modification storage and evaluation.
#include "VoxelDiffLayer.h"
namespace
{
std::atomic<uint64> GNextDiffLayerLifetimeId{0};
bool IsFiniteVector(const FVector& Value)
{
return VoxelMath::IsFinite(Value.X)
&& VoxelMath::IsFinite(Value.Y)
&& VoxelMath::IsFinite(Value.Z);
}
bool IsValidModificationGeometry(const FVoxelModification& Mod)
{
if (!IsFiniteVector(Mod.Center) || !IsFiniteVector(Mod.BoxExtent)
|| !IsFiniteVector(Mod.CapsuleEnd)
|| !VoxelMath::IsFinite(Mod.Radius)
|| !VoxelMath::IsFinite(Mod.Strength)
|| !VoxelMath::IsFinite(Mod.Falloff)
|| Mod.Radius <= 0.0f || Mod.Falloff < 0.0f)
{
return false;
}
switch (Mod.Shape)
{
case EVoxelBrushShape::Sphere:
return true;
case EVoxelBrushShape::Box:
return Mod.BoxExtent.X > 0.0f
&& Mod.BoxExtent.Y > 0.0f
&& Mod.BoxExtent.Z > 0.0f;
case EVoxelBrushShape::Capsule:
return true; // A coincident pair of endpoints is a valid spherical capsule.
default:
return false;
}
}
double GetModificationBudgetVolume(const FVoxelModification& Mod)
{
switch (Mod.Shape)
{
case EVoxelBrushShape::Box:
{
// The SDF's falloff is a rounded outer band, so the expanded axis-aligned box is a
// conservative charge and remains cheap to compute.
const double Ex = static_cast<double>(Mod.BoxExtent.X) + static_cast<double>(Mod.Falloff);
const double Ey = static_cast<double>(Mod.BoxExtent.Y) + static_cast<double>(Mod.Falloff);
const double Ez = static_cast<double>(Mod.BoxExtent.Z) + static_cast<double>(Mod.Falloff);
return 8.0 * Ex * Ey * Ez;
}
case EVoxelBrushShape::Capsule:
{
// Charge the full capsule including its falloff shell: cylinder + two hemispheres.
const double OuterRadius = static_cast<double>(Mod.Radius) + static_cast<double>(Mod.Falloff);
const double SegmentLength = static_cast<double>(FVector::Dist(Mod.Center, Mod.CapsuleEnd));
return PI * OuterRadius * OuterRadius * SegmentLength
+ (4.0 / 3.0) * PI * OuterRadius * OuterRadius * OuterRadius;
}
case EVoxelBrushShape::Sphere:
default:
{
const double Radius = static_cast<double>(Mod.Radius);
return (4.0 / 3.0) * PI * Radius * Radius * Radius;
}
}
}
}
uint64 UVoxelDiffLayer::GetCacheLifetimeId() const
{
uint64 LifetimeId = CacheLifetimeId.load(std::memory_order_acquire);
if (LifetimeId != 0)
{
return LifetimeId;
}
const uint64 NewLifetimeId = GNextDiffLayerLifetimeId.fetch_add(1, std::memory_order_relaxed) + 1;
if (CacheLifetimeId.compare_exchange_strong(
LifetimeId,
NewLifetimeId,
std::memory_order_release,
std::memory_order_acquire))
{
return NewLifetimeId;
}
// Another worker initialized the identity first. Its value is written into LifetimeId by
// compare_exchange_strong on failure.
return LifetimeId;
}
//=============================================================================
// BUDGET CONFIGURATION
//=============================================================================
void UVoxelDiffLayer::SetBudget(int32 InMaxMods, float InMaxRadius, float InMaxVolume)
{
BudgetMaxMods = InMaxMods;
BudgetMaxRadius = InMaxRadius;
BudgetMaxVolume = InMaxVolume;
UE_LOG(LogTemp, Log, TEXT("[DiffLayer] Budget set: MaxMods=%d, MaxRadius=%.1f, MaxVolume=%.0f (0=unlimited)"),
BudgetMaxMods, BudgetMaxRadius, BudgetMaxVolume);
}
bool UVoxelDiffLayer::CanModify(float Radius) const
{
if (!VoxelMath::IsFinite(Radius) || Radius <= 0.0f)
{
return false;
}
// Check radius limit (always enforced, even if "unlimited" count/volume)
if (BudgetMaxRadius > 0.0f && Radius > BudgetMaxRadius)
{
return false;
}
// Check modification count limit
if (BudgetMaxMods > 0 && ModificationCount >= BudgetMaxMods)
{
return false;
}
// Check volume limit: would this brush push us over?
if (BudgetMaxVolume > 0.0f)
{
const double BrushVolume = (4.0 / 3.0) * PI
* static_cast<double>(Radius) * static_cast<double>(Radius) * static_cast<double>(Radius);
const double NewTotal = static_cast<double>(AccumulatedVolume) + BrushVolume;
if (!VoxelMath::IsFinite(BrushVolume) || BrushVolume > static_cast<double>(FLT_MAX)
|| !VoxelMath::IsFinite(NewTotal)
|| NewTotal > static_cast<double>(BudgetMaxVolume))
{
return false;
}
}
return true;
}
bool UVoxelDiffLayer::CanModify(const FVoxelModification& Mod) const
{
if (!IsValidModificationGeometry(Mod)) return false;
// Radius is the radial cap for spheres/capsules. A box's largest half-extent is its analogous
// brush radius; checking it separately prevents a direct API caller from hiding a huge box behind
// an unrelated small Radius field.
const float GeometryRadius = (Mod.Shape == EVoxelBrushShape::Box)
? FMath::Max3(Mod.BoxExtent.X, Mod.BoxExtent.Y, Mod.BoxExtent.Z)
: Mod.Radius;
if (BudgetMaxRadius > 0.0f && GeometryRadius > BudgetMaxRadius) return false;
if (BudgetMaxMods > 0 && ModificationCount >= BudgetMaxMods) return false;
const double BrushVolume = GetModificationBudgetVolume(Mod);
const double NewTotal = static_cast<double>(AccumulatedVolume) + BrushVolume;
if (!VoxelMath::IsFinite(BrushVolume) || BrushVolume > static_cast<double>(FLT_MAX)
|| !VoxelMath::IsFinite(NewTotal)) return false;
if (BudgetMaxVolume > 0.0f && NewTotal > static_cast<double>(BudgetMaxVolume)) return false;
return true;
}
int32 UVoxelDiffLayer::GetRemainingModifications() const
{
if (BudgetMaxMods <= 0) return -1; // Unlimited
return FMath::Max(0, BudgetMaxMods - ModificationCount);
}
float UVoxelDiffLayer::GetRemainingVolume() const
{
if (BudgetMaxVolume <= 0.0f) return -1.0f; // Unlimited
return FMath::Max(0.0f, BudgetMaxVolume - AccumulatedVolume);
}
//=============================================================================
// APPLY MODIFICATION
//=============================================================================
TArray<FIntVector> UVoxelDiffLayer::ApplyModification(const FVoxelModification& Mod)
{
TArray<FIntVector> AffectedChunks;
// Validate the complete geometry before radius clamping, budget accounting, or AABB math.
// FMath::Min(NaN, limit) and the shape switch's default sphere path would otherwise turn
// malformed API input into either a counter update or an invalid chunk range.
if (!IsValidModificationGeometry(Mod))
{
UE_LOG(LogTemp, Warning,
TEXT("[DiffLayer] Modification REJECTED — non-finite or invalid brush geometry"));
return AffectedChunks;
}
// --- Budget enforcement ---
// Preserve the historical radius clamp for spheres/capsules when a finite per-brush cap is set;
// the shape-aware check below still charges the resulting geometry and rejects oversized boxes.
const float ClampedRadius = (BudgetMaxRadius > 0.0f)
? FMath::Min(Mod.Radius, BudgetMaxRadius) : Mod.Radius;
FVoxelModification ClampedMod = Mod;
ClampedMod.Radius = ClampedRadius;
if (!CanModify(ClampedMod))
{
UE_LOG(LogTemp, Warning,
TEXT("[DiffLayer] Modification REJECTED — budget exceeded (count=%d/%d, volume=%.0f/%.0f)"),
ModificationCount, BudgetMaxMods, AccumulatedVolume, BudgetMaxVolume);
return AffectedChunks; // Empty = rejected
}
// Complete the world-AABB calculation before mutating the budget counters. Finite input can
// still overflow during Center +/- extent or address a chunk outside FIntVector's domain;
// neither case is a modification that this layer can store safely.
FVector BoundsMin, BoundsMax;
ClampedMod.GetWorldBounds(BoundsMin, BoundsMax);
auto WorldToChunk = [](float WorldCoord, int32& OutChunk) -> bool
{
if (!VoxelMath::IsFinite(WorldCoord))
{
return false;
}
const double ChunkCoord = FMath::FloorToDouble(
static_cast<double>(WorldCoord) / static_cast<double>(CHUNK_SIZE));
if (!VoxelMath::IsFinite(ChunkCoord)
|| ChunkCoord < static_cast<double>(MIN_int32)
|| ChunkCoord > static_cast<double>(MAX_int32))
{
return false;
}
OutChunk = static_cast<int32>(ChunkCoord);
return true;
};
int32 MinCX = 0, MaxCX = 0, MinCY = 0, MaxCY = 0, MinCZ = 0, MaxCZ = 0;
if (!WorldToChunk(BoundsMin.X, MinCX) || !WorldToChunk(BoundsMax.X, MaxCX)
|| !WorldToChunk(BoundsMin.Y, MinCY) || !WorldToChunk(BoundsMax.Y, MaxCY)
|| !WorldToChunk(BoundsMin.Z, MinCZ) || !WorldToChunk(BoundsMax.Z, MaxCZ))
{
UE_LOG(LogTemp, Warning,
TEXT("[DiffLayer] Modification REJECTED — brush bounds are not representable"));
return TArray<FIntVector>();
}
// Update budget counters
ModificationCount++;
const double BrushVolume = GetModificationBudgetVolume(ClampedMod);
AccumulatedVolume += static_cast<float>(BrushVolume);
{
// Write lock: blocks worker-thread readers (HasModifications / GetDensityOffset) while the map
// is mutated/rehashed. AffectedChunks is local; populate it in the same pass.
FWriteScopeLock Lock(ModsLock);
for (int32 CZ = MinCZ;; ++CZ)
{
for (int32 CY = MinCY;; ++CY)
{
for (int32 CX = MinCX;; ++CX)
{
FIntVector ChunkCoord(CX, CY, CZ);
// Store the modification in this chunk's list
ChunkMods.FindOrAdd(ChunkCoord).Add(ClampedMod);
// Track which chunks need re-meshing
AffectedChunks.Add(ChunkCoord);
if (CX == MaxCX) break;
}
if (CY == MaxCY) break;
}
if (CZ == MaxCZ) break;
}
// Publish: subsequent readers must now take the lock instead of fast-rejecting, and
// worker-side snapshots keyed on the version re-copy their chunk's list.
bHasAnyMods.store(true, std::memory_order_release);
ModsVersion.fetch_add(1, std::memory_order_release);
}
UE_LOG(LogTemp, Log,
TEXT("[DiffLayer] Modification #%d at (%.0f, %.0f, %.0f) R=%.1f S=%.1f -> %d chunks (budget: %d/%d mods, %.0f/%.0f vol)"),
ModificationCount,
ClampedMod.Center.X, ClampedMod.Center.Y, ClampedMod.Center.Z,
ClampedMod.Radius, ClampedMod.Strength,
AffectedChunks.Num(),
ModificationCount, BudgetMaxMods,
AccumulatedVolume, BudgetMaxVolume);
return AffectedChunks;
}
//=============================================================================
// DENSITY EVALUATION
//=============================================================================
float UVoxelDiffLayer::GetDensityOffset(const FIntVector& ChunkCoord,
float WorldX, float WorldY, float WorldZ) const
{
// Lock-free fast reject: no carves anywhere -> nothing to offset.
if (!bHasAnyMods.load(std::memory_order_acquire)) return 0.0f;
// Hold the read lock for the whole body: Mods points INTO the map and is dereferenced through the
// falloff loop, so the map must not be rehashed by a concurrent writer meanwhile. NOTE: hot-path
// callers (the generator) should prefer GetChunkModsSnapshot + EvaluateMods — one lock per chunk
// instead of one per voxel.
FReadScopeLock Lock(ModsLock);
const TArray<FVoxelModification>* Mods = ChunkMods.Find(ChunkCoord);
if (!Mods || Mods->Num() == 0) return 0.0f;
return EvaluateMods(*Mods, WorldX, WorldY, WorldZ);
}
void UVoxelDiffLayer::GetChunkModsSnapshot(const FIntVector& ChunkCoord, TArray<FVoxelModification>& Out) const
{
Out.Reset();
if (!bHasAnyMods.load(std::memory_order_acquire)) return;
FReadScopeLock Lock(ModsLock);
if (const TArray<FVoxelModification>* Mods = ChunkMods.Find(ChunkCoord))
{
Out = *Mods;
}
}
bool UVoxelDiffLayer::HasAnyModInChunkRange(const FIntVector& MinChunk, const FIntVector& MaxChunk) const
{
if (!bHasAnyMods.load(std::memory_order_acquire)) return false;
if (MinChunk.X > MaxChunk.X || MinChunk.Y > MaxChunk.Y || MinChunk.Z > MaxChunk.Z)
{
return false;
}
// ApplyModification registers a mod in every chunk its AABB overlaps, so looking up the keys in
// the queried range is conservative and proportional to that range. Do not walk ChunkMods:
// the edit history can grow without bound while a sealed-solid candidate is being proved.
FReadScopeLock Lock(ModsLock);
for (int32 CZ = MinChunk.Z;; ++CZ)
{
for (int32 CY = MinChunk.Y;; ++CY)
{
for (int32 CX = MinChunk.X;; ++CX)
{
if (const TArray<FVoxelModification>* Mods = ChunkMods.Find(FIntVector(CX, CY, CZ)))
{
if (Mods->Num() > 0) return true;
}
if (CX == MaxChunk.X) break;
}
if (CY == MaxChunk.Y) break;
}
if (CZ == MaxChunk.Z) break;
}
return false;
}
float UVoxelDiffLayer::EvaluateMods(const TArray<FVoxelModification>& Mods,
float WorldX, float WorldY, float WorldZ)
{
float TotalOffset = 0.0f;
const FVector Pos(WorldX, WorldY, WorldZ);
// smoothstep helper (full strength when t<=0, zero when t>=1).
auto Smooth01 = [](float T) -> float
{
T = FMath::Clamp(T, 0.0f, 1.0f);
return 1.0f - T * T * (3.0f - 2.0f * T);
};
for (const FVoxelModification& Mod : Mods)
{
float Falloff = 0.0f;
switch (Mod.Shape)
{
case EVoxelBrushShape::Box:
{
// Box SDF (negative inside), then fade over the Falloff band outside.
const FVector Q = (Pos - Mod.Center).GetAbs() - Mod.BoxExtent;
const float Outside = FVector(FMath::Max(Q.X, 0.0f), FMath::Max(Q.Y, 0.0f), FMath::Max(Q.Z, 0.0f)).Size();
const float Inside = FMath::Min(FMath::Max3(Q.X, Q.Y, Q.Z), 0.0f);
const float S = Outside + Inside; // <=0 inside the box
const float Band = FMath::Max(Mod.Falloff, 0.01f);
if (S >= Band) continue;
Falloff = Smooth01(FMath::Max(S, 0.0f) / Band);
break;
}
case EVoxelBrushShape::Capsule:
{
// Distance to the segment minus the tube radius, faded over Falloff.
const FVector AB = Mod.CapsuleEnd - Mod.Center;
const FVector AP = Pos - Mod.Center;
const float T = FMath::Clamp(FVector::DotProduct(AP, AB) / FMath::Max(FVector::DotProduct(AB, AB), KINDA_SMALL_NUMBER), 0.0f, 1.0f);
const float SegDist = FVector::Dist(Pos, Mod.Center + AB * T);
const float S = SegDist - Mod.Radius; // <=0 inside the tube
const float Band = FMath::Max(Mod.Falloff, 0.01f);
if (S >= Band) continue;
Falloff = Smooth01(FMath::Max(S, 0.0f) / Band);
break;
}
case EVoxelBrushShape::Sphere:
default:
{
const float Dist = FVector::Dist(Pos, Mod.Center);
if (Dist >= Mod.Radius) continue;
Falloff = Smooth01(Dist / Mod.Radius);
break;
}
}
TotalOffset += Mod.Strength * Falloff;
}
return TotalOffset;
}
bool UVoxelDiffLayer::HasModifications(const FIntVector& ChunkCoord) const
{
// Lock-free fast reject: no carves anywhere -> the map is empty, skip lock + lookup entirely.
if (!bHasAnyMods.load(std::memory_order_acquire)) return false;
FReadScopeLock Lock(ModsLock); // worker threads read concurrently; serialised vs. writers
const TArray<FVoxelModification>* Mods = ChunkMods.Find(ChunkCoord);
return Mods && Mods->Num() > 0;
}
//=============================================================================
// MANAGEMENT
//=============================================================================
void UVoxelDiffLayer::Clear()
{
int32 Count = GetTotalModificationCount();
{
// Write lock: workers may be mid-read. Flip the fast-path flag false BEFORE emptying so any
// reader that loads it afterwards skips the map without locking.
FWriteScopeLock Lock(ModsLock);
bHasAnyMods.store(false, std::memory_order_release);
ChunkMods.Empty();
ModsVersion.fetch_add(1, std::memory_order_release); // invalidate worker snapshots
}
// Reset budget counters — player gets a fresh budget after clear/season reset
ModificationCount = 0;
AccumulatedVolume = 0.0f;
UE_LOG(LogTemp, Log, TEXT("[DiffLayer] Cleared %d modifications, budget reset"), Count);
}
int32 UVoxelDiffLayer::GetTotalModificationCount() const
{
FReadScopeLock Lock(ModsLock);
int32 Total = 0;
for (const auto& Pair : ChunkMods)
{
Total += Pair.Value.Num();
}
return Total;
}
int32 UVoxelDiffLayer::GetModifiedChunkCount() const
{
FReadScopeLock Lock(ModsLock);
return ChunkMods.Num();
}
@@ -0,0 +1,54 @@
// VoxelForgeModule.cpp
// Module implementation - boilerplate
#include "VoxelForgeModule.h"
#include "VoxelStackSampler.h"
#include "VoxelGenerator.h"
#include "VoxelMarchingCubesMesher.h"
#include "Misc/CommandLine.h"
#include "Misc/Parse.h"
#if PLATFORM_WINDOWS
#include <math.h>
#endif
// This macro registers our module with Unreal
// "VoxelForge" must match the module name in VoxelForge.Build.cs and .uplugin
IMPLEMENT_MODULE(FVoxelForgeModule, VoxelForge)
void FVoxelForgeModule::StartupModule()
{
// Called when plugin loads
// Keep this diagnostic before any plugin-owned generation can begin. The UCRT may select
// its FMA3 math implementations during process startup; this switch lets the canonical export
// compare that path with the non-FMA3 implementation on the same machine.
#if PLATFORM_WINDOWS
const int32 Fma3Before = _get_FMA3_enable();
int32 RequestedFma3 = 0;
const bool bHasFma3Override = FParse::Value(
FCommandLine::Get(), TEXT("voxel.CrtFma3="), RequestedFma3);
if (bHasFma3Override)
{
_set_FMA3_enable(RequestedFma3 != 0 ? 1 : 0);
}
const int32 Fma3After = _get_FMA3_enable();
UE_LOG(LogTemp, Log,
TEXT("VoxelForge CRT FMA3 diagnostic: before=%d override=%s requested=%d after=%d"),
Fma3Before,
bHasFma3Override ? TEXT("yes") : TEXT("no"),
RequestedFma3,
Fma3After);
#endif
// Resolve the command line once while the module is starting, before any generation task is
// submitted, instead of lazily from worker-side density/mesher calls.
VoxelGenLOD::InitializeConsoleSwitches();
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
UE_LOG(LogTemp, Log, TEXT("VoxelForge module started!"));
}
void FVoxelForgeModule::ShutdownModule()
{
// Called when plugin unloads
FVoxelStackSampler::Get().Shutdown();
UE_LOG(LogTemp, Log, TEXT("VoxelForge module shutdown."));
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,497 @@
// VoxelHeightOpStack.cpp
// Les cinq opérateurs d'espace-hauteur de SurfaceWorld.
// The five height-space operators of SurfaceWorld.
//
// FIDÉLITÉ / FIDELITY
// Chaque corps est une transcription LITTÉRALE du bloc correspondant de
// `SampleSurfaceStructuralZ` / `ComputeSurfaceTerrainZ` — mêmes offsets, mêmes octaves, même ordre
// d'opérations flottantes. Depuis que `FPSemantics = Precise` est posé (AUDIT §C9), l'égalité
// BIT À BIT est atteignable et atteinte pour Maze et Slab : c'est donc la barre ici aussi, et
// `VoxelForge.OpStack.SurfaceHeightEquivalence` la vérifie.
//
// ⚠️ LE DÉTOUR PAR `FVector` EST DÉLIBÉRÉ, comme ailleurs dans ce refactor : `FractalNoise3D` prend
// un `FVector` (donc des DOUBLES en UE5) et re-descend en float. Passer directement des floats
// saute un arrondi. Reproduire le détour, c'est reproduire l'arrondi.
// The FVector round-trip is deliberate: FVector is double in UE5, so the original rounds through a
// double. Going straight through floats skips a rounding step.
#include "VoxelHeightOp.h"
#include "VoxelCaveMorphology.h" // VoxelHash::SeedOffset — AUDIT §C1 (bounded, site-salted)
#include "VoxelNoise.h" // VoxelNoise::FBM / Ridged / Perlin3D
#include "VoxelTypes.h" // SmoothStep01, VOXEL_NOISE_SCALE
namespace
{
//=========================================================================
// HELPERS — les mêmes enveloppes que VoxelGenerator.cpp, transcrites
//=========================================================================
// `FractalNoise3D` et `RidgedNoise3D` sont `static` dans VoxelGenerator.cpp, donc invisibles
// ici. Elles sont recopiées à l'identique plutôt qu'exportées : les exporter changerait leur
// contexte d'inlining, et sous /fp:precise comme sous /fp:fast la règle est la même — on ne
// touche à rien de ce qui entoure une expression flottante qu'on veut reproduire.
FORCEINLINE float HFractalNoise3D(const FVector& Position, int32 Octaves = 4,
float Lacunarity = 2.0f, float Persistence = 0.5f)
{
return VoxelNoise::FBM((float)Position.X, (float)Position.Y, (float)Position.Z,
Octaves, Lacunarity, Persistence);
}
FORCEINLINE float HRidgedNoise3D(const FVector& Position, int32 Octaves = 4,
float Lacunarity = 2.0f, float Persistence = 0.5f)
{
return VoxelNoise::Ridged((float)Position.X, (float)Position.Y, (float)Position.Z,
Octaves, Lacunarity, Persistence);
}
/** Transcription de `UVoxelGenerator::SampleRelief`. Champ [0,1] partagé avec la carte de
* biomes, pour que la géographie et le terrain qu'elle module restent d'accord. */
FORCEINLINE float HSampleRelief(float WorldX, float WorldY, uint32 SeedU,
float Frequency, float Contrast)
{
float R = HFractalNoise3D(FVector(
WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 7.3f),
WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 2.1f),
VoxelHash::SeedOffset(SeedU, 0.5f)), 2) * 0.5f + 0.5f; // [0,1]
R = FMath::Clamp((R - 0.5f) * Contrast + 0.5f, 0.0f, 1.0f);
return SmoothStep01(R);
}
//=========================================================================
// SOURCE — CHAMP STRUCTUREL / STRUCTURAL HEIGHT FIELD
//=========================================================================
// Continents + montagnes + détail, sous une frame de domain-warp. Produit les DEUX canaux.
class FStructuralHeightSource final : public IVoxelHeightOp
{
public:
FStructuralHeightSource(const FSurfaceGenerationParams& InP, int32 InSeed)
: P(InP), SeedU((uint32)InSeed) {}
void Eval(float WorldX, float WorldY, FVoxelHeightSample& InOut) const override
{
float M = 1.0f;
InOut.Height = SampleZ(WorldX, WorldY, M); // Replace : racine de pile
InOut.Relief = M;
}
/**
* Le champ nu, exposé parce que `FCliffHeightMod` doit le RÉ-ÉCHANTILLONNER en différences
* centrées. C'est une dépendance réelle du code d'origine (`ComputeSurfaceTerrainZ` appelle
* `SampleSurfaceStructuralZ` quatre fois de plus), pas un raccourci : la pente doit venir du
* champ STRUCTUREL, sans rétroaction des ops, sinon le cliff se nourrirait de lui-même.
*/
float SampleZ(float WorldX, float WorldY, float& OutM) const
{
const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ;
const float BottomZ = P.StrateBottomWorldZ;
const float GroundBase = BottomZ + H * P.BaseGroundRelative;
// Domain-warp des coords STRUCTURELLES (continents + montagnes). Le bruit de détail
// reste sur le vrai XY pour que les bosses fines restent nettes et décorrélées.
float QX = WorldX, QY = WorldY;
if (P.HeightWarpStrength > 0.0f)
{
const float WF = P.HeightWarpFrequency;
const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.31f), WorldY * WF + 4.2f, VoxelHash::SeedOffset(SeedU, 1.7f)));
const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 8.6f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.53f), VoxelHash::SeedOffset(SeedU, 2.9f)));
QX += wx * VOXEL_NOISE_SCALE * P.HeightWarpStrength;
QY += wy * VOXEL_NOISE_SCALE * P.HeightWarpStrength;
}
const float Relief = HSampleRelief(WorldX, WorldY, SeedU, P.ReliefFrequency, P.ReliefContrast);
const float M = FMath::Lerp(1.0f, Relief, P.ReliefStrength);
float Cont = HFractalNoise3D(FVector(
QX * P.ContinentFrequency + VoxelHash::SeedOffset(SeedU, 3.1f),
QY * P.ContinentFrequency + VoxelHash::SeedOffset(SeedU, 5.7f),
VoxelHash::SeedOffset(SeedU, 0.7f)), 4); // [-1,1]
float Detail = HFractalNoise3D(FVector(
WorldX * P.DetailFrequency + 11.0f,
WorldY * P.DetailFrequency + 22.0f,
VoxelHash::SeedOffset(SeedU, 1.3f)), 3); // [-1,1]
float Mountain = 0.0f;
if (P.MountainStrength > 0.0f)
{
float Ridge = HRidgedNoise3D(FVector(
QX * P.MountainFrequency + 99.0f,
QY * P.MountainFrequency + 77.0f,
VoxelHash::SeedOffset(SeedU, 0.9f)), 4); // [-1,1]
Ridge = Ridge * 0.5f + 0.5f; // [0,1] sommets
Mountain = Ridge * P.MountainStrength * M; // les montagnes ne montent qu'en haut relief
}
// Les plaines gardent une fraction du gonflement continental ; les hautes terres tout.
const float ContScale = FMath::Lerp(0.45f, 1.0f, M);
float Terrain = GroundBase
+ Cont * P.ElevationRange * 0.5f * ContScale
+ Mountain * P.ElevationRange
+ Detail * P.SurfaceRoughness;
OutM = M;
return Terrain;
}
// Une SOURCE pose l'altitude, elle ne la déplace pas : la notion de « déplacement max » ne
// s'applique pas. La borne d'une colonne se calcule à partir de la source elle-même
// (GroundBase ± ElevationRange ± SurfaceRoughness), pas ici — d'où FLT_MAX, honnête.
float MaxDisplacement() const override { return FLT_MAX; }
private:
FSurfaceGenerationParams P;
uint32 SeedU;
};
//=========================================================================
// MOD — FALAISE / CLIFF (raidissement conditionné par la pente)
//=========================================================================
class FCliffHeightMod final : public IVoxelHeightOp
{
public:
FCliffHeightMod(const FSurfaceGenerationParams& InP, const FStructuralHeightSource* InSrc)
: P(InP), Src(InSrc) {}
void Eval(float WorldX, float WorldY, FVoxelHeightSample& InOut) const override
{
if (P.CliffStrength <= 0.0f || Src == nullptr) { return; }
const float D = FMath::Max(P.CliffSampleDist, 0.5f);
float Ms; // relief scratch — on ne veut que les hauteurs
const float Zxp = Src->SampleZ(WorldX + D, WorldY, Ms);
const float Zxm = Src->SampleZ(WorldX - D, WorldY, Ms);
const float Zyp = Src->SampleZ(WorldX, WorldY + D, Ms);
const float Zym = Src->SampleZ(WorldX, WorldY - D, Ms);
const float dZdX = (Zxp - Zxm) / (2.0f * D);
const float dZdY = (Zyp - Zym) / (2.0f * D);
const float Slope = FMath::Sqrt(dZdX * dZdX + dZdY * dZdY);
const float Thr = FMath::Max(P.CliffSlopeThreshold, 0.05f);
const float SlopeGate = FMath::Clamp((Slope - Thr) / Thr, 0.0f, 1.0f);
if (SlopeGate > 0.0f)
{
const float Ref = 0.25f * (Zxp + Zxm + Zyp + Zym);
const float Gain = P.CliffStrength * SlopeGate * P.CliffSharpness;
InOut.Height += (InOut.Height - Ref) * Gain;
}
}
private:
FSurfaceGenerationParams P;
const FStructuralHeightSource* Src;
};
//=========================================================================
// MOD — TERRASSES / TERRACE (gaté par le relief : le canal Relief sert ICI)
//=========================================================================
class FTerraceHeightMod final : public IVoxelHeightOp
{
public:
explicit FTerraceHeightMod(const FSurfaceGenerationParams& InP) : P(InP) {}
void Eval(float, float, FVoxelHeightSample& InOut) const override
{
if (P.TerraceStrength <= 0.0f || P.TerraceHeight <= 0.0f) { return; }
const float StepH = P.TerraceHeight;
const float T = InOut.Height / StepH;
const float K = FMath::FloorToFloat(T);
const float Frac = T - K;
const float W = FMath::Lerp(0.5f, 0.03f, FMath::Clamp(P.TerraceHardness, 0.0f, 1.0f));
const float Fs = SmoothStep01(FMath::Clamp((Frac - (0.5f - W)) / (2.0f * W), 0.0f, 1.0f));
const float Stepped = (K + Fs) * StepH;
// `* InOut.Relief` : c'est le `* M` de l'original — la raison d'être du second canal.
InOut.Height = FMath::Lerp(InOut.Height, Stepped, P.TerraceStrength * InOut.Relief);
}
// Le terrace interpole VERS une hauteur quantifiée : l'écart ne dépasse jamais un palier.
float MaxDisplacement() const override
{
return (P.TerraceStrength > 0.0f) ? FMath::Max(P.TerraceHeight, 0.0f) : 0.0f;
}
private:
FSurfaceGenerationParams P;
};
//=========================================================================
// MOD — LIGNES DE STRATES / LAYER LINES
//=========================================================================
class FLayerLineHeightMod final : public IVoxelHeightOp
{
public:
explicit FLayerLineHeightMod(const FSurfaceGenerationParams& InP) : P(InP) {}
void Eval(float, float, FVoxelHeightSample& InOut) const override
{
if (P.LayerLineDepth <= 0.0f || P.LayerLineSpacing <= 0.0f) { return; }
const float Phase = InOut.Height * (2.0f * PI / P.LayerLineSpacing);
InOut.Height -= VoxelMath::DetSin(Phase) * P.LayerLineDepth;
}
// `sin` ∈ [-1,1] ⇒ borne exacte.
float MaxDisplacement() const override
{
return (P.LayerLineSpacing > 0.0f) ? FMath::Max(P.LayerLineDepth, 0.0f) : 0.0f;
}
private:
FSurfaceGenerationParams P;
};
//=========================================================================
// MOD — PLAGE / BEACH (aplatissement vers la ligne d'eau)
//=========================================================================
class FBeachHeightMod final : public IVoxelHeightOp
{
public:
explicit FBeachHeightMod(const FSurfaceGenerationParams& InP) : P(InP) {}
void Eval(float, float, FVoxelHeightSample& InOut) const override
{
// Le niveau d'eau est GLOBAL à la strate (forcé depuis la strate) pour que le plan
// d'eau reste continu — d'où le calcul depuis les bornes de strate, pas depuis un param
// par biome.
const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ;
const float WaterZ = P.StrateBottomWorldZ + H * P.WaterLevelRelative;
if (P.WaterLevelRelative <= 0.0f || P.BeachWidth <= 0.0f) { return; }
const float DAbs = FMath::Abs(InOut.Height - WaterZ);
if (DAbs < P.BeachWidth)
{
float T = SmoothStep01(DAbs / P.BeachWidth);
InOut.Height = FMath::Lerp(WaterZ, InOut.Height, T);
}
}
// N'agit que dans `BeachWidth` de l'eau, et ne fait qu'y RAPPROCHER.
float MaxDisplacement() const override
{
return (P.WaterLevelRelative > 0.0f) ? FMath::Max(P.BeachWidth, 0.0f) : 0.0f;
}
private:
FSurfaceGenerationParams P;
};
//=========================================================================
// SOURCE — LE CIEL / SKY CAP (c'est une ALTITUDE, donc c'est un op de hauteur)
//=========================================================================
// `ComputeSurfaceCeiling` rend un Z, exactement comme le terrain. `OPSTACK-DECOMPOSITION §5` le
// range en `FSkyCapSource` côté DENSITÉ (« Subtract »), mais c'est le même glissement que pour
// les ops de terrain : ce que la fonction produit est une hauteur, et la soustraction n'arrive
// qu'après, dans le combine. Le mettre ici lui donne gratuitement l'invariance de fenêtre
// testée, la pureté XY garantie par le type, et le cache de colonne.
// The sky cap returns a Z, so it belongs in height space; the subtraction happens later, in the
// density-side combine.
class FSkyCapHeightSource final : public IVoxelHeightOp
{
public:
FSkyCapHeightSource(const FSurfaceGenerationParams& InP, int32 InSeed)
: P(InP), SeedU((uint32)InSeed) {}
void Eval(float WorldX, float WorldY, FVoxelHeightSample& InOut) const override
{
const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ;
float CeilZ = P.StrateBottomWorldZ + H * P.CeilingRelative;
// Domain-warp des coords larges/ridge (miroir du HeightWarp du sol). Les bosses fines
// restent sur le vrai XY pour rester nettes et décorrélées. 0 ⇒ pas de warp.
float QX = WorldX, QY = WorldY;
if (P.CeilingWarpStrength > 0.0f)
{
const float WF = P.CeilingWarpFrequency;
const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.71f), WorldY * WF + 2.3f, VoxelHash::SeedOffset(SeedU, 3.3f)));
const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 6.1f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.19f), VoxelHash::SeedOffset(SeedU, 4.7f)));
QX += wx * VOXEL_NOISE_SCALE * P.CeilingWarpStrength;
QY += wy * VOXEL_NOISE_SCALE * P.CeilingWarpStrength;
}
// Gonflement large SIGNÉ : monte/descend toute la voûte.
if (P.CeilingUndulation > 0.0f)
{
const float Swell = HFractalNoise3D(FVector(
QX * P.CeilingUndulationFrequency + VoxelHash::SeedOffset(SeedU, 1.9f),
QY * P.CeilingUndulationFrequency + 13.0f,
VoxelHash::SeedOffset(SeedU, 0.5f)), 3); // [-1,1]
CeilZ += Swell * VOXEL_NOISE_SCALE * P.CeilingUndulation;
}
// Pendage vers le BAS uniquement : tout est >= 0, donc rien ne perce vers le haut dans
// le seal. Bosses fines + lames ridgées s'additionnent.
float Hang = 0.0f;
if (P.CeilingRoughness > 0.0f)
{
Hang += FMath::Abs(HFractalNoise3D(FVector(
WorldX * P.CeilingRoughnessFrequency + 5.0f,
WorldY * P.CeilingRoughnessFrequency + 6.0f,
VoxelHash::SeedOffset(SeedU, 2.1f)), 3)) * VOXEL_NOISE_SCALE * P.CeilingRoughness;
}
if (P.CeilingRidgeStrength > 0.0f)
{
float Ridge = HRidgedNoise3D(FVector(
QX * P.CeilingRidgeFrequency + 31.0f,
QY * P.CeilingRidgeFrequency + 47.0f,
VoxelHash::SeedOffset(SeedU, 1.1f)), 4); // [-1,1]
Ridge = Ridge * 0.5f + 0.5f; // [0,1] lignes de crête pendantes
Hang += Ridge * P.CeilingRidgeStrength;
}
InOut.Height = CeilZ - Hang; // Replace : racine de sa propre pile
// Relief laissé intact : le ciel n'en produit pas et personne ne le lui demande.
}
float MaxDisplacement() const override { return FLT_MAX; } // source, pas modificateur
private:
FSurfaceGenerationParams P;
uint32 SeedU;
};
//=========================================================================
// COMBINER `Mask` — MÉLANGE DE BIOMES / BIOME BLEND
//=========================================================================
// Une pile complète par biome ; le champ dit lequel domine ; on interpole les HAUTEURS.
//
// ⚠️ Chaque pile calcule SON PROPRE relief `M` en interne et l'utilise pour son propre gate de
// terrace — exactement comme l'original, où `ComputeSurfaceTerrainZ(X, Y, *PD)` et
// `(…, *PN)` sont deux appels complets et indépendants dont seules les SORTIES sont mêlées.
// Le canal `Relief` qui ressort ici est celui du DOMINANT : il est informatif, personne en aval
// ne s'en sert pour re-gater quoi que ce soit.
//
// Each biome stack computes its own relief internally and gates its own terrace with it, exactly
// as the original makes two independent full calls and blends only the OUTPUTS.
class FBiomeBlendHeightSource final : public IVoxelHeightOp
{
public:
FBiomeBlendHeightSource(const TArray<FSurfaceGenerationParams>& PerBiome, int32 Seed,
const IVoxelBiomeField* InField, bool bCeilingOnly)
: Field(InField)
{
Stacks.Reserve(PerBiome.Num());
for (const FSurfaceGenerationParams& BP : PerBiome)
{
FVoxelHeightStack S;
if (bCeilingOnly) { VoxelHeightOps::BuildSurfaceCeilingStack(S, BP, Seed); }
else { VoxelHeightOps::BuildSurfaceHeightStack(S, BP, Seed); }
Stacks.Add(MoveTemp(S));
}
bBlend = !bCeilingOnly; // le plafond SÉLECTIONNE, il ne mélange pas
}
void Eval(float WorldX, float WorldY, FVoxelHeightSample& InOut) const override
{
if (Stacks.Num() == 0) { return; }
FVoxelBiomeWeights W;
if (Field) { W = Field->SampleAt(WorldX, WorldY); }
const int32 D = Stacks.IsValidIndex(W.Dominant) ? W.Dominant : 0;
InOut = Stacks[D].EvalSample(WorldX, WorldY);
// Le plafond ne se mélange pas (voir la fabrique) ; le sol si, et seulement dans la
// bande de frontière où le poids est non nul.
if (bBlend && W.NeighborWeight > 0.0f && Stacks.IsValidIndex(W.Neighbor))
{
const float HN = Stacks[W.Neighbor].EvalHeight(WorldX, WorldY);
InOut.Height = FMath::Lerp(InOut.Height, HN, W.NeighborWeight);
}
}
float MaxDisplacement() const override { return FLT_MAX; } // source composite
private:
TArray<FVoxelHeightStack> Stacks;
const IVoxelBiomeField* Field;
bool bBlend = true;
};
} // ⚠️ FIN DU NAMESPACE ANONYME — TOUT NOUVEL OPÉRATEUR SE MET AU-DESSUS DE CETTE LIGNE.
// En dessous commence `namespace VoxelHeightOps` (les fabriques). Y insérer une classe la sort
// de la liaison interne, et l'accolade qu'on ajoute avec elle ne ferme rien → C2059. Erreur
// commise DEUX fois (7cd2bed, puis à nouveau ici) en s'ancrant sur la bannière « FABRIQUES »,
// qui est de l'autre côté de cette accolade.
// END OF THE ANONYMOUS NAMESPACE — new operators go ABOVE this line. Anchoring on the FACTORIES
// banner below puts them outside it, and the brace added with them closes nothing.
//=============================================================================
// FABRIQUES / FACTORIES
//=============================================================================
namespace VoxelHeightOps
{
TUniquePtr<IVoxelHeightOp> MakeBiomeBlendHeightSource(
const TArray<FSurfaceGenerationParams>& PerBiomeParams, int32 Seed,
const IVoxelBiomeField* Field)
{
return MakeUnique<FBiomeBlendHeightSource>(PerBiomeParams, Seed, Field, /*bCeilingOnly*/false);
}
TUniquePtr<IVoxelHeightOp> MakeBiomeSelectCeilingSource(
const TArray<FSurfaceGenerationParams>& PerBiomeParams, int32 Seed,
const IVoxelBiomeField* Field)
{
return MakeUnique<FBiomeBlendHeightSource>(PerBiomeParams, Seed, Field, /*bCeilingOnly*/true);
}
TUniquePtr<IVoxelHeightOp> MakeSkyCapHeightSource(const FSurfaceGenerationParams& P, int32 Seed)
{
return MakeUnique<FSkyCapHeightSource>(P, Seed);
}
void BuildSurfaceCeilingStack(FVoxelHeightStack& OutStack, const FSurfaceGenerationParams& P, int32 Seed)
{
// Un seul op aujourd'hui — et c'est une information, pas un manque : le plafond n'a pas
// d'équivalent des quatre modificateurs du sol. Le jour où on veut des terrasses au
// plafond, on ajoute la ligne ; c'est exactement le genre de composition que le refactor
// existe pour rendre possible.
OutStack.Add(MakeSkyCapHeightSource(P, Seed));
}
TUniquePtr<IVoxelHeightOp> MakeStructuralHeightSource(const FSurfaceGenerationParams& P, int32 Seed,
const IVoxelHeightOp** OutSource)
{
TUniquePtr<FStructuralHeightSource> Src = MakeUnique<FStructuralHeightSource>(P, Seed);
if (OutSource) { *OutSource = Src.Get(); }
return Src;
}
TUniquePtr<IVoxelHeightOp> MakeCliffHeightMod(const FSurfaceGenerationParams& P,
const IVoxelHeightOp* StructuralSource)
{
// `static_cast` plutôt que `Cast<>` : ce ne sont pas des UObject, et le contrat de la
// fabrique est qu'on lui rend exactement le pointeur sorti de MakeStructuralHeightSource.
return MakeUnique<FCliffHeightMod>(
P, static_cast<const FStructuralHeightSource*>(StructuralSource));
}
TUniquePtr<IVoxelHeightOp> MakeTerraceHeightMod(const FSurfaceGenerationParams& P)
{
return MakeUnique<FTerraceHeightMod>(P);
}
TUniquePtr<IVoxelHeightOp> MakeLayerLineHeightMod(const FSurfaceGenerationParams& P)
{
return MakeUnique<FLayerLineHeightMod>(P);
}
TUniquePtr<IVoxelHeightOp> MakeBeachHeightMod(const FSurfaceGenerationParams& P)
{
return MakeUnique<FBeachHeightMod>(P);
}
void BuildSurfaceHeightStack(FVoxelHeightStack& OutStack, const FSurfaceGenerationParams& P, int32 Seed)
{
// L'ORDRE EST CELUI DE `ComputeSurfaceTerrainZ`, et il porte du sens :
// le cliff raidit le champ brut, le terrace quantifie le résultat raidi, les lignes de
// strates se posent dessus, et la plage écrase tout près de l'eau.
const IVoxelHeightOp* Structural = nullptr;
OutStack.Add(MakeStructuralHeightSource(P, Seed, &Structural));
OutStack.Add(MakeCliffHeightMod(P, Structural));
OutStack.Add(MakeTerraceHeightMod(P));
OutStack.Add(MakeLayerLineHeightMod(P));
OutStack.Add(MakeBeachHeightMod(P));
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,135 @@
#include "VoxelSeasonAsset.h"
#include "VoxelSeasonManifest.h"
#include "VoxelDensityOpStack.h"
#if WITH_EDITOR
#include "Misc/FileHelper.h"
#endif
namespace
{
bool ValidateRuntimeRecipes(const FVoxelSeasonManifest& Manifest, FString& OutReport)
{
for (const FVoxelSeasonStrate& Strate : Manifest.Strates)
{
if (!Strate.bUsesRecipe) continue;
FVoxelOpStack ValidationStack;
FVoxelOpContext ValidationContext;
FString BuildError;
if (!VF_BuildSeasonStrateStack(
Strate, Manifest.OriginSpineRadius, nullptr,
ValidationStack, ValidationContext, &BuildError))
{
OutReport = FString::Printf(
TEXT("season slot %d recipe is not materialisable: %s"),
Strate.DepthIndex, *BuildError);
return false;
}
}
return true;
}
}
FPrimaryAssetId UVoxelSeasonAsset::GetPrimaryAssetId() const
{
return FPrimaryAssetId(TEXT("VoxelSeason"), GetFName());
}
bool UVoxelSeasonAsset::LoadManifest(FVoxelSeasonManifest& OutManifest, FString& OutReport) const
{
if (ManifestJson.IsEmpty())
{
OutReport = TEXT("season asset contains no imported manifest JSON");
OutManifest = FVoxelSeasonManifest();
return false;
}
if (!VF_DeserializeVoxelSeasonManifest(ManifestJson, OutManifest, OutReport))
{
return false;
}
if (!OutManifest.ContentHash.Equals(ManifestContentHash, ESearchCase::IgnoreCase)
|| OutManifest.SchemaVersion != SchemaVersion
|| OutManifest.Season != SeasonNumber
|| OutManifest.Seed != SeasonSeed
|| FMemory::Memcmp(&OutManifest.OriginSpineRadius,
&OriginSpineRadius, sizeof(float)) != 0
|| FMemory::Memcmp(&OutManifest.WorldRadiusVoxels,
&WorldRadiusVoxels, sizeof(float)) != 0)
{
OutReport = TEXT("season asset metadata does not match its embedded manifest; re-import the JSON");
OutManifest.bValid = false;
OutManifest.Error = OutReport;
return false;
}
if (!ValidateRuntimeRecipes(OutManifest, OutReport))
{
OutManifest.bValid = false;
OutManifest.Error = OutReport;
return false;
}
return true;
}
bool UVoxelSeasonAsset::SetManifestJson(const FString& InManifestJson, FString& OutReport)
{
FVoxelSeasonManifest Parsed;
if (!VF_DeserializeVoxelSeasonManifest(InManifestJson, Parsed, OutReport))
{
return false;
}
if (!ValidateRuntimeRecipes(Parsed, OutReport))
{
return false;
}
Modify();
ManifestJson = InManifestJson;
SchemaVersion = Parsed.SchemaVersion;
SeasonNumber = Parsed.Season;
SeasonSeed = Parsed.Seed;
OriginSpineRadius = Parsed.OriginSpineRadius;
WorldRadiusVoxels = Parsed.WorldRadiusVoxels;
ManifestContentHash = Parsed.ContentHash;
ReferencedDefinitions.Reset();
TSet<FString> SeenDefinitionPaths;
for (const FVoxelSeasonStrate& Strate : Parsed.Strates)
{
if (!Strate.SourceDefinitionPath.IsEmpty()
&& !SeenDefinitionPaths.Contains(Strate.SourceDefinitionPath))
{
SeenDefinitionPaths.Add(Strate.SourceDefinitionPath);
ReferencedDefinitions.Add(TSoftObjectPtr<UVoxelStrateDefinition>(
FSoftObjectPath(Strate.SourceDefinitionPath)));
}
}
MarkPackageDirty();
OutReport = FString::Printf(
TEXT("imported season %d seed %d (%d strates, hash %s)"),
SeasonNumber, SeasonSeed, Parsed.Strates.Num(), *ManifestContentHash);
return true;
}
void UVoxelSeasonAsset::ImportSeasonManifestJson()
{
#if WITH_EDITOR
FString JsonText;
FString Report;
if (SourceManifestJson.FilePath.IsEmpty()
|| !FFileHelper::LoadFileToString(JsonText, *SourceManifestJson.FilePath))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelSeasonAsset] Could not read manifest JSON '%s'."),
*SourceManifestJson.FilePath);
return;
}
if (!SetManifestJson(JsonText, Report))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelSeasonAsset] Import failed: %s"), *Report);
return;
}
UE_LOG(LogTemp, Log, TEXT("[VoxelSeasonAsset] %s"), *Report);
#else
UE_LOG(LogTemp, Error, TEXT("[VoxelSeasonAsset] JSON import is editor-only."));
#endif
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,51 @@
#include "VoxelSettings.h"
#include "VoxelSeasonAsset.h"
int32 UVoxelSettings::GetEffectiveWorldSeed() const
{
if (!Season.IsNull())
{
if (const UVoxelSeasonAsset* Asset = Season.LoadSynchronous())
{
return Asset->GetSeasonSeed();
}
}
return Seed;
}
float UVoxelSettings::GetEffectiveOriginSpineRadius() const
{
if (!Season.IsNull())
{
if (const UVoxelSeasonAsset* Asset = Season.LoadSynchronous())
{
return Asset->GetOriginSpineRadius();
}
}
return OriginSpineRadius;
}
float UVoxelSettings::GetEffectiveWorldRadiusVoxels() const
{
if (!Season.IsNull())
{
if (const UVoxelSeasonAsset* Asset = Season.LoadSynchronous())
{
return Asset->GetWorldRadiusVoxels();
}
}
return WorldRadiusVoxels;
}
int32 UVoxelSettings::GetEffectiveSeasonNumber() const
{
if (!Season.IsNull())
{
if (const UVoxelSeasonAsset* Asset = Season.LoadSynchronous())
{
return Asset->GetSeasonNumber();
}
}
return CurrentSeason;
}
@@ -0,0 +1,23 @@
// VoxelStats.cpp
// Definitions for the VoxelForge runtime statistics.
// Définitions des statistiques runtime de VoxelForge.
#include "VoxelStats.h"
DEFINE_STAT(STAT_VoxelForgeTilesClassified);
DEFINE_STAT(STAT_VoxelForgeTilesSkippedAllSolid);
DEFINE_STAT(STAT_VoxelForgeTilesSkippedAllAir);
DEFINE_STAT(STAT_VoxelForgeTilesMeshed);
DEFINE_STAT(STAT_VoxelForgeTilesOpStackSolid);
DEFINE_STAT(STAT_VoxelForgeTilesOpStackAir);
DEFINE_STAT(STAT_VoxelForgeCaveMixOutOfLayout);
DEFINE_STAT(STAT_VoxelForgeCaveMixGap);
DEFINE_STAT(STAT_VoxelForgeCaveMixSurfaceWorld);
DEFINE_STAT(STAT_VoxelForgeCaveBailTwoCaveSlots);
DEFINE_STAT(STAT_VoxelForgeCaveBailParams);
DEFINE_STAT(STAT_VoxelForgeCaveBailStackVerdict);
DEFINE_STAT(STAT_VoxelForgeCaveBailFoldConflict);
DEFINE_STAT(STAT_VoxelForgeCaveBailDisturbance);
DEFINE_STAT(STAT_VoxelForgeCaveBailNoStack);
DEFINE_STAT(STAT_VoxelForgeColumnMemoHit);
DEFINE_STAT(STAT_VoxelForgeColumnMemoMiss);
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,79 @@
// Serialized strate-definition migrations.
#include "VoxelStrateDefinition.h"
#include "Serialization/CustomVersion.h"
#include "VoxelPassageGeometry.h"
namespace
{
// The field name is retained for package compatibility, but version 1 changes its meaning
// from a raw tangent/gradient to authored degrees. This GUID is local to the plugin.
const FGuid GVoxelForgeStrateSerializationVersion(
0xA76D5E31, 0x4B2C4F9A, 0xB8C1D204, 0x6E9F7738);
constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = 1;
FCustomVersionRegistration GVoxelForgeStrateSerializationRegistration(
GVoxelForgeStrateSerializationVersion,
GVoxelForgeStrateGentleSlopeDegreesVersion,
TEXT("VoxelForgeStrateSerialization"));
float LegacyGradientToDegrees(float LegacyGradient)
{
if (!VoxelMath::IsFinite(LegacyGradient) || LegacyGradient <= 0.0f)
{
return 0.0f;
}
if (LegacyGradient >= 4096.0f)
{
return VoxelPassageGeometry::TunnelFloorGentleSlopeMaximumDegrees;
}
// Invert the old gradient without atan/tan. The same deterministic sine/cosine helper
// used by generation makes this migration independent of the host CRT.
float LowDegrees = 0.0f;
float HighDegrees = VoxelPassageGeometry::TunnelFloorGentleSlopeMaximumDegrees;
for (int32 Iteration = 0; Iteration < 32; ++Iteration)
{
const float MidDegrees = (LowDegrees + HighDegrees) * 0.5f;
float SinValue = 0.0f;
float CosValue = 0.0f;
VoxelMath::DetSinCos(
SinValue, CosValue, MidDegrees * (PI / 180.0f));
const float MidGradient = FMath::Min(
FMath::Abs(SinValue) / FMath::Max(FMath::Abs(CosValue), 1.0e-3f),
4096.0f);
if (MidGradient < LegacyGradient)
{
LowDegrees = MidDegrees;
}
else
{
HighDegrees = MidDegrees;
}
}
return FMath::Clamp(
(LowDegrees + HighDegrees) * 0.5f,
0.0f,
VoxelPassageGeometry::TunnelFloorGentleSlopeMaximumDegrees);
}
}
void UVoxelStrateDefinition::PostLoad()
{
Super::PostLoad();
// The field remains reflected solely so old packages deserialize without shifting the
// following members. Tunnel geometry owns the floor now, so every serialized legacy value
// is normalized to the ignored compatibility default before any runtime copy is made.
GenerationParams.bTunnelFloorEnabled = true;
if (GetLinkerCustomVersion(GVoxelForgeStrateSerializationVersion)
>= GVoxelForgeStrateGentleSlopeDegreesVersion)
{
return;
}
const float LegacyGradient = GenerationParams.TunnelFloorGentleSlopeThreshold;
GenerationParams.TunnelFloorGentleSlopeThreshold =
LegacyGradientToDegrees(LegacyGradient);
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,87 @@
// VoxelTerrainOpDefinition.cpp
// Applies a terrain operation's parameters to the generation params struct.
#include "VoxelTerrainOpDefinition.h"
void UVoxelTerrainOpDefinition::ApplyTo(FStrateGenerationParams& OutParams, float Weight) const
{
// Weight scales the "primary" field of each op type — the one that controls
// whether the op is active and how strong it is. Other fields (radii, spacing,
// ratios) are copied directly since they define shape, not intensity.
switch (Type)
{
case EVoxelTerrainOpType::Terrace:
// TerraceStepHeight is the activation field (0 = disabled)
OutParams.TerraceStepHeight = TerraceStepHeight * Weight;
OutParams.TerraceHardness = TerraceHardness;
OutParams.TerraceNoiseDisplacement = TerraceNoiseDisplacement;
break;
case EVoxelTerrainOpType::LayerLines:
// LayerLineSpacing is the activation field (0 = disabled)
OutParams.LayerLineSpacing = LayerLineSpacing * Weight;
OutParams.LayerLineDepth = LayerLineDepth;
break;
case EVoxelTerrainOpType::Ribbing:
// RibbingSpacing is the activation field (0 = disabled)
OutParams.RibbingSpacing = RibbingSpacing * Weight;
OutParams.RibbingDepth = RibbingDepth;
break;
case EVoxelTerrainOpType::Cliff:
OutParams.CliffStrength = CliffStrength * Weight;
break;
case EVoxelTerrainOpType::Scallop:
OutParams.ScallopStrength = ScallopStrength * Weight;
OutParams.ScallopFrequency = ScallopFrequency;
break;
case EVoxelTerrainOpType::Overhang:
OutParams.OverhangStrength = OverhangStrength * Weight;
OutParams.OverhangDepth = OverhangDepth;
OutParams.OverhangFrequency = OverhangFrequency;
break;
case EVoxelTerrainOpType::Arch:
OutParams.ArchDensity = ArchDensity * Weight;
OutParams.ArchMinRadius = ArchMinRadius;
OutParams.ArchMaxRadius = ArchMaxRadius;
break;
case EVoxelTerrainOpType::Column:
OutParams.ColumnDensity = ColumnDensity * Weight;
OutParams.ColumnMinRadius = ColumnMinRadius;
OutParams.ColumnMaxRadius = ColumnMaxRadius;
break;
case EVoxelTerrainOpType::Pit:
OutParams.PitDensity = PitDensity * Weight;
OutParams.PitMinRadius = PitMinRadius;
OutParams.PitMaxRadius = PitMaxRadius;
OutParams.PitDepth = PitDepth;
break;
case EVoxelTerrainOpType::Chimney:
OutParams.ChimneyDensity = ChimneyDensity * Weight;
OutParams.ChimneyMinRadius = ChimneyMinRadius;
OutParams.ChimneyMaxRadius = ChimneyMaxRadius;
OutParams.ChimneyHeight = ChimneyHeight;
break;
case EVoxelTerrainOpType::Dome:
OutParams.DomeDensity = DomeDensity * Weight;
OutParams.DomeMinRadius = DomeMinRadius;
OutParams.DomeMaxRadius = DomeMaxRadius;
OutParams.DomeHeightRatio = DomeHeightRatio;
break;
case EVoxelTerrainOpType::Pinch:
OutParams.PinchDensity = PinchDensity * Weight;
OutParams.PinchStrength = PinchStrength;
OutParams.PinchLength = PinchLength;
break;
}
}
@@ -0,0 +1,88 @@
// VoxelWormField.cpp
// Shared deterministic exact worm-field evaluation.
#include "VoxelWormField.h"
#include "VoxelCaveMorphology.h" // VoxelHash::SeedOffset
#include "VoxelNoise.h"
#include "VoxelTypes.h" // VOXEL_NOISE_SCALE
namespace
{
FORCEINLINE FVector3f MakeWormNoiseBase(
float WorldX,
float WorldY,
float WorldZ,
const VoxelWormField::FParameters& Parameters)
{
const float EffectiveZ = (Parameters.VerticalScale != 1.0f
&& Parameters.VerticalScale > 0.0f)
? WorldZ / Parameters.VerticalScale : WorldZ;
const float WormZFrequency = Parameters.Frequency * Parameters.HorizontalBias;
return FVector3f(
WorldX * Parameters.Frequency
+ VoxelHash::SeedOffset(Parameters.Seed, 1.0f),
WorldY * Parameters.Frequency
+ VoxelHash::SeedOffset(Parameters.Seed, 1.7f),
EffectiveZ * WormZFrequency
+ VoxelHash::SeedOffset(Parameters.Seed, 2.3f));
}
FORCEINLINE float EvaluateN1Field(
float WorldX,
float WorldY,
float WorldZ,
const VoxelWormField::FParameters& Parameters,
float NoiseScale)
{
const FVector3f NoiseBase = MakeWormNoiseBase(
WorldX, WorldY, WorldZ, Parameters);
return FMath::Abs(VoxelNoise::Perlin3D(NoiseBase) * NoiseScale);
}
FORCEINLINE float EvaluateN2Field(
float WorldX,
float WorldY,
float WorldZ,
const VoxelWormField::FParameters& Parameters,
float NoiseScale)
{
const FVector3f NoiseBase = MakeWormNoiseBase(
WorldX, WorldY, WorldZ, Parameters);
const FVector3f N2Position = NoiseBase + FVector3f(137.0f, 259.0f, 431.0f);
return FMath::Abs(VoxelNoise::Perlin3D(N2Position) * NoiseScale);
}
FORCEINLINE float EvaluateExactField(
float WorldX,
float WorldY,
float WorldZ,
float Threshold,
const VoxelWormField::FParameters& Parameters)
{
const float NoiseScale = VOXEL_NOISE_SCALE;
const float N1 = EvaluateN1Field(
WorldX, WorldY, WorldZ, Parameters, NoiseScale);
// N2 is non-negative, so this preserves the exact evaluator's short-circuit.
if (N1 >= Threshold)
{
return N1;
}
return N1 + EvaluateN2Field(
WorldX, WorldY, WorldZ, Parameters, NoiseScale);
}
}
namespace VoxelWormField
{
float Evaluate(
float WorldX,
float WorldY,
float WorldZ,
float Threshold,
const FParameters& Parameters)
{
return EvaluateExactField(WorldX, WorldY, WorldZ, Threshold, Parameters);
}
}
@@ -0,0 +1,344 @@
// MarchingCubesTables.h
// Standard lookup tables for the Marching Cubes algorithm
//
// These tables are reference data — everyone uses the same ones.
// Originally from Paul Bourke's Marching Cubes implementation.
//
// CUBE CORNER LAYOUT:
// -------------------
// 4-------5
// /| /|
// / | / |
// 7-------6 |
// | 0----|--1
// | / | /
// |/ |/
// 3-------2
//
// Corner indices and their positions relative to (X, Y, Z):
// 0: (0, 0, 0) 4: (0, 0, 1)
// 1: (1, 0, 0) 5: (1, 0, 1)
// 2: (1, 1, 0) 6: (1, 1, 1)
// 3: (0, 1, 0) 7: (0, 1, 1)
//
// EDGE INDICES:
// -------------
// Edge 0: corner 0 - corner 1 (bottom front)
// Edge 1: corner 1 - corner 2 (bottom right)
// Edge 2: corner 2 - corner 3 (bottom back)
// Edge 3: corner 3 - corner 0 (bottom left)
// Edge 4: corner 4 - corner 5 (top front)
// Edge 5: corner 5 - corner 6 (top right)
// Edge 6: corner 6 - corner 7 (top back)
// Edge 7: corner 7 - corner 4 (top left)
// Edge 8: corner 0 - corner 4 (vertical left-front)
// Edge 9: corner 1 - corner 5 (vertical right-front)
// Edge 10: corner 2 - corner 6 (vertical right-back)
// Edge 11: corner 3 - corner 7 (vertical left-back)
#pragma once
#include "CoreMinimal.h"
// EdgeTable[caseIndex] = 12-bit bitmask
// Each bit tells you if that edge is intersected by the surface.
// Example: EdgeTable[1] = 0x109 = 0000 0001 0000 1001
// → edges 0, 3, and 8 are intersected
static const int32 MCEdgeTable[256] = {
0x000, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
0x190, 0x099, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90,
0x230, 0x339, 0x033, 0x13a, 0x636, 0x73f, 0x435, 0x53c,
0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
0x3a0, 0x2a9, 0x1a3, 0x0aa, 0x7a6, 0x6af, 0x5a5, 0x4ac,
0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0,
0x460, 0x569, 0x663, 0x76a, 0x066, 0x16f, 0x265, 0x36c,
0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60,
0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0x0ff, 0x3f5, 0x2fc,
0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x055, 0x15c,
0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950,
0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0x0cc,
0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0,
0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc,
0x0cc, 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c,
0x15c, 0x055, 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650,
0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
0x2fc, 0x3f5, 0x0ff, 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0,
0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c,
0x36c, 0x265, 0x16f, 0x066, 0x76a, 0x663, 0x569, 0x460,
0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac,
0x4ac, 0x5a5, 0x6af, 0x7a6, 0x0aa, 0x1a3, 0x2a9, 0x3a0,
0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c,
0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x033, 0x339, 0x230,
0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x099, 0x190,
0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x000
};
// TriTable[caseIndex][up to 16]
// Lists edge indices that form triangles, in groups of 3.
// -1 terminates the list.
// Example: TriTable[1] = {0, 8, 3, -1, ...}
// → one triangle using edges 0, 8, and 3
static const int32 MCTriTable[256][16] = {
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 2, 8, 3, 2, 10, 8, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1},
{ 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 11, 2, 8, 11, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 11, 2, 1, 9, 11, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1},
{ 3, 10, 1, 11, 10, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 10, 1, 0, 8, 10, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1},
{ 3, 9, 0, 3, 11, 9, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1},
{ 9, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 3, 0, 7, 3, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 1, 9, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 1, 9, 4, 7, 1, 7, 3, 1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 4, 7, 3, 0, 4, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1},
{ 9, 2, 10, 9, 0, 2, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1},
{ 2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4, -1, -1, -1, -1},
{ 8, 4, 7, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{11, 4, 7, 11, 2, 4, 2, 0, 4, -1, -1, -1, -1, -1, -1, -1},
{ 9, 0, 1, 8, 4, 7, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1},
{ 4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1, -1, -1, -1, -1},
{ 3, 10, 1, 3, 11, 10, 7, 8, 4, -1, -1, -1, -1, -1, -1, -1},
{ 1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4, -1, -1, -1, -1},
{ 4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3, -1, -1, -1, -1},
{ 4, 7, 11, 4, 11, 9, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1},
{ 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 5, 4, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 5, 4, 1, 5, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 8, 5, 4, 8, 3, 5, 3, 1, 5, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 0, 8, 1, 2, 10, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1},
{ 5, 2, 10, 5, 4, 2, 4, 0, 2, -1, -1, -1, -1, -1, -1, -1},
{ 2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8, -1, -1, -1, -1},
{ 9, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 11, 2, 0, 8, 11, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1},
{ 0, 5, 4, 0, 1, 5, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1},
{ 2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5, -1, -1, -1, -1},
{10, 3, 11, 10, 1, 3, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1},
{ 4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10, -1, -1, -1, -1},
{ 5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3, -1, -1, -1, -1},
{ 5, 4, 8, 5, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1},
{ 9, 7, 8, 5, 7, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 3, 0, 9, 5, 3, 5, 7, 3, -1, -1, -1, -1, -1, -1, -1},
{ 0, 7, 8, 0, 1, 7, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1},
{ 1, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 7, 8, 9, 5, 7, 10, 1, 2, -1, -1, -1, -1, -1, -1, -1},
{10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3, -1, -1, -1, -1},
{ 8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2, -1, -1, -1, -1},
{ 2, 10, 5, 2, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1},
{ 7, 9, 5, 7, 8, 9, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1},
{ 9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11, -1, -1, -1, -1},
{ 2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7, -1, -1, -1, -1},
{11, 2, 1, 11, 1, 7, 7, 1, 5, -1, -1, -1, -1, -1, -1, -1},
{ 9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11, -1, -1, -1, -1},
{ 5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0, -1},
{11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0, -1},
{11, 10, 5, 7, 11, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 0, 1, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 8, 3, 1, 9, 8, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1},
{ 1, 6, 5, 2, 6, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 6, 5, 1, 2, 6, 3, 0, 8, -1, -1, -1, -1, -1, -1, -1},
{ 9, 6, 5, 9, 0, 6, 0, 2, 6, -1, -1, -1, -1, -1, -1, -1},
{ 5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8, -1, -1, -1, -1},
{ 2, 3, 11, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{11, 0, 8, 11, 2, 0, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1},
{ 0, 1, 9, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1},
{ 5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11, -1, -1, -1, -1},
{ 6, 3, 11, 6, 5, 3, 5, 1, 3, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6, -1, -1, -1, -1},
{ 3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9, -1, -1, -1, -1},
{ 6, 5, 9, 6, 9, 11, 11, 9, 8, -1, -1, -1, -1, -1, -1, -1},
{ 5, 10, 6, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 3, 0, 4, 7, 3, 6, 5, 10, -1, -1, -1, -1, -1, -1, -1},
{ 1, 9, 0, 5, 10, 6, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1},
{10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4, -1, -1, -1, -1},
{ 6, 1, 2, 6, 5, 1, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7, -1, -1, -1, -1},
{ 8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6, -1, -1, -1, -1},
{ 7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9, -1},
{ 3, 11, 2, 7, 8, 4, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1},
{ 5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11, -1, -1, -1, -1},
{ 0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1},
{ 9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6, -1},
{ 8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6, -1, -1, -1, -1},
{ 5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11, -1},
{ 0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7, -1},
{ 6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9, -1, -1, -1, -1},
{10, 4, 9, 6, 4, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 10, 6, 4, 9, 10, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1},
{10, 0, 1, 10, 6, 0, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1},
{ 8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10, -1, -1, -1, -1},
{ 1, 4, 9, 1, 2, 4, 2, 6, 4, -1, -1, -1, -1, -1, -1, -1},
{ 3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4, -1, -1, -1, -1},
{ 0, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 8, 3, 2, 8, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1},
{10, 4, 9, 10, 6, 4, 11, 2, 3, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6, -1, -1, -1, -1},
{ 3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10, -1, -1, -1, -1},
{ 6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1, -1},
{ 9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3, -1, -1, -1, -1},
{ 8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1, -1},
{ 3, 11, 6, 3, 6, 0, 0, 6, 4, -1, -1, -1, -1, -1, -1, -1},
{ 6, 4, 8, 11, 6, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 7, 10, 6, 7, 8, 10, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1},
{ 0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10, -1, -1, -1, -1},
{10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0, -1, -1, -1, -1},
{10, 6, 7, 10, 7, 1, 1, 7, 3, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7, -1, -1, -1, -1},
{ 2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9, -1},
{ 7, 8, 0, 7, 0, 6, 6, 0, 2, -1, -1, -1, -1, -1, -1, -1},
{ 7, 3, 2, 6, 7, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7, -1, -1, -1, -1},
{ 2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7, -1},
{ 1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11, -1},
{11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1, -1, -1, -1, -1},
{ 8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6, -1},
{ 0, 9, 1, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0, -1, -1, -1, -1},
{ 7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 0, 8, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 1, 9, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 8, 1, 9, 8, 3, 1, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1},
{10, 1, 2, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, 3, 0, 8, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1},
{ 2, 9, 0, 2, 10, 9, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1},
{ 6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8, -1, -1, -1, -1},
{ 7, 2, 3, 6, 2, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 7, 0, 8, 7, 6, 0, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1},
{ 2, 7, 6, 2, 3, 7, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1},
{ 1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6, -1, -1, -1, -1},
{10, 7, 6, 10, 1, 7, 1, 3, 7, -1, -1, -1, -1, -1, -1, -1},
{10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8, -1, -1, -1, -1},
{ 0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7, -1, -1, -1, -1},
{ 7, 6, 10, 7, 10, 8, 8, 10, 9, -1, -1, -1, -1, -1, -1, -1},
{ 6, 8, 4, 11, 8, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 6, 11, 3, 0, 6, 0, 4, 6, -1, -1, -1, -1, -1, -1, -1},
{ 8, 6, 11, 8, 4, 6, 9, 0, 1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6, -1, -1, -1, -1},
{ 6, 8, 4, 6, 11, 8, 2, 10, 1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6, -1, -1, -1, -1},
{ 4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9, -1, -1, -1, -1},
{10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3, -1},
{ 8, 2, 3, 8, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1},
{ 0, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8, -1, -1, -1, -1},
{ 1, 9, 4, 1, 4, 2, 2, 4, 6, -1, -1, -1, -1, -1, -1, -1},
{ 8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1, -1, -1, -1, -1},
{10, 1, 0, 10, 0, 6, 6, 0, 4, -1, -1, -1, -1, -1, -1, -1},
{ 4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3, -1},
{10, 9, 4, 6, 10, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 9, 5, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, 4, 9, 5, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1},
{ 5, 0, 1, 5, 4, 0, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1},
{11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5, -1, -1, -1, -1},
{ 9, 5, 4, 10, 1, 2, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1},
{ 6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5, -1, -1, -1, -1},
{ 7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2, -1, -1, -1, -1},
{ 3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6, -1},
{ 7, 2, 3, 7, 6, 2, 5, 4, 9, -1, -1, -1, -1, -1, -1, -1},
{ 9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7, -1, -1, -1, -1},
{ 3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0, -1, -1, -1, -1},
{ 6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8, -1},
{ 9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7, -1, -1, -1, -1},
{ 1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4, -1},
{ 4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10, -1},
{ 7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10, -1, -1, -1, -1},
{ 6, 9, 5, 6, 11, 9, 11, 8, 9, -1, -1, -1, -1, -1, -1, -1},
{ 3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5, -1, -1, -1, -1},
{ 0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11, -1, -1, -1, -1},
{ 6, 11, 3, 6, 3, 5, 5, 3, 1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6, -1, -1, -1, -1},
{ 0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10, -1},
{11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5, -1},
{ 6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3, -1, -1, -1, -1},
{ 5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2, -1, -1, -1, -1},
{ 9, 5, 6, 9, 6, 0, 0, 6, 2, -1, -1, -1, -1, -1, -1, -1},
{ 1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8, -1},
{ 1, 5, 6, 2, 1, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6, -1},
{10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0, -1, -1, -1, -1},
{ 0, 3, 8, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{11, 5, 10, 7, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{11, 5, 10, 11, 7, 5, 8, 3, 0, -1, -1, -1, -1, -1, -1, -1},
{ 5, 11, 7, 5, 10, 11, 1, 9, 0, -1, -1, -1, -1, -1, -1, -1},
{10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1, -1, -1, -1, -1},
{11, 1, 2, 11, 7, 1, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11, -1, -1, -1, -1},
{ 9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7, -1, -1, -1, -1},
{ 7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2, -1},
{ 2, 5, 10, 2, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1},
{ 8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5, -1, -1, -1, -1},
{ 9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2, -1, -1, -1, -1},
{ 9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2, -1},
{ 1, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 7, 0, 7, 1, 1, 7, 5, -1, -1, -1, -1, -1, -1, -1},
{ 9, 0, 3, 9, 3, 5, 5, 3, 7, -1, -1, -1, -1, -1, -1, -1},
{ 9, 8, 7, 5, 9, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 5, 8, 4, 5, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1},
{ 5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0, -1, -1, -1, -1},
{ 0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5, -1, -1, -1, -1},
{10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4, -1},
{ 2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8, -1, -1, -1, -1},
{ 0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11, -1},
{ 0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5, -1},
{ 9, 4, 5, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4, -1, -1, -1, -1},
{ 5, 10, 2, 5, 2, 4, 4, 2, 0, -1, -1, -1, -1, -1, -1, -1},
{ 3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9, -1},
{ 5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2, -1, -1, -1, -1},
{ 8, 4, 5, 8, 5, 3, 3, 5, 1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 4, 5, 1, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5, -1, -1, -1, -1},
{ 9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 11, 7, 4, 9, 11, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11, -1, -1, -1, -1},
{ 1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11, -1, -1, -1, -1},
{ 3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4, -1},
{ 4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2, -1, -1, -1, -1},
{ 9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3, -1},
{11, 7, 4, 11, 4, 2, 2, 4, 0, -1, -1, -1, -1, -1, -1, -1},
{11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4, -1, -1, -1, -1},
{ 2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9, -1, -1, -1, -1},
{ 9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7, -1},
{ 3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10, -1},
{ 1, 10, 2, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 9, 1, 4, 1, 7, 7, 1, 3, -1, -1, -1, -1, -1, -1, -1},
{ 4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1, -1, -1, -1, -1},
{ 4, 0, 3, 7, 4, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 0, 9, 3, 9, 11, 11, 9, 10, -1, -1, -1, -1, -1, -1, -1},
{ 0, 1, 10, 0, 10, 8, 8, 10, 11, -1, -1, -1, -1, -1, -1, -1},
{ 3, 1, 10, 11, 3, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 11, 1, 11, 9, 9, 11, 8, -1, -1, -1, -1, -1, -1, -1},
{ 3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9, -1, -1, -1, -1},
{ 0, 2, 11, 8, 0, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 2, 3, 8, 2, 8, 10, 10, 8, 9, -1, -1, -1, -1, -1, -1, -1},
{ 9, 10, 2, 0, 9, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8, -1, -1, -1, -1},
{ 1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 3, 8, 9, 1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}
};
@@ -0,0 +1,159 @@
// VoxelBiomeDefinition.h
// Data asset defining one biome: where it lives (climate), how it reshapes the base
// terrain (modulation), and what content/atmosphere it brings.
//
// HOW TO USE:
// -----------
// 1. Content Browser → Miscellaneous → Data Asset → "VoxelBiomeDefinition".
// 2. Set its climate box (ReliefMin/Max, MoistureMin/Max), its terrain override, and its
// Decorations / atmosphere / water.
// 3. Add it to a UVoxelStrateDefinition's Biomes[] list.
//
// A biome is generator-agnostic: the same asset works inside any archetype strate
// (surface biomes today, cave biomes later). The strate's base archetype params are
// the baseline; this asset modulates on top.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "GameplayTagContainer.h"
#include "VoxelBiomeTypes.h"
#include "VoxelStrateTypes.h" // FStrateDecoration / FStrateAmbientActor
#include "VoxelBiomeDefinition.generated.h"
// IWYU : utilisé en pointeur seulement ⇒ déclaration avant. Le PCH partagé ne la fournit pas :
// `FPSemantics = Precise` nous en sort. / Pointer-only use, so a forward declaration is enough.
// The shared PCH does not provide it (FPSemantics = Precise opts this module out of it).
class UMaterialInterface;
/**
* UVoxelBiomeDefinition — one biome's identity, placement, terrain modulation and content.
*/
UCLASS(BlueprintType)
class VOXELFORGE_API UVoxelBiomeDefinition : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
//=========================================================================
// IDENTITY
//=========================================================================
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Biome")
FText BiomeName;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Biome", meta = (MultiLine = true))
FText BiomeDescription;
// Colour used by the 2D biome-preview bake to identify this biome's regions.
// Pick something distinct per biome so the baked map is readable.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome")
FLinearColor DebugColor = FLinearColor(0.5f, 0.5f, 0.5f, 1.0f);
//=========================================================================
// CLIMATE PLACEMENT
//=========================================================================
// A biome wins the Voronoi cells whose site climate (relief, moisture) falls inside
// — or, failing any match, nearest to — this box. Both axes are [0,1].
// Mountains / snow → high ReliefMin. Lush forest → high Moisture.
// Desert → low Moisture. Plains → mid relief, any moisture.
// Boxes may overlap; ties are broken deterministically per cell.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Climate", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float ReliefMin = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Climate", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float ReliefMax = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Climate", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float MoistureMin = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Climate", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float MoistureMax = 1.0f;
//=========================================================================
// TERRAIN OVERRIDE — a biome is a "mini-strate-variant"
//=========================================================================
// When enabled, this biome supplies its OWN archetype params for its regions,
// replacing the strate's. Heightfield archetypes (SurfaceWorld) blend the output
// across biome borders, so even frequency/shape differences stay seamless.
// Global/structural fields (strate Z bounds, boundary seal, base density, water
// level) are always forced from the strate — a biome can't break seals/connectivity.
//
// GeneratorType must match the strate this biome is used in; if it doesn't, the
// override is ignored and the strate's params are used. (Currently wired: SurfaceWorld.
// Other archetypes' param overrides arrive as each is hooked up — see CODEMAP §8.14.)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Terrain")
bool bOverrideTerrain = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Terrain",
meta = (EditCondition = "bOverrideTerrain"))
ECaveGeneratorType GeneratorType = ECaveGeneratorType::SurfaceWorld;
// Open-sky terrain override (used when bOverrideTerrain && GeneratorType == SurfaceWorld).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Terrain",
meta = (EditCondition = "bOverrideTerrain && GeneratorType == ECaveGeneratorType::SurfaceWorld"))
FSurfaceGenerationParams SurfaceParams;
//=========================================================================
// CONTENT PROFILE (consumed by the content / atmosphere managers)
//=========================================================================
// Decorations placed in this biome's regions. When set, these REPLACE the strate's
// decorations for chunks whose dominant biome is this one (falls back to the strate
// list when empty).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Content")
TArray<FStrateDecoration> Decorations;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Content")
TArray<FStrateAmbientActor> AmbientActors;
//=========================================================================
// ATMOSPHERE OVERRIDE (optional — beats the strate's atmosphere for this biome)
//=========================================================================
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Atmosphere")
bool bOverrideAtmosphere = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Atmosphere", meta = (EditCondition = "bOverrideAtmosphere"))
FLinearColor FogColor = FLinearColor(0.05f, 0.05f, 0.1f, 1.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Atmosphere", meta = (EditCondition = "bOverrideAtmosphere", ClampMin = "0.0", ClampMax = "1.0"))
float FogDensity = 0.3f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Atmosphere", meta = (EditCondition = "bOverrideAtmosphere"))
FLinearColor AmbientLightColor = FLinearColor(0.1f, 0.1f, 0.15f, 1.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Atmosphere", meta = (EditCondition = "bOverrideAtmosphere", ClampMin = "0.0"))
float AmbientLightIntensity = 0.5f;
//=========================================================================
// WATER
//=========================================================================
// Water material override for this biome (null = use the strate's WaterMaterial).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Water")
UMaterialInterface* WaterMaterial = nullptr;
//=========================================================================
// MATERIAL — F6 vertex-colour triplanar palette
//=========================================================================
// Which palette layer this biome's terrain uses in the master triplanar material.
// It is baked into the mesh vertex colour (R channel) at mesh time, so ONE material
// re-skins the terrain per biome and cross-fades across biome borders (the neighbour
// biome's index + a blend weight ride in the A/B channels). The terrain material is
// still the strate's OverrideMaterial / Settings->VoxelMaterial — author it as the
// master palette material and switch its layers on this index. 0 = default layer.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Material", meta = (ClampMin = "0", ClampMax = "255"))
int32 MaterialPaletteIndex = 0;
//=========================================================================
// GAMEPLAY TAGS
//=========================================================================
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Gameplay")
FGameplayTagContainer GameplayTags;
};
@@ -0,0 +1,264 @@
// VoxelBiomeTypes.h
// Shared vocabulary for the biome system (Stage 1).
//
// A biome is a "mini-strate-variant": it can carry a FULL archetype param override
// (e.g. its own FSurfaceGenerationParams) plus a content profile, placed across the
// world by a deterministic, window-invariant XY field (warped Voronoi + climate).
//
// Resolution discipline (protects CODEMAP §8.4 + §8.10):
// - The biome assigned at any XY is a PURE function of world coords + seed + the
// strate's biome context. No dependence on the chunk window.
// - Heightfield archetypes (Surface) resolve dominant + neighbour biome per voxel and
// blend the OUTPUT height — so ANY param difference (even frequencies) stays seamless,
// which per-param blending could never do. The expensive climate classification is
// cached once per chunk; per voxel is just a warp + 3x3 lookup.
// - Structural/global fields (Z bounds, seal, base density, water level) are always
// forced from the STRATE so seals / spine / passages / water plane stay intact.
#pragma once
#include "CoreMinimal.h"
#include "VoxelBiomeTypes.generated.h"
class UVoxelBiomeDefinition;
/**
* FBiomeMapParams — controls the world-XY biome field: how big biome regions are,
* how organic their borders look, and the two climate fields that ASSIGN a biome to
* each Voronoi cell. Lives on UVoxelStrateDefinition (one map per strate).
*/
USTRUCT(BlueprintType)
struct VOXELFORGE_API FBiomeMapParams
{
GENERATED_BODY()
// Average biome cell size in voxels. Larger = bigger, sweeping biome regions.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Map", meta = (ClampMin = "32.0"))
float CellSize = 800.0f;
// Domain-warp the cell lookup by up to this many voxels → organic, non-hexagonal
// borders. 0 = raw Voronoi cells (straight-ish borders).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Map", meta = (ClampMin = "0.0"))
float WarpStrength = 250.0f;
// Frequency of the border-warp noise. Lower = broader, sweeping border bends.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Map")
float WarpFrequency = 0.0018f;
// Width (voxels) of the smooth blend band between neighbouring biomes. The density
// scalars cross-fade across this band so there is no hard wall at a biome border.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Map", meta = (ClampMin = "0.0"))
float BorderBlend = 120.0f;
// ----- Climate fields (assign each cell its biome) -----
//
// IMPORTANT: for COHERENT geography (mountain ranges, desert regions — not confetti)
// the climate must vary much more SLOWLY than CellSize: aim for ~4-6 cells per climate
// feature, i.e. ClimateFrequency ≈ 1 / (5 * CellSize). If the climate wavelength is
// near or below CellSize, neighbouring cells sample unrelated climate → salt-and-pepper.
// Relief ("elevation") field, [0,1]. Mountain / snow biomes live where this is high.
// Default tuned for CellSize≈800 (wavelength ~3300 ≈ 4 cells). Lower → bigger regions.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Climate")
float ReliefFrequency = 0.0003f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Climate", meta = (ClampMin = "0.25", ClampMax = "4.0"))
float ReliefContrast = 2.0f;
// Moisture field, [0,1] — the second climate axis (wet lowlands vs arid). Same
// coherence rule as relief: keep the wavelength several cells wide.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome|Climate")
float MoistureFrequency = 0.0004f;
};
/**
* Channel selector for the 2D biome-preview bake (AVoxelWorld::BakeBiomePreview).
*/
UENUM(BlueprintType)
enum class EBiomePreviewChannel : uint8
{
Biome UMETA(DisplayName = "Biome (debug colours)"),
Relief UMETA(DisplayName = "Relief / elevation field"),
Moisture UMETA(DisplayName = "Moisture field")
};
/**
* FVoxelBiomeQuery — a Blueprint-readable snapshot of the biome field at one world point.
* Returned by AVoxelWorld::GetBiomeAtWorldLocation so BP can read "which biome is under the
* cursor" — the SAME resolution the decoration scatter uses per column. DecorationCount is the
* effective number of decorations the dominant biome would place (its own list, or the strate's
* when empty): 0 here explains a "band of nothing" — that biome simply has no decorations.
*/
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelBiomeQuery
{
GENERATED_BODY()
// False when the point's strate has no biome field (then only the climate fields are filled).
UPROPERTY(BlueprintReadOnly, Category = "Biome") bool bHasBiomes = false;
// Dominant biome asset at this point (what a decoration column here primarily uses). May be null.
UPROPERTY(BlueprintReadOnly, Category = "Biome") UVoxelBiomeDefinition* DominantBiome = nullptr;
// Nearest neighbouring biome (the one a border blend fades toward). May be null / same as dominant.
UPROPERTY(BlueprintReadOnly, Category = "Biome") UVoxelBiomeDefinition* NeighborBiome = nullptr;
// Dominant biome display name (falls back to the asset name when BiomeName is unset).
UPROPERTY(BlueprintReadOnly, Category = "Biome") FText DominantName;
// Dominant biome's preview/debug colour (handy to tint a BP debug draw to match the bake).
UPROPERTY(BlueprintReadOnly, Category = "Biome") FLinearColor DebugColor = FLinearColor::Black;
// Border blend weight toward the neighbour: 0 deep inside a cell → ~0.5 at the shared border.
UPROPERTY(BlueprintReadOnly, Category = "Biome") float NeighborWeight = 0.0f;
// Climate fields at this XY, both [0,1] (always filled, even when bHasBiomes is false).
UPROPERTY(BlueprintReadOnly, Category = "Biome") float Relief = 0.0f;
UPROPERTY(BlueprintReadOnly, Category = "Biome") float Moisture = 0.0f;
// Effective decoration count for the dominant biome (its list, else the strate's). 0 ⇒ empty band.
UPROPERTY(BlueprintReadOnly, Category = "Biome") int32 DominantDecorationCount = 0;
// Dominant biome's index in the strate's biome context (matches the decoration column tag). -1 = none.
UPROPERTY(BlueprintReadOnly, Category = "Biome") int32 DominantContextIndex = -1;
//--- DECORATION STREAMING STATE of the region covering this point (debug "why is this chunk blank?") ---
// Discriminates the three failure modes for a visibly-bare patch:
// • bDecoRegionApplied && DecoAppliedInstances > 0 → instances EXIST + are uploaded; if still not
// visible it is a RENDER drop (proxy/culling), not data/streaming.
// • bDecoRegionApplied && DecoAppliedInstances == 0 → the region applied but the per-column MARCH
// produced no spawns here (config/determinism), not a render or streaming bug.
// • bDecoRegionBuilding (not applied) → region is STILL marching; if it never finishes
// (DecoCellsAccounted stuck < DecoCellsTotal) it is a streaming/accounting leak.
// • neither flag set → region was never requested (desired-set / range).
// True if the region covering this XY is in DecoRegions (applied; HISMs built).
UPROPERTY(BlueprintReadOnly, Category = "Biome|Deco") bool bDecoRegionApplied = false;
// Total HISM instances actually uploaded across the applied region's components.
UPROPERTY(BlueprintReadOnly, Category = "Biome|Deco") int32 DecoAppliedInstances = 0;
// True if the region is in RegionBuilds (cells still being marched / merged, not yet applied).
UPROPERTY(BlueprintReadOnly, Category = "Biome|Deco") bool bDecoRegionBuilding = false;
// Cells accounted vs the region's total (R*R). Building but accounted < total = in progress; if it
// never reaches total the region is stuck (the permanent-blank-until-regen signature).
UPROPERTY(BlueprintReadOnly, Category = "Biome|Deco") int32 DecoCellsAccounted = 0;
UPROPERTY(BlueprintReadOnly, Category = "Biome|Deco") int32 DecoCellsTotal = 0;
// PER-CELL decisive probe: re-runs the decoration march for the EXACT cell (chunk footprint) under
// this point, synchronously, with the current strate context — i.e. how many spawns the deterministic
// scatter produces here RIGHT NOW. Region state above is R*R-cell coarse and can't see a single blank
// cell; this can. On a visibly-BLANK cell: >0 ⇒ the march works, the spawns were lost in merge/apply or
// not drawn (streaming/render bug); 0 ⇒ the march genuinely makes nothing here (data/config, or — if a
// regen brings grass back at the same spot — a non-determinism bug). -1 ⇒ couldn't run (no strate
// context / point not in the player's current strate).
UPROPERTY(BlueprintReadOnly, Category = "Biome|Deco") int32 DecoLiveMarchSpawns = -1;
// FINAL discriminator, paired with DecoLiveMarchSpawns. Counts the applied region's HISM instances
// that actually fall inside THIS cell's footprint (not the whole region). On a blank cell where the
// live march says N>0:
// • DecoInstancesInCell ≈ N → the instances ARE uploaded here but not visible → RENDER drop.
// • DecoInstancesInCell ≈ 0 → the march's spawns never reached the HISM → MERGE/apply loss.
UPROPERTY(BlueprintReadOnly, Category = "Biome|Deco") int32 DecoInstancesInCell = 0;
};
//=============================================================================
// RUNTIME STRUCTS (plain C++ — not USTRUCT; live in the thread-local hot path)
//=============================================================================
/**
* One biome flattened from its asset for fast, asset-free evaluation. The biome field
* and the per-chunk cache only ever see these PODs — never a UObject — so they stay
* cheap to copy and safe to touch from worker threads.
*/
struct FBiomeResolved
{
// Index back into the owning strate's Biomes[] array (for param / content lookup).
int32 Index = 0;
// Climate placement box in (relief, moisture) space, both [0,1].
float ReliefMin = 0.0f, ReliefMax = 1.0f;
float MoistureMin = 0.0f, MoistureMax = 1.0f;
// Colour used by the 2D preview bake.
FColor DebugColor = FColor::White;
// Palette layer this biome's terrain uses in the master triplanar material (F6).
// Baked into the mesh vertex colour so one material can re-skin per biome. 0 = default.
int32 MaterialPaletteIndex = 0;
};
/**
* Everything the biome field needs for one strate. Built once by StrateManager
* (GetBiomeContextForChunk) from the strate definition. Empty Biomes ⇒ biomes
* disabled for this strate (legacy behaviour, bit-identical output).
*/
struct FBiomeContext
{
TArray<FBiomeResolved> Biomes;
FBiomeMapParams Map;
bool IsValid() const { return Biomes.Num() > 0; }
};
/**
* Result of a single biome query at a world XY: the dominant biome cell, the nearest
* neighbour cell, and a blend weight toward that neighbour (0 deep inside a cell,
* → 0.5 at the shared border). Indices are positions into FBiomeContext::Biomes.
*/
struct FBiomeSample
{
int32 DominantIndex = -1;
int32 NeighborIndex = -1;
float NeighborWeight = 0.0f;
};
/**
* Per-chunk biome cache for the density hot path. The EXPENSIVE part of a biome query
* (classifying each Voronoi cell by its climate — several noise samples per cell) is
* done ONCE here, into a small grid covering the chunk footprint + margin; per voxel
* the resolver then only warps + does a cheap 3x3 lookup + blend.
*
* VALIDITY IS A WORLD-XY BOX (+ ChunkZ + Seed + owner identities), NOT a chunk key (CODEMAP §8.10). The
* grid covers a halo beyond the chunk, so gradient-normal samples and the +X/+Y chunk
* boundary corners stay inside the valid box and DO NOT thrash the noise-heavy rebuild.
* The box logic is identical in spirit to the SDF cache in GetDensityWithParams.
*/
struct FChunkBiomeCache
{
// World-XY box (voxel coords) over which the cached grid answers correctly.
float ValidMinX = 1.0f, ValidMaxX = -1.0f; // start invalid (min > max)
float ValidMinY = 0.0f, ValidMaxY = 0.0f;
int32 ChunkZ = MIN_int32; // which strate slice this was built for
int32 Seed = MIN_int32;
uint64 OwnerId = 0; // generator/world identity
uint64 ManagerLifetimeId = 0; // strate-manager lifetime identity
bool bActive = false; // does this strate have biomes?
FBiomeContext Ctx; // resolved biomes + map (for blending)
// Cell grid (row-major, CellsX × CellsY), each entry = biome index into Ctx.Biomes.
int32 BaseCellX = 0, BaseCellY = 0, CellsX = 0, CellsY = 0;
TArray<int32> CellBiome;
bool Contains(float X, float Y, int32 InChunkZ, int32 InSeed,
uint64 InOwnerId, uint64 InManagerLifetimeId) const
{
return InSeed == Seed && InChunkZ == ChunkZ
&& InOwnerId == OwnerId && InManagerLifetimeId == ManagerLifetimeId
&& X >= ValidMinX && X <= ValidMaxX
&& Y >= ValidMinY && Y <= ValidMaxY;
}
/** AUDIT C2 — force a rebuild on the next query. The validity box says nothing about the
* FBiomeContext the cells were classified AGAINST, so when the strate layout is rebuilt
* (RebuildStrates / an editor live edit) the grid is stale even though the box still
* covers the query. Callers that key on GetLayoutVersion() call this on a version change.
* Le box de validité ne dit rien du contexte de biome ayant servi à classer les cellules :
* après un rebuild de layout, la grille est périmée alors que la boîte couvre encore. */
void Invalidate()
{
ValidMinX = 1.0f; ValidMaxX = -1.0f; // min > max ⇒ Contains() is false everywhere
ChunkZ = MIN_int32;
OwnerId = 0;
ManagerLifetimeId = 0;
}
};
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,60 @@
// VoxelDensityAblation.h
// Development-only, world-changing measurement overrides for TunnelNetwork density stages.
#pragma once
#include "CoreMinimal.h"
#include <atomic>
namespace VoxelDensityAblation
{
/**
* A bit in this mask means that the named stage is forced off. These are measurement
* overrides, not gameplay settings: they change the generated field, are resolved once per
* process, and must match across peers and regenerated worlds.
*/
enum class EStage : uint32
{
CaveWarp = 1u << 0,
DetailOps = 1u << 1,
RoomSDF = 1u << 2,
TunnelSDF = 1u << 3,
TunnelCore = 1u << 4,
PassageCarving = 1u << 5,
PassageStructuralPosts= 1u << 6,
NativeFloor = 1u << 7,
Disturbances = 1u << 8,
OriginSpine = 1u << 9,
BoundarySeal = 1u << 10,
LandingPosts = 1u << 11,
XYEdgeSeal = 1u << 12,
PitChimneySDF = 1u << 13,
};
/** Resolve command-line/CVar values once and return the process-wide mask. */
VOXELFORGE_API uint32 GetResolvedMask();
FORCEINLINE bool IsForcedOff(EStage Stage)
{
return (GetResolvedMask() & static_cast<uint32>(Stage)) != 0;
}
FORCEINLINE bool IsCaveWarpOff() { return IsForcedOff(EStage::CaveWarp); }
FORCEINLINE bool IsDetailOpsOff() { return IsForcedOff(EStage::DetailOps); }
FORCEINLINE bool IsRoomSDFOff() { return IsForcedOff(EStage::RoomSDF); }
FORCEINLINE bool IsTunnelSDFOff() { return IsForcedOff(EStage::TunnelSDF); }
FORCEINLINE bool IsTunnelCoreOff() { return IsForcedOff(EStage::TunnelCore); }
FORCEINLINE bool IsPassageCarvingOff() { return IsForcedOff(EStage::PassageCarving); }
FORCEINLINE bool IsPassageStructuralPostsOff()
{
return IsForcedOff(EStage::PassageStructuralPosts);
}
FORCEINLINE bool IsNativeFloorOff() { return IsForcedOff(EStage::NativeFloor); }
FORCEINLINE bool IsDisturbancesOff() { return IsForcedOff(EStage::Disturbances); }
FORCEINLINE bool IsOriginSpineOff() { return IsForcedOff(EStage::OriginSpine); }
FORCEINLINE bool IsBoundarySealOff() { return IsForcedOff(EStage::BoundarySeal); }
FORCEINLINE bool IsLandingPostsOff() { return IsForcedOff(EStage::LandingPosts); }
FORCEINLINE bool IsXYEdgeSealOff() { return IsForcedOff(EStage::XYEdgeSeal); }
FORCEINLINE bool IsPitChimneySDFOff() { return IsForcedOff(EStage::PitChimneySDF); }
}
@@ -0,0 +1,934 @@
// VoxelDensityOp.h
// LE CONTRAT de la pile d'opérateurs de densité / THE density operator stack CONTRACT.
// Design : Docs/archive/OPSTACK-PLAN.md. The contract is implemented by the stack path in
// UVoxelGenerator::GetDensityAt, which generates every strate.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// POURQUOI / WHY
// ─────────────────────────────────────────────────────────────────────────────────────────
// Chaque archétype (ECaveGeneratorType) est une composition d'opérateurs, pas une fonction de
// densité monolithique qui posséderait le voxel entier. Une idée de monde s'exprime en empilant
// sources, combinateurs et modificateurs — et les idées peuvent donc SE COMBINER : « une strate de
// surface dont les montagnes contiennent un réseau de salles, avec des îles flottantes au-dessus ».
//
// Each archetype (ECaveGeneratorType) is a composition of operators, not a monolithic density
// function that owns the whole voxel. A world idea is expressed by stacking sources, combiners and
// modifiers — so ideas can COMBINE.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// ⚠️ CE N'EST PAS L'ANCIEN SYSTÈME DE « ROOM OPERATIONS » / THIS IS NOT THE OLD ROOM-OPS SYSTEM
// ─────────────────────────────────────────────────────────────────────────────────────────
// UVoxelTerrainOpDefinition (Terrace, Ribbing, Cliff, Scallop, Overhang, Arch, Column, Pit…) ne
// sait que PERTURBER une densité près d'une surface qui existe déjà. Il ne décide jamais ce que le
// champ EST — c'est le rôle des sources de champ ci-dessous.
//
// D'où QUATRE RÔLES, dont l'ancien système n'occupait que le troisième :
//
// 1. FIELD SOURCE — fabrique un champ À PARTIR DE RIEN. C'est ce qui fait qu'une grotte est
// une grotte et qu'un monde ouvert est un monde ouvert. Chaque archétype
// est fondamentalement l'une de ces sources.
// 2. COMBINER — comment deux champs fusionnent (min/max/smooth/mask). C'EST le rôle qui
// achète la composition ; sans lui il n'y a pas de refactor.
// 3. DETAIL MODIFIER — l'ancien système, rétrogradé à un rôle sur quatre. Inchangé.
// 4. STRUCTURAL POST — spine (0,0) → seal vertical → passages → seal XY → diff layer. Des INVARIANTS de monde,
// pas des choix créatifs : toujours ajoutés, dans cet ordre, jamais
// omissibles par l'auteur.
//
// The old system only ever had role 3. Roles 1 and 2 are the new thing, and role 4 is what keeps
// the descent structure intact no matter what an author assembles.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// LA CLÉ DE VOÛTE : EffectOverBox + intervalle SDF / THE KEYSTONE: EffectOverBox + SDF interval
// ─────────────────────────────────────────────────────────────────────────────────────────
// Un verdict de boîte doit être une preuve. Rendre `Both` coûte du CPU ; rendre `Identity` ou une
// direction fausse supprime de la géométrie et peut laisser un joueur tomber au travers du monde.
// En cas de doute, `Mixed`.
//
// Un opérateur qui écrit seulement le canal SDF ne peut pas honnêtement répondre pour le convertisseur
// qui viendra peut-être après lui : il doit propager un intervalle SDF, et le convertisseur doit plier
// cet intervalle avec sa propre formule. Chaque opérateur répond ainsi pour lui-même, et la composition
// reste sûre même pour une combinaison que personne n'a écrite à la main auparavant.
//
// A box verdict is a proof. `Both` costs CPU; a false `Identity` or direction removes geometry and can
// let a player fall through the world. When in doubt, return `Mixed`.
// An SDF-only writer reports an SDF interval; a later converter folds that interval through its own
// response. Each operator answers for itself, so an unverified composition remains sound.
//
// Presque tout opérateur est UNIDIRECTIONNEL : il ne fait que creuser, ou que remplir. La direction
// suffit donc à exprimer GÉNÉRIQUEMENT les gardes de ClassifyTile :
//
// « passages ⇒ bCanSolid = false » EST CarveOnly
// « ponts/arêtes ⇒ bCanAir = false » EST FillOnly
// « aucun passage près de cette boîte » EST Identity
//
// La direction reste le repli conservateur par défaut pour les opérateurs qui ne connaissent pas
// d'intervalle ; elle ne permet jamais à un écrivain SDF d'usurper la réponse d'un voisin.
//
// Direction remains the conservative fallback for operators without an interval implementation;
// it never lets an SDF writer impersonate a downstream neighbour.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// CLASSIFYTILE
// ─────────────────────────────────────────────────────────────────────────────────────────
// ClassifyTile garde ses preuves exactes pour les gaps de bedrock et SurfaceWorld ; les slots de
// cave passent par le pliage `ClassifyBox` de la pile même qui produit leur densité. Le post XY
// fournit en plus une preuve globale de coque : une tuile entièrement dans la bande forcée peut être
// sautée sans connaître l'archétype ou le Z.
//
// ClassifyTile keeps its exact proofs for bedrock gaps and SurfaceWorld; cave slots go through the
// `ClassifyBox` fold of the same stack that produces their density. The XY post adds a global
// shell proof: a tile entirely inside the forced band is skipped without knowing archetype or Z.
//
// NOTE sur les UENUM : ces types sont volontairement du C++ nu (pas d'UHT, pas de .generated.h).
// Ils deviendront UENUM/USTRUCT en Phase 3, quand les opérateurs deviendront des data assets et
// auront besoin d'être édités dans l'éditeur. Les promouvoir plus tôt n'achèterait rien et
// ajouterait une étape UHT à chaque itération.
#pragma once
#include "CoreMinimal.h"
#include "VoxelTypes.h" // CHUNK_SIZE, EVoxelTileClass
struct FBiomeContext;
struct FTunnelCoreWorldEvaluation;
struct FChunkSDFCache;
//=============================================================================
// LES QUATRE RÔLES / THE FOUR ROLES
//=============================================================================
// Le rôle n'est pas décoratif : le compilateur de pile s'en sert pour ORDONNER. Les
// StructuralPost sont toujours ajoutés en dernier, dans l'ordre fixe spine → seal → passage →
// seal XY → diff, quoi que l'auteur ait assemblé. Un auteur ne peut pas les omettre : la descente doit
// rester possible, les seals doivent tenir, les passages doivent percer, les éditions du joueur
// gagnent toujours.
enum class EVoxelOpRole : uint8
{
// Rôle 1 — fabrique un champ à partir de rien. Une pile en a au moins un (sa racine).
FieldSource,
// Rôle 2 — fusionne le champ précédent avec le suivant. Voir EVoxelOpCombine.
Combiner,
// Rôle 3 — perturbe un champ existant près de sa surface (l'ancien UVoxelTerrainOpDefinition).
DetailModifier,
// Rôle 4 — invariants de monde. Ajoutés automatiquement, ordre fixe, non omissibles.
StructuralPost,
};
//=============================================================================
// CANAUX DE L'ÉCHANTILLON / SAMPLE CHANNELS
//=============================================================================
// `FVoxelOpSample` is deliberately a small, explicit state record. Keep its channel set here,
// next to the interface that declares which fields an operator consumes and publishes. The enum
// values are bits so a declaration can name more than one channel without introducing another
// per-voxel object or a dynamic container.
//
// `Density` is the internal positive=solid field; `Sdf` is the standard negative=inside field.
// If a future field is added to `FVoxelOpSample`, it must also be added here and to every operator
// declaration before the composer can treat that field as part of the dependency graph.
enum class EVoxelOpChannel : uint8
{
None = 0,
Density = 1 << 0,
Sdf = 1 << 1,
};
using EVoxelOpChannelMask = uint8;
namespace VoxelOpChannels
{
static constexpr EVoxelOpChannelMask None = 0;
static constexpr EVoxelOpChannelMask Density = static_cast<EVoxelOpChannelMask>(EVoxelOpChannel::Density);
static constexpr EVoxelOpChannelMask Sdf = static_cast<EVoxelOpChannelMask>(EVoxelOpChannel::Sdf);
static constexpr EVoxelOpChannelMask All = Density | Sdf;
}
//=============================================================================
// OPERATOR RESOURCES / RESSOURCES D'OPERATEUR
//=============================================================================
// Sample-channel declarations describe the FVoxelOpSample only. Some operators also consume
// stateful geometry published by an earlier source (the room graph, shaft field, or surface
// column). Keep that dependency explicit as a second, tiny graph so the composer cannot place a
// room modifier on a stack that has no room source merely because its Density/Sdf masks happen to
// fit.
enum class EVoxelOpResource : uint8
{
None = 0,
RoomGeometry = 1 << 0,
ShaftGeometry = 1 << 1,
SurfaceColumn = 1 << 2,
};
using EVoxelOpResourceMask = uint8;
namespace VoxelOpResources
{
static constexpr EVoxelOpResourceMask None = 0;
static constexpr EVoxelOpResourceMask RoomGeometry = static_cast<EVoxelOpResourceMask>(EVoxelOpResource::RoomGeometry);
static constexpr EVoxelOpResourceMask ShaftGeometry = static_cast<EVoxelOpResourceMask>(EVoxelOpResource::ShaftGeometry);
static constexpr EVoxelOpResourceMask SurfaceColumn = static_cast<EVoxelOpResourceMask>(EVoxelOpResource::SurfaceColumn);
static constexpr EVoxelOpResourceMask All = RoomGeometry | ShaftGeometry | SurfaceColumn;
}
//=============================================================================
// COMPOSITION / COMBINERS
//=============================================================================
// Vocabulaire délibérément petit, et il réutilise ce qui existe déjà
// (VoxelSDF::SmoothMin / SmoothMax).
//
// ⚠️⚠️ RAPPEL DE SIGNE — LA source n°1 de confusion du plugin, et il y a DEUX conventions en jeu.
// Lire ceci en entier avant d'écrire un opérateur.
//
// • CANAL DENSITÉ, à l'intérieur de la pile : convention INTERNE, **POSITIF = SOLIDE**.
// C'est celle dans laquelle CHAQUE fonction d'archétype est écrite aujourd'hui. La négation
// vers la convention marching-cubes (négatif = solide) se fait UNE FOIS, tout à la fin, par
// l'appelant. Donc ici : « ajouter du solide » = MAX, « creuser de l'air » = MIN.
//
// • CANAL SDF : convention SDF standard, **NÉGATIF = À L'INTÉRIEUR de la primitive**.
// Réunir deux formes = MIN (c'est `SmoothMin`, ce que fait déjà le code pour salle+puits).
// Le sens de « min » est donc l'INVERSE d'un canal à l'autre. Ce n'est pas une incohérence :
// un SDF décrit une FORME, une densité décrit de la MATIÈRE.
//
// DENSITY channel inside the stack: INTERNAL convention, **POSITIVE = SOLID** (what every
// archetype body already uses; the MC negate happens once, at the end, in the caller). So
// "add solid" is max(), "carve air" is min().
// SDF channel: standard SDF, **NEGATIVE = INSIDE the primitive**; unioning shapes is min().
// The meaning of min() is therefore opposite between the two channels — an SDF describes a
// SHAPE, a density describes MATTER.
enum class EVoxelOpCombine : uint8
{
Replace, // ignore l'entrée — racine de pile (heightfield, densité de base)
Union, // max() sur la DENSITÉ — ajoute du solide : ponts, îles, colonnes
Subtract, // min() sur la DENSITÉ — creuse de l'air : salles, tunnels, passages, spine
SmoothUnion, // VoxelSDF::SmoothMin(k) — jonctions organiques
SmoothSubtract, // VoxelSDF::SmoothMax(k)
Add, // accumulation scalaire — termes de bruit / rugosité
Mask, // met à l'échelle l'opérateur SUIVANT par un champ [0,1]
// (poids de biome, porte de pente, relief, profondeur).
// C'est Mask qui achète le plus d'expressivité : « cet opérateur, mais
// seulement dans les régions à fort relief » devient de la composition
// au lieu d'une garde codée en dur dans chaque opérateur.
};
//=============================================================================
// EFFET SUR UNE BOÎTE / EFFECT OVER A BOX
//=============================================================================
// CONSERVATIF PAR CONSTRUCTION. Rendre `Both` est TOUJOURS SÛR (ça ne coûte que du CPU) ;
// rendre le mauvais est un TROU — pas de géométrie, pas de collision, invisible jusqu'à ce
// qu'un joueur tombe au travers. En cas de doute : `Both`.
//
// CONSERVATIVE BY CONSTRUCTION. Returning `Both` is ALWAYS SAFE (it only costs CPU); returning
// the wrong one is a HOLE. When unsure: `Both`.
enum class EVoxelOpEffect : uint8
{
// Prouvablement aucun effet sur cette boîte. C'est le early-out qui rend la pile rapide,
// et c'est ce qui rendra le bedrock profond sautable pour les strates de grotte.
Identity,
// Ne peut que pousser la densité vers l'AIR ⇒ tue l'hypothèse « tout solide ».
CarveOnly,
// Ne peut que pousser la densité vers le SOLIDE ⇒ tue l'hypothèse « tout air ».
FillOnly,
// Non contraint ⇒ tue les deux hypothèses.
Both,
};
//=============================================================================
// CONTEXTE DE CHUNK / CHUNK CONTEXT
//=============================================================================
// Miroir de ce que le bloc thread_local CP_* résout aujourd'hui dans GetDensityAt.
//
// ⚠️ LayoutVersion est ici PAR CONSTRUCTION, pas par politesse. AUDIT C2 : trois caches
// existants (CP_Chunk, OC_Chunk, BM_Chunk) sont clés sur ChunkCoord SEUL, donc après un
// RebuildStrates ou une édition à chaud un worker dont le cache est encore chaud pour ce chunk
// saute le refetch et génère avec les ANCIENS params. En faisant porter LayoutVersion par le
// contexte, un nouvel opérateur ne PEUT PAS oublier de l'inclure dans sa clé.
//
// LayoutVersion is here BY CONSTRUCTION, not by politeness — see AUDIT C2. Carrying it in the
// context means a new op CANNOT forget to put it in its cache key.
struct FVoxelOpContext
{
FIntVector ChunkCoord = FIntVector::ZeroValue;
// Pas d'échantillonnage LOD (1/2/4…). Un opérateur a le droit de se simplifier quand Step
// est grand — c'est le contrat T2.b : le bruit volumétrique par voxel perd des octaves au
// loin, le bruit de champ XY délibérément non (il alimente des caches box-validés partagés).
int32 Step = 1;
uint32 Seed = 0;
// Compteur de génération du layout (UVoxelStrateManager::GetLayoutVersion()).
// DOIT faire partie de toute clé de cache. Voir AUDIT C2.
uint32 LayoutVersion = 0;
// Global XY world-edge invariant. These are copied from UVoxelSettings by the generator and
// travel with the chunk context so the automatically appended edge op cannot be omitted by a
// builder. Radius 0 is the legacy unbounded/no-op mode.
float WorldRadiusVoxels = 8192.0f;
float EdgeSealThickness = 128.0f;
// Bornes Z de la strate en coords VOXEL (pas cm).
float StrateTopWorldZ = 0.0f;
float StrateBottomWorldZ = 0.0f;
// ClassifyTile may ask box bounds about the exact marching-cubes lattice rather than the
// continuous volume between samples. Production voxel evaluation leaves this disabled;
// when enabled, operators may tighten a conservative bound only for points
// LatticeOriginVoxels + n * Step that lie inside the queried box.
bool bUseLatticeProof = false;
FIntVector LatticeOriginVoxels = FIntVector::ZeroValue;
// A mixed tile may opt into a tighter, still conservative finite-box bound for domain-warped
// sources. The normal classifier leaves this off; ClassifyTile retries only the few boxes
// that the cheap global proof could not resolve.
bool bTightenWarpProof = false;
// null = cette strate n'a pas de champ de biome.
const FBiomeContext* Biome = nullptr;
};
//=============================================================================
// L'ÉTAT QUI TRAVERSE LA PILE / THE STATE THE STACK THREADS THROUGH
//=============================================================================
// DEUX canaux, pas un. Ce n'est pas de la généralité gratuite — c'est ce que le code fait déjà :
//
// CaveSDF = EvaluateSDFCached(salles + tunnels) ← espace SDF
// CaveSDF = SmoothMin(CaveSDF, PitSDF, BlendK) ← espace SDF
// CaveSDF = SmoothMin(CaveSDF, ChimneySDF, BlendK) ← espace SDF
// → UN SEUL carve à la fin : Density -= CarveFactor · BaseDensity · 2
//
// Maze, VerticalShafts et FloatingIslands ont la même forme, et TROIS d'entre eux appliquent la
// rugosité au **SDF** (`MazeSDF += bruit·Rough`), pas à la densité. Sur la densité, le même bruit
// est mis à l'échelle par le gradient local : effet visiblement différent.
//
// Avec un seul canal, un opérateur ne peut qu'ÉCRASER le précédent — les jonctions SmoothMin
// (salle↔puits, et demain « un graphe de salles creusé DANS une montagne ») sont impossibles.
// Un `SmoothMin` entre deux SOURCES différentes est précisément ce qui fait qu'une idée composée
// a l'air d'appartenir au lieu au lieu d'y avoir été percée. Coût : un float.
//
// Two channels, not one — because that is what the code already does, and because SmoothMin between
// two different SOURCES is precisely what makes a composed idea look like it belongs there rather
// than like a hole punched in something else. Cost: one float.
struct FVoxelOpSample
{
// Convention INTERNE : POSITIF = SOLIDE. Négation vers MC une seule fois, par l'appelant.
// INTERNAL convention: POSITIVE = SOLID. Negated to MC once, by the caller.
float Density = 0.0f;
// Convention SDF standard : NÉGATIF = à l'intérieur de la primitive.
// FLT_MAX = « aucune surface à proximité » (l'état initial, et le early-out des sources
// placées quand aucune primitive n'atteint ce voxel).
// FLT_MAX = "no surface nearby" — the initial state, and the early-out placed sources use.
float Sdf = FLT_MAX;
// Block evaluation keeps the room source's per-sample selection explicit. The scalar path
// still uses the source's worker-local state, but a later block-capable modifier must never
// observe the state belonging to the last sample of the preceding operator.
const FChunkSDFCache* RoomCache = nullptr;
int32 NearestRoomIndex = INDEX_NONE;
// The common generator tail consumes the continuous swept-shape metadata after the block has
// left the stack. Keeping it in the sample avoids re-running the world-space tunnel scan or
// consulting a last-sample TLS value after an op-major evaluation.
bool bHasTunnelCoreWorldEvaluation = false;
float TunnelCoreWorldSDF = FLT_MAX;
bool bTunnelCoreRoomFloor = false;
bool bHasTunnelCoreSweptFloor = false;
float TunnelCoreSweptFloorZ = -FLT_MAX;
float TunnelCoreSweptFloorRadius = 0.0f;
};
/**
* A regular world lattice handed to one operator at a time.
*
* The default implementation is deliberately scalar and therefore source-compatible with custom
* operators. An operator may override EvalBlock only when it preserves the exact per-sample
* semantics of Eval. Samples are laid out X-fastest, then Y, then Z; all coordinates are voxel
* coordinates and are formed as Origin + index * Step using the same integer arithmetic as the
* mesher's scalar path.
*/
struct FVoxelOpBlock
{
FIntVector OriginVoxels = FIntVector::ZeroValue;
int32 Step = 1;
int32 SizeX = 0;
int32 SizeY = 0;
int32 SizeZ = 0;
FVoxelOpSample* Samples = nullptr;
int32 NumSamples() const { return SizeX * SizeY * SizeZ; }
FORCEINLINE FVoxelOpSample& At(int32 X, int32 Y, int32 Z) const
{
return Samples[(Z * SizeY + Y) * SizeX + X];
}
};
/**
* Intervalle conservateur du canal SDF sur une boîte.
*
* `Min` et `Max` sont les bornes SUPRÊMES de toutes les valeurs que l'opérateur peut produire dans
* la boîte. `[FLT_MAX, FLT_MAX]` est l'identité exacte : aucune surface connue. Un intervalle inconnu
* est volontairement large ; il force le fold à rester `Mixed` plutôt que de risquer un faux tile
* uniforme. Les setters refusent toute borne non finie ou inversée : un opérateur qui ne peut pas
* prouver son intervalle perd le skip, jamais la géométrie.
*
* Conservative SDF interval over a box. `Min`/`Max` bound every value the operator can produce.
* `[FLT_MAX, FLT_MAX]` is the exact "no surface" identity. Unknown is deliberately wide: it costs
* a skip, never a hole. Invalid bounds become unknown instead of being guessed.
*/
struct FVoxelBoxSdfInterval
{
bool bKnown = true;
float Min = FLT_MAX;
float Max = FLT_MAX;
bool IsKnown() const { return bKnown; }
void SetUnknown()
{
bKnown = false;
Min = -FLT_MAX;
Max = FLT_MAX;
}
void Set(float InMin, float InMax)
{
if (!VoxelMath::IsFinite(InMin) || !VoxelMath::IsFinite(InMax) || InMin > InMax)
{
SetUnknown();
return;
}
bKnown = true;
Min = InMin;
Max = InMax;
}
/** Output of `min(Input, Other)` when both intervals are known. */
void MinWith(const FVoxelBoxSdfInterval& Other)
{
if (!bKnown || !Other.bKnown)
{
SetUnknown();
return;
}
Min = FMath::Min(Min, Other.Min);
Max = FMath::Min(Max, Other.Max);
}
};
// Forward declaration: the interval-aware EffectOverBox overloads consume the hypotheses that
// have already been folded, while the hypotheses themselves are declared immediately below the
// interface.
struct FVoxelBoxHypotheses;
//=============================================================================
// L'INTERFACE / THE INTERFACE
//=============================================================================
// Trois méthodes, et elles FORMALISENT CE QUE LE CODE FAIT DÉJÀ À LA MAIN : chaque archétype
// hisse déjà son travail constant-par-chunk dans un cache thread_local (= PrepareChunk), évalue
// à bas coût par voxel (= Eval), et possède déjà dans ClassifyTile une déclaration écrite à la
// main de ce qu'il peut faire à une tuile (= EffectOverBox). Ce n'est pas une nouvelle
// discipline, c'est la discipline existante, nommée.
class IVoxelDensityOp
{
public:
virtual ~IVoxelDensityOp() = default;
virtual EVoxelOpRole GetRole() const = 0;
/**
* Channel contract for the composer. A read is a field inspected from `InOut` before this op
* publishes its result; a write is a field whose value may differ when the op returns. A
* read/write declaration therefore means "transform the current version". `+=` and `-=` are
* both reads and writes; an assignment that ignores the old field is a write-only producer /
* replacement. Context, coordinates, caches and referenced objects are not sample channels.
*
* These are graph metadata, not voxel work: the stack snapshots them while it is assembled and
* the validator reads the snapshot. They must never be called from `Eval`.
*/
virtual EVoxelOpChannelMask ChannelReads() const = 0;
virtual EVoxelOpChannelMask ChannelWrites() const = 0;
/**
* Non-sample state required by this op. A declaration is satisfied only by a provider that
* appeared earlier in the stack. Defaults are deliberately empty so existing independent
* operators and external test doubles remain source-compatible.
*/
virtual EVoxelOpResourceMask RequiredResources() const
{
return VoxelOpResources::None;
}
/** State published for later consumers, e.g. FRoomGraphSource's room geometry cache. */
virtual EVoxelOpResourceMask ProvidedResources() const
{
return VoxelOpResources::None;
}
/**
* True when this op contributes a delta to its written channels without replacing, clamping,
* selecting, or otherwise depending on the previous value of those written channels. Such an
* op may be shuffled with another compatible additive delta; a false result means its position
* is explicit. A gate on a channel the op itself writes is transformative even if its final
* arithmetic contains `+=`.
*/
virtual bool IsAdditive() const = 0;
/**
* Hisser ici TOUT le travail constant sur le chunk : listes de salles, grilles de biome,
* caches de colonnes, cuissons de treillis. Appelé une fois par chunk et par worker.
* C'est ici que déménagent les caches thread_local d'aujourd'hui.
*
* THREADING : appelé sur des workers. L'opérateur ne doit écrire QUE son propre état
* par-chunk ; le Generator / le Mesher / le StrateManager restent en LECTURE SEULE
* (invariant ARCHITECTURE §8.10). Toute clé de cache DOIT inclure Ctx.LayoutVersion.
*/
virtual void PrepareChunk(const FVoxelOpContext& Ctx) = 0;
/**
* Par voxel. `InOut` est l'état que la pile a produit jusqu'ici (voir FVoxelOpSample).
* Coordonnées en VOXELS, pas en cm.
*
* INVARIANCE DE FENÊTRE (ARCHITECTURE §8.4) : fonction PURE de (coords monde, seed, layout).
* Le même point évalué depuis une autre tuile, un autre ordre, un autre thread doit rendre le
* float BIT-IDENTIQUE. Pas « proche » : 1 ULP d'écart entre deux fenêtres est une couture
* visible, et en multijoueur une divergence de monde. Le test
* VoxelForge.Determinism.DensityPurity vérifie cela.
*/
virtual void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const = 0;
/**
* Op-major evaluation seam. The conservative default is intentionally not clever: it calls
* the canonical scalar Eval for every sample. This lets the stack and mesher be converted in
* reviewable stages while byte-identical output remains the acceptance test. Stateful ops
* may restore their per-sample hand-off from FVoxelOpSample in PrepareBlockSample; the default
* invokes that hook before each scalar fallback call.
*/
virtual void EvalBlock(const FVoxelOpBlock& Block) const
{
if (Block.Samples == nullptr || Block.Step <= 0
|| Block.SizeX <= 0 || Block.SizeY <= 0 || Block.SizeZ <= 0)
{
return;
}
for (int32 Z = 0; Z < Block.SizeZ; ++Z)
{
for (int32 Y = 0; Y < Block.SizeY; ++Y)
{
for (int32 X = 0; X < Block.SizeX; ++X)
{
FVoxelOpSample& Sample = Block.At(X, Y, Z);
PrepareBlockSample(Sample);
Eval(
static_cast<float>(Block.OriginVoxels.X + X * Block.Step),
static_cast<float>(Block.OriginVoxels.Y + Y * Block.Step),
static_cast<float>(Block.OriginVoxels.Z + Z * Block.Step),
Sample);
}
}
}
}
/** Restore a source-owned per-sample context before a scalar fallback in EvalBlock. */
virtual void PrepareBlockSample(const FVoxelOpSample&) const {}
/** Optional hand-off for the common structural tail. The default keeps custom operators
* conservative: the caller uses its canonical cache path when no source publishes a result. */
virtual bool TryGetLastTunnelCoreWorldEvaluation(
FTunnelCoreWorldEvaluation& OutEvaluation) const
{
return false;
}
/**
* CONSERVATIF. The two-argument method is the intrinsic fallback. The state-aware overload
* below is what the stack calls: it sees the SDF interval already produced by earlier ops.
* Returning Both is always safe; returning the wrong direction is a hole.
*
* Le contrat est « conservatif », pas « forme close » : un opérateur A LE DROIT
* D'ÉCHANTILLONNER pour répondre. C'est exactement ce que fait ClassifyTile aujourd'hui pour
* SurfaceWorld — il évalue ComputeSurfaceColumn sur le treillis EXACT du mesher, mêmes
* fonctions, mêmes floats, donc verdict exact plutôt qu'estimé. Cela DOIT survivre au portage.
*
* The contract is "conservative", not "closed-form": an op MAY sample to answer.
*
* An SDF-only source MUST NOT answer for a converter that happens to follow it. It reports its
* own interval through `PropagateSdfOverBox`; the converter's state-aware overload then folds
* the interval through its own formula. This is the isolation rule that makes free composition
* safe.
*/
virtual EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const = 0;
/** State-aware effect. The default preserves old custom operators' conservative direction. */
virtual EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
const FVoxelBoxHypotheses&) const
{
return EffectOverBox(VoxelBox, Ctx);
}
/**
* ⚠️ LE PLIAGE QUI PORTE DES NOMBRES — `OPSTACK-DECOMPOSITION §0.2`, et le plus gros poste de
* perf du plan.
*
* La direction seule ne suffit pas pour les opérateurs FIELDÉS. Un carve à seuil de bruit (les
* vers de TunnelNetwork, la rugosité de paroi) n'a AUCUNE borne spatiale : il rend `CarveOnly`
* sur CHAQUE boîte de CHAQUE strate qui l'active, donc il tue l'hypothèse `AllSolid` partout et
* l'archétype ne saute pas une tuile. Aucun raffinement de `EffectOverBox` ne peut le récupérer,
* parce que la réponse « oui, je peux creuser ici » est VRAIE.
*
* **Mais son AMPLITUDE est bornée, et souvent triviale** : pour un ver, `t ∈ [0,1]` et
* `Mask ∈ [0,1]`, donc il ne peut déplacer la densité vers l'air que de `WormStrength` au plus.
* Si le roc est solide d'une marge SUPÉRIEURE à la somme de tous les carves restants, la boîte
* est prouvablement pleine — quel que soit le bruit.
*
* D'où deux nombres, en unités de DENSITÉ (convention interne, positif = solide) :
* • `MaxCarveOverBox` — de combien AU PLUS cet opérateur peut baisser la densité sur la boîte,
* • `MaxFillOverBox` — de combien AU PLUS il peut la monter.
*
* **`FLT_MAX` = « je ne sais pas », et c'est le DÉFAUT.** Un opérateur qui ne redéfinit rien se
* comporte donc EXACTEMENT comme avant ce changement : le pliage retire `FLT_MAX` à la marge,
* elle passe sous zéro, l'hypothèse meurt. Les treize tests d'équivalence et
* `VoxelForge.OpStack.BoxVerdictFold` ne bougent pas d'un verdict.
*
* ⚠️ SENS DE L'ERREUR : SUR-estimer une amplitude coûte du CPU (une tuile maillée pour rien) ;
* SOUS-estimer produit un TROU. Comme partout ailleurs dans ce fichier, en cas de doute rendre
* `FLT_MAX`. Ce n'est pas une borne « raisonnable », c'est une borne PROUVÉE ou rien.
*
* The fold carries NUMBERS, not just directions. A fielded noise carve has no spatial bound but
* its AMPLITUDE is bounded, so "the rock is solid by more than the sum of every remaining carve"
* becomes provable. FLT_MAX means "unknown" and is the default, so every existing op is
* unchanged. Over-estimating costs CPU; under-estimating is a hole.
*/
virtual float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
{
return FLT_MAX;
}
/** State-aware amplitude. The default is the old, conservative amplitude. */
virtual float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
const FVoxelBoxHypotheses&) const
{
return MaxCarveOverBox(VoxelBox, Ctx);
}
virtual float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
{
return FLT_MAX;
}
/** State-aware amplitude. The default is the old, conservative amplitude. */
virtual float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
const FVoxelBoxHypotheses&) const
{
return MaxFillOverBox(VoxelBox, Ctx);
}
/**
* Propagate this operator's own SDF result through a box query. Only call this for an operator
* whose cached channel declaration writes `Sdf`. The default is unknown by construction: a new
* SDF writer cannot accidentally make a false uniform verdict merely because it forgot this
* method. Implementations that assign SDF replace the interval; min/accumulating writers use
* `FVoxelBoxSdfInterval::MinWith`.
*/
virtual void PropagateSdfOverBox(FVoxelBoxSdfInterval& InOut, const FBox& VoxelBox,
const FVoxelOpContext&) const
{
InOut.SetUnknown();
}
/**
* Pour un opérateur FORÇANT (celui dont `ClassifyBox` rend autre chose que `Mixed`) : de combien
* la densité est-elle garantie du bon côté de zéro, PARTOUT dans la boîte ?
*
* C'est l'autre moitié du pliage numérique. `MaxCarveOverBox` dit ce qu'on peut RETIRER ; ceci
* dit ce qu'il y avait à retirer. Sans les deux, la soustraction n'a pas de premier terme.
*
* Exemple, et c'est LE cas qui compte : `FConstantFieldSource` pose `Density = BaseDensity`
* partout. Sa marge est donc exactement `BaseDensity`. Un ver à `WormStrength = 0.6` sur un roc
* à `BaseDensity = 1.0` laisse 0.4 de marge ⇒ la boîte reste prouvablement pleine.
*
* **0 = « je ne sais pas », et c'est le DÉFAUT** : la marge tombe à zéro, le premier carve la
* fait passer sous zéro, l'hypothèse meurt — le comportement d'avant, à l'identique.
*/
virtual float ForcedMarginOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
{
return 0.0f;
}
/**
* OPÉRATEURS FORÇANTS. Certains opérateurs ne « déplacent » pas la densité d'entrée : ils
* l'ÉCRASENT. La question « dans quelle direction peux-tu bouger ce champ ? » n'a alors pas de
* sens ; la bonne question est « sais-tu prouver que toute cette boîte est d'un seul côté,
* QUELLE QUE SOIT l'entrée ? ».
*
* Deux familles répondent autre chose que Mixed :
* • les SOURCES (rôle 1) — elles posent le champ, donc elles le savent par construction ;
* • les op STRUCTURELS forçants — ApplyBoundarySeal et ApplyXYEdgeSeal — qui, dans leur bande
* prouvée, font `Max(Density, SealFactor·BaseDensity)` avec SealFactor > 0 : le résultat est
* solide garanti quoi qu'il y ait eu avant. C'est exactement ce que ClassifyTile encode
* aujourd'hui avec « bande de seal ⇒ bCanAir = false » — et un simple FillOnly ne suffirait
* PAS à le reproduire (voir VF_FoldOp plus bas).
*
* Par défaut Mixed = « je ne sais pas », toujours sûr. Une source à primitives placées (graphe
* de salles, îles, puits) répond en testant ses bornes ; une source heightfield répond en
* échantillonnant ses colonnes sur le treillis exact, exactement comme aujourd'hui.
*
* FORCING OPS. Some ops do not *move* the input density, they *overwrite* it. Default Mixed =
* "I don't know", always safe. The vertical and XY edge seals are the non-source examples, and
* they are the reason this method exists at all rather than being folded into EffectOverBox.
*/
virtual EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
{
return EVoxelTileClass::Mixed;
}
/**
* Déclare si Eval dépend de Z. Les opérateurs purs en XY reçoivent le traitement du cache de
* colonnes T1.a de façon GÉNÉRIQUE, au lieu que SurfaceWorld en ait un sur mesure.
*
* ⚠️ Le cache de colonnes est clé sur (boîte XY, StrateKey, Seed) SANS ChunkZ et il est
* partagé sur TOUTE la pile verticale de chunks. Mettre une donnée dépendante de Z dans un
* opérateur qui se déclare XY-pur corrompt silencieusement chaque chunk de la colonne, et
* ValidateDeterminism — qui échantillonne le long d'une frontière en X — ne le verrait pas.
*/
virtual bool IsXYPure() const { return false; }
/**
* DIAGNOSTIC UNIQUEMENT — le nom que les rapports de test impriment pour cet opérateur.
*
* ⚠️ POURQUOI CETTE MÉTHODE EXISTE. Sans nom par opérateur, un rapport « 0 tuile prouvée sur
* 40 » ne peut dire que « ou bien les tuiles traversent toutes une grotte, ou bien la source
* n'atteint pas sa branche `Identity` » — deux causes, zéro nombre pour les départager, alors
* que la vraie cause peut être un TROISIÈME opérateur (p. ex. un ver qui rend `CarveOnly`
* partout). Avec un nom par opérateur, `ClassifyBoxAttributed` répond « c'est celui-là » au
* lieu de laisser deviner.
*
* N'entre dans AUCUNE clé de cache, dans aucun hash, dans aucune décision de génération : le
* changer ne peut pas changer le monde. Le défaut est volontairement laconique — un opérateur
* sans nom se repère à son index, ce qui suffit à savoir où regarder.
*
* Diagnostic only: the first build of the spatial EffectOverBox came back green with 0 tiles
* proved, and the report could not name which operator was killing the hypothesis. It was a
* third one nobody was looking at. Never part of a cache key or any generation decision.
*/
virtual const TCHAR* DebugName() const { return TEXT("(unnamed op)"); }
};
//=============================================================================
// LE PLIAGE : comment la pile devient un verdict de tuile
// THE FOLD: how a stack becomes a tile verdict
//=============================================================================
// C'est l'algorithme générique qui remplace le ClassifyTile écrit à la main, et il REPRODUIT
// EXACTEMENT le comportement d'aujourd'hui — vérifié ligne à ligne contre VoxelGenerator.cpp :
//
// source gap bedrock → ClassifyBox = AllSolid
// source SurfaceWorld → ClassifyBox échantillonne les colonnes sur le treillis exact
// ApplyPassageCarving → CarveOnly (tue AllSolid) ≡ « AnyPassageNearBox ⇒ bCanSolid=false »
// ApplyOriginSpine → CarveOnly (tue AllSolid) ≡ le test cercle/boîte XY
// ApplyBoundarySeal → ClassifyBox = AllSolid DANS sa bande (opérateur forçant),
// FillOnly ailleurs ≡ « bande de seal ⇒ bCanAir=false »
// ApplyXYEdgeSeal → ClassifyBox = AllSolid DANS sa bande radiale (forçant) ; il gagne
// après les passages, donc la coque extérieure reste fermée.
// disturbances chasms → CarveOnly ≡ « ChasmDensity > 0 ⇒ bCanSolid=false »
// disturbances ponts/arêtes → FillOnly ≡ « Bridge/RidgeDensity > 0 ⇒ bCanAir=false »
// diff layer → Both si des mods touchent la boîte, sinon Identity
// ≡ « HasAnyModInChunkRange ⇒ Mixed »
//
// et le verdict final « exactement une hypothèse survit, sinon Mixed » est littéralement le
// `if (bCanSolid == bCanAir) return Mixed;` de la fin de ClassifyTile.
//
// This fold reproduces today's hand-written ClassifyTile exactly — verified line by line against
// VoxelGenerator.cpp. That correspondence is the evidence that the abstraction fits this codebase
// rather than being imposed on it.
/** Les deux hypothèses que ClassifyTile poursuit, sous forme d'état pliable. */
struct FVoxelBoxHypotheses
{
bool bCanBeAllSolid = true;
bool bCanBeAllAir = true;
/**
* ⚠️ LES DEUX NOMBRES DU PLIAGE (`OPSTACK-DECOMPOSITION §0.2`).
* `SolidMargin` = de combien la densité est encore garantie AU-DESSUS de zéro partout dans la
* boîte, SOUS l'hypothèse « tout solide ». Un opérateur forçant la pose ; chaque carve en retire
* son amplitude maximale ; quand elle n'est plus strictement positive, l'hypothèse meurt.
* `AirMargin` est son miroir.
*
* **Elles valent 0 sur un état neuf, et c'est ce qui rend le changement rétro-compatible :**
* sans opérateur forçant qui déclare une marge, le premier carve fait `0 − FLT_MAX < 0` et tue
* l'hypothèse — le comportement exact d'avant le pliage numérique.
*/
float SolidMargin = 0.0f;
float AirMargin = 0.0f;
/** SDF interval after all SDF writers folded so far. It starts at the exact no-surface identity. */
FVoxelBoxSdfInterval Sdf;
bool IsDead() const { return !bCanBeAllSolid && !bCanBeAllAir; }
/** Verdict final : exactement une hypothèse doit survivre. Égalité = prudence ⇒ Mixed. */
EVoxelTileClass Resolve() const
{
if (bCanBeAllSolid == bCanBeAllAir) { return EVoxelTileClass::Mixed; }
return bCanBeAllSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir;
}
};
/** Poser l'état depuis un verdict FORÇANT (source, ou seal dans sa bande) : l'opérateur écrase
* l'entrée, donc il écrase aussi tout ce que la pile avait conclu avant lui. Un verdict « tout
* air » affirme du même coup « pas tout solide », et réciproquement. */
/** @param Margin de combien la densité est garantie du bon côté de zéro dans toute la boîte.
* 0 (le défaut) = « je ne sais pas » ⇒ comportement d'avant le pliage numérique. */
FORCEINLINE void VF_ForceHypotheses(FVoxelBoxHypotheses& H, EVoxelTileClass ForcedVerdict,
float Margin = 0.0f)
{
switch (ForcedVerdict)
{
case EVoxelTileClass::AllSolid: H.bCanBeAllSolid = true; H.bCanBeAllAir = false;
H.SolidMargin = Margin; H.AirMargin = 0.0f; break;
case EVoxelTileClass::AllAir: H.bCanBeAllSolid = false; H.bCanBeAllAir = true;
H.SolidMargin = 0.0f; H.AirMargin = Margin; break;
case EVoxelTileClass::Mixed:
default: H.bCanBeAllSolid = false; H.bCanBeAllAir = false;
H.SolidMargin = 0.0f; H.AirMargin = 0.0f; break;
}
}
/**
* Plier l'effet d'un opérateur dans l'état. **Monotone : ne fait que tuer**, jamais ressusciter —
* c'est la propriété de sûreté, et le pliage numérique ne l'affaiblit pas : une marge ne peut que
* DESCENDRE, jamais remonter, en dehors d'un opérateur forçant.
*
* ⚠️ `MaxCarve` / `MaxFill` valent `FLT_MAX` par défaut = « amplitude inconnue ». Une borne finie
* nulle signifie au contraire « identité prouvée » pour cette direction : l'opérateur peut être
* géométriquement présent mais ne modifie aucun échantillon de la boîte. La soustraction
* fait alors passer la marge très en dessous de zéro et l'hypothèse meurt, exactement comme la
* version purement directionnelle de ce pliage. Aucun opérateur existant ne change de verdict.
* (Arithmétique volontairement laissée en float sans garde : `0 − FLT_MAX` vaut `−FLT_MAX`,
* `−FLT_MAX − FLT_MAX` sature à `−inf`, et `−inf > 0` est faux. Pas de NaN possible, les deux
* termes étant de même signe.)
*/
FORCEINLINE void VF_FoldEffect(FVoxelBoxHypotheses& H, EVoxelOpEffect Effect,
float MaxCarve = FLT_MAX, float MaxFill = FLT_MAX)
{
switch (Effect)
{
case EVoxelOpEffect::Identity:
break;
case EVoxelOpEffect::CarveOnly:
// A finite zero bound is a proved identity for this direction. This matters for
// state-aware structural guards: a passage may be geometrically present in a box while
// its exact lattice carve factor is zero, and that must not kill an otherwise valid
// AllSolid hypothesis merely because its margin is zero/unknown.
if (MaxCarve == 0.0f)
{
break;
}
if (!(MaxCarve >= 0.0f))
{
H.bCanBeAllSolid = false;
H.SolidMargin = -FLT_MAX;
break;
}
H.SolidMargin -= MaxCarve;
if (!(H.SolidMargin > 0.0f)) { H.bCanBeAllSolid = false; }
break;
case EVoxelOpEffect::FillOnly:
if (MaxFill == 0.0f)
{
break;
}
if (!(MaxFill >= 0.0f))
{
H.bCanBeAllAir = false;
H.AirMargin = -FLT_MAX;
break;
}
H.AirMargin -= MaxFill;
if (!(H.AirMargin > 0.0f)) { H.bCanBeAllAir = false; }
break;
case EVoxelOpEffect::Both:
default:
if (MaxCarve != 0.0f)
{
if (!(MaxCarve >= 0.0f))
{
H.bCanBeAllSolid = false;
H.SolidMargin = -FLT_MAX;
}
else
{
H.SolidMargin -= MaxCarve;
if (!(H.SolidMargin > 0.0f)) { H.bCanBeAllSolid = false; }
}
}
if (MaxFill != 0.0f)
{
if (!(MaxFill >= 0.0f))
{
H.bCanBeAllAir = false;
H.AirMargin = -FLT_MAX;
}
else
{
H.AirMargin -= MaxFill;
if (!(H.AirMargin > 0.0f)) { H.bCanBeAllAir = false; }
}
}
break;
}
}
/**
* Plier UN opérateur. L'ORDRE COMPTE ICI, et c'est délibéré : un opérateur forçant écrase ce que
* la pile avait conclu AVANT lui, tandis que les opérateurs qui suivent continuent de s'appliquer.
*
* Exemple à garder en tête, parce qu'il est le piège : sur une boîte entièrement dans la bande de
* seal, la source dit peut-être « tout air » (on est au-dessus du terrain), puis le seal FORCE
* « tout solide » — verdict AllSolid, comme aujourd'hui. Si le seal ne savait dire que FillOnly,
* on obtiendrait « les deux hypothèses mortes ⇒ Mixed » : pas un trou, mais la perte pure et
* simple d'une des tuiles triviales que T1.d sait sauter. C'est pour cela que ClassifyBox existe.
*
* Réciproquement, un passage qui traverse cette même boîte rend CarveOnly APRÈS le seal et retue
* l'hypothèse solide ⇒ Mixed. Identique au code actuel, où la garde passage et la garde seal se
* neutralisent en `bCanSolid == bCanAir`.
*
* ORDER MATTERS HERE, deliberately: a forcing op overwrites what the stack concluded before it,
* while ops after it still apply. This is what lets the seal recover an AllSolid verdict that a
* pure FillOnly would have thrown away, while still letting a passage take it back.
*/
FORCEINLINE void VF_FoldOp(FVoxelBoxHypotheses& H, const IVoxelDensityOp& Op,
const FBox& VoxelBox, const FVoxelOpContext& Ctx,
bool bWritesSdf)
{
// Soundness bias: an unknown SDF interval is allowed to kill a skip, never to manufacture a
// uniform tile. The interval is propagated only after this op's own effect has been folded, so
// the next op sees exactly the SDF version that Eval would expose at this point in the stack.
const EVoxelTileClass Forced = Op.ClassifyBox(VoxelBox, Ctx);
if (Forced != EVoxelTileClass::Mixed)
{
VF_ForceHypotheses(H, Forced, Op.ForcedMarginOverBox(VoxelBox, Ctx));
if (bWritesSdf) { Op.PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); }
return;
}
VF_FoldEffect(H, Op.EffectOverBox(VoxelBox, Ctx, H),
Op.MaxCarveOverBox(VoxelBox, Ctx, H),
Op.MaxFillOverBox(VoxelBox, Ctx, H));
if (bWritesSdf) { Op.PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); }
}
/** Backward-compatible helper for unit tests and callers with no cached declaration metadata. */
FORCEINLINE void VF_FoldOp(FVoxelBoxHypotheses& H, const IVoxelDensityOp& Op,
const FBox& VoxelBox, const FVoxelOpContext& Ctx)
{
VF_FoldOp(H, Op, VoxelBox, Ctx,
(Op.ChannelWrites() & VoxelOpChannels::Sdf) != 0);
}
@@ -0,0 +1,730 @@
// VoxelDensityOpStack.h
// La PILE : un conteneur ordonné d'opérateurs, plus les fabriques d'opérateurs concrets.
// The STACK: an ordered container of operators, plus the concrete-operator factories.
//
// ⚠️ CECI EST LE CHEMIN DE GÉNÉRATION DU JEU.
// `UVoxelGenerator::GetDensityAt` construit la pile par chunk pour chaque strate :
// **Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork,
// Underwater (8 sur 8)**. `ClassifyTile` construit la même pile pour les archétypes de cave et plie
// `ClassifyBox`; les gaps et SurfaceWorld gardent leurs preuves exactes dédiées.
//
// THIS IS THE GAME'S GENERATION PATH. GetDensityAt builds the stack per chunk for every strate.
// ClassifyTile uses the same stack for cave archetypes; gaps and SurfaceWorld retain their exact
// hand-written proofs.
//
// POURQUOI CETTE FORME / WHY THIS SHAPE
// La question à laquelle cette forme répond n'est pas « est-ce que ça marche ? » mais
// **« est-ce que la séparation source / modifier tombe naturellement du code existant ? »**
// (OPSTACK-PLAN §4). Chaque archétype a été porté puis comparé bit à bit à sa fonction
// d'origine avant qu'elle ne soit retirée : la réponse a été MESURÉE plutôt qu'une opinion.
#pragma once
#include "CoreMinimal.h"
#include "VoxelDensityOp.h"
#include "VoxelDensityProfile.h"
#include "VoxelStrateTypes.h" // FMazeGenerationParams
#include "VoxelHeightOp.h" // IVoxelBiomeField — BuildSurfaceStack takes ownership of one
class UVoxelGenerator;
class UVoxelStrateManager;
struct FVoxelOpStackRecipe;
struct FVoxelStrateArchetypeParams;
struct FVoxelStrateRegionManifest;
enum class EVoxelStrateOpClass : uint8;
/** The declarations the offline structure roller reads before it ever materialises an op. */
struct VOXELFORGE_API FVoxelStrateOpContract
{
EVoxelOpRole Role = EVoxelOpRole::DetailModifier;
EVoxelOpChannelMask Reads = VoxelOpChannels::None;
EVoxelOpChannelMask Writes = VoxelOpChannels::None;
bool bAdditive = false;
EVoxelOpResourceMask RequiredResources = VoxelOpResources::None;
EVoxelOpResourceMask ProvidedResources = VoxelOpResources::None;
};
/**
* A lowered evaluator selected when a stack has a proven, canonical implementation.
*
* The authored operator list remains present for classification, diagnostics, tests, and the
* fallback evaluator. This is a plan choice made while the stack is assembled, not a runtime
* CPU-feature dispatch and not a second semantic graph.
*/
enum class EVoxelOpFusedEvaluator : uint8
{
None,
TunnelNetwork,
};
/**
* FVoxelOpStack — une liste ordonnée d'opérateurs + le pliage de verdict de boîte.
*
* PROPRIÉTÉ (rôle 4) : les opérateurs STRUCTURELS sont ajoutés par `AppendStructuralPost` et
* l'ordre spine → seal → passage → seal XY est garanti par cette fonction, pas par l'auteur. Un auteur ne
* peut pas les omettre ni les réordonner — ce sont des invariants de monde (la descente doit rester
* possible, les seals doivent tenir, les passages doivent percer).
*
* PROPRIÉTÉ (threading) : la pile est LUE par les workers. Les opérateurs concrets qui ont besoin
* d'un cache par cellule/chunk le tiennent en `thread_local` à l'intérieur de leur `Eval`, comme le
* fait déjà chaque fonction d'archétype. En Phase 3, quand les opérateurs deviendront des assets
* partagés, il faudra un objet d'état PAR WORKER — noté ici pour que ça ne surprenne personne.
*
* THREADING: the stack is READ by workers. Concrete ops that need a per-cell/per-chunk cache keep it
* thread_local inside Eval, exactly as every archetype function already does. Phase 3 (ops as shared
* assets) will need a per-worker state object — flagged here so it is not a surprise.
*/
class FVoxelOpStack
{
private:
/** Metadata is snapshotted when an op enters the stack so no declaration virtual is ever
* dispatched from `EvalSample`'s per-voxel loop. */
struct FOpEntry
{
TUniquePtr<IVoxelDensityOp> Op;
EVoxelOpChannelMask Reads = VoxelOpChannels::None;
EVoxelOpChannelMask Writes = VoxelOpChannels::None;
bool bAdditive = false;
EVoxelOpResourceMask RequiredResources = VoxelOpResources::None;
EVoxelOpResourceMask ProvidedResources = VoxelOpResources::None;
const TCHAR* ProfileName = nullptr;
VoxelDensityProfile::EBucket ProfileBucket = VoxelDensityProfile::EBucket::OtherOp;
};
public:
// DÉPLAÇABLE, PAS COPIABLE — et c'est la bonne sémantique, pas un contournement de compilateur :
// une pile POSSÈDE ses opérateurs de façon unique. La copier voudrait dire cloner des opérateurs
// polymorphes, ce qui n'a pas de sens ici (une pile n'existe qu'une fois par strate).
//
// ⚠️ NOTE COMPILATEUR : ne PAS remettre `VOXELFORGE_API` sur la classe. Sous MSVC, dllexport sur
// une classe force l'instanciation de TOUS ses membres implicites, y compris l'opérateur
// d'affectation par copie — impossible à générer pour un `TArray<FOpEntry>`, d'où
// l'erreur C2280 « fonction supprimée ». L'export va sur la seule méthode hors-ligne.
//
// MOVE-ONLY, and that is the correct semantics rather than a compiler workaround: a stack
// uniquely OWNS its operators. Do NOT put VOXELFORGE_API back on the class — under MSVC,
// dllexport forces instantiation of every implicit member including copy-assignment, which
// cannot be generated for a TArray<FOpEntry> (error C2280). Export the out-of-line
// method instead.
FVoxelOpStack() = default;
FVoxelOpStack(FVoxelOpStack&&) = default;
FVoxelOpStack& operator=(FVoxelOpStack&&) = default;
FVoxelOpStack(const FVoxelOpStack&) = delete;
FVoxelOpStack& operator=(const FVoxelOpStack&) = delete;
void Add(TUniquePtr<IVoxelDensityOp> Op)
{
// Adding an op invalidates any previously selected lowered plan. Factories mark the plan
// only after the complete authored graph has been assembled.
FusedEvaluator = EVoxelOpFusedEvaluator::None;
FOpEntry Entry;
Entry.Op = MoveTemp(Op);
if (Entry.Op.Get() != nullptr)
{
// Assembly-time metadata only. ValidateChannelOrder reuses this snapshot, and Eval
// never asks an operator for its declaration.
Entry.Reads = Entry.Op->ChannelReads();
Entry.Writes = Entry.Op->ChannelWrites();
Entry.bAdditive = Entry.Op->IsAdditive();
Entry.RequiredResources = Entry.Op->RequiredResources();
Entry.ProvidedResources = Entry.Op->ProvidedResources();
Entry.ProfileName = Entry.Op->DebugName();
Entry.ProfileBucket = VoxelDensityProfile::BucketFromName(Entry.ProfileName);
}
Ops.Add(MoveTemp(Entry));
}
int32 Num() const { return Ops.Num(); }
/** Select the lowered evaluator for this complete authored graph. */
void SetFusedEvaluator(EVoxelOpFusedEvaluator InEvaluator)
{
FusedEvaluator = InEvaluator;
}
/** The lowered plan, or None when the general interpreted/block path must be used. */
EVoxelOpFusedEvaluator GetFusedEvaluator() const
{
return FusedEvaluator;
}
/** True when this stack publishes the requested immutable geometry resource. */
bool ProvidesResource(EVoxelOpResourceMask Resource) const
{
if (Resource == VoxelOpResources::None)
{
return false;
}
for (const FOpEntry& Entry : Ops)
{
if ((Entry.ProvidedResources & Resource) == Resource)
{
return true;
}
}
return false;
}
// Allocator-backed bytes owned by the stack, including each concrete operator allocation.
// The enclosing cache entry adds sizeof(FVoxelOpStack) separately.
SIZE_T GetAllocatedSize() const;
/** Hoist chunk-constant work for every op. Une fois par chunk et par worker.
* Non-const : ça MUTE l'état par-chunk des opérateurs, et le prétendre const serait un
* mensonge utile qui finirait par masquer une course. */
void PrepareChunk(const FVoxelOpContext& Ctx)
{
PreparedContext = Ctx;
bHasPreparedContext = true;
for (const FOpEntry& Entry : Ops) { Entry.Op->PrepareChunk(Ctx); }
}
/**
* Évalue la pile complète en un point. Rend la densité en convention INTERNE
* (positif = solide) — l'appelant négate UNE FOIS pour le marching cubes.
*
* Returns INTERNAL-convention density (positive = solid). The caller negates once for MC.
*/
float EvalInternal(float WorldX, float WorldY, float WorldZ) const
{
return EvalSample(WorldX, WorldY, WorldZ).Density;
}
/** L'état COMPLET (densité + SDF) après toute la pile. Diagnostic : quand deux évaluations
* divergent, c'est le canal SDF qui dit si l'écart naît avant ou après la conversion.
* / The full state after the stack — the SDF channel is what says whether a divergence is
* born before or after the carve. */
FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const
{
FVoxelOpSample S;
// Fine operation timers are a diagnostic mode. Keeping their construction out of the
// ordinary scalar path matters for callers that still use fractional samples or an
// archetype without a block session; the block path already times one scope per op/block.
const bool bProfileOps = VoxelDensityProfile::GetMode() == VoxelDensityProfile::EMode::Full;
for (const FOpEntry& Entry : Ops)
{
if (bProfileOps)
{
VoxelDensityProfile::FScopedTimer ProfileTimer(Entry.ProfileBucket);
Entry.Op->Eval(WorldX, WorldY, WorldZ, S);
}
else
{
Entry.Op->Eval(WorldX, WorldY, WorldZ, S);
}
}
// The room-graph source publishes the authored floor ownership while evaluating the
// shape, but it must not be reasserted here. This stack is followed by the common
// disturbance/passage writers; composing the floor at this point would make every authored
// floor sample look like a post-hoc clamp (and counted 100% of them). The generator
// consumes the same immutable core result after those writers and composes the floor once,
// at the final MC boundary. The metadata remains on the sample for that hand-off and for
// diagnostics, but it is not allowed to modify density inside the operator stack.
return S;
}
/**
* Evaluate one regular lattice in operator order. The caller owns and initializes the sample
* array (normally to FVoxelOpSample defaults); each operator transforms that same array before
* the next operator runs. The timer is intentionally one scope per operator/block rather than
* one scope per operator/sample. Operators without a block implementation use the conservative
* scalar fallback supplied by IVoxelDensityOp, so this entry point is safe to introduce before
* converting the concrete operators.
*/
void EvalBlock(const FVoxelOpBlock& Block) const
{
if (Block.Samples == nullptr || Block.Step <= 0
|| Block.SizeX <= 0 || Block.SizeY <= 0 || Block.SizeZ <= 0)
{
return;
}
const bool bProfileOps = VoxelDensityProfile::GetMode() == VoxelDensityProfile::EMode::Full;
for (const FOpEntry& Entry : Ops)
{
if (bProfileOps)
{
VoxelDensityProfile::FScopedTimer ProfileTimer(Entry.ProfileBucket);
Entry.Op->EvalBlock(Block);
}
else
{
Entry.Op->EvalBlock(Block);
}
}
}
/** Evaluate only the entries that the interval fold proved non-identity for this block. */
void EvalBlock(const FVoxelOpBlock& Block, const TArray<int32>& ActiveOps) const
{
if (Block.Samples == nullptr || Block.Step <= 0
|| Block.SizeX <= 0 || Block.SizeY <= 0 || Block.SizeZ <= 0)
{
return;
}
const bool bProfileOps = VoxelDensityProfile::GetMode() == VoxelDensityProfile::EMode::Full;
for (const int32 OpIndex : ActiveOps)
{
if (!Ops.IsValidIndex(OpIndex)) { continue; }
const FOpEntry& Entry = Ops[OpIndex];
if (bProfileOps)
{
VoxelDensityProfile::FScopedTimer ProfileTimer(Entry.ProfileBucket);
Entry.Op->EvalBlock(Block);
}
else
{
Entry.Op->EvalBlock(Block);
}
}
}
/**
* Build a conservative active list for one exact mesher lattice block. An operator may be
* omitted only when its state-aware box contract says Identity and it does not publish SDF.
* SDF writers remain in the list even when their density effect is Identity: their SDF is a
* channel consumed by a later converter, and an unchanged-looking interval is not by itself a
* pointwise proof. Unknown costs the operator visit, never the geometry.
*/
int32 BuildActiveOpList(const FBox& VoxelBox, int32 InStep,
FIntVector InLatticeOrigin, TArray<int32>& OutActiveOps) const
{
OutActiveOps.Reset();
if (!bHasPreparedContext)
{
for (int32 Index = 0; Index < Ops.Num(); ++Index) { OutActiveOps.Add(Index); }
return OutActiveOps.Num();
}
FVoxelOpContext Ctx = PreparedContext;
Ctx.Step = FMath::Max(InStep, 1);
Ctx.LatticeOriginVoxels = InLatticeOrigin;
Ctx.bUseLatticeProof = true;
FVoxelBoxHypotheses H;
for (int32 Index = 0; Index < Ops.Num(); ++Index)
{
const FOpEntry& Entry = Ops[Index];
const bool bWritesSdf = (Entry.Writes & VoxelOpChannels::Sdf) != 0;
const bool bPublishesResource = Entry.ProvidedResources != VoxelOpResources::None;
const EVoxelTileClass Forced = Entry.Op->ClassifyBox(VoxelBox, Ctx);
bool bCanSkip = false;
if (Forced != EVoxelTileClass::Mixed)
{
VF_ForceHypotheses(H, Forced, Entry.Op->ForcedMarginOverBox(VoxelBox, Ctx));
if (bWritesSdf) { Entry.Op->PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); }
}
else
{
const EVoxelOpEffect Effect =
Entry.Op->EffectOverBox(VoxelBox, Ctx, H);
bCanSkip = Effect == EVoxelOpEffect::Identity
&& !bWritesSdf
&& !bPublishesResource;
VF_FoldEffect(H,
Effect,
Entry.Op->MaxCarveOverBox(VoxelBox, Ctx, H),
Entry.Op->MaxFillOverBox(VoxelBox, Ctx, H));
if (bWritesSdf) { Entry.Op->PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); }
}
if (!bCanSkip) { OutActiveOps.Add(Index); }
}
return OutActiveOps.Num();
}
/** Le même, négaté pour le mesher (négatif = solide). */
float EvalMC(float WorldX, float WorldY, float WorldZ) const
{
return -EvalInternal(WorldX, WorldY, WorldZ);
}
/** Return a same-sample structural-core result published by a source in this stack. */
bool TryGetLastTunnelCoreWorldEvaluation(
FTunnelCoreWorldEvaluation& OutEvaluation) const;
/**
* Le pliage générique qui remplacera les gardes écrites à la main dans ClassifyTile.
* Voir `VF_FoldOp` (VoxelDensityOp.h) pour la sémantique — en particulier pourquoi un
* opérateur FORÇANT (les seals dans leurs bandes) écrase ce que la pile avait conclu avant lui.
*/
EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
{
FVoxelBoxHypotheses H;
for (const FOpEntry& Entry : Ops)
{
VF_FoldOp(H, *Entry.Op, VoxelBox, Ctx,
(Entry.Writes & VoxelOpChannels::Sdf) != 0);
// Do not early-out on a dead hypothesis: a later forcing structural post may
// deliberately overwrite it (the XY edge seal is appended after passage tube/landing
// carving and support).
}
return H.Resolve();
}
/**
* LE MÊME PLIAGE, MAIS QUI DIT **QUI** A TUÉ CHAQUE HYPOTHÈSE. Diagnostic, réservé aux tests.
*
* ⚠️ IL DOIT RENDRE EXACTEMENT LE MÊME VERDICT QUE `ClassifyBox` — même boucle, même ordre,
* y compris le fait de laisser un post forçant ultérieur ressusciter une hypothèse morte. Un diagnostic qui emprunte un chemin légèrement différent de celui qu'il explique
* est pire que pas de diagnostic : il envoie chercher le bug ailleurs. Si l'un des deux change,
* l'autre change avec lui.
*
* `OutSolidKiller` / `OutAirKiller` reçoivent l'INDEX du premier opérateur qui fait passer
* l'hypothèse correspondante de vraie à fausse, ou `INDEX_NONE` si elle a survécu. Le nom
* lisible s'obtient par `GetOpDebugName(index)`.
*
* Same fold, but it reports WHICH op killed each hypothesis. Must stay verdict-identical to
* ClassifyBox, including later forcing posts that may overwrite a dead hypothesis — a
* diagnostic that takes a slightly different path sends you hunting in the wrong place.
*/
EVoxelTileClass ClassifyBoxAttributed(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
int32& OutSolidKiller, int32& OutAirKiller) const
{
OutSolidKiller = INDEX_NONE;
OutAirKiller = INDEX_NONE;
FVoxelBoxHypotheses H;
for (int32 i = 0; i < Ops.Num(); ++i)
{
const bool bSolidBefore = H.bCanBeAllSolid;
const bool bAirBefore = H.bCanBeAllAir;
VF_FoldOp(H, *Ops[i].Op, VoxelBox, Ctx,
(Ops[i].Writes & VoxelOpChannels::Sdf) != 0);
if (bSolidBefore && !H.bCanBeAllSolid && OutSolidKiller == INDEX_NONE) { OutSolidKiller = i; }
if (bAirBefore && !H.bCanBeAllAir && OutAirKiller == INDEX_NONE) { OutAirKiller = i; }
}
return H.Resolve();
}
/**
* Per-operator box-bound audit. This deliberately replays the same fold as ClassifyBox while
* also brute-forcing Eval on the queried lattice. It is report/test infrastructure only: it
* must never be used to make a generation decision.
*/
struct FOpBoxDiagnostic
{
int32 Index = INDEX_NONE;
FString Name;
EVoxelOpEffect Effect = EVoxelOpEffect::Identity;
EVoxelTileClass ForcedVerdict = EVoxelTileClass::Mixed;
bool bForced = false;
bool bWritesSdf = false;
float ForcedMargin = 0.0f;
float MaxCarve = 0.0f;
float MaxFill = 0.0f;
bool bHasDensityDelta = false;
float ActualDensityDeltaMin = FLT_MAX;
float ActualDensityDeltaMax = -FLT_MAX;
bool bHasSdfValue = false;
float ActualSdfMin = FLT_MAX;
float ActualSdfMax = -FLT_MAX;
bool bHasSdfBound = false;
float SdfBoundMin = FLT_MAX;
float SdfBoundMax = FLT_MAX;
bool bCarveBoundViolated = false;
bool bFillBoundViolated = false;
uint64 SampleCount = 0;
};
VOXELFORGE_API void DiagnoseBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
int32 SampleStep, TArray<FOpBoxDiagnostic>& OutDiagnostics,
EVoxelTileClass* OutVerdict = nullptr) const;
/** Nom lisible d'un opérateur, pour les rapports de test. Voir `IVoxelDensityOp::DebugName`. */
const TCHAR* GetOpDebugName(int32 Index) const
{
return Ops.IsValidIndex(Index) ? Ops[Index].Op->DebugName() : TEXT("(none)");
}
/**
* Validate the stack's channel dependency DAG without evaluating a voxel.
*
* Rule: each channel has a current version. A channel is unavailable until an earlier op
* writes it. The one deliberate exception is a root `FieldSource` that reads and writes the
* same channel as an identity fold (for example `min(FLT_MAX, Sdf)`); it may consume that
* channel's `FVoxelOpSample` initial identity while publishing the first real version. An op
* may read only the version visible on entry; a read/write op consumes that version and
* publishes the next one. A write-only op is a replacement/producer, so it is legal only as
* the first producer of that channel; allowing a second write-only producer would silently
* clobber the earlier field. An additive op must read every channel it writes, because an
* additive delta cannot introduce or replace a channel. All producer-to-consumer edges
* therefore point forward in the list, and the list is a legal topological ordering of the
* channel DAG. A later read deliberately sees the writer's post-op version; a consumer that
* needs a pre-write value must be placed before that writer. The declarations do not invent
* dependencies from names, roles, or Eval side effects.
*
* This is assembly/diagnostic work only. It must not be called from Eval or any voxel loop.
* Returns false and optionally describes the first violation.
*/
VOXELFORGE_API bool ValidateChannelOrder(FString* OutError = nullptr) const;
/**
* RÔLE 4 — ajoute les invariants de monde, dans l'ordre fixe, à la fin de la pile.
* spine (0,0) → seal de frontière → carve de passage → [posts spécifiques] → seal de limite XY.
*
* ⚠️ La couche de diff (édits joueur) n'est PAS ici : elle vit dans `GetDensityAt`, APRÈS la
* négation MC, avec les disturbances. Elle rejoindra la pile quand les disturbances seront
* portées et que la question de convention MC-vs-interne sera tranchée pour de bon
* (OPSTACK-DECOMPOSITION §10.2). Tant que la pile n'alimente pas le jeu, c'est sans effet.
*
* The diff layer is NOT here: it lives in GetDensityAt, AFTER the MC negate, with disturbances.
* It joins the stack when disturbances are ported. Harmless while the stack feeds nothing.
*
* @param StrateManager peut être nullptr → pas de carve de passage (comme le fallback actuel).
* Le seal XY reste toujours présent ; son rayon/thickness viennent de Ctx
* au moment de PrepareChunk (0 = monde non borné).
*/
VOXELFORGE_API void AppendStructuralPost(float StrateTopWorldZ, float StrateBottomWorldZ,
float SealThickness, float BaseDensity, float SpineRadius,
const UVoxelStrateManager* StrateManager,
bool bAppendEdgeSeal = true);
private:
TArray<FOpEntry> Ops;
FVoxelOpContext PreparedContext;
bool bHasPreparedContext = false;
EVoxelOpFusedEvaluator FusedEvaluator = EVoxelOpFusedEvaluator::None;
};
//=============================================================================
// FABRIQUES / FACTORIES
//=============================================================================
namespace VoxelDensityOps
{
/** Query the concrete op declaration used by the offline recipe roller. */
VOXELFORGE_API bool GetStrateOpContract(EVoxelStrateOpClass OpClass,
FVoxelStrateOpContract& OutContract);
/** Rôle 1 — `Density = BaseDensity` partout. `ClassifyBox` → AllSolid, exact et gratuit.
* Racine de TunnelNetwork, Maze, VerticalShafts et des gaps de bedrock. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeConstantRockSource(float BaseDensity);
/** Rôle 1 — le MÊME opérateur au signe près : `Density = -BaseDensity`, un grand vide ouvert.
* `ClassifyBox` → **AllAir**, ce qu'aucune source n'avait encore su rendre — c'est ce qui rend
* une strate d'îles flottantes (surtout vide) sautable là où aucune île n'arrive. Racine de
* FloatingIslands. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeConstantVoidSource(float BaseDensity);
/** Rôle 1 — les couloirs de Maze : capsules sur les arêtes ouvertes d'un treillis 3D.
* Écrit le canal SDF uniquement. Chaque nœud hors origine choisit un parent vers l'origine;
* les arêtes canoniques (nœud inférieur + axe) testent localement les deux parents. La source
* met en cache les décisions de la fenêtre {-1,0}³ par cellule; les chunks adjacents voient
* donc exactement le même graphe, sans collect global ni couture. */
/** The source answers only for its own SDF field. Its box query publishes an SDF interval;
* each later converter or modifier consumes that interval through the generic fold. It does
* not know, and must not answer for, any downstream operator. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeLatticeCorridorSource(const FMazeGenerationParams& P,
int32 Seed);
/** Rôle 3 — rugosité de paroi appliquée au canal SDF (variante Maze/Shafts/Islands).
* `Frequency` est codée en dur au site d'appel aujourd'hui (0.12 pour Maze) ; l'exposer est
* un gain d'authoring gratuit, et §2.6 autorise explicitement le re-tune. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeSdfRoughnessMod(float Strength, float Frequency,
int32 BaseOctaves, float ApplyWithin);
/** Rôle 2 — conversion SDF → densité : creuse de l'air là où le SDF est à l'intérieur.
* Les six mêmes lignes apparaissent aujourd'hui dans TunnelNetwork, Maze et VerticalShafts.
* @param MinDivisor plancher du diviseur `Blend·2`. **TunnelNetwork passe 1.0** (son original
* écrit `FMath::Max(SDFBlendRadius·2, 1)`) ; Maze/Shafts laissent 0, où
* `Max(x,0) == x` exactement. Les deux formules divergent si `Blend·2 < 1`,
* donc ce paramètre est une vraie différence, pas une précaution. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeSdfCarve(float Blend, float BaseDensity,
float MinDivisor = 0.0f);
/** Rôle 2 — la même conversion, signe opposé : REMPLIT du solide là où le SDF est à l'intérieur.
* C'est ce que fait FloatingIslands (`Density += Fill·Base·2`), et la multiplication par ±1
* étant exacte en IEEE-754, le chemin carve reste bit pour bit ce qu'il était. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeSdfFill(float Blend, float BaseDensity);
/** Rôle 1 — la dalle : surface de sol + surface de plafond → champ de vide. **XY-PUR** depuis
* OPSTACK-DECOMPOSITION §3.1 (le terme en Z des deux bruits est parti), ce qui lui donne un
* `ClassifyBox` EXACT sans échantillonnage : les deux surfaces vivent dans des bandes en Z
* bornées par le supremum prouvé de FBM (1.5, not the nominal parameter label). Sert
* FlatPlain **et** CrystalChamber. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeSlabVoidSource(const FSlabGenerationParams& P, int32 Seed);
/** Rôle 3 — cylindres de hauteur infinie sur une grille monde. N'ajoute que du solide ⇒
* `FillOnly` quand une colonne atteint la boîte, `Identity` (le cas courant) sinon. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeGridColumnMod(const FSlabGenerationParams& P, int32 Seed);
/**
* SurfaceWorld, COMPLET : colonne (sol + voûte) → densité, overhang 3D, post structurel, et le
* mélange de biomes quand `PerBiomeParams` est non vide.
*
* @param PerBiomeParams vide ⇒ pas de biomes, chemin d'origine strictement inchangé. Non vide
* ⇒ une pile de hauteur COMPLÈTE par biome, sol mélangé / voûte
* sélectionnée, amplitude d'overhang interpolée (§5, combiner `Mask`).
* @param BiomeField **transféré** à la pile, qui le possède. Doit répondre pour les mêmes
* indices que `PerBiomeParams`. `nullptr` avec des params non vides ⇒
* biome 0 partout (dégradation sûre, pas un crash).
*/
VOXELFORGE_API void BuildSurfaceStack(FVoxelOpStack& OutStack, const FSurfaceGenerationParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
const TArray<FSurfaceGenerationParams>& PerBiomeParams =
TArray<FSurfaceGenerationParams>(),
TUniquePtr<IVoxelBiomeField> BiomeField = nullptr,
bool bAppendStructuralPosts = true);
/**
* FlatPlain ET CrystalChamber — la même pile, **sans branchement sur le type** :
* SlabVoidSource → GridColumnMod → [structural post ×4]
*
* Deux des huit archétypes tiennent dans un opérateur (OPSTACK-PLAN §4) : leur différence est
* un jeu de valeurs par défaut, pas du code.
*/
VOXELFORGE_API void BuildSlabStack(FVoxelOpStack& OutStack, const FSlabGenerationParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
/**
* VerticalShafts — 11 ops, dont les connecteurs air/sol et les quatre structural posts :
* ConstantRock → ShaftField → SdfRoughness → SdfCarve → ShaftLedge
* → ShaftConnectorAir → ShaftConnectorFloor → [structural post ×4]
*
* Trois de ces ops sont ceux de Maze (`§2.5`), avec une source différente et d'autres réglages
* (fréquence 0.1 au lieu de 0.12, fenêtre `rough + 4` au lieu de `R + rough + 2`).
*/
VOXELFORGE_API void BuildVerticalShaftStack(FVoxelOpStack& OutStack, const FVerticalShaftParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
/**
* TunnelNetwork — **COMPLET** :
* ConstantRock → RoomGraph(warp + pits + cheminées) → SdfCarve → CaveRoughness(4b)
* → Terrace → LayerLines → Ribbing → Overhang → Cliff → Scallop → Arch → RoomColumn(4d)
* → Dome(4g) → Pinch(4h) → FloorBias → Worms → [spine, boundary, passage, graph-air, XY seal]
*
* L'override d'op PAR SALLE (étape C1) n'ajoute aucun opérateur : `FRoomGraphSource` publie
* `LocalParams()` — les params de la strate avec l'op de la salle la plus proche appliqué — et
* ONZE des douze modificateurs y lisent leurs champs. La rugosité (4b) lit les params de la
* STRATE, parce que dans l'original elle précède la déclaration du shadow.
*
* ⚠️ `FRoomGraphSource` **APPELLE** `BuildChunkCache`/`EvaluateSDFCached`, il ne les transcrit
* pas : c'est là que vit la discipline d'invariance de fenêtre à deux régions (`ARCHITECTURE
* §8.4`), et en faire une copie serait le pire résultat possible pour un refactor dont le but est
* d'avoir UNE définition de chaque idée.
*/
VOXELFORGE_API void BuildTunnelNetworkStack(FVoxelOpStack& OutStack,
const FStrateGenerationParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
/**
* DIAGNOSTIC — la ventilation par CLASSE DE PRIMITIVE de la dernière propagation d'intervalle
* de `FRoomGraphSource` évaluée sur ce thread. **Tests uniquement. N'entre dans aucune décision
* de génération.**
*
* ⚠️ POURQUOI ÇA EXISTE PLUTÔT QUE D'ÊTRE REFAIT DANS LE TEST. Le test a déjà tout ce qu'il faut
* pour rejouer le critère — il appelle `BuildChunkCache` ailleurs. Le rejouer serait une
* DEUXIÈME définition du critère, qui dériverait de la vraie et mentirait exactement le jour où
* on la croirait. C'est la même raison qui a fait exister `VF_BuildOpStackForChunk`. On expose
* donc ce que l'opérateur a réellement calculé.
*
* `Hit*` = combien de primitives de cette classe atteignent la boîte (0 partout ⇒ `Identity`).
* `Num*` = combien le cache en contenait, ce qui distingue « aucune n'atteint » de « il n'y en
* avait aucune » — deux zéros de sens opposé.
*
* Reads back what the operator actually computed, rather than letting the test re-derive the
* criterion: a second copy would drift and would lie on the day it was believed.
*/
struct FRoomBoxDiagnostic
{
int32 HitRooms = 0, HitTunnels = 0, HitPits = 0, HitChimneys = 0;
int32 NumRooms = 0, NumTunnels = 0, NumPits = 0, NumChimneys = 0;
/** Les mêmes comptes si la dilatation de warp valait ZÉRO, et de combien de voxels la boîte
* est effectivement dilatée. `Hit* - Hit*NoWarp` = la part du blocage due à MA boîte plutôt
* qu'à la géométrie. Cette mesure manquait, et son absence a coûté trois builds de
* resserrement autour du mauvais terme. */
int32 HitRoomsNoWarp = 0, HitTunnelsNoWarp = 0;
float WarpDilation = 0.0f;
/** Conservative candidate for the world-space tunnel-core post that runs after EvalMC.
* This is derived from the final post's own world-chain bounds, not from the warped SDF
* source's broader threshold hit count. */
bool bMayHaveTunnelCoreAir = false;
bool bMayHaveTunnelSupportFloor = false;
bool bMayHaveTunnelCoreTail = false;
uint32 ExactTailQueries = 0;
uint32 ExactTailEvaluated = 0;
uint64 ExactTailCycles = 0;
uint64 PropagateCycles = 0;
uint64 ExactPrimitiveCycles = 0;
uint64 CacheWindowCycles = 0;
int32 NumRoomFloorJoins = 0;
};
VOXELFORGE_API FRoomBoxDiagnostic GetLastRoomBoxDiagnostic();
/** Current worker-local room-graph cache footprint; profiling/diagnostic use only. */
VOXELFORGE_API void ReportWorkerRoomGraphCacheFootprint();
/** Current process-wide immutable room-graph proof-cache footprint; diagnostic use only. */
VOXELFORGE_API void ReportSharedRoomGraphCacheFootprint();
/** Drop completed proof caches from older manager lifetimes, or all of them when zero. */
VOXELFORGE_API void TrimSharedRoomGraphCache(uint64 KeepManagerLifetimeId = 0);
/**
* FloatingIslands — 8 ops, et **la pile tourne à l'ENVERS** :
* ConstantVoid → IslandBlob → SdfRoughness → SdfFill → [structural post ×4]
*
* Les quatre archétypes portés jusqu'ici partent de ROC et CREUSENT ; celui-ci part du VIDE et
* REMPLIT. Aucune des deux extrémités n'a demandé d'opérateur neuf — `FConstantFieldSource` et
* `FSdfConvertOp` sont les mêmes classes au signe près, et `FSdfRoughnessMod` est repris sans
* une ligne de changement (4ᵉ archétype). Seul le blob d'île est nouveau.
*
* The stack that runs backwards: void source + fill instead of rock source + carve, using the
* SAME operators with the opposite sign.
*/
VOXELFORGE_API void BuildFloatingIslandStack(FVoxelOpStack& OutStack, const FFloatingIslandParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
/**
* La pile Maze complète, décomposée — PAS un `FMazeOp` monolithique :
* ConstantRockSource → LatticeCorridorSource → SdfRoughnessMod → SdfCarve → [structural post]
*
* C'est le test de la Phase 1 : si Maze ne se décompose pas ainsi, l'abstraction est mauvaise
* pour ce domaine (OPSTACK-PLAN §4, déclencheur d'arrêt).
*/
VOXELFORGE_API void BuildMazeStack(FVoxelOpStack& OutStack, const FMazeGenerationParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
}
/** Materialise an offline recipe. Structural posts are appended by default; lateral region cores
* deliberately pass false so the parent stack can append the global posts exactly once. */
VOXELFORGE_API bool VF_BuildStackFromRecipe(const FVoxelOpStackRecipe& Recipe,
const FVoxelStrateArchetypeParams& Params,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr,
bool bAppendStructuralPosts = true);
/** Build the region combiner stack. Creative cores are independent; structural posts are global. */
#if WITH_EDITOR
VOXELFORGE_API bool VF_BuildStrateRegionStack(
const FVoxelStrateRegionManifest& Manifest,
float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr);
#endif
@@ -0,0 +1,382 @@
// VoxelDensityPrimitives.h
// Les quatre post-traitements STRUCTURELS partagés par chaque archétype.
// The four STRUCTURAL post-processes every archetype shares.
//
// POURQUOI CE FICHIER EXISTE / WHY THIS FILE EXISTS
// Les trois primitives verticales étaient `static` dans VoxelGenerator.cpp et appelées à l'identique
// par les six fonctions de densité. La pile d'opérateurs a besoin des MÊMES, donc elles montent ici :
// UNE copie partagée. Le scellement XY est ajouté dans ce même fichier pour que les deux chemins
// partagent aussi la nouvelle invariance — les invariants de monde ne sont pas des choix créatifs.
//
// The three vertical primitives were `static` in VoxelGenerator.cpp and called identically by all six
// density functions. The operator stack needs the same ones, so they move here: ONE shared copy. The
// XY seal is added beside them so both paths share the new invariant too.
//
// ⚠️ CONVENTION DE SIGNE — la source n°1 de confusion du plugin.
// Ces primitives travaillent en convention INTERNE : **positif = SOLIDE, négatif = AIR**.
// C'est la convention dans laquelle chaque fonction d'archétype est écrite ; la négation vers la
// convention marching-cubes (négatif = solide) se fait UNE FOIS, sur le `return`.
// SIGN CONVENTION: these work in INTERNAL convention — **positive = SOLID**. The negate to MC
// convention happens ONCE, at the caller's return.
//
// Les trois corps déplacés restent identiques; le quatrième est la nouvelle frontière globale.
// The three moved bodies remain identical; the fourth is the new global boundary.
#pragma once
#include "CoreMinimal.h"
#include "VoxelTypes.h" // SmoothStep01
#include "VoxelPassageGeometry.h" // shared body-sized landing/tunnel geometry
#include "VoxelDensityAblation.h" // development-only world-changing measurements
#include "VoxelTilePostReach.h" // worker-local landing/structural post reach bits
//=============================================================================
// SEAL DE FRONTIÈRE / BOUNDARY SEAL
//=============================================================================
// Seal solide aux bords haut et bas de la strate. Fade smoothstep sur `Thickness` voxels depuis
// chaque bord. N'AJOUTE que de la densité (FMath::Max), jamais n'en enlève → le joueur ne peut
// jamais percer le seal "par accident", seulement via les passages.
//
// ⚠️ C'est un opérateur FORÇANT, pas seulement un FillOnly : à l'intérieur de la bande, avec
// SealFactor > 0 et BaseDensity > 0, le résultat est solide GARANTI quelle qu'ait été l'entrée.
// C'est exactement ce qu'encode `IVoxelDensityOp::ClassifyBox` (voir VoxelDensityOp.h), et la
// raison pour laquelle cette méthode existe.
FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
float StrateTopZ, float StrateBottomZ,
float Thickness, float BaseDensity)
{
if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return;
const float DistTop = StrateTopZ - WorldZ; // + si on est sous le plafond
const float DistBot = WorldZ - StrateBottomZ; // + si on est au-dessus du sol
if (DistTop >= 0.0f && DistTop < Thickness)
{
float SealFactor = 1.0f - (DistTop / Thickness);
SealFactor = SmoothStep01(SealFactor);
Density = FMath::Max(Density, SealFactor * BaseDensity);
}
if (DistBot >= 0.0f && DistBot < Thickness)
{
float SealFactor = 1.0f - (DistBot / Thickness);
SealFactor = SmoothStep01(SealFactor);
Density = FMath::Max(Density, SealFactor * BaseDensity);
}
}
//=============================================================================
// CARVE DE PASSAGE / PASSAGE CARVING
//=============================================================================
// Creuse un passage inter-strates. Évalué APRÈS le seal pour que les passages puissent percer à
// travers le bouchon solide. Le rayon de blend est hard-codé à 4.0f — à exposer via
// UVoxelSettings si on veut pouvoir le tweaker.
FORCEINLINE void VF_ApplyPassageCarving(float& Density, float ModSDF,
float BaseDensity, float SealThickness)
{
if (VoxelDensityAblation::IsPassageCarvingOff()) return;
constexpr float PASSAGE_BLEND_RADIUS = 4.0f;
if (ModSDF >= PASSAGE_BLEND_RADIUS) return;
float CarveFactor = FMath::Clamp(
(PASSAGE_BLEND_RADIUS - ModSDF) / (PASSAGE_BLEND_RADIUS * 2.0f),
0.0f, 1.0f);
CarveFactor = SmoothStep01(CarveFactor);
// FORCE the density toward guaranteed AIR so the passage punches through ANYTHING in
// its path (seals, columns, surface roughness, terrain ops). A plain subtraction can
// be out-paced by stacked density additions, leaving solid plugs mid-tunnel — which is
// why the shaft "didn't go all the way through". Lerp toward a strongly negative target
// and take the min so we only ever make it MORE air (never refill an existing cave).
const float AirTarget = -(BaseDensity * 2.0f + SealThickness + 4.0f);
Density = FMath::Min(Density, FMath::Lerp(Density, AirTarget, CarveFactor));
}
// Landing air is also reasserted after the MC-space disturbance post. Unlike the legacy tube
// carve above, this threshold is independent of the incoming density: applying it a second time
// is therefore bit-stable. The structural passage op and the post-disturbance backstop can share
// the same operation without turning an otherwise unchanged live-vs-direct op-stack comparison
// into a second, deeper smooth carve. The tube intentionally retains its old current-density
// interpolation because that is part of the legacy structural path's contract.
FORCEINLINE void VF_ApplyPassageLandingCarving(float& Density, float LandingSDF,
float BaseDensity, float SealThickness)
{
constexpr float PASSAGE_BLEND_RADIUS = 4.0f;
if (LandingSDF >= PASSAGE_BLEND_RADIUS) return;
float CarveFactor = FMath::Clamp(
(PASSAGE_BLEND_RADIUS - LandingSDF) / (PASSAGE_BLEND_RADIUS * 2.0f),
0.0f, 1.0f);
CarveFactor = SmoothStep01(CarveFactor);
const float AirTarget = -(BaseDensity * 2.0f + SealThickness + 4.0f);
const float LandingThreshold = FMath::Lerp(BaseDensity, AirTarget, CarveFactor);
Density = FMath::Min(Density, LandingThreshold);
}
//=============================================================================
// LANDING ROOM ORIGINE (0,0) / ORIGIN LANDING ROOM
//=============================================================================
// The old implementation carved a full-height cylinder. That made the origin visually open but
// left no floor anywhere except at the bottom, so it was a fall shaft rather than a landing. The
// origin primitive is now a top-anchored rounded room with a real flat floor in every strate.
// It never crosses either vertical seal. The inter-strate seal remains solid until a passage (or
// the player) opens it; there is no default vertical bore between rooms.
FORCEINLINE void VF_ApplyOriginSpine(float& Density, float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
if (VoxelDensityAblation::IsOriginSpineOff() || Radius <= 0.0f) return;
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
VoxelPassageGeometry::BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, Radius);
if (!Geometry.bValid) return;
const float RoomSDF = VoxelPassageGeometry::OriginLandingRoomSDF(
FVector(WorldX, WorldY, WorldZ), Geometry);
VF_ApplyPassageLandingCarving(
Density, RoomSDF, BaseDensity, SealThickness);
// The floor is intentionally written after the room carve. It is also reasserted after the
// passage/disturbance posts by the callers below; doing it here makes the standalone primitive
// itself a complete landing-room operation.
if (WorldZ <= Geometry.FloorZ + KINDA_SMALL_NUMBER
&& WorldZ > Geometry.FloorZ - Geometry.FloorThickness
&& FMath::Abs(WorldX) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f)
&& FMath::Abs(WorldY) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f))
{
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker())
{
Density = FMath::Max(Density, BaseDensity);
}
}
}
/** Reassert only the origin-room air volume after a post-process that may add rock. */
FORCEINLINE void VF_ApplyOriginLandingAir(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
if (VoxelDensityAblation::IsLandingPostsOff() || Radius <= 0.0f) return;
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
VoxelPassageGeometry::BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, Radius);
if (!Geometry.bValid) return;
const float RoomSDF = VoxelPassageGeometry::OriginLandingRoomSDF(
FVector(WorldX, WorldY, WorldZ), Geometry);
VF_ApplyPassageLandingCarving(
Density, RoomSDF, BaseDensity, SealThickness);
}
/** Reassert only the origin-room support slab after an air carve/post-process. */
FORCEINLINE void VF_ApplyOriginLandingFloor(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
if (VoxelDensityAblation::IsLandingPostsOff() || Radius <= 0.0f) return;
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
VoxelPassageGeometry::BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, Radius);
if (!Geometry.bValid) return;
if (WorldZ <= Geometry.FloorZ + KINDA_SMALL_NUMBER
&& WorldZ > Geometry.FloorZ - Geometry.FloorThickness
&& FMath::Abs(WorldX) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f)
&& FMath::Abs(WorldY) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f))
{
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker())
{
Density = FMath::Max(Density, BaseDensity);
}
}
}
/** MC-facing origin-room air backstop for the post-disturbance path. */
FORCEINLINE void VF_ApplyOriginLandingAirMC(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
if (!VoxelGenLOD::IsOriginLandingReachable()
&& !VoxelGenLOD::IsTilePostReachBypassActive())
{
VoxelGenLOD::CompareSkippedPost(Density, [&]()
{
VF_ApplyOriginLandingAirMC(
Density, WorldX, WorldY, WorldZ,
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
}, VoxelGenLOD::ETilePostComparisonKind::OriginAir);
return;
}
float InternalDensity = -Density;
VF_ApplyOriginLandingAir(InternalDensity, WorldX, WorldY, WorldZ,
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
Density = -InternalDensity;
}
/** MC-facing origin-room floor backstop for the post-disturbance path. */
FORCEINLINE void VF_ApplyOriginLandingFloorMC(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
if (!VoxelGenLOD::IsOriginLandingReachable()
&& !VoxelGenLOD::IsTilePostReachBypassActive())
{
VoxelGenLOD::CompareSkippedPost(Density, [&]()
{
VF_ApplyOriginLandingFloorMC(
Density, WorldX, WorldY, WorldZ,
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
}, VoxelGenLOD::ETilePostComparisonKind::OriginFloor);
return;
}
float InternalDensity = -Density;
VF_ApplyOriginLandingFloor(InternalDensity, WorldX, WorldY, WorldZ,
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
Density = -InternalDensity;
}
//=============================================================================
// XY EDGE SEAL / SCELLEMENT DE LA LIMITE XY
//=============================================================================
// The bounded-world rim is a radial solid ramp in actor-space XY. This helper uses the same
// internal convention as VF_ApplyBoundarySeal (positive = solid); the MC-facing wrapper below
// negates it so the output convention remains negative = solid.
//
// Radius == 0 is deliberately checked before any arithmetic: it is a true legacy no-op, not a
// nearly-invisible setting-dependent perturbation. The common interior case also returns from a
// squared-distance test and pays no sqrt.
FORCEINLINE void VF_ApplyXYEdgeSeal(float& Density, float WorldX, float WorldY,
float WorldRadiusVoxels, float Thickness, float BaseDensity)
{
if (VoxelDensityAblation::IsXYEdgeSealOff()
|| !(WorldRadiusVoxels > 0.0f) || !(Thickness > 0.0f) || !(BaseDensity > 0.0f)) return;
if (!VoxelMath::IsFinite(WorldX) || !VoxelMath::IsFinite(WorldY)
|| !VoxelMath::IsFinite(WorldRadiusVoxels) || !VoxelMath::IsFinite(Thickness)
|| !VoxelMath::IsFinite(BaseDensity)) return;
const float InnerRadius = WorldRadiusVoxels - Thickness;
const float InnerRadiusSq = InnerRadius * InnerRadius;
const float DistSq = WorldX * WorldX + WorldY * WorldY;
const float RadiusSq = WorldRadiusVoxels * WorldRadiusVoxels;
// The overwhelmingly common case: well inside the bounded world.
if (InnerRadius > 0.0f && DistSq <= InnerRadiusSq) return;
// Keep the outer-boundary comparison in squared space too. This makes the exact force branch
// and the ClassifyBox proof use the same predicate: at or beyond R the result is BaseDensity,
// with no sqrt-rounding ambiguity in the forced-solid margin.
if (DistSq >= RadiusSq)
{
Density = FMath::Max(Density, BaseDensity);
return;
}
const float DistXY = FMath::Sqrt(DistSq);
// 0 at the inner edge, 1 at the world radius; smoothstep removes a cylindrical cliff.
const float SealFactor = SmoothStep01(FMath::Clamp(
(DistXY - InnerRadius) / Thickness, 0.0f, 1.0f));
// At/beyond WorldRadiusVoxels the branch above already forced exactly BaseDensity.
Density = FMath::Max(Density, SealFactor * BaseDensity);
}
/** MC-facing form used after post-processes that already operate in the output convention. */
FORCEINLINE void VF_ApplyXYEdgeSealMC(float& Density, float WorldX, float WorldY,
float WorldRadiusVoxels, float Thickness, float BaseDensity)
{
if (VoxelDensityAblation::IsXYEdgeSealOff()
|| !(WorldRadiusVoxels > 0.0f) || !(Thickness > 0.0f)) return;
float InternalDensity = -Density;
VF_ApplyXYEdgeSeal(InternalDensity, WorldX, WorldY,
WorldRadiusVoxels, Thickness, BaseDensity);
Density = -InternalDensity;
}
//=============================================================================
// XY EDGE PROOF / PREUVE DE BOITE
//=============================================================================
// The closest point of an AABB to the XY origin gives the minimum radius over the whole box.
// The helpers below are shared by ClassifyBox, ClassifyTile's global shell early-out, and the
// soundness test. They intentionally require one voxel of radial margin in the ramp: at the
// mathematical inner edge the helper is a true no-op, so claiming AllSolid there would be false.
namespace VoxelDensityReach
{
constexpr float PassageBlend = 4.0f;
constexpr float EdgeProofMargin = 1.0f;
}
FORCEINLINE bool VF_IsValidXYEdgeSealProofInput(const FBox& VoxelBox,
float WorldRadiusVoxels, float Thickness, float BaseDensity)
{
return VoxelBox.IsValid
&& VoxelMath::IsFinite(VoxelBox.Min.X) && VoxelMath::IsFinite(VoxelBox.Min.Y)
&& VoxelMath::IsFinite(VoxelBox.Max.X) && VoxelMath::IsFinite(VoxelBox.Max.Y)
&& VoxelMath::IsFinite(WorldRadiusVoxels) && VoxelMath::IsFinite(Thickness)
&& VoxelMath::IsFinite(BaseDensity)
&& WorldRadiusVoxels > 0.0f && Thickness > 0.0f && BaseDensity > 0.0f;
}
FORCEINLINE float VF_XYEdgeClosestRadiusSq(const FBox& VoxelBox)
{
const float ClosestX = (VoxelBox.Min.X > 0.0f) ? VoxelBox.Min.X
: (VoxelBox.Max.X < 0.0f) ? VoxelBox.Max.X : 0.0f;
const float ClosestY = (VoxelBox.Min.Y > 0.0f) ? VoxelBox.Min.Y
: (VoxelBox.Max.Y < 0.0f) ? VoxelBox.Max.Y : 0.0f;
return ClosestX * ClosestX + ClosestY * ClosestY;
}
FORCEINLINE float VF_XYEdgeFarthestRadiusSq(const FBox& VoxelBox)
{
const float FarthestX = FMath::Max(FMath::Abs(VoxelBox.Min.X), FMath::Abs(VoxelBox.Max.X));
const float FarthestY = FMath::Max(FMath::Abs(VoxelBox.Min.Y), FMath::Abs(VoxelBox.Max.Y));
return FarthestX * FarthestX + FarthestY * FarthestY;
}
/**
* Return the actual positive lower-bound margin imposed by the edge seal over a box, or 0 when
* the box is not provably forced. The ramp bound is intentionally reduced by 2× after the
* one-voxel proof margin, so float evaluation error can only make this estimate too small.
*/
FORCEINLINE float VF_XYEdgeSealForcedMarginOverBox(const FBox& VoxelBox,
float WorldRadiusVoxels, float Thickness, float BaseDensity)
{
if (!VF_IsValidXYEdgeSealProofInput(VoxelBox, WorldRadiusVoxels, Thickness, BaseDensity)) return 0.0f;
const float ClosestRadiusSq = VF_XYEdgeClosestRadiusSq(VoxelBox);
const float RadiusSq = WorldRadiusVoxels * WorldRadiusVoxels;
if (ClosestRadiusSq >= RadiusSq)
{
// The per-voxel helper takes the >= WorldRadiusVoxels branch and forces BaseDensity.
return BaseDensity;
}
const float InnerRadius = WorldRadiusVoxels - Thickness;
const float ProofRadius = InnerRadius + VoxelDensityReach::EdgeProofMargin;
if (!(Thickness > VoxelDensityReach::EdgeProofMargin)
|| !(ClosestRadiusSq >= ProofRadius * ProofRadius)) return 0.0f;
const float GuaranteedFactor = SmoothStep01(
FMath::Clamp(VoxelDensityReach::EdgeProofMargin / Thickness, 0.0f, 1.0f));
return BaseDensity * GuaranteedFactor * 0.5f;
}
FORCEINLINE bool VF_XYEdgeSealBoxIsForcedSolid(const FBox& VoxelBox,
float WorldRadiusVoxels, float Thickness, float BaseDensity)
{
return VF_XYEdgeSealForcedMarginOverBox(
VoxelBox, WorldRadiusVoxels, Thickness, BaseDensity) > 0.0f;
}
FORCEINLINE bool VF_XYEdgeSealBoxTouchesBand(const FBox& VoxelBox,
float WorldRadiusVoxels, float Thickness)
{
if (!(WorldRadiusVoxels > 0.0f) || !(Thickness > 0.0f)
|| !VoxelMath::IsFinite(WorldRadiusVoxels) || !VoxelMath::IsFinite(Thickness)) return false;
const float InnerRadius = WorldRadiusVoxels - Thickness;
return VF_XYEdgeFarthestRadiusSq(VoxelBox) > InnerRadius * InnerRadius;
}
//=============================================================================
// PORTÉES / REACHES — les rayons dont ClassifyTile et EffectOverBox ont besoin
//=============================================================================
// The constants are named above so a future bounds test cannot let them drift from the hot path.
@@ -0,0 +1,459 @@
// Opt-in density-path profiling used by the editor commandlet and targeted PIE diagnostics.
// Disabled by default so the shipping density path pays only one predictable branch in the
// scoped timers below.
#pragma once
#include "CoreMinimal.h"
#include "HAL/PlatformTime.h"
#include <atomic>
namespace VoxelDensityProfile
{
enum class EMode : uint8
{
Disabled,
Sampled,
// Low-overhead attribution: keeps the parent and stage ledger trustworthy, samples
// hot component scopes, and collects classifier detail without the old full-scope storm.
Attribution,
Full,
};
// Count-based sampling is deterministic with respect to the profiler's own counters, but the
// report is intentionally diagnostic only. It never participates in density or mesh output.
constexpr uint32 DefaultSampleInterval = 128;
enum class EBucket : uint8
{
GetDensityAt,
ApplyDisturbances,
PassageModifier,
PassageLandingAir,
PassageLandingFloor,
PassageLandingRoomFloor,
PassageTunnelAir,
PassageStructuralPosts,
TunnelCoreSupport,
TunnelCoreSdf,
TunnelCorePosts,
StructuralTail,
RoomGraphSource,
SdfCarve,
CaveRoughnessMod,
CaveTerraceMod,
LayerLineMod,
RibbingMod,
CaveOverhangMod,
CaveCliffMod,
ScallopMod,
CaveArchMod,
RoomColumnMod,
DomeMod,
PinchMod,
FloorBiasMod,
WormFieldSource,
OriginSpineOp,
BoundarySealOp,
PassageCarveOp,
XYEdgeSealOp,
OtherOp,
// Attribution components. These are nested diagnostics; the non-overlapping phase
// buckets below remain the density accounting ledger.
FusedEvaluator,
InterpretedOpStack,
OperatorBlock,
RoomGraphBuild,
RoomGraphSdf,
TunnelCoreWorld,
FusedDetail,
FusedWorm,
FusedStructural,
ClassifierTotal,
ClassifierIntervalProof,
ClassifierExactCore,
ClassifierExactFinal,
ClassifierRoomTail,
RuntimeStreamBuilding,
// Non-overlapping top-level phases. The named operation buckets above are intentionally
// nested diagnostics; these phase buckets are the accounting ledger that must add up to
// GetDensityAt without double-counting a stack op inside its enclosing phase.
DensityPrologue,
DensityCore,
DensityDisturbances,
DensityStructuralPosts,
DensityBoundarySeal,
DensityDiffLayer,
DensityTail,
// Mesher-side accounting. GenerateMesh is the inclusive total; the following buckets
// cover its regular-grid work and its non-grid setup/finalisation.
MesherGenerateMesh,
MesherDensityGrid,
MesherCellClassification,
MesherGradientNormals,
MesherVertexInterpolation,
MesherStreamBuilding,
MesherOther,
Count
};
constexpr int32 BucketCount = static_cast<int32>(EBucket::Count);
enum class ECounter : uint8
{
TunnelCacheLookup,
TunnelCacheHit,
TunnelCacheMiss,
SdfCacheBuild,
SdfSpatialIndexBuild,
SdfBrickIndexBuild,
RoomGraphBuildGeneratorTile,
RoomGraphBuildGeneratorTunnelCore,
RoomGraphBuildOpShared,
RoomGraphBuildOpLocal,
RoomGraphBuildClassifierShared,
RoomGraphBuildClassifierLocal,
RoomGraphBuildUnknown,
CaveRoomCandidates,
CaveRoomEvaluated,
CaveTunnelCandidates,
CaveTunnelEvaluated,
TunnelCoreCandidates,
TunnelCoreEvaluated,
TunnelSupportColumnBuilds,
TunnelSupportColumnCandidates,
TunnelSupportFloorQueries,
TunnelSupportFloorChecks,
TunnelSupportFloorBackstopFires,
TunnelRoomFloorBackstopFires,
TunnelAuthoredFloorSamples,
TunnelAuthoredFloorProtectionClamps,
TunnelAuthoredFloorDisturbanceSkips,
TunnelFloorProfileFallbacks,
TunnelFloorProfileBuilds,
TunnelFloorProfileSegments,
TunnelFloorProfileLedges,
TunnelFloorProfileLedgesBelow2Voxels,
TunnelFloorProfileLedges2To4Voxels,
TunnelFloorProfileLedges4To8Voxels,
TunnelFloorProfileLedges8PlusVoxels,
// One count per authored graph edge. MultiStepEdges is an invariant guard: it must stay
// zero now that old per-segment staircase quantisation is removed.
TunnelFloorSteepEdges,
TunnelFloorWindingEdges,
TunnelFloorDropEdges,
TunnelFloorMultiStepEdges,
PassageSupportFloorBackstopFires,
PassageNativeFloorCompositions,
PassageCandidates,
PassageEvaluated,
MesherBlockTests,
MesherBlockAllSolid,
MesherBlockAllAir,
TileClassifyCalls,
TileClassifyAllSolid,
TileClassifyAllAir,
TileClassifyMixed,
FusedTunnelSamples,
FusedTunnelDetailSamples,
FusedTunnelDetailSkipped,
WormEligibleSamples,
WormBlockProofs,
WormBlockSkippedSamples,
OpBlockBuilds,
OpBlockSamples,
OpBlockOperators,
OpBlockActiveOperators,
OpBlockPrunedOperators,
MesherDensityGridBytes,
MesherSharedGridReadBytes,
MesherOutputArrayBytes,
MesherOutputAllocatedBytes,
OpBlockScratchBytes,
OpBlockCopyBytes,
Count
};
constexpr int32 CounterCount = static_cast<int32>(ECounter::Count);
// Export the one fast-path bit so callers in the editor module can skip instrumentation
// without crossing the DLL boundary for every scoped timer or counter. It is diagnostic
// state only; it never participates in density or mesh decisions.
VOXELFORGE_API extern std::atomic<bool> GEnabled;
VOXELFORGE_API extern std::atomic<EMode> GMode;
inline thread_local bool GSampledScopeActive = false;
// Resident allocator-backed bytes for one worker's generation cache family. The profiler
// aggregates this by worker; the fields intentionally describe allocations owned by the
// cache, not the inline size of the enclosing C++ object. SlotStorageBytes is the one
// exception: it is the inline/static storage reserved for the worker's fixed cache table.
struct FCacheMemoryBreakdown
{
uint64 SlotStorageBytes = 0;
uint64 OpStackBytes = 0;
uint64 RoomsBytes = 0;
uint64 RoomFloorJoinsBytes = 0;
uint64 TunnelsBytes = 0;
uint64 PitsBytes = 0;
uint64 ChimneysBytes = 0;
uint64 ColumnsBytes = 0;
uint64 SupportColumnEntriesBytes = 0;
uint64 SupportColumnsBytes = 0;
uint64 SupportColumnIntervalsBytes = 0;
uint64 SpatialIndexBytes = 0;
uint64 TotalBytes() const
{
return SlotStorageBytes + OpStackBytes
+ RoomsBytes + RoomFloorJoinsBytes + TunnelsBytes + PitsBytes
+ ChimneysBytes + ColumnsBytes + SupportColumnEntriesBytes
+ SupportColumnsBytes + SupportColumnIntervalsBytes + SpatialIndexBytes;
}
uint64 DynamicBytes() const
{
return TotalBytes() - SlotStorageBytes;
}
FCacheMemoryBreakdown& operator+=(const FCacheMemoryBreakdown& Other)
{
SlotStorageBytes += Other.SlotStorageBytes;
OpStackBytes += Other.OpStackBytes;
RoomsBytes += Other.RoomsBytes;
RoomFloorJoinsBytes += Other.RoomFloorJoinsBytes;
TunnelsBytes += Other.TunnelsBytes;
PitsBytes += Other.PitsBytes;
ChimneysBytes += Other.ChimneysBytes;
ColumnsBytes += Other.ColumnsBytes;
SupportColumnEntriesBytes += Other.SupportColumnEntriesBytes;
SupportColumnsBytes += Other.SupportColumnsBytes;
SupportColumnIntervalsBytes += Other.SupportColumnIntervalsBytes;
SpatialIndexBytes += Other.SpatialIndexBytes;
return *this;
}
};
struct FSnapshot
{
uint64 Calls[BucketCount]{};
// Sum of sampled worker time. Useful for CPU-work accounting, but not directly comparable
// with an export wall clock when the mesher runs in parallel.
uint64 Cycles[BucketCount]{};
uint64 Samples[BucketCount]{};
// Largest estimated worker contribution. This is the report-facing parallel wall
// estimate; it avoids mistaking several parallel workers for several exports.
uint64 WallCycles[BucketCount]{};
uint32 ActiveWorkers[BucketCount]{};
uint64 Counters[CounterCount]{};
// Classifier detail is sampled separately from the timer buckets. Calls is the exact
// number of classifier invocations; the remaining fields are worker-local samples
// expanded at Snapshot() using Calls / SampledCalls.
struct FClassifierStats
{
uint64 Calls = 0;
uint64 SampledCalls = 0;
uint64 RefineNodes = 0;
uint64 StackBoxCalls = 0;
uint64 WholeMixedNodes = 0;
uint64 WholeSolidNodes = 0;
uint64 WholeAirNodes = 0;
uint64 NeedsFinalFieldNodes = 0;
uint64 SplitNodes = 0;
uint64 MaxRefinementDepth = 0;
uint64 ExactCoreSamples = 0;
uint64 ExactFinalSamples = 0;
uint64 ExactCoreCacheHits = 0;
uint64 ExactFinalCacheHits = 0;
uint64 ExactCoreLeaves = 0;
uint64 ExactFinalLeaves = 0;
uint64 StackBoxCycles = 0;
uint64 ExactCoreCycles = 0;
uint64 ExactFinalCycles = 0;
uint64 RoomTailQueries = 0;
uint64 RoomTailEvaluated = 0;
uint64 RoomTailCycles = 0;
uint64 RoomPropagateCycles = 0;
uint64 RoomExactPrimitiveCycles = 0;
uint64 RoomCacheWindowCycles = 0;
uint64 RoomNumRooms = 0;
uint64 RoomNumTunnels = 0;
uint64 RoomNumRoomFloorJoins = 0;
uint64 RoomNumPits = 0;
uint64 RoomNumChimneys = 0;
};
FClassifierStats Classifier;
uint64 TimerPairCycles = 0;
uint32 SampleInterval = DefaultSampleInterval;
EMode Mode = EMode::Disabled;
// Opt-in worker-cache footprint snapshot. These are current per-thread values (not
// allocation-event counters), so Snapshot() reports the resident cache footprint at the
// end of a profiling run rather than multiplying bytes by cache churn.
uint64 TunnelCacheWorkers = 0;
uint64 TunnelCacheCapacityEntries = 0;
uint64 TunnelCacheValidEntries = 0;
uint64 TunnelCacheStaticBytes = 0;
uint64 TunnelCacheDynamicBytes = 0;
uint64 TunnelCacheEntryBytes = 0;
uint64 TunnelCacheLargestEntryBytes = 0;
uint64 TunnelCacheLargestWorkerValidEntries = 0;
uint64 TunnelCacheLargestWorkerBytes = 0;
FCacheMemoryBreakdown TunnelCacheBreakdown;
uint64 RoomGraphCacheWorkers = 0;
uint64 RoomGraphCacheCapacityEntries = 0;
uint64 RoomGraphCacheValidEntries = 0;
uint64 RoomGraphCacheStaticBytes = 0;
uint64 RoomGraphCacheDynamicBytes = 0;
uint64 RoomGraphCacheEntryBytes = 0;
uint64 RoomGraphCacheLargestEntryBytes = 0;
uint64 RoomGraphCacheLargestWorkerValidEntries = 0;
uint64 RoomGraphCacheLargestWorkerBytes = 0;
FCacheMemoryBreakdown RoomGraphCacheBreakdown;
};
VOXELFORGE_API void SetMode(EMode Mode, uint32 SampleInterval = DefaultSampleInterval);
// Counters are collected only while a diagnostic mode is enabled. In the ordinary clean
// path this is a single relaxed load, so the measurement hooks stay dormant.
FORCEINLINE bool IsEnabledFast()
{
return GEnabled.load(std::memory_order_relaxed);
}
FORCEINLINE bool AreCountersEnabled() { return IsEnabledFast(); }
FORCEINLINE bool ShouldProbeScopeFast(EBucket Bucket)
{
const EMode Mode = GMode.load(std::memory_order_relaxed);
if (Mode == EMode::Full)
{
return true;
}
if (Mode == EMode::Sampled)
{
return Bucket == EBucket::GetDensityAt
|| Bucket == EBucket::MesherGenerateMesh
|| Bucket == EBucket::MesherDensityGrid
|| Bucket == EBucket::MesherOther;
}
if (Mode == EMode::Attribution)
{
switch (Bucket)
{
case EBucket::GetDensityAt:
case EBucket::MesherGenerateMesh:
case EBucket::MesherDensityGrid:
case EBucket::MesherCellClassification:
case EBucket::MesherGradientNormals:
case EBucket::MesherVertexInterpolation:
case EBucket::MesherStreamBuilding:
case EBucket::MesherOther:
case EBucket::RoomGraphBuild:
return true;
default:
return GSampledScopeActive;
}
}
return false;
}
VOXELFORGE_API bool IsEnabled();
VOXELFORGE_API bool IsCycleTimingEnabled();
VOXELFORGE_API EMode GetMode();
VOXELFORGE_API uint32 GetSampleInterval();
VOXELFORGE_API uint64 GetTimerPairCycles();
VOXELFORGE_API void CalibrateTimer();
VOXELFORGE_API void Reset();
VOXELFORGE_API FSnapshot Snapshot();
// Worker-local snapshot for a tile profile. GenerateTileResult is single-owner on its worker,
// so this avoids attributing another worker's concurrent tile to the current tile diagnostic.
VOXELFORGE_API FSnapshot SnapshotCurrentThread();
VOXELFORGE_API void AddCounter(ECounter Counter, uint64 Amount = 1);
VOXELFORGE_API void AddMeasurement(EBucket Bucket, uint64 Cycles, uint64 Calls = 1);
VOXELFORGE_API void RecordCall(EBucket Bucket);
VOXELFORGE_API void RecordClassifierCall(bool bSampled);
VOXELFORGE_API void AddClassifierStats(const FSnapshot::FClassifierStats& Stats);
VOXELFORGE_API bool ShouldSample(EBucket Bucket);
VOXELFORGE_API void AddSampledMeasurement(EBucket Bucket, uint64 RawCycles);
VOXELFORGE_API void SetWorkerTunnelCacheFootprint(
uint64 CapacityEntries, uint64 ValidEntries,
uint64 StaticBytes, uint64 DynamicBytes,
uint64 EntryBytes, uint64 LargestEntryBytes,
uint64 LargestWorkerValidEntries,
const FCacheMemoryBreakdown& Breakdown);
VOXELFORGE_API void SetWorkerRoomGraphCacheFootprint(
uint64 CapacityEntries, uint64 ValidEntries,
uint64 StaticBytes, uint64 DynamicBytes,
uint64 EntryBytes, uint64 LargestEntryBytes,
uint64 LargestWorkerValidEntries,
const FCacheMemoryBreakdown& Breakdown);
VOXELFORGE_API const TCHAR* CounterName(ECounter Counter);
VOXELFORGE_API EBucket BucketFromName(const TCHAR* Name);
VOXELFORGE_API const TCHAR* BucketName(EBucket Bucket);
struct FScopeToken
{
bool bEntered;
bool bSampled;
bool bBlockTiming;
bool bPreviousSampled;
bool bPreviousSampledScopeActive;
uint64 StartCycles;
};
VOXELFORGE_API FScopeToken BeginScope(EBucket Bucket);
VOXELFORGE_API void EndScope(EBucket Bucket, const FScopeToken& Token);
class FScopedTimer final
{
public:
explicit FScopedTimer(EBucket InBucket)
{
if (IsEnabledFast() && ShouldProbeScopeFast(InBucket))
{
Bucket = InBucket;
Token = BeginScope(InBucket);
bActive = Token.bEntered;
}
}
explicit FScopedTimer(const TCHAR* InName)
{
if (IsEnabledFast())
{
Bucket = BucketFromName(InName);
if (ShouldProbeScopeFast(Bucket))
{
Token = BeginScope(Bucket);
bActive = Token.bEntered;
}
}
}
FORCEINLINE ~FScopedTimer()
{
End();
}
// End a range before its lexical scope ends. This lets GetDensityAt split its enclosing
// total into disjoint prologue/core/post phases without moving the large cache block.
FORCEINLINE void End()
{
if (!bActive)
{
return;
}
EndScope(Bucket, Token);
Token.bEntered = false;
bActive = false;
}
FScopedTimer(const FScopedTimer&) = delete;
FScopedTimer& operator=(const FScopedTimer&) = delete;
private:
EBucket Bucket;
FScopeToken Token;
bool bActive = false;
};
}
@@ -0,0 +1,315 @@
// VoxelDiffLayer.h
// Runtime terrain modification system (player carving & filling).
//
// HOW IT WORKS:
// =============
// The procedural density function generates cave terrain deterministically.
// When the player carves or fills terrain, we DON'T modify the procedural
// output — instead, we store modifications as a "diff layer" on top.
//
// During density evaluation:
// Final density = Procedural density + Diff offset
//
// This keeps procedural generation deterministic while supporting freeform editing.
// Modifications are stored spatially (per-chunk) for O(1) lookup.
//
// MODIFICATION LIFECYCLE:
// 1. Player uses a tool at position P with radius R and strength S
// 2. ApplyModification() stores the mod in all overlapping chunks
// 3. Returns the list of affected chunk coords → world re-meshes those chunks
// 4. During re-meshing, GetDensityOffset() returns the combined diff at each voxel
//
// PERSISTENCE:
// Diffs are stored in memory. The game layer is responsible for saving/loading
// them to disk (server-side) and clearing them on season reset.
#pragma once
#include "CoreMinimal.h"
#include "HAL/CriticalSection.h" // FRWLock
#include "Misc/ScopeRWLock.h" // FReadScopeLock / FWriteScopeLock
#include "VoxelTypes.h"
#include <atomic>
#include "VoxelDiffLayer.generated.h"
/**
* EVoxelBrushShape — geometry of a modification brush.
* Sphere : Center + Radius. Classic round brush.
* Box : Center + BoxExtent (half-extents) + Falloff band.
* Capsule : segment Center→CapsuleEnd + Radius (tube) + Falloff band. Good for tunnels.
*/
UENUM(BlueprintType)
enum class EVoxelBrushShape : uint8
{
Sphere UMETA(DisplayName = "Sphere"),
Box UMETA(DisplayName = "Box"),
Capsule UMETA(DisplayName = "Capsule")
};
/**
* FVoxelModification — A single edit to the density field.
*
* Represents one brush stroke at a world position. Multiple overlapping
* modifications combine additively.
*
* Strength convention (INTERNAL density, before MC negate):
* Negative → subtract density → create air → CARVE (remove rock)
* Positive → add density → create solid → FILL (add rock)
*/
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelModification
{
GENERATED_BODY()
// Actor-local voxel-space center of the brush (endpoint A for Capsule). The AVoxelWorld helper
// converts world centimetres to this space before calling the diff layer.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification")
FVector Center = FVector::ZeroVector;
// Sphere radius / capsule tube radius, in voxels.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification", meta = (ClampMin = "0.5"))
float Radius = 3.0f;
// Density change: negative = carve (remove rock), positive = fill (add rock).
// -10 → strong carve (punches through BaseDensity=8) · +10 → strong fill
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification")
float Strength = -10.0f;
// Brush geometry.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification")
EVoxelBrushShape Shape = EVoxelBrushShape::Sphere;
// Box half-extents in voxels (Box shape only).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification",
meta = (EditCondition = "Shape == EVoxelBrushShape::Box"))
FVector BoxExtent = FVector(5.0f, 5.0f, 5.0f);
// Capsule second endpoint in actor-local voxel coords (Capsule shape only).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification",
meta = (EditCondition = "Shape == EVoxelBrushShape::Capsule"))
FVector CapsuleEnd = FVector::ZeroVector;
// Soft falloff band (voxels) around the Box/Capsule surface for smooth edges.
// Sphere ignores this — its whole Radius is the falloff.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modification", meta = (ClampMin = "0.1"))
float Falloff = 2.0f;
// Conservative voxel-space AABB the brush can affect (used to find overlapping chunks).
void GetWorldBounds(FVector& OutMin, FVector& OutMax) const
{
switch (Shape)
{
case EVoxelBrushShape::Box:
{
const FVector E = BoxExtent + FVector(Falloff);
OutMin = Center - E;
OutMax = Center + E;
break;
}
case EVoxelBrushShape::Capsule:
{
const float Pad = Radius + Falloff;
OutMin = FVector(FMath::Min(Center.X, CapsuleEnd.X), FMath::Min(Center.Y, CapsuleEnd.Y), FMath::Min(Center.Z, CapsuleEnd.Z)) - FVector(Pad);
OutMax = FVector(FMath::Max(Center.X, CapsuleEnd.X), FMath::Max(Center.Y, CapsuleEnd.Y), FMath::Max(Center.Z, CapsuleEnd.Z)) + FVector(Pad);
break;
}
case EVoxelBrushShape::Sphere:
default:
OutMin = Center - FVector(Radius);
OutMax = Center + FVector(Radius);
break;
}
}
};
/**
* UVoxelDiffLayer — Stores all player modifications as a spatial diff.
*
* Owned by AVoxelWorld, passed to the generator for density evaluation.
* Modifications are grouped by chunk coord for fast spatial lookup —
* when evaluating density for a chunk, we only check modifications
* stored in that chunk (no global scan).
*
* When a modification sphere overlaps multiple chunks, it's stored in ALL
* of them so each chunk's lookup is self-contained.
*/
UCLASS()
class VOXELFORGE_API UVoxelDiffLayer : public UObject
{
GENERATED_BODY()
public:
/** Process-unique identity for worker-local modification snapshots. */
uint64 GetCacheLifetimeId() const;
//=========================================================================
// BUDGET CONFIGURATION
//=========================================================================
/**
* Set the budget limits for player modifications.
* Called by AVoxelWorld during BeginPlay from VoxelSettings values.
* A value of 0 means unlimited (no cap).
*
* @param InMaxMods - Max number of operations (0 = unlimited)
* @param InMaxRadius - Max brush radius per operation
* @param InMaxVolume - Max cumulative brush volume in cubic voxels (0 = unlimited)
*/
void SetBudget(int32 InMaxMods, float InMaxRadius, float InMaxVolume);
/**
* Check if a modification would be allowed under the current budget.
* Does NOT consume budget — use this for UI feedback (greyed-out tool, etc.)
*
* @param Radius - The brush radius the player wants to use
* @return true if the modification is allowed
*/
bool CanModify(float Radius) const;
/** Shape-aware budget check used by ApplyModification. Boxes charge their affected outer box and
* capsules charge their full swept volume, so a long tunnel cannot be billed as a small sphere. */
bool CanModify(const FVoxelModification& Mod) const;
/**
* Get remaining modification count (how many more operations the player can do).
* Returns -1 if unlimited.
*/
UFUNCTION(BlueprintPure, Category = "Voxel Diff Layer")
int32 GetRemainingModifications() const;
/**
* Get remaining volume budget (cubic voxels of modification still allowed).
* Returns -1.0 if unlimited.
*/
UFUNCTION(BlueprintPure, Category = "Voxel Diff Layer")
float GetRemainingVolume() const;
//=========================================================================
// APPLY MODIFICATIONS
//=========================================================================
/**
* Apply a carve or fill operation.
*
* Stores the modification in every chunk the brush volume overlaps.
* Returns the list of affected chunk coordinates so the world knows
* which chunks need re-meshing.
*
* Budget enforcement: if the modification would exceed any configured
* limit (count, radius, or volume), it is REJECTED and an empty array
* is returned. Check CanModify() first for UI feedback.
*
* @param Mod - The modification to apply
* @return Array of chunk coordinates that need re-meshing (empty if rejected)
*/
TArray<FIntVector> ApplyModification(const FVoxelModification& Mod);
//=========================================================================
// DENSITY EVALUATION
//=========================================================================
/**
* Get the combined density offset from all modifications at a world position.
*
* Called per-voxel by the generator during density evaluation.
* Only checks modifications stored for the given chunk coordinate,
* so it's fast when most chunks have no modifications.
*
* Each modification contributes with a smoothstep falloff from center to edge.
* Multiple overlapping modifications combine additively.
*
* @param ChunkCoord - The chunk being evaluated (for spatial lookup)
* @param WorldX, WorldY, WorldZ - The voxel position
* @return Total density offset (negative = carve, positive = fill)
*/
float GetDensityOffset(const FIntVector& ChunkCoord,
float WorldX, float WorldY, float WorldZ) const;
/**
* Check if a chunk has any modifications (fast reject for hot path).
*
* @param ChunkCoord - The chunk to check
* @return true if there are modifications affecting this chunk
*/
bool HasModifications(const FIntVector& ChunkCoord) const;
//=========================================================================
// HOT-PATH SNAPSHOT API (per-chunk, lock-amortised)
//=========================================================================
// Once ANY carve exists, calling HasModifications + GetDensityOffset per voxel costs two
// ModsLock acquisitions + two TMap finds per density sample (~86k lock ops per tile task —
// during carve gameplay, exactly when re-mesh latency matters). Instead, a worker snapshots a
// chunk's mod list ONCE per (chunk, version) and evaluates it lock-free via EvaluateMods; the
// version bump on ApplyModification/Clear invalidates worker-side caches.
/** Lock-free: true once any modification exists anywhere (false = the common streaming case). */
bool HasAnyMods() const { return bHasAnyMods.load(std::memory_order_acquire); }
/** Monotonic mod-state version — bumped by ApplyModification and Clear. */
uint32 GetModsVersion() const { return ModsVersion.load(std::memory_order_acquire); }
/** Copy this chunk's modification list under ONE read lock (Out emptied if none). */
void GetChunkModsSnapshot(const FIntVector& ChunkCoord, TArray<FVoxelModification>& Out) const;
/** True si un chunk modifié intersecte [MinChunk, MaxChunk] (inclusif). Conservatif par
* construction : ApplyModification enregistre le mod dans TOUS les chunks que son rayon
* touche, donc le test par clé de chunk suffit. La recherche parcourt directement les clés de
* la plage demandée, pas tout l'historique des chunks — utilisé par ClassifyTile, PAS par voxel. */
bool HasAnyModInChunkRange(const FIntVector& MinChunk, const FIntVector& MaxChunk) const;
/** Evaluate a mod list at a voxel — the lock-free core shared by GetDensityOffset and the
* generator's snapshot path. Pure function (deterministic). Returns the combined offset
* (negative = carve, positive = fill). */
static float EvaluateMods(const TArray<FVoxelModification>& Mods,
float WorldX, float WorldY, float WorldZ);
//=========================================================================
// MANAGEMENT
//=========================================================================
/** Clear ALL modifications (e.g., on season reset). */
void Clear();
/** Get total number of stored modification entries (for debug/stats). */
int32 GetTotalModificationCount() const;
/** Get number of chunks that have modifications. */
int32 GetModifiedChunkCount() const;
private:
// Never reused for the lifetime of the process: a recycled UObject address must not make a
// worker-local snapshot from another diff layer look current when its chunk/version match.
mutable std::atomic<uint64> CacheLifetimeId{0};
//=========================================================================
// STORAGE
//=========================================================================
// Modifications grouped by chunk coordinate.
// Each chunk's list contains ALL modifications that overlap it
// (including mods centered in neighboring chunks whose radius reaches here).
//
// THREADING: written on the game thread (ApplyModification / Clear) but read by MANY mesher
// WORKER threads (GetDensityAt -> HasModifications / GetDensityOffset). TMap is not thread-safe —
// a read landing during a write's rehash reads freed hash memory (EXCEPTION_ACCESS_VIOLATION in
// TSet::FindId). All access to ChunkMods MUST hold ModsLock (read lock for queries, write lock for
// mutation). bHasAnyMods is a lock-free fast reject: while it's false (the common streaming case,
// no carves) readers skip the map AND the lock entirely, so unmodified worlds pay nothing.
TMap<FIntVector, TArray<FVoxelModification>> ChunkMods;
mutable FRWLock ModsLock;
std::atomic<bool> bHasAnyMods{ false };
std::atomic<uint32> ModsVersion{ 1 }; // see the snapshot API above
//=========================================================================
// BUDGET TRACKING
//=========================================================================
// Configured limits (0 = unlimited)
int32 BudgetMaxMods = 0;
float BudgetMaxRadius = 50.0f;
float BudgetMaxVolume = 0.0f;
// Running totals — reset when Clear() is called
int32 ModificationCount = 0; // Total operations applied
float AccumulatedVolume = 0.0f; // Sum of all brush sphere volumes (4/3 * PI * R^3)
};
@@ -0,0 +1,479 @@
// VoxelGenerator.h
// Fournit le champ de densité du monde (positif = solide, négatif = air).
//
// Pipeline density-only: plus de grille de blocs, plus de surface heightfield.
// Le mesher appelle GetDensityAt() par voxel pour reconstruire la géométrie.
// Chaque strate est générée par sa pile d'opérateurs (VoxelDensityOpStack.h), pilotée par les
// params de strate récupérés auprès du StrateManager.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "VoxelTypes.h"
#include "VoxelStrateTypes.h"
#include "VoxelBiomeTypes.h"
#include "VoxelGenerator.generated.h"
// Forward decls (évite les includes transitifs)
class UVoxelSettings;
class UVoxelStrateManager;
class UVoxelDiffLayer;
class UVoxelBiomeDefinition;
struct FTunnelCoreWorldEvaluation;
/** Guards which may be disabled only by the sealed-solid proof's soundness tests. */
enum class EVoxelSealedSolidProofGuard : uint8
{
None = 0,
DiffLayer = 1u << 0,
Passages = 1u << 1,
OriginLanding = 1u << 2,
DisturbanceChasms = 1u << 3,
};
//=============================================================================
// LOD-AWARE OCTAVE REDUCTION (T2.b)
//=============================================================================
// At Step=2/4, noise octaves whose wavelength is smaller than the sampling cell
// are pure aliasing cost — they can't shape the coarse isosurface, only shift it
// by sub-cell noise. Réduction d'octaves sur les tuiles lointaines (LOD).
//
// OctaveBias = octaves dropped from PER-VOXEL volumetric noise for the tile
// currently being meshed on THIS thread. Default 0 = full quality; set (via
// TGuardValue) by UVoxelMarchingCubesMesher::GenerateMesh from its Step and the
// opt-in UVoxelSettings::LODOctaveDrop, restored when the tile finishes — so
// game-thread queries, deco snapping and the density-volume fill thread always
// see 0. fBM/Ridged add octaves low→high frequency, so dropping the TAIL keeps
// the coarse shape identical; only sub-cell detail (already unrepresentable at
// that Step) disappears. LOD0 (Step=1) is byte-identical regardless.
//
// Deliberately NOT applied to XY-field noise (heightfield, ceiling, relief,
// moisture): those feed box-validated caches that can outlive a tile task on
// the same thread, and biome/climate must stay LOD-independent.
namespace VoxelGenLOD
{
// The TLS variables themselves stay module-local because MSVC forbids exporting thread_local
// data (C2492). These tiny accessors let the editor explorer build a shared LOD grid without
// linking directly to the storage in the game module.
extern thread_local int32 OctaveBias;
// Sampling step of the tile currently being meshed on this worker. GetDensityAt itself has
// no Step parameter because callers outside the mesher are point queries; this TLS is only a
// read-only hint for caches that may safely widen their spatial window for coarse tiles.
extern thread_local int32 SampleStep;
// Exact lattice footprint of the tile currently being meshed. The cache-window switch
// uses this rather than inferring a region from an individual sample coordinate, so every
// sample in one tile shares one deterministic window even at tile/chunk boundaries.
extern thread_local FIntVector TileOriginVoxels;
extern thread_local int32 TileCellsPerAxis;
VOXELFORGE_API int32 GetThreadOctaveBias();
VOXELFORGE_API void SetThreadOctaveBias(int32 Value);
VOXELFORGE_API int32 GetThreadSampleStep();
VOXELFORGE_API void SetThreadSampleStep(int32 Value);
// Called by the module's game-thread startup before any worker can enter GetDensityAt.
VOXELFORGE_API void InitializeConsoleSwitches();
VOXELFORGE_API bool IsTileCacheWindowEnabled(bool bFusedPath = true);
#if WITH_EDITOR
/** Research-only override for the bounded editor region evaluator; default false per thread. */
VOXELFORGE_API void SetResearchRegionCacheWindowEnabled(bool bEnabled);
#endif
VOXELFORGE_API bool GetThreadTileCacheWindow(
FIntVector& OutOriginVoxels, int32& OutStep, int32& OutCellsPerAxis);
VOXELFORGE_API void SetThreadTileContext(
const FIntVector& OriginVoxels, int32 CellsPerAxis);
// Effective octave count for a per-voxel noise call site.
// At least 1 octave always survives (the coarse base shape).
FORCEINLINE int32 Eff(int32 Octaves) { return FMath::Max(1, Octaves - OctaveBias); }
}
// NOTE: EVoxelTileClass (le verdict T1.d) a déménagé dans VoxelTypes.h — inclus ci-dessus —
// pour que VoxelDensityOp.h puisse le partager sans dépendre d'un header UCLASS.
// EVoxelTileClass (the T1.d verdict) moved to VoxelTypes.h, included above.
/**
* UVoxelGenerator
*
* Objet UObject léger — ne stocke pas de données lourdes.
* Tient les paramètres globaux copiés depuis les settings (seed, spine, bord XY) et les services
* dont il a besoin (StrateManager pour les params par chunk, DiffLayer pour
* les carves du joueur).
*/
UCLASS(BlueprintType)
class VOXELFORGE_API UVoxelGenerator : public UObject
{
GENERATED_BODY()
public:
UVoxelGenerator();
//=========================================================================
// SEED (source unique: Settings->Seed)
//=========================================================================
// Les paramètres globaux sont copiés au démarrage pour éviter un déréférencement par voxel.
// Les formes créatives viennent ensuite des params de strate.
int32 Seed = 0;
// Authored radius (voxels) of the guaranteed flat-floored landing room at world XY (0,0) in
// every strate — the old “spine” setting now sizes a room and never creates a vertical bore.
// 0 disables the origin landing rooms. Copied from VoxelSettings.
float OriginSpineRadius = 28.0f;
// Radius (voxels) of the bounded world in actor-space XY, measured from (0,0).
// 0 keeps the legacy unbounded behavior and is a true no-op for the edge seal.
float WorldRadiusVoxels = 8192.0f;
// Width (voxels) of the smooth solid ramp at the XY world edge.
float EdgeSealThickness = 128.0f;
//=========================================================================
// SERVICES (injectés par AVoxelWorld::BeginPlay)
//=========================================================================
// Manager des strates — fournit les params de cave par chunk.
// Peut être nullptr: dans ce cas on utilise des params par défaut.
UPROPERTY()
const UVoxelStrateManager* StrateManager = nullptr;
// Diff layer des modifications joueur (carve/fill).
// Peut être nullptr: dans ce cas pas de modifs appliquées.
UPROPERTY()
const UVoxelDiffLayer* DiffLayer = nullptr;
void SetStrateManager(const UVoxelStrateManager* InManager)
{
StrateManager = InManager;
}
void SetDiffLayer(const UVoxelDiffLayer* InDiffLayer)
{
DiffLayer = InDiffLayer;
}
// Copie le seed depuis les settings. Appelé au démarrage et lors d'un ChangeSeed.
void InitializeSettings(const UVoxelSettings* Settings);
//=========================================================================
// API DE DENSITÉ
//=========================================================================
/**
* Densité combinée (strates + diff du joueur).
*
* Convention de sortie (MC): négatif = solide, positif = air.
* C'est ce que le marching cubes attend comme champ scalaire.
*
* @param WorldX, WorldY, WorldZ - coordonnées monde en voxels (pas cm)
*/
float GetDensityAt(float WorldX, float WorldY, float WorldZ,
float* OutPreEditDensity = nullptr) const;
/**
* Open a worker-local lattice hand-off for the following GetDensityAt calls. The mesher uses
* this around one tile's density-grid fill. The first sample in each resolved chunk lets the
* generator run the operator stack once in EvalBlock; later samples consume the exact cached
* per-sample result while the ordinary post-stack tail still runs in canonical order.
*
* This is intentionally a scoped service rather than a persistent world cache: its lifetime is
* one tile task, it is thread-local, and EndDensityBlock must be called on every exit path.
*/
void BeginDensityBlock(FIntVector OriginVoxels, int32 Step,
int32 SizeX, int32 SizeY, int32 SizeZ) const;
void EndDensityBlock() const;
#if WITH_EDITOR
/** Editor-only chunk program; supported routes enter GetDensityAt once to seed a whole block. */
bool EvaluateDensityChunk(FIntVector OriginVoxels, int32 SizeX, int32 SizeY, int32 SizeZ,
TArray<float>& OutDensities, int32& OutFallbackSamples,
uint64& OutGeneralPathEntries,
uint64& OutSupportedBlockSetupEntries) const;
#endif
/**
* Prove that the mesher's core lattice is entirely solid from the cheap, structural rules
* that own inter-strate gaps and boundary seals. This never samples density. It is intended
* for an empty-tile fast path; a false answer is harmless, while a true answer must make every
* core lattice vertex negative in the final MC field.
*
* The proof is deliberately narrower than ClassifyTile: it accepts only a tile made entirely
* of gap/seal lattice rows and rejects recipes/composer overrides plus every known air-writing
* structural feature. The mesher's +/-1 normal halo is not part of the cell lattice, matching
* ClassifyTile and GenerateMesh.
*/
bool TryProveSealedSolidTile(const FIntVector& OriginVoxels, int32 Step,
int32 CellsPerAxis) const;
#if WITH_DEV_AUTOMATION_TESTS
/** Test hook: disable named proof guards and require the brute-force oracle to catch it. */
bool TryProveSealedSolidTileForTest(
const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis,
uint8 DisabledGuards) const;
#endif
/**
* Densité pour une strate TunnelNetwork (rooms + tunnels + worm noise).
* C'est l'évaluateur fusionné de la pile TunnelNetwork/Underwater dans GetDensityAt.
* Exposée pour permettre des tests isolés avec des params custom.
*
* ⚠️ `ParamsFingerprint` ET `LayoutVersion` SONT OBLIGATOIRES (`AUDIT §C2`). Le cache SDF
* interne est clé sur (boîte XY, strate, seed) et PAS sur les params. Or `GetGenerationParams` BLENDE les params à l'intérieur d'une même
* strate — `Alpha` dépend du chunk Z en mode `Gradient` (le DÉFAUT, avec
* `TransitionBlendChunks = 2`) et du chunk XY en plus en mode `Interleaved`. Deux chunks de la
* même strate, même seed, donc même clé, mais des params DIFFÉRENTS : le worker évalue le
* deuxième chunk qu'il construit contre les salles du premier. Et comme *quel* chunk vient en
* premier dépend de l'ordre des workers, **deux pairs divergent depuis la même seed** — ce que
* `OPSTACK-PLAN §2.6.1` interdit explicitement.
*
* Pourquoi une empreinte PASSÉE plutôt qu'un `MemCrc32` calculé ici : ce serait ~300 octets de
* CRC PAR VOXEL sur le chemin le plus chaud du plugin. L'appelant la calcule UNE fois par
* chunk, là où le mémo de params vit déjà (`CP_*`), donc le coût par voxel est exactement deux
* comparaisons d'entiers. Pas de valeur par défaut : un appelant qui oublie doit ne pas
* compiler, pas hériter silencieusement du trou (la discipline de `FVoxelOpContext`).
*
* ⚠️ POUR LES TESTS : passez `FCrc::MemCrc32(&Params, sizeof(Params))`. Un oracle qui partage
* le défaut qu'il teste ne prouve rien — c'est précisément ce que la note de
* `VoxelForgeOpStackTunnelTest.cpp` (contrôle 3) décrivait comme le trou de l'original.
*
* The SDF cache key had neither the params nor anything that determines them, while the params
* are blended per chunk INSIDE a strate — so a worker could evaluate one chunk against another
* chunk's rooms, and which came first depends on worker order. Passing a once-per-chunk
* fingerprint keeps the fix off the per-voxel path. No default: forgetting it must not compile.
*/
float GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
const FStrateGenerationParams& Params,
uint32 ParamsFingerprint, uint32 LayoutVersion,
bool bApplyLegacyStructuralPosts = true,
// Optional hand-off used by a lowered canonical evaluator. When
// non-null, this receives the pre-disturbance TunnelNetwork core;
// it does not alter the scalar field result.
FTunnelCoreWorldEvaluation* OutTunnelCore = nullptr,
// Diagnostics are opt-in so ordinary/reference calls do not pay
// for fused-path counters.
bool bCollectFusedDiagnostics = false,
// Optional same-sample hand-off from EvaluateSDFCached. This is
// only a room-shape ownership bit; it does not alter the scalar
// result and avoids a duplicate room-index query.
bool* OutRoomOwnsBottom = nullptr) const;
//=========================================================================
// CLIMATE & BIOME FIELDS (pure XY, deterministic, window-invariant)
//=========================================================================
/**
* Relief ("elevation") field at a world XY → [0,1], contrast-shaped & smoothed.
* The single source of truth for the relief map M: SurfaceWorld terrain and the
* biome climate map both call this, so they agree when given the same freq/contrast.
*/
float SampleRelief(float WorldX, float WorldY, float Frequency, float Contrast) const;
/**
* La chaîne de hauteur complète de SurfaceWorld : structural → cliff → terrace → layer lines →
* plage. Rend une ALTITUDE monde en voxels, pas une densité.
*
* PUBLIQUE pour permettre un test isolé : c'est la référence de
* `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui compare la pile d'opérateurs de hauteur
* (`VoxelHeightOp.h`) à cette fonction point par point.
* Public so the height-op stack can be measured against it.
*/
float ComputeSurfaceTerrainZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const;
/**
* La colonne de surface : terrain Z, plafond, et le gate d'OVERHANG résolu par colonne
* (amplitude + direction amont). C'est la colonne que `ClassifyTile` teste pour SurfaceWorld ;
* publique pour que les tests puissent placer des échantillons dans la fenêtre d'overhang.
* The surface column ClassifyTile tests for SurfaceWorld; public so tests can aim samples at
* the overhang window.
*/
void ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ,
const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx,
const TArray<FSurfaceGenerationParams>& BiomeParams, FChunkBiomeCache& BiomeCache,
float& OutTerrainZ, float& OutCeilSurf,
float& OutOverhangAmp, float& OutDirX, float& OutDirY) const;
/**
* Moisture field at a world XY → [0,1]. The second climate axis for biome placement.
*/
float SampleMoisture(float WorldX, float WorldY, float Frequency) const;
/**
* Resolve the biome at a world XY: dominant cell + nearest neighbour + blend weight.
* Warped Voronoi over a jittered grid; each cell's biome is chosen by its site's
* (relief, moisture) against the context's climate boxes. PURE function of (XY, seed,
* Ctx) — no chunk-window dependence (CODEMAP §8.4). Returns an empty sample when the
* context has no biomes. Used by the 2D preview bake and (next) the density path.
*/
FBiomeSample SampleBiomeAt(float WorldX, float WorldY, const FBiomeContext& Ctx) const;
/**
* Hot-path biome resolve: the dominant + neighbour biome and blend weight at a world
* XY for the given strate slice. Uses (and lazily rebuilds) Cache — a box-validated
* per-chunk grid — so the noise-heavy cell classification happens once per chunk, not
* per voxel. Bit-identical to SampleBiomeAt, so the baked preview matches the terrain.
*/
FBiomeSample ResolveBiomeSampleAt(float WorldX, float WorldY, int32 ChunkZ,
const FBiomeContext& Ctx, FChunkBiomeCache& Cache) const;
/**
* Dominant biome ASSET at a world XY for the given strate slice (chunk Z), or nullptr
* when the strate has no biomes / the point is outside the strate range. Game-thread
* helper for content + atmosphere selection — uses the same field as the density path.
*/
const UVoxelBiomeDefinition* GetDominantBiomeAt(float WorldX, float WorldY, int32 ChunkZ) const;
/**
* Rich biome probe at a world XY for a strate slice (ChunkZ): dominant + neighbour biome assets,
* climate fields, border blend weight, and the dominant biome's EFFECTIVE decoration count. Mirrors
* exactly what the decoration scatter resolves per column, so it is a faithful "what's under here?"
* diagnostic (DominantDecorationCount == 0 explains an empty biome region). Game-thread, uncached.
*/
void QueryBiomeAt(float WorldX, float WorldY, int32 ChunkZ, FVoxelBiomeQuery& Out) const;
/**
* Per-vertex material data for the master triplanar palette material (F6). Resolves the
* biome field at a world XY/Z and returns the dominant + neighbour MaterialPaletteIndex
* and the border blend weight (0 deep in a cell → 0.5 at the border). The mesher packs
* these into vertex colour so one material re-skins terrain per biome and cross-fades
* across biome borders. No biomes ⇒ Dominant=Neighbour=0, Weight=0 (default palette).
* Thread-safe: own per-chunk thread_local context + box cache (clustered tile queries
* stay warm). Window-invariant (ResolveBiomeSampleAt is bit-identical to SampleBiomeAt).
*/
void GetBiomeMaterialAt(float WorldX, float WorldY, float WorldZ,
int32& OutDominantPalette, int32& OutNeighborPalette,
float& OutBlendWeight) const;
/**
* F7 AWARE PLACEMENT: evaluate an entry's relational placement conditions at a candidate voxel XY.
* True = ALL conditions pass (AND); empty list = true (zero cost). Pure query of the analytic fields
* (relief/moisture/biome-border) — deterministic + worker-safe. The caller passes the strate's
* already-resolved biome context (freq/contrast + Voronoi map), so no re-resolve. Also the shared core
* of the future quest FindFeature locator (same predicate, run as an outward search).
*/
bool EvaluateTerrainConditions(const TArray<FTerrainCondition>& Conditions,
float WorldX, float WorldY, const FBiomeContext& BiomeCtx) const;
/**
* SurfaceWorld HEIGHT ORACLE: the terrain surface Z + sky-cap ceiling Z (voxel coords) at a world
* XY for the given strate slice (ChunkZ), WITHOUT ray-marching the density column. Returns false
* (outs untouched) when the chunk is NOT a SurfaceWorld heightfield — callers fall back to marching.
* Shares the density path's surface helpers, so a decoration snapped to OutTerrainZ sits exactly on
* the rendered ground. Does NOT include passage/spine/seal carving — verify with one GetDensityAt at
* the result if a column might be carved. Thread-safe (own per-chunk thread_local cache).
*/
bool GetSurfaceHeightAt(float WorldX, float WorldY, int32 ChunkZ,
float& OutTerrainZ, float& OutCeilSurf) const;
/**
* T1.d — classification conservative d'une tuile AVANT le pré-échantillonnage du mesher.
* (OriginVoxels, Step, CellsPerAxis) = les MÊMES arguments que GenerateMesh ; le verdict
* porte sur le treillis exact que le mesher échantillonnerait (marge ±1 incluse).
*
* GAP (bedrock), hors-layout (air constant) et SurfaceWorld plient leurs hypothèses par Z ;
* SurfaceWorld évalue ses colonnes terrain/plafond avec le MÊME ComputeSurfaceColumn que le
* chemin densité. Un unique slot cave peut ajouter le verdict conservatif de sa pile,
* sous gardes d'archétype et de params bit-identiques. Spine/passages/disturbances/diff restent
* des gardes conservatrices. Worker-safe (lecture seule + caches thread_local partagés avec
* GetDensityAt — un verdict Mixed laisse les colonnes chaudes pour la génération qui suit).
*/
EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step,
int32 CellsPerAxis) const;
EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step,
int32 CellsPerAxis,
struct FVoxelTileClassificationStats* OutStats) const;
/**
* Prove the exact constant-air field above/below the strate layout for a mesher lattice.
* This is separate from TryProveSealedSolidTile: it never supplies collision support and its
* caller records AllAir, not AllSolid. It shares the layout/diff/structural guards with the
* production classifier and returns false for every mixed or uncertain box.
*/
bool TryProveOutOfLayoutAirTile(const FIntVector& OriginVoxels, int32 Step,
int32 CellsPerAxis) const;
private:
/** Identité process-unique du propriétaire des caches `CP_*` thread_local.
* Process-unique owner identity for the `CP_*` thread-local cache key. */
uint64 DensityCacheOwnerId = 0;
EVoxelTileClass ClassifyTileUncached(
const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis,
struct FVoxelTileClassificationStats* OutStats) const;
/** Pick the biome (index into Ctx.Biomes) for a Voronoi site, by its climate. */
int32 ClassifyBiomeAtSite(float SiteX, float SiteY, const FBiomeContext& Ctx, uint32 SiteHash) const;
// ComputeSurfaceTerrainZ a été DÉPLACÉE en `public` (voir plus haut) pour que
// VoxelForge.OpStack.SurfaceHeightEquivalence puisse s'y comparer. Une seule déclaration.
// Moved to public above so the height-stack test can compare against it. One declaration only.
/** F20 — the RAW structural heightfield (continents + mountains + detail), BEFORE any
* terrain op (cliff/terrace/layer-lines/beach). Ops in ComputeSurfaceTerrainZ build on
* this; Cliff re-samples it at an XY offset for a cheap analytic slope. OutM = the relief
* "mountainous-ness" [0,1], reused to relief-condition the ops. */
float SampleSurfaceStructuralZ(float WorldX, float WorldY,
const FSurfaceGenerationParams& Params, float& OutM) const;
/** The SurfaceWorld sky-cap ceiling surface Z at a world XY (also pure per-XY). */
float ComputeSurfaceCeiling(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const;
/** Resolve a chunk's SurfaceWorld params (strate base + per-biome overrides, structural fields
* forced from the strate). Shared by GetDensityAt's per-chunk cache and the GetSurfaceHeightAt
* oracle so both produce the SAME surface. */
void ResolveSurfaceChunkParams(const FIntVector& ChunkCoord,
FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx,
TArray<FSurfaceGenerationParams>& OutBiomeParams) const;
/** (Re)build the per-chunk biome cell grid covering chunk (X,Y) footprint + margin. */
void RebuildBiomeGrid(int32 ChunkX, int32 ChunkY, int32 ChunkZ,
const FBiomeContext& Ctx, FChunkBiomeCache& Cache,
uint64 OwnerId, uint64 ManagerLifetimeId) const;
};
namespace VoxelGenLOD
{
// -1 uses the measured path/LOD policy; 0/1 force the spatial broad phase off/on for an A/B
// run. The policy affects candidate enumeration only, never cache contents or ordering.
VOXELFORGE_API bool ShouldUseSpatialIndex(bool bFusedPath);
}
/**
* Optional diagnostics for one ClassifyTile call. This is populated only when the caller asks
* for it (the streaming profiler does); it is never consulted by the proof and cannot affect a
* verdict. Keeping the counters beside the classifier lets the game profile distinguish cache,
* interval refinement, and exact-lattice work without sampling a different code path.
*/
struct FVoxelTileClassificationStats
{
uint32 RefineNodes = 0;
uint32 StackBoxCalls = 0;
uint32 WholeMixedNodes = 0;
uint32 WholeSolidNodes = 0;
uint32 WholeAirNodes = 0;
uint32 NeedsFinalFieldNodes = 0;
uint32 SplitNodes = 0;
uint32 MaxRefinementDepth = 0;
uint32 ExactCoreSamples = 0;
uint32 ExactFinalSamples = 0;
uint32 ExactCoreIntervalChecks = 0;
uint32 ExactCoreIntervalDisagreements = 0;
uint32 ExactCoreCacheHits = 0;
uint32 ExactFinalCacheHits = 0;
uint32 ExactCoreLeaves = 0;
uint32 ExactFinalLeaves = 0;
uint64 StackBoxCycles = 0;
uint64 ExactCoreCycles = 0;
uint64 ExactFinalCycles = 0;
uint32 RoomTailQueries = 0;
uint32 RoomTailEvaluated = 0;
uint64 RoomTailCycles = 0;
uint64 RoomPropagateCycles = 0;
uint64 RoomExactPrimitiveCycles = 0;
uint64 RoomCacheWindowCycles = 0;
int32 RoomNumRooms = 0;
int32 RoomNumTunnels = 0;
int32 RoomNumRoomFloorJoins = 0;
int32 RoomNumPits = 0;
int32 RoomNumChimneys = 0;
};
@@ -0,0 +1,273 @@
// VoxelHeightOp.h
// L'ESPACE DES HAUTEURS — une seconde famille d'opérateurs, et pourquoi elle DOIT exister.
// HEIGHT SPACE — a second operator family, and why it has to exist.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// LE CONSTAT QUI FORCE CE FICHIER
// ─────────────────────────────────────────────────────────────────────────────────────────
// `OPSTACK-DECOMPOSITION §5` décompose SurfaceWorld ainsi :
//
// FHeightfieldSource ← toute la chaîne de colonne, XY-pure
// ├─ FStructuralHeightField
// ├─ FCliffHeightMod
// ├─ FTerraceHeightMod
// ├─ FLayerLineHeightMod
// └─ FBeachHeightMod
//
// et note, sans en tirer la conséquence : *« les ops de hauteur opèrent sur des valeurs Z dans la
// colonne, pas sur la densité »*. En lisant `ComputeSurfaceTerrainZ`, c'est littéralement vrai :
// c'est une suite de blocs qui lisent et écrivent **un seul float `Terrain`**, une altitude.
//
// **Ils ne rentrent donc PAS dans `IVoxelDensityOp`.** Sa signature est
// `Eval(x, y, z, FVoxelOpSample&)` — par voxel, deux canaux densité/SDF. Un op de hauteur n'a pas
// de Z d'entrée (il en PRODUIT un), ne veut pas être appelé par voxel (il est XY-pur, une fois par
// colonne), et n'écrit ni densité ni SDF. Les forcer dans le contrat densité demanderait soit un
// troisième canal par voxel — alors que la hauteur est une propriété de COLONNE, pas de voxel —,
// soit de replier les cinq en un seul op opaque, ce que `§2.5` appelle précisément l'échec du
// refactor.
//
// **Donc : une seconde famille, dans son propre espace.** C'est la même leçon que `§0.1` (il fallait
// un canal SDF en plus de la densité), un cran plus loin : certaines choses ne sont pas un canal de
// plus, elles sont un ESPACE de plus.
//
// The height ops read and write a single float ALTITUDE. They have no input Z (they produce one),
// are XY-pure (once per column, not per voxel), and write neither density nor SDF. Forcing them into
// IVoxelDensityOp would need either a per-voxel third channel for what is a COLUMN property, or
// collapsing all five into one opaque op — which §2.5 calls the failure mode. Hence a second family.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// CE QUE ÇA ACHÈTE / WHAT IT BUYS
// ─────────────────────────────────────────────────────────────────────────────────────────
// • **Le cache de colonne T1.a tombe naturellement.** Une pile de hauteur est XY-pure PAR
// CONSTRUCTION — il n'y a pas de Z à mettre dedans par erreur. `AUDIT §6.3` avertit qu'une donnée
// dépendante de Z glissée dans `FSurfaceColumn` corrompt silencieusement toute la pile verticale
// de chunks, et que `ValidateDeterminism` ne le verrait pas. Ici c'est le TYPE qui l'interdit.
// • **La composition d'idées de terrain devient de l'authoring**, comme pour la densité.
// • Les mêmes ops resserviront à VerticalShafts (ledges) et FloatingIslands.
//
// ⚠️ CE FICHIER NE TOUCHE PAS AU JEU. Il est bâti et exercé par
// `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui le compare à `ComputeSurfaceTerrainZ` point par
// point. Le branchement dans le chemin densité est l'étape SUIVANTE (§5 : `FHeightfieldSource`,
// `FSkyCapSource`, `FOverhangShelfMod`), délibérément séparée pour que la question d'architecture
// — *« l'espace des hauteurs se décompose-t-il vraiment ? »* — reçoive une réponse MESURÉE avant
// qu'on écrive l'adaptateur qui en dépend.
#pragma once
#include "CoreMinimal.h"
#include "Templates/UniquePtr.h"
#include "VoxelStrateTypes.h" // FSurfaceGenerationParams
/**
* L'état qui traverse une pile de hauteur. DEUX canaux, exactement comme `FVoxelOpSample` — et
* pour la même raison : le code le fait déjà.
*
* `Relief` (le `M` de `SampleSurfaceStructuralZ`) est PRODUIT par la source structurelle et CONSOMMÉ
* par le gate du terrace (`TerraceStrength * M`). Sans ce second canal, le terrace devrait
* ré-échantillonner le champ de relief — plus lent, et surtout une occasion de diverger de la valeur
* que la source a réellement utilisée.
*
* Two channels, for the same reason as FVoxelOpSample: Relief (the `M` of the structural field) is
* produced by the source and consumed by the terrace gate. Threading it beats resampling it.
*/
struct FVoxelHeightSample
{
/** Altitude monde en VOXELS (pas cm). */
float Height = 0.0f;
/** « Montagnosité » [0,1]. 1 = uniforme (ReliefStrength = 0). */
float Relief = 1.0f;
};
/**
* Le résultat d'une requête de champ de biome en un XY : qui domine, qui est le voisin, et à quel
* poids on va vers lui dans la bande de frontière.
*/
struct FVoxelBiomeWeights
{
int32 Dominant = 0;
int32 Neighbor = -1;
float NeighborWeight = 0.0f; // 0 ⇒ pas de mélange, le dominant seul
};
/**
* LE CHAMP DE BIOMES, VU COMME UNE INTERFACE — et c'est délibérément une interface, pas un pointeur
* vers `UVoxelGenerator`.
*
* Le résolveur de biome réel est une Voronoï warpée avec un cache par chunk, qui vit sur le
* générateur. Un opérateur ne doit PAS en dépendre : la Phase 3 veut que les opérateurs deviennent
* des DONNÉES (des assets), et un op qui tient un `UVoxelGenerator*` ne peut pas le devenir. En
* passant par cette interface, l'adaptateur qui connaît le générateur reste du côté du générateur,
* et l'opérateur ne connaît qu'« un truc qui répond (dominant, voisin, poids) en XY ».
*
* Deliberately an interface rather than a UVoxelGenerator*: Phase 3 wants ops to become data, and an
* op holding a generator pointer never can. The adapter that knows the generator stays on the
* generator's side; the op only knows "something that answers (dominant, neighbour, weight) at XY".
*/
class IVoxelBiomeField
{
public:
virtual ~IVoxelBiomeField() = default;
/** PURE en XY, et bit-identique quel que soit le thread ou l'ordre — même contrat que le reste
* de l'espace-hauteur, puisque le résultat alimente le cache de colonne T1.a. */
virtual FVoxelBiomeWeights SampleAt(float WorldX, float WorldY) const = 0;
};
/**
* Un opérateur d'espace-hauteur. Trois différences avec `IVoxelDensityOp`, toutes voulues :
* • pas de Z d'entrée — la pile en PRODUIT un ;
* • XY-pur par construction, donc pas de `IsXYPure()` à déclarer ni à oublier ;
* • pas de `PrepareChunk` — ces ops sont déjà appelés une fois par colonne, ce qui EST la
* granularité que `PrepareChunk` sert à obtenir côté densité.
*/
class IVoxelHeightOp
{
public:
virtual ~IVoxelHeightOp() = default;
/**
* INVARIANCE DE FENÊTRE (ARCHITECTURE §8.4) : fonction PURE de (X, Y, seed, params). Le même XY
* évalué depuis une autre tuile, un autre ordre, un autre thread doit rendre le float
* BIT-IDENTIQUE — le cache de colonne T1.a est partagé sur toute la pile verticale de chunks,
* donc une impureté ici se propage à tous les Z d'un coup.
*/
virtual void Eval(float WorldX, float WorldY, FVoxelHeightSample& InOut) const = 0;
/**
* Majorant CONSERVATIF du déplacement vertical que cet op peut ajouter, en voxels.
* Sert à borner la colonne pour un futur `ClassifyBox` exact du heightfield — la même logique
* que les bandes de `FSlabVoidSource`, qui prouvent 36-40 tuiles sur 60.
* Rendre trop grand coûte du CPU ; rendre trop petit serait un TROU. `FLT_MAX` = « je ne sais
* pas », toujours sûr, et c'est le défaut.
*/
virtual float MaxDisplacement() const { return FLT_MAX; }
};
/**
* Pile de hauteur : source → modificateurs, dans l'ordre. Déplaçable, pas copiable, exactement
* comme `FVoxelOpStack` et pour la même raison (elle POSSÈDE ses opérateurs).
*/
class FVoxelHeightStack
{
public:
FVoxelHeightStack() = default;
FVoxelHeightStack(FVoxelHeightStack&&) = default;
FVoxelHeightStack& operator=(FVoxelHeightStack&&) = default;
FVoxelHeightStack(const FVoxelHeightStack&) = delete;
FVoxelHeightStack& operator=(const FVoxelHeightStack&) = delete;
void Add(TUniquePtr<IVoxelHeightOp> Op) { Ops.Add(MoveTemp(Op)); }
int32 Num() const { return Ops.Num(); }
/** L'altitude après toute la pile. */
float EvalHeight(float WorldX, float WorldY) const
{
return EvalSample(WorldX, WorldY).Height;
}
/** L'état complet (altitude + relief). */
FVoxelHeightSample EvalSample(float WorldX, float WorldY) const
{
FVoxelHeightSample S;
for (const TUniquePtr<IVoxelHeightOp>& Op : Ops) { Op->Eval(WorldX, WorldY, S); }
return S;
}
/** Somme des majorants. `FLT_MAX` dès qu'un seul op ne sait pas répondre. */
float MaxTotalDisplacement() const
{
float Total = 0.0f;
for (const TUniquePtr<IVoxelHeightOp>& Op : Ops)
{
const float D = Op->MaxDisplacement();
if (D >= FLT_MAX) { return FLT_MAX; }
Total += D;
}
return Total;
}
private:
TArray<TUniquePtr<IVoxelHeightOp>> Ops;
};
//=============================================================================
// FABRIQUES / FACTORIES
//=============================================================================
namespace VoxelHeightOps
{
/**
* La source structurelle : continents + montagnes + détail, sous une frame de warp.
* Produit `Height` ET `Relief`. Transcription littérale de `SampleSurfaceStructuralZ`.
*
* ⚠️ Rend un pointeur NON-POSSÉDANT via `OutSource` : `FCliffHeightMod` doit pouvoir
* RÉ-ÉCHANTILLONNER ce champ (4 fois, en différences centrées) et doit le faire sur la MÊME
* fonction, pas sur une copie qui pourrait dériver. La pile garde la propriété ; la source vit
* donc aussi longtemps que le modificateur qui la référence, parce que le constructeur de pile
* les ajoute ensemble et que la pile ne réordonne jamais.
*/
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeStructuralHeightSource(
const FSurfaceGenerationParams& P, int32 Seed, const IVoxelHeightOp** OutSource);
/** Raidissement conditionné par la pente. Le seul op qui coûte des échantillons en plus
* (4 resamples structurels), et seulement quand il est activé. */
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeCliffHeightMod(
const FSurfaceGenerationParams& P, const IVoxelHeightOp* StructuralSource);
/** Plateaux quantifiés, gatés par le relief (`TerraceStrength * M`) — d'où le canal Relief. */
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeTerraceHeightMod(const FSurfaceGenerationParams& P);
/** Bandes sédimentaires : `Height -= sin(Height · 2π / Spacing) · Depth`. */
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeLayerLineHeightMod(const FSurfaceGenerationParams& P);
/** Aplatissement vers la ligne d'eau dans `BeachWidth`. */
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeBeachHeightMod(const FSurfaceGenerationParams& P);
/**
* La pile de hauteur complète de SurfaceWorld, dans l'ordre de `ComputeSurfaceTerrainZ` :
* structural → cliff → terrace → layer lines → beach
*
* L'ordre n'est PAS négociable : le terrace quantifie une hauteur que le cliff a déjà raidie,
* les layer lines se posent sur le résultat, et la plage écrase tout près de l'eau. C'est
* l'ordre du code d'origine, et le test échouerait bruyamment sur toute permutation.
*/
VOXELFORGE_API void BuildSurfaceHeightStack(FVoxelHeightStack& OutStack,
const FSurfaceGenerationParams& P, int32 Seed);
/** La voûte : warp + gonflement signé + pendage vers le bas uniquement. C'est une ALTITUDE,
* donc un op de hauteur — la soustraction n'arrive qu'au combine côté densité. */
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeSkyCapHeightSource(const FSurfaceGenerationParams& P,
int32 Seed);
/** La pile de plafond de SurfaceWorld. Un seul op aujourd'hui, et c'est une information : le
* plafond n'a pas d'équivalent des quatre modificateurs du sol. */
VOXELFORGE_API void BuildSurfaceCeilingStack(FVoxelHeightStack& OutStack,
const FSurfaceGenerationParams& P, int32 Seed);
/**
* LE COMBINER `Mask` — mélange de biomes, et `OPSTACK-DECOMPOSITION §5` en fait le prototype
* de la Phase 3 entière : « unifier strates et biomes » EST ce mécanisme, généralisé.
*
* Une pile de hauteur COMPLÈTE par biome, plus un champ qui dit lequel domine en (X,Y). Ce sont
* les **HAUTEURS** qui sont interpolées, pas les params — c'est ce que fait déjà le code
* d'origine, et c'est ce qui rend les frontières continues quelle que soit la différence de
* params entre deux biomes (interpoler des params ferait passer un terrace de « fort » à
* « faible » à travers des états intermédiaires qui n'ont de sens pour personne).
*
* Each biome gets a COMPLETE height stack; the HEIGHTS are lerped, not the params — which is
* what keeps borders continuous across any param difference.
*
* @param Field non possédé, doit survivre à la pile. `nullptr` ⇒ biome 0 partout.
*/
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeBiomeBlendHeightSource(
const TArray<FSurfaceGenerationParams>& PerBiomeParams, int32 Seed,
const IVoxelBiomeField* Field);
/** Idem pour le plafond — mais le plafond N'EST PAS mélangé : le code d'origine prend celui du
* biome DOMINANT seul. Reproduit tel quel, pas « amélioré » : une voûte interpolée changerait
* la silhouette du monde et ce portage n'est pas l'endroit pour décider ça. */
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeBiomeSelectCeilingSource(
const TArray<FSurfaceGenerationParams>& PerBiomeParams, int32 Seed,
const IVoxelBiomeField* Field);
}
@@ -0,0 +1,144 @@
// VoxelMarchingCubesMesher.h
// Mesh lisse à partir du champ de densité du générateur.
//
// Principe:
// 1. Chaque cellule du chunk (cube 2x2x2 voxels) lit la densité aux 8 coins.
// 2. Un index 8 bits indique quels coins sont au-dessus du IsoLevel.
// 3. Les tables MC (EdgeTable + TriTable) donnent les triangles à générer.
// 4. On interpole la position du vertex le long de chaque arête traversée.
//
// LOD: Step > 1 échantillonne moins souvent → moins de triangles à distance.
#pragma once
#include "CoreMinimal.h"
#include <atomic>
#include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc.
#include "VoxelGenerator.h"
#include "VoxelMarchingCubesMesher.generated.h"
// A run-scoped, read-only density lattice. The explorer owns the storage and installs the
// pointer only while its canonical tiles are being meshed. The lattice includes the one-point
// halo needed by every tile, so each world sample is generated once and copied into both adjacent
// tile scratch grids. It is deliberately not a UObject or a cache with eviction: ownership,
// lifetime, and iteration order remain explicit and deterministic.
struct FVoxelSharedDensityGrid
{
FIntVector OriginVoxels = FIntVector::ZeroValue;
int32 Step = 1;
int32 Dim = 0;
TArray<float> Samples;
void Reset()
{
OriginVoxels = FIntVector::ZeroValue;
Step = 1;
Dim = 0;
Samples.Reset();
}
};
UCLASS(BlueprintType)
class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject
{
GENERATED_BODY()
public:
// Parse command-line switches on the module's game-thread startup. GenerateMesh is a worker
// entry point and therefore only reads the resulting values.
static void InitializeConsoleSwitches();
/**
* Génère le mesh d'une TUILE de clipmap (voir FVoxelTileKey).
*
* @param OriginVoxels - Coin min de la tuile en coords VOXEL (= Tile.OriginVoxels()).
* @param Step - Taille de cellule en voxels. La tuile couvre CellsPerAxis*Step voxels.
* @param CellsPerAxis - Nombre de cellules par axe. Les tuiles GROSSIÈRES en utilisent MOINS
* (gen moins chère, maillage plus grossier au loin) tout en couvrant la
* même étendue (extent = CellsPerAxis*Step). Niveau 0 = CHUNK_SIZE.
* @param OutCaptureGrid - CAPTURE-DURING-MESHING (optionnel). Si non-null ET CellsPerAxis==CHUNK_SIZE
* (tuile pleine résolution, Step==1<<Level, donc 1:1 avec une cellule du clipmap
* de densité), on y recopie les CHUNK_SIZE³ points intérieurs de la grille de
* densité déjà échantillonnée, quantifiés via VF_QuantizeDensity. Cela évite à
* UVoxelDensityVolume de re-sampler GetDensityAt pour ces cellules (le mesher
* les a déjà calculées). Vidé puis rempli ; reste vide si non éligible.
* @param BandZMinVox / BandZMaxVox - COUPE DE CONTENU PAR STRATE (optionnel, voxels Z INCLUSIFS,
* cf. UVoxelSettings::StrateContentCutMinLevel) : seules les cellules dont
* l'intervalle Z chevauche la bande sont maillées. Le runtime ne passe
* jamais de bande aux tuiles LOD0, car elles possèdent la collision du pawn.
* Une tuile grossière qui
* chevauche une frontière de strate ne maille que la strate du joueur —
* supprime les trous d'aliasing (le bouchon seal/gap plus fin que Step
* tombait entre deux points du treillis) et le mélange de matériaux entre
* strates. Les cellules maillées restent identiques au bit près (mêmes
* échantillons monde purs). Jamais combiné avec OutCaptureGrid.
*/
FVoxelMeshData GenerateMesh(FIntVector OriginVoxels, int32 Step = 1, int32 CellsPerAxis = CHUNK_SIZE,
TArray<uint8>* OutCaptureGrid = nullptr,
int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX,
int64* OutDensitySampleCount = nullptr,
FVoxelSharedDensityGrid* OutPreEditDensityGrid = nullptr,
const FVoxelSharedDensityGrid* InSharedDensityGrid = nullptr);
//=========================================================================
// SERVICES (injectés par AVoxelWorld)
//=========================================================================
// Le générateur fournit la densité par voxel.
UPROPERTY()
const UVoxelGenerator* Generator = nullptr;
void SetGenerator(const UVoxelGenerator* InGenerator) { Generator = InGenerator; }
// AVoxelWorld installs its bShuttingDown flag here. The commandlet path leaves it null.
// This is intentionally read-only: workers may observe shutdown, but never mutate world state.
void SetShutdownFlag(const std::atomic<bool>* InShutdownFlag) { ShutdownFlag = InShutdownFlag; }
// Optional run-scoped shared lattice. The caller must keep it alive and immutable until all
// GenerateMesh calls finish. A malformed/incomplete lattice is ignored and falls back to
// the canonical generator path rather than changing geometry.
void SetSharedDensityGrid(const FVoxelSharedDensityGrid* InGrid) { SharedDensityGrid = InGrid; }
private:
FORCEINLINE bool ShouldAbortWork() const
{
return ShutdownFlag && ShutdownFlag->load(std::memory_order_relaxed);
}
const std::atomic<bool>* ShutdownFlag = nullptr;
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
public:
//=========================================================================
// SETTINGS
//=========================================================================
// Seuil de l'isosurface dans le champ de densité.
// Convention MC: densité < IsoLevel = solide, >= = air.
float IsoLevel = 0.0f;
// SKIRTS — bouchent les fissures aux frontières de tuiles entre niveaux de clipmap voisins
// (résolutions différentes → les iso-surfaces ne se rejoignent pas exactement). Une jupe
// (mur court) est extrudée vers le solide depuis chaque arête de surface posée sur une des 6
// faces externes de la tuile. Voir GenerateMesh.
bool bGenerateSkirts = true;
// Profondeur de la jupe, en CELLULES de la tuile (× Step × VOXEL_SIZE). Four L0 cells preserve
// the 0.5 m default extrusion at 12.5 cm per voxel.
float SkirtCells = 4.0f;
// Conservative 8x8x8 block classifier. Uniform blocks skip the marching-cubes cell loop;
// when every relevant block has the same verdict, the density grid is skipped as well.
// ClassifyTile is proof-only, so no samples, resolution, or geometry are removed.
// The tile classifier is the cheap, whole-tile proof. Keep the optional 8^3-block refinement
// available for focused diagnostics, but leave it disabled in streaming: repeating a proof on
// every mixed tile can cost more than the density grid it is meant to avoid.
bool bUseBlockEarlyOut = false;
// T2.b — LOD-aware octave reduction (opt-in, copied from UVoxelSettings::LODOctaveDrop).
// Octaves dropped from per-voxel volumetric noise PER Step doubling: a tile at Step=S
// drops LODOctaveDrop * log2(S) octaves (see VoxelGenLOD in VoxelGenerator.h).
// 0 (default) = off — every LOD samples full octaves, byte-identical to before.
// Réduction d'octaves sur les tuiles grossières ; 0 = désactivé.
int32 LODOctaveDrop = 0;
};
@@ -0,0 +1,438 @@
// VoxelNoise.h
// Float, SIMD-batched gradient-noise core (T2.a). Replaces UE's double-precision
// FMath::PerlinNoise3D on the density hot path (~6.6 ms/chunk was all noise math).
//
// WHY THIS EXISTS
// FMath::PerlinNoise3D is double-precision with a permutation-table lookup. The
// density field calls it >1M times per surface chunk (fBm octaves + domain warps).
// This core is:
// - FLOAT (no double math),
// - table-free hash-gradient Perlin (pure arithmetic → vectorizes cleanly),
// - SIMD-BATCHED across fBm octaves: one FractalNoise/Ridged call evaluates up to
// 4 octaves' Perlin samples in one 4-wide SSE pass.
//
// It is a DIFFERENT noise field than FMath's, so worlds re-tune ONCE (accepted).
//
// DETERMINISM / CACHES
// Perlin3D is a pure function of (x,y,z) exactly like the old call — every box-validity
// cache (SDF §8.10, biome, surface column) stays valid. No invariant changes.
//
// SCALAR vs SIMD
// Perlin3D (scalar) and Perlin3D_x4 (SSE) use the IDENTICAL formula, op-for-op, so on
// x86 (SSE math == scalar-float math, same IEEE rounding) they produce bit-identical
// results. The SSE path is therefore a pure speedup with no second re-tune. If it ever
// fails to build on a given toolchain, force the scalar fallback with one line:
// #define VF_NOISE_USE_SIMD 0 // (before including this header, or here)
// The scalar path alone is still a large win over the old double-precision core.
//
// REQUIRES: SSE4.1 for the SIMD path (UE5 x64 baseline is SSE4.2 → always available).
#pragma once
#include "CoreMinimal.h"
#include "VoxelTypes.h"
#if !defined(VF_NOISE_USE_SIMD)
#if PLATFORM_ENABLE_VECTORINTRINSICS && PLATFORM_CPU_X86_FAMILY
#define VF_NOISE_USE_SIMD 1
#else
#define VF_NOISE_USE_SIMD 0
#endif
#endif
#if VF_NOISE_USE_SIMD
#include <immintrin.h> // SSE4.1: _mm_floor_ps / _mm_mullo_epi32 / _mm_blendv_ps
#endif
namespace VoxelNoise
{
namespace Detail
{
// Table-free integer hash of a lattice corner → gradient selector. Pure mul/xor/shift
// so it vectorizes 1:1 (see the SSE Hash lambda below — must stay in lock-step).
FORCEINLINE uint32 HashCorner(int32 ix, int32 iy, int32 iz)
{
uint32 h = (uint32)ix * 0x9E3779B1u
^ (uint32)iy * 0x85EBCA77u
^ (uint32)iz * 0xC2B2AE3Du;
h ^= h >> 15; h *= 0x2C1B3C6Du;
h ^= h >> 12; h *= 0x297A2D39u;
h ^= h >> 15;
return h;
}
// Quintic fade 6t^5-15t^4+10t^3, factored as t^3 * (t*(6t-15)+10) so the SSE twin
// can mirror the exact grouping.
FORCEINLINE float Fade(float t)
{
const float inner = t * (t * 6.0f - 15.0f) + 10.0f;
const float t3 = t * t * t;
return t3 * inner;
}
// Ken Perlin's 12-gradient dot (hash&15 picks the gradient). Branchy form here;
// the SSE twin reproduces it with selects.
FORCEINLINE float GradDot(uint32 hash, float x, float y, float z)
{
const uint32 h = hash & 15u;
const float u = (h & 8u) == 0u ? x : y;
float v;
if ((h & 12u) == 0u) v = y; // h < 4
else if ((h & 13u) == 12u) v = x; // h == 12 or 14
else v = z;
const float ru = (h & 1u) == 0u ? u : -u;
const float rv = (h & 2u) == 0u ? v : -v;
return ru + rv;
}
FORCEINLINE float Lerp(float a, float b, float t) { return a + t * (b - a); }
}
// Single 3D Perlin sample, ~[-1,1] (typically [-0.7,0.7], same character as the old core
// so VOXEL_NOISE_SCALE still applies). Used for all single-sample domain warps.
FORCEINLINE float Perlin3D(float x, float y, float z)
{
using namespace Detail;
const float xf = FMath::FloorToFloat(x);
const float yf = FMath::FloorToFloat(y);
const float zf = FMath::FloorToFloat(z);
const int32 X = (int32)xf, Y = (int32)yf, Z = (int32)zf;
const float fx = x - xf, fy = y - yf, fz = z - zf;
const float su = Fade(fx), sv = Fade(fy), sw = Fade(fz);
const uint32 h000 = HashCorner(X, Y, Z );
const uint32 h100 = HashCorner(X+1, Y, Z );
const uint32 h010 = HashCorner(X, Y+1, Z );
const uint32 h110 = HashCorner(X+1, Y+1, Z );
const uint32 h001 = HashCorner(X, Y, Z+1);
const uint32 h101 = HashCorner(X+1, Y, Z+1);
const uint32 h011 = HashCorner(X, Y+1, Z+1);
const uint32 h111 = HashCorner(X+1, Y+1, Z+1);
const float fx1 = fx - 1.0f, fy1 = fy - 1.0f, fz1 = fz - 1.0f;
const float n000 = GradDot(h000, fx, fy, fz );
const float n100 = GradDot(h100, fx1, fy, fz );
const float n010 = GradDot(h010, fx, fy1, fz );
const float n110 = GradDot(h110, fx1, fy1, fz );
const float n001 = GradDot(h001, fx, fy, fz1);
const float n101 = GradDot(h101, fx1, fy, fz1);
const float n011 = GradDot(h011, fx, fy1, fz1);
const float n111 = GradDot(h111, fx1, fy1, fz1);
const float x00 = Lerp(n000, n100, su);
const float x10 = Lerp(n010, n110, su);
const float x01 = Lerp(n001, n101, su);
const float x11 = Lerp(n011, n111, su);
const float y0 = Lerp(x00, x10, sv);
const float y1 = Lerp(x01, x11, sv);
return Lerp(y0, y1, sw);
}
FORCEINLINE float Perlin3D(const FVector& P)
{
return Perlin3D((float)P.X, (float)P.Y, (float)P.Z);
}
// Hot density callers already own float voxel coordinates. Keeping this overload separate from
// FVector avoids constructing UE5's double-precision FVector merely to round the coordinates back
// to float at the noise entry point. It is the same scalar formula and remains deterministic on
// every build of the module.
FORCEINLINE float Perlin3D(const FVector3f& P)
{
return Perlin3D(P.X, P.Y, P.Z);
}
//=============================================================================
// LIPSCHITZ BOUND FOR THE ACTUAL 3D FIELD
//=============================================================================
// This is deliberately derived from the implementation above, rather than borrowed from a
// textbook Perlin constant. GradDot's h&15 table has the twelve UNNORMALIZED gradients
//
// (±1,±1,0), (±1,0,±1), (0,±1,±1)
//
// (the four duplicate selectors do not add any vectors). For one partial derivative, write the
// four x-edges of the trilinear interpolation as independent gradient pairs. With
// d = Fade'(x) = 30 x²(1-x)², each edge contributes the support of the actual gradient set at
//
// c0 = (1-x-dx, -dy, -dz), c1 = (x-d(1-x), dy, dz).
//
// The support is max(|a|+|b|, |a|+|c|, |b|+|c|), because every allowed gradient has exactly two
// non-zero ±1 components. Taking the four y/z edge weights and the exact fade derivative, a
// finite case split over those three support branches has supremum 15/4 on [0,1]^3 (attained at
// x=y=z=1/2). This maximization permits every corner hash to choose any member of the actual
// set, so it is an upper bound for every hash cell; it is also the supremum of that finite support
// relaxation, not the looser 1 + 4*max(Fade') = 8.5 bound used by the unrelated 2D floor relief.
//
// The vector bound below follows the same per-axis-bound × sqrt(dimension) method used by the
// floor-relief proof. Abs is 1-Lipschitz and VOXEL_NOISE_SCALE is positive, so the scaled N1
// field has this bound in noise-space coordinates. WormBlockSkip computes its radius after the
// anisotropic world->noise transform, so the HBias and VerticalScale factors are included there.
static constexpr double Perlin3DPartialAbsBound = 15.0 / 4.0;
static constexpr double Perlin3DGradientBound =
Perlin3DPartialAbsBound * 1.732050807568877293527446341505872L;
static constexpr double WormN1NoiseSpaceLipschitzBound =
static_cast<double>(VOXEL_NOISE_SCALE) * Perlin3DGradientBound;
// A four-point side gives the natural small worker-local blocks for the mesher's 35-point
// (32 cells + one-sample normal halo) LOD0 lattice. The cache is only an optimization; a failed
// proof falls through to the original per-sample N1/N2 code.
static constexpr int32 WormBlockSampleSide = 4;
// The center value is evaluated with the same float Perlin expression as N1. The endpoint noise
// coordinates are already float results of monotone positive/negative affine transforms, so their
// min/max box contains every float lattice sample exactly. The 1e-3 scaled-N1 margin covers the
// remaining center-evaluation/interpolation rounding with substantial headroom; the proof remains
// strict because both the Lipschitz term and this margin are subtracted before comparing with T.
static constexpr double WormBlockOutputRoundMargin = 1.0e-3;
FORCEINLINE bool ProvesScaledAbsPerlin3DAbove(
const FVector3f& CenterNoisePosition,
const FVector3f& MinNoisePosition,
const FVector3f& MaxNoisePosition,
float Threshold,
double ScaledFieldLipschitzBound)
{
if (!FMath::IsFinite(CenterNoisePosition.X)
|| !FMath::IsFinite(CenterNoisePosition.Y)
|| !FMath::IsFinite(CenterNoisePosition.Z)
|| !FMath::IsFinite(MinNoisePosition.X)
|| !FMath::IsFinite(MinNoisePosition.Y)
|| !FMath::IsFinite(MinNoisePosition.Z)
|| !FMath::IsFinite(MaxNoisePosition.X)
|| !FMath::IsFinite(MaxNoisePosition.Y)
|| !FMath::IsFinite(MaxNoisePosition.Z)
|| !FMath::IsFinite(Threshold)
|| !(Threshold > 0.0f)
|| !FMath::IsFinite(ScaledFieldLipschitzBound)
|| !(ScaledFieldLipschitzBound > 0.0))
{
return false;
}
const double Dx = FMath::Max(
FMath::Abs(static_cast<double>(MinNoisePosition.X)
- static_cast<double>(CenterNoisePosition.X)),
FMath::Abs(static_cast<double>(MaxNoisePosition.X)
- static_cast<double>(CenterNoisePosition.X)));
const double Dy = FMath::Max(
FMath::Abs(static_cast<double>(MinNoisePosition.Y)
- static_cast<double>(CenterNoisePosition.Y)),
FMath::Abs(static_cast<double>(MaxNoisePosition.Y)
- static_cast<double>(CenterNoisePosition.Y)));
const double Dz = FMath::Max(
FMath::Abs(static_cast<double>(MinNoisePosition.Z)
- static_cast<double>(CenterNoisePosition.Z)),
FMath::Abs(static_cast<double>(MaxNoisePosition.Z)
- static_cast<double>(CenterNoisePosition.Z)));
const double Radius = FMath::Sqrt(Dx * Dx + Dy * Dy + Dz * Dz);
// Keep this operation in float and in the same order as the production N1 expression. The
// double conversion is only for the conservative comparison below.
const float CenterN1 = FMath::Abs(
Perlin3D(CenterNoisePosition) * VOXEL_NOISE_SCALE);
if (!FMath::IsFinite(CenterN1))
{
return false;
}
return static_cast<double>(CenterN1)
- ScaledFieldLipschitzBound * Radius
>= static_cast<double>(Threshold) + WormBlockOutputRoundMargin;
}
//=============================================================================
// 4-WIDE BATCH — the SIMD multiplier. Computes 4 independent Perlin samples.
// Inputs are 4-element arrays; unused lanes must be zero-filled by the caller
// (FBM/Ridged below do). Results written to Out[0..3].
//=============================================================================
#if VF_NOISE_USE_SIMD
FORCEINLINE void Perlin3D_x4(const float* Xs, const float* Ys, const float* Zs, float* Out)
{
const __m128 x = _mm_loadu_ps(Xs);
const __m128 y = _mm_loadu_ps(Ys);
const __m128 z = _mm_loadu_ps(Zs);
const __m128 xf = _mm_floor_ps(x);
const __m128 yf = _mm_floor_ps(y);
const __m128 zf = _mm_floor_ps(z);
const __m128i X = _mm_cvttps_epi32(xf);
const __m128i Y = _mm_cvttps_epi32(yf);
const __m128i Z = _mm_cvttps_epi32(zf);
const __m128 fx = _mm_sub_ps(x, xf);
const __m128 fy = _mm_sub_ps(y, yf);
const __m128 fz = _mm_sub_ps(z, zf);
const __m128 c6 = _mm_set1_ps(6.0f);
const __m128 c15 = _mm_set1_ps(15.0f);
const __m128 c10 = _mm_set1_ps(10.0f);
auto Fade = [&](const __m128 t) -> __m128
{
const __m128 inner = _mm_add_ps(_mm_mul_ps(t, _mm_sub_ps(_mm_mul_ps(t, c6), c15)), c10);
const __m128 t3 = _mm_mul_ps(_mm_mul_ps(t, t), t);
return _mm_mul_ps(t3, inner);
};
const __m128 su = Fade(fx);
const __m128 sv = Fade(fy);
const __m128 sw = Fade(fz);
const __m128i one = _mm_set1_epi32(1);
const __m128i X1 = _mm_add_epi32(X, one);
const __m128i Y1 = _mm_add_epi32(Y, one);
const __m128i Z1 = _mm_add_epi32(Z, one);
const __m128i k1 = _mm_set1_epi32((int32)0x9E3779B1u);
const __m128i k2 = _mm_set1_epi32((int32)0x85EBCA77u);
const __m128i k3 = _mm_set1_epi32((int32)0xC2B2AE3Du);
const __m128i m1 = _mm_set1_epi32((int32)0x2C1B3C6Du);
const __m128i m2 = _mm_set1_epi32((int32)0x297A2D39u);
auto Hash = [&](const __m128i ix, const __m128i iy, const __m128i iz) -> __m128i
{
__m128i h = _mm_xor_si128(_mm_xor_si128(_mm_mullo_epi32(ix, k1),
_mm_mullo_epi32(iy, k2)),
_mm_mullo_epi32(iz, k3));
h = _mm_xor_si128(h, _mm_srli_epi32(h, 15)); h = _mm_mullo_epi32(h, m1);
h = _mm_xor_si128(h, _mm_srli_epi32(h, 12)); h = _mm_mullo_epi32(h, m2);
h = _mm_xor_si128(h, _mm_srli_epi32(h, 15));
return h;
};
const __m128 fx1 = _mm_sub_ps(fx, _mm_set1_ps(1.0f));
const __m128 fy1 = _mm_sub_ps(fy, _mm_set1_ps(1.0f));
const __m128 fz1 = _mm_sub_ps(fz, _mm_set1_ps(1.0f));
const __m128i i8 = _mm_set1_epi32(8);
const __m128i i12 = _mm_set1_epi32(12);
const __m128i i13 = _mm_set1_epi32(13);
const __m128i i15 = _mm_set1_epi32(15);
const __m128i i1 = _mm_set1_epi32(1);
const __m128i i2 = _mm_set1_epi32(2);
const __m128i izero = _mm_setzero_si128();
const __m128 sgn = _mm_set1_ps(-0.0f);
auto Grad = [&](const __m128i hash, const __m128 gx, const __m128 gy, const __m128 gz) -> __m128
{
const __m128i h = _mm_and_si128(hash, i15);
// u = (h&8)==0 ? gx : gy
const __m128 mU = _mm_castsi128_ps(_mm_cmpeq_epi32(_mm_and_si128(h, i8), izero));
const __m128 u = _mm_blendv_ps(gy, gx, mU);
// v = (h&12)==0 ? gy : ((h&13)==12 ? gx : gz)
const __m128 mLt4 = _mm_castsi128_ps(_mm_cmpeq_epi32(_mm_and_si128(h, i12), izero));
const __m128 m1214 = _mm_castsi128_ps(_mm_cmpeq_epi32(_mm_and_si128(h, i13), i12));
const __m128 vTmp = _mm_blendv_ps(gz, gx, m1214);
const __m128 v = _mm_blendv_ps(vTmp, gy, mLt4);
// signs: (h&1)? -u:u + (h&2)? -v:v
const __m128 mNegU = _mm_castsi128_ps(_mm_cmpeq_epi32(_mm_and_si128(h, i1), i1));
const __m128 mNegV = _mm_castsi128_ps(_mm_cmpeq_epi32(_mm_and_si128(h, i2), i2));
const __m128 ru = _mm_blendv_ps(u, _mm_xor_ps(u, sgn), mNegU);
const __m128 rv = _mm_blendv_ps(v, _mm_xor_ps(v, sgn), mNegV);
return _mm_add_ps(ru, rv);
};
const __m128 n000 = Grad(Hash(X, Y, Z ), fx, fy, fz );
const __m128 n100 = Grad(Hash(X1, Y, Z ), fx1, fy, fz );
const __m128 n010 = Grad(Hash(X, Y1, Z ), fx, fy1, fz );
const __m128 n110 = Grad(Hash(X1, Y1, Z ), fx1, fy1, fz );
const __m128 n001 = Grad(Hash(X, Y, Z1), fx, fy, fz1);
const __m128 n101 = Grad(Hash(X1, Y, Z1), fx1, fy, fz1);
const __m128 n011 = Grad(Hash(X, Y1, Z1), fx, fy1, fz1);
const __m128 n111 = Grad(Hash(X1, Y1, Z1), fx1, fy1, fz1);
auto Lerp = [&](const __m128 a, const __m128 b, const __m128 t) -> __m128
{
return _mm_add_ps(a, _mm_mul_ps(t, _mm_sub_ps(b, a)));
};
const __m128 x00 = Lerp(n000, n100, su);
const __m128 x10 = Lerp(n010, n110, su);
const __m128 x01 = Lerp(n001, n101, su);
const __m128 x11 = Lerp(n011, n111, su);
const __m128 y0 = Lerp(x00, x10, sv);
const __m128 y1 = Lerp(x01, x11, sv);
_mm_storeu_ps(Out, Lerp(y0, y1, sw));
}
#else
FORCEINLINE void Perlin3D_x4(const float* Xs, const float* Ys, const float* Zs, float* Out)
{
for (int32 i = 0; i < 4; ++i) Out[i] = Perlin3D(Xs[i], Ys[i], Zs[i]);
}
#endif
// Three independent channels are the hot cave-warp shape. On x86, evaluate them as three lanes
// of the existing four-wide implementation; the unused fourth lane is deliberate. The scalar
// fallback keeps the exact three-call behavior on platforms without the SIMD path. This helper is
// the owner of the cave-warp channel evaluation so generator, op-stack, and room-fit callers cannot
// drift into different noise formulas or channel offsets.
FORCEINLINE void Perlin3D_x3(
float X0, float Y0, float Z0,
float X1, float Y1, float Z1,
float X2, float Y2, float Z2,
float& Out0, float& Out1, float& Out2)
{
#if VF_NOISE_USE_SIMD
const float Xs[4] = { X0, X1, X2, X0 };
const float Ys[4] = { Y0, Y1, Y2, Y0 };
const float Zs[4] = { Z0, Z1, Z2, Z0 };
float Out[4];
Perlin3D_x4(Xs, Ys, Zs, Out);
Out0 = Out[0];
Out1 = Out[1];
Out2 = Out[2];
#else
Out0 = Perlin3D(X0, Y0, Z0);
Out1 = Perlin3D(X1, Y1, Z1);
Out2 = Perlin3D(X2, Y2, Z2);
#endif
}
//=============================================================================
// fBm / Ridged — octaves evaluated 4 at a time through Perlin3D_x4.
// Accumulation stays scalar in octave order, so the result is independent of
// whether the SIMD or scalar Perlin3D_x4 is used (bit-identical either way).
//=============================================================================
FORCEINLINE float FBM(float x, float y, float z,
int32 Octaves = 4, float Lacunarity = 2.0f, float Persistence = 0.5f)
{
float Total = 0.0f, MaxValue = 0.0f, Freq = 1.0f, Amp = 1.0f;
for (int32 o = 0; o < Octaves; )
{
const int32 N = FMath::Min(4, Octaves - o);
float Xs[4] = {}, Ys[4] = {}, Zs[4] = {}, Out[4];
float f = Freq;
for (int32 i = 0; i < N; ++i) { Xs[i] = x * f; Ys[i] = y * f; Zs[i] = z * f; f *= Lacunarity; }
Perlin3D_x4(Xs, Ys, Zs, Out);
for (int32 i = 0; i < N; ++i) { Total += Out[i] * Amp; MaxValue += Amp; Amp *= Persistence; }
Freq = f;
o += N;
}
return Total / MaxValue;
}
FORCEINLINE float Ridged(float x, float y, float z,
int32 Octaves = 4, float Lacunarity = 2.0f, float Persistence = 0.5f)
{
// Matches the original RidgedNoise3D fold exactly (NS scale, square, weight feedback).
static constexpr float NS = 1.25f;
float Total = 0.0f, MaxValue = 0.0f, Freq = 1.0f, Amp = 1.0f, Weight = 1.0f;
for (int32 o = 0; o < Octaves; )
{
const int32 N = FMath::Min(4, Octaves - o);
float Xs[4] = {}, Ys[4] = {}, Zs[4] = {}, Out[4];
float f = Freq;
for (int32 i = 0; i < N; ++i) { Xs[i] = x * f; Ys[i] = y * f; Zs[i] = z * f; f *= Lacunarity; }
Perlin3D_x4(Xs, Ys, Zs, Out);
for (int32 i = 0; i < N; ++i)
{
float Nn = Out[i] * NS;
Nn = 1.0f - FMath::Abs(Nn); // fold → ridge at zero-crossings
Nn = Nn * Nn; // sharpen
Nn *= Weight; // detail follows previous ridge
Weight = FMath::Clamp(Nn * 2.0f, 0.0f, 1.0f);
Total += Nn * Amp; MaxValue += Amp; Amp *= Persistence;
}
Freq = f;
o += N;
}
return (Total / MaxValue) * 2.0f - 1.0f;
}
} // namespace VoxelNoise
@@ -0,0 +1,711 @@
// Shared dimensions and construction laws for walkable passages and origin landings.
//
// This header is deliberately free of UObjects. The density path, the operator stack, the
// passage generator, and the headless measurements all include the same arithmetic so a geometry
// change cannot leave one of those paths with a different room or slope contract.
#pragma once
#include "CoreMinimal.h"
#include "VoxelTypes.h"
namespace VoxelPassageGeometry
{
// Authored scale and player body used by the §6.5 landing calculation.
constexpr float VoxelSizeMeters = VOXEL_SIZE / 100.0f;
constexpr float MetersToVoxels(float Meters)
{
return Meters / VoxelSizeMeters;
}
constexpr float PlayerRadiusVoxels = MetersToVoxels(0.34f);
constexpr float PlayerHalfHeightVoxels = MetersToVoxels(0.88f);
constexpr float PlayerHeightVoxels = 2.0f * PlayerHalfHeightVoxels;
constexpr float HeadroomVoxels = MetersToVoxels(1.0f);
constexpr float MinimumTurnFloorWidthVoxels = MetersToVoxels(3.0f);
constexpr float MinimumLandingHalfWidthVoxels =
MinimumTurnFloorWidthVoxels * 0.5f;
constexpr float MinimumRoomHeightVoxels = MetersToVoxels(3.0f);
constexpr float LandingFloorThicknessVoxels = MetersToVoxels(0.75f);
constexpr float MaxStepHeightVoxels = MetersToVoxels(0.45f);
// The A-to-B walk corridor is budgeted as a full player-width rectangle. Its vertical
// envelope starts above the legal step and extends one player height above that raised floor.
constexpr float WalkCorridorPlayerWidthVoxels = MetersToVoxels(0.68f);
constexpr float WalkCorridorPlayerHeightVoxels = MetersToVoxels(1.76f);
constexpr float WalkCorridorLatticeMarginVoxels = MetersToVoxels(0.25f);
constexpr float SealSafetyMarginVoxels = MetersToVoxels(1.0f);
constexpr float RoomRoundingVoxels = MetersToVoxels(0.375f);
constexpr float LandingCarveBlendVoxels = MetersToVoxels(1.0f);
// The origin landing is reserved for the same straight spine that the vertical-shaft
// generator uses: OriginSpineRadius on each side, with a six-voxel / 0.75 m floor ledge
// around it after the support patch's two-voxel inset. At the default radius (28 voxels) this
// is a 28-voxel / 7 m future shaft diameter.
constexpr float OriginShaftFloorClearanceVoxels = MetersToVoxels(1.0f);
// Level tunnel apron outside each room-floor edge. It covers the room/tunnel SDF blend and
// keeps the ramp from starting while the route is still over the room's square support patch.
constexpr float WalkableTunnelLandingApronVoxels = MetersToVoxels(1.5f);
// A level transition after a bend lets the route turn before its sloped segment begins.
constexpr float WalkableTunnelTurnTransitionVoxels = MetersToVoxels(1.5f);
// Keep the tunnel-air post 12.5 cm above the authored support plane. This is a numerical
// ownership band: the floor writer owns the plane, while the air writer owns the capsule
// volume above it even when two overlapping passage projections differ by a fraction.
constexpr float WalkableTunnelFloorAirClearanceVoxels = MetersToVoxels(0.125f);
// A 15 degree ramp is comfortably below the 44 degree engine walkability ceiling. The
// latter is a scramble limit, not a sensible default for a tunnel the player walks through.
constexpr float WalkableTunnelMaxGradientDegrees = 15.0f;
constexpr float WalkableTunnelMaxGradient = 0.2679491924311227f; // tan(15 degrees)
// Player-fit measurements use the engine's 44 degree walkability contract. The 15 degree
// value above remains the preferred authored tunnel ramp; this wider bound is only the
// fallback threshold at which a room-to-room floor must become a terrace.
constexpr float PlayerWalkableFloorMaxGradient = 0.9656887748070738f; // tan(44 degrees)
// TunnelFloorGentleSlopeThreshold is authored as degrees. Zero is still a legal UI value,
// but the construction path uses this small positive floor so a malformed/overly strict
// value cannot turn the graph collect window into an unbounded switchback search.
constexpr float TunnelFloorGentleSlopeMinimumDegrees = 1.0f;
constexpr float TunnelFloorGentleSlopeMaximumDegrees = 90.0f;
// The route solver uses at most six half-waves. The canonical tunnel envelope is the default
// 360-voxel tunnel reach plus its endpoint correction; round that direct-run envelope up to
// 640 voxels, then derive the total cap from the same wave limit. This leaves room above the
// proven canonical route while keeping the 0.1-degree collect window finite and bounded.
constexpr int32 TunnelFloorMaximumWindWaves = 6;
constexpr float TunnelFloorMaximumSingleWaveRunVoxels = MetersToVoxels(160.0f);
constexpr float TunnelFloorMaximumRouteLengthVoxels =
static_cast<float>(TunnelFloorMaximumWindWaves)
* TunnelFloorMaximumSingleWaveRunVoxels;
FORCEINLINE float EffectiveTunnelFloorGentleSlopeDegrees(float AuthoredDegrees)
{
if (!VoxelMath::IsFinite(AuthoredDegrees))
{
return 44.0f;
}
return FMath::Clamp(
FMath::Max(AuthoredDegrees, TunnelFloorGentleSlopeMinimumDegrees),
TunnelFloorGentleSlopeMinimumDegrees,
TunnelFloorGentleSlopeMaximumDegrees);
}
FORCEINLINE float TunnelFloorGradientFromDegrees(float AuthoredDegrees)
{
const float Degrees = EffectiveTunnelFloorGentleSlopeDegrees(AuthoredDegrees);
float SinValue = 0.0f;
float CosValue = 0.0f;
VoxelMath::DetSinCos(
SinValue, CosValue, Degrees * (PI / 180.0f));
// At 90 degrees the mathematical tangent is unbounded. The route solver only needs a
// finite comparison, so preserve the ordering while keeping every downstream bound finite.
return FMath::Min(
FMath::Abs(SinValue) / FMath::Max(FMath::Abs(CosValue), 1.0e-3f),
4096.0f);
}
// The walkability contract is about the floor carried by the tube, not only its centreline.
// A tapered tube changes that floor by the radius delta, so the construction and the audit
// use this same conservative floor delta.
FORCEINLINE float TunnelFloorZ(const FVector& Centreline, float Radius)
{
return Centreline.Z - FMath::Abs(Radius);
}
FORCEINLINE float NativePassageFloorSupportRadius(float StartRadius, float EndRadius)
{
return FMath::Max(
FMath::Min(FMath::Abs(StartRadius), FMath::Abs(EndRadius))
- MetersToVoxels(0.125f),
PlayerRadiusVoxels);
}
// Keep two switchback floor branches apart far enough that the upper branch's final floor
// post cannot enter the lower player's full-width walk corridor. The 0.25 m margin covers
// the scalar lattice/interpolation boundary; Eval displacement is already followed by the
// MC-facing tunnel and native-floor stages before collision density is returned.
FORCEINLINE float WalkableTunnelTurnLegSeparation(
float StartRadius, float EndRadius)
{
return NativePassageFloorSupportRadius(StartRadius, EndRadius)
+ 0.5f * WalkCorridorPlayerWidthVoxels
+ WalkCorridorLatticeMarginVoxels;
}
// Vertical-shaft tree links are round air capsules, but the player-fit graph needs a
// deterministic support plane to walk between shafts. Keep that plane on the first
// seal-safe ledge shared by the whole strate, so every tree edge has the same walk level.
// This is shaft-network support, not an origin landing connector.
FORCEINLINE float VerticalShaftConnectorFloorThickness(float Radius)
{
return FMath::Min(
LandingFloorThicknessVoxels,
FMath::Max(MetersToVoxels(0.125f), FMath::Abs(Radius) * 0.5f));
}
FORCEINLINE float VerticalShaftConnectorFloorZ(
float StrateBottomZ,
float InteriorBottomZ,
float InteriorTopZ,
float LedgeSpacing,
float LedgeDepth,
float ConnectorRadius)
{
const float FloorThickness = VerticalShaftConnectorFloorThickness(ConnectorRadius);
const float MinimumFloor = InteriorBottomZ
+ FloorThickness + SealSafetyMarginVoxels;
const float MaximumFloor = InteriorTopZ
- PlayerHeightVoxels - SealSafetyMarginVoxels;
if (!VoxelMath::IsFinite(StrateBottomZ)
|| !VoxelMath::IsFinite(InteriorBottomZ)
|| !VoxelMath::IsFinite(InteriorTopZ)
|| !VoxelMath::IsFinite(LedgeSpacing)
|| !VoxelMath::IsFinite(LedgeDepth)
|| !VoxelMath::IsFinite(MinimumFloor)
|| !VoxelMath::IsFinite(MaximumFloor)
|| !(MaximumFloor > MinimumFloor))
{
return 0.5f * (InteriorBottomZ + InteriorTopZ);
}
if (LedgeSpacing > 0.0f && LedgeDepth > 0.0f)
{
const float RelativeBottom = InteriorBottomZ - StrateBottomZ;
const int32 FirstPeriod = FMath::FloorToInt(RelativeBottom / LedgeSpacing);
// The small fixed bound keeps malformed authored values from turning generation into
// an unbounded search. Normal shaft strates find the first or second period.
constexpr int32 MaxPeriods = 64;
for (int32 PeriodOffset = 0; PeriodOffset < MaxPeriods; ++PeriodOffset)
{
const float PeriodStart = static_cast<float>(FirstPeriod + PeriodOffset)
* LedgeSpacing;
const float Candidate = StrateBottomZ + PeriodStart + LedgeDepth;
if (Candidate > MinimumFloor && Candidate < MaximumFloor)
{
return Candidate;
}
}
}
return FMath::Clamp(
0.5f * (InteriorBottomZ + InteriorTopZ), MinimumFloor, MaximumFloor);
}
FORCEINLINE float VerticalShaftConnectorCenterZ(
float StrateBottomZ,
float InteriorBottomZ,
float InteriorTopZ,
float LedgeSpacing,
float LedgeDepth,
float ConnectorRadius)
{
const float SafeRadius = FMath::Abs(ConnectorRadius);
const float FloorThickness = VerticalShaftConnectorFloorThickness(SafeRadius);
return VerticalShaftConnectorFloorZ(
StrateBottomZ, InteriorBottomZ, InteriorTopZ,
LedgeSpacing, LedgeDepth, SafeRadius)
+ SafeRadius - FloorThickness;
}
/** Add a flat, inset support band below one horizontal shaft-tree capsule. */
FORCEINLINE void VF_ApplyVerticalShaftConnectorFloor(
float& Density,
float WorldX,
float WorldY,
float WorldZ,
const FVector& Start,
const FVector& End,
float Radius,
float FloorZ,
float BaseDensity)
{
const float SafeRadius = FMath::Abs(Radius);
const float FloorThickness = VerticalShaftConnectorFloorThickness(SafeRadius);
const float FloorRadius = FMath::Max(
MetersToVoxels(0.125f), SafeRadius - MetersToVoxels(0.125f));
if (!(SafeRadius > 0.0f)
|| !(BaseDensity > 0.0f)
|| !VoxelMath::IsFinite(WorldX)
|| !VoxelMath::IsFinite(WorldY)
|| !VoxelMath::IsFinite(WorldZ)
|| !VoxelMath::IsFinite(FloorZ)
|| !VoxelMath::IsFinite(Start.X) || !VoxelMath::IsFinite(Start.Y)
|| !VoxelMath::IsFinite(End.X) || !VoxelMath::IsFinite(End.Y))
{
return;
}
const FVector2D A(Start.X, Start.Y);
const FVector2D Delta(End.X - Start.X, End.Y - Start.Y);
const float LengthSquared = Delta.SizeSquared();
const float T = LengthSquared > KINDA_SMALL_NUMBER
? FMath::Clamp(
FVector2D::DotProduct(FVector2D(WorldX, WorldY) - A, Delta)
/ LengthSquared,
0.0f, 1.0f)
: 0.0f;
const FVector2D Closest = A + Delta * T;
if ((FVector2D(WorldX, WorldY) - Closest).SizeSquared()
> FMath::Square(FloorRadius))
{
return;
}
if (WorldZ <= FloorZ + KINDA_SMALL_NUMBER
&& WorldZ > FloorZ - FloorThickness)
{
Density = FMath::Max(Density, BaseDensity);
}
}
// ApplyVerticalShaftConnectorAir runs before the generator's shared disturbance pass. Keep
// this one-bit, call-local hand-off so a bridge/ridge cannot refill the core it just opened.
// It is thread-local state, never a per-voxel cache, and is reset around every density query.
FORCEINLINE bool& VerticalShaftConnectorAirMarker()
{
static thread_local bool bMarked = false;
return bMarked;
}
FORCEINLINE void ResetVerticalShaftConnectorAirMarker()
{
VerticalShaftConnectorAirMarker() = false;
}
/** Reassert the walkable air core above one horizontal shaft-tree floor band. */
FORCEINLINE void VF_ApplyVerticalShaftConnectorAir(
float& Density,
float WorldX,
float WorldY,
float WorldZ,
const FVector& Start,
const FVector& End,
float Radius,
float FloorZ,
float BaseDensity)
{
const float SafeRadius = FMath::Abs(Radius);
// Leave a one-voxel wall band for the roughness/solid shell, while retaining more than
// enough width for the 1.36-voxel player capsule and its support stencil.
const float CoreRadius = FMath::Max(
MetersToVoxels(0.125f), SafeRadius - MetersToVoxels(0.25f));
if (!(SafeRadius > 0.0f)
|| !(BaseDensity > 0.0f)
|| !VoxelMath::IsFinite(WorldX)
|| !VoxelMath::IsFinite(WorldY)
|| !VoxelMath::IsFinite(WorldZ)
|| !VoxelMath::IsFinite(FloorZ)
|| !VoxelMath::IsFinite(Start.X) || !VoxelMath::IsFinite(Start.Y)
|| !VoxelMath::IsFinite(Start.Z)
|| !VoxelMath::IsFinite(End.X) || !VoxelMath::IsFinite(End.Y)
|| !VoxelMath::IsFinite(End.Z))
{
return;
}
// The exact floor sample belongs to the air core's lower ownership edge. The floor
// writer only owns the three voxels below FloorZ, so clearing from the half-voxel band
// above it cannot remove support.
if (WorldZ + KINDA_SMALL_NUMBER
< FloorZ + WalkableTunnelFloorAirClearanceVoxels)
{
return;
}
const FVector Delta = End - Start;
const float LengthSquared = Delta.SizeSquared();
const float T = LengthSquared > KINDA_SMALL_NUMBER
? FMath::Clamp(FVector::DotProduct(FVector(WorldX, WorldY, WorldZ) - Start, Delta)
/ LengthSquared, 0.0f, 1.0f)
: 0.0f;
const FVector Closest = Start + Delta * T;
if ((FVector(WorldX, WorldY, WorldZ) - Closest).SizeSquared()
> FMath::Square(CoreRadius))
{
return;
}
// Internal convention: negative is air, positive is solid. This is intentionally a
// post-ledge write; a partial shaft shelf must not cap the tree route's player-height
// envelope after the capsule has already carved it.
VerticalShaftConnectorAirMarker() = true;
Density = FMath::Min(Density, -BaseDensity);
}
FORCEINLINE float TunnelFloorGradient(
const FVector& Start, float StartRadius,
const FVector& End, float EndRadius)
{
const float HorizontalRun = FVector2D(
End.X - Start.X, End.Y - Start.Y).Size();
if (HorizontalRun <= KINDA_SMALL_NUMBER)
{
return FMath::Abs(TunnelFloorZ(End, EndRadius)
- TunnelFloorZ(Start, StartRadius)) > KINDA_SMALL_NUMBER
? FLT_MAX : 0.0f;
}
return FMath::Abs(TunnelFloorZ(End, EndRadius)
- TunnelFloorZ(Start, StartRadius)) / HorizontalRun;
}
FORCEINLINE float RequiredHorizontalRunForFloorDrop(
float FirstFloorDrop, float SecondFloorDrop)
{
return 2.0f * FMath::Max(
FMath::Abs(FirstFloorDrop), FMath::Abs(SecondFloorDrop))
/ WalkableTunnelMaxGradient;
}
/** Project a query into the nearest horizontal control segment of a walkable tunnel. */
FORCEINLINE bool ProjectWalkableTunnelFloor(
const TArray<FVector>& ControlPoints,
const TArray<float>& ControlRadii,
const FVector& Position,
float& OutFloorZ,
float& OutSupportRadius,
int32* OutSegmentIndex = nullptr)
{
if (OutSegmentIndex != nullptr)
{
*OutSegmentIndex = INDEX_NONE;
}
if (ControlPoints.Num() < 2
|| ControlRadii.Num() != ControlPoints.Num())
{
return false;
}
const double QueryX = Position.X;
const double QueryY = Position.Y;
const int32 ControlPointCount = ControlPoints.Num();
const FVector* ControlPointData = ControlPoints.GetData();
const float* ControlRadiusData = ControlRadii.GetData();
float BestDistanceSquared = FLT_MAX;
float BestHorizontalDistanceSquared = FLT_MAX;
bool bFoundSegment = false;
for (int32 SegmentIndex = 0;
SegmentIndex + 1 < ControlPointCount;
++SegmentIndex)
{
const FVector& A = ControlPointData[SegmentIndex];
const FVector& B = ControlPointData[SegmentIndex + 1];
const double AXY_X = A.X;
const double AXY_Y = A.Y;
const double DeltaX = B.X - AXY_X;
const double DeltaY = B.Y - AXY_Y;
const float LengthSquared = static_cast<float>(
DeltaX * DeltaX + DeltaY * DeltaY);
if (LengthSquared <= KINDA_SMALL_NUMBER)
{
continue;
}
const double HorizontalDot = (QueryX - AXY_X) * DeltaX
+ (QueryY - AXY_Y) * DeltaY;
const float HorizontalT = FMath::Clamp(
HorizontalDot / LengthSquared,
0.0f, 1.0f);
const double ClosestX = AXY_X + DeltaX * HorizontalT;
const double ClosestY = AXY_Y + DeltaY * HorizontalT;
const double DistanceX = QueryX - ClosestX;
const double DistanceY = QueryY - ClosestY;
const float HorizontalDistanceSquared = static_cast<float>(
DistanceX * DistanceX + DistanceY * DistanceY);
const float StartRadius = FMath::Abs(ControlRadiusData[SegmentIndex]);
const float EndRadius = FMath::Abs(ControlRadiusData[SegmentIndex + 1]);
const float StartFloorZ = TunnelFloorZ(A, StartRadius);
const float EndFloorZ = TunnelFloorZ(B, EndRadius);
const double FloorDeltaZ = static_cast<double>(EndFloorZ - StartFloorZ);
const double FullLengthSquared =
LengthSquared + FloorDeltaZ * FloorDeltaZ;
if (!(FullLengthSquared > KINDA_SMALL_NUMBER))
{
continue;
}
// A switchback may place two different floors at the same XY. Select the segment
// nearest to the complete query point on the floor curve, so a lower passage leg
// cannot inherit the floor height of a vertically separated upper leg. Keep floor
// interpolation horizontal after choosing the branch: FloorZ remains a function of XY.
const double FullDot = HorizontalDot
+ (static_cast<double>(Position.Z) - StartFloorZ) * FloorDeltaZ;
const float SpatialT = FMath::Clamp(
FullDot / FullLengthSquared,
0.0f, 1.0f);
const double SpatialX = AXY_X + DeltaX * SpatialT;
const double SpatialY = AXY_Y + DeltaY * SpatialT;
const double SpatialZ = StartFloorZ + FloorDeltaZ * SpatialT;
const double SpatialDistanceX = QueryX - SpatialX;
const double SpatialDistanceY = QueryY - SpatialY;
const double SpatialDistanceZ = Position.Z - SpatialZ;
const float DistanceSquared = static_cast<float>(
SpatialDistanceX * SpatialDistanceX
+ SpatialDistanceY * SpatialDistanceY
+ SpatialDistanceZ * SpatialDistanceZ);
if (DistanceSquared >= BestDistanceSquared)
{
continue;
}
OutFloorZ = FMath::Lerp(
StartFloorZ,
EndFloorZ,
HorizontalT);
OutSupportRadius = FMath::Max(
FMath::Min(StartRadius, EndRadius) - MetersToVoxels(0.125f),
PlayerRadiusVoxels);
if (OutSegmentIndex != nullptr)
{
*OutSegmentIndex = SegmentIndex;
}
BestDistanceSquared = DistanceSquared;
BestHorizontalDistanceSquared = HorizontalDistanceSquared;
bFoundSegment = true;
}
return bFoundSegment
&& VoxelMath::IsFinite(OutFloorZ)
&& VoxelMath::IsFinite(OutSupportRadius)
&& OutSupportRadius > 0.0f
&& BestHorizontalDistanceSquared <= FMath::Square(OutSupportRadius);
}
struct FOriginLandingGeometry
{
bool bValid = false;
float FloorZ = 0.0f;
float CeilingZ = 0.0f;
float HalfWidth = 0.0f;
float FloorThickness = LandingFloorThicknessVoxels;
};
FORCEINLINE float LandingHalfWidthForRadius(float Radius)
{
const float SafeRadius = FMath::Max(
FMath::Abs(Radius), MetersToVoxels(0.25f));
return FMath::Max(MinimumLandingHalfWidthVoxels,
SafeRadius + MetersToVoxels(0.5f));
}
FORCEINLINE float LandingHeightForRadius(float Radius)
{
const float SafeRadius = FMath::Max(
FMath::Abs(Radius), MetersToVoxels(0.25f));
return FMath::Max(
FMath::Max(MinimumRoomHeightVoxels, PlayerHeightVoxels + HeadroomVoxels),
2.0f * SafeRadius + MetersToVoxels(0.5f));
}
/**
* Build the one origin landing in a strate.
*
* `OriginRadius` is the old spine setting and becomes the room's authored mouth radius. The
* room dimensions therefore reuse the §6.5 formula exactly:
* half-width = max(1.5 m, radius + 0.5 m),
* height = max(3 m, capsule height + 1 m, 2 * radius + 0.5 m).
* The floor is placed against the top of the safe interior interval. This makes the first
* landing the place reached from above, while every lower landing remains sealed above until
* progression opens its inter-strate passage.
*/
FORCEINLINE FOriginLandingGeometry BuildOriginLandingGeometry(
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
{
FOriginLandingGeometry Result;
if (!(OriginRadius > 0.0f)
|| !VoxelMath::IsFinite(StrateTopZ)
|| !VoxelMath::IsFinite(StrateBottomZ)
|| !VoxelMath::IsFinite(SealThickness)
|| !VoxelMath::IsFinite(OriginRadius))
{
return Result;
}
// Keep a solid walking ledge around the future shaft, not merely a room whose wall is
// tangent to it. The shaft diameter is 2 * OriginRadius; the eight-voxel / 1 m extra
// leaves six voxels / 0.75 m of floor after the support patch's two-voxel / 0.25 m inset.
Result.HalfWidth = FMath::Max(
LandingHalfWidthForRadius(OriginRadius),
FMath::Max(FMath::Abs(OriginRadius), MetersToVoxels(0.25f))
+ OriginShaftFloorClearanceVoxels);
const float DesiredHeight = LandingHeightForRadius(OriginRadius);
const float SafeSeal = FMath::Max(SealThickness, 0.0f);
const float InnerBottom = StrateBottomZ + SafeSeal;
const float InnerTop = StrateTopZ - SafeSeal;
const float MinimumFloor = InnerBottom
+ Result.FloorThickness + SealSafetyMarginVoxels;
const float MaximumFloor = InnerTop
- DesiredHeight - SealSafetyMarginVoxels;
if (!VoxelMath::IsFinite(InnerBottom) || !VoxelMath::IsFinite(InnerTop)
|| !(InnerTop > InnerBottom)
|| !VoxelMath::IsFinite(MinimumFloor)
|| !VoxelMath::IsFinite(MaximumFloor)
|| MaximumFloor < MinimumFloor)
{
Result.HalfWidth = 0.0f;
return Result;
}
Result.FloorZ = MaximumFloor;
Result.CeilingZ = Result.FloorZ + DesiredHeight;
Result.bValid = true;
return Result;
}
FORCEINLINE bool IntersectsClosedInterval(float MinA, float MaxA,
float MinB, float MaxB)
{
return MaxA >= MinB && MinA <= MaxB;
}
/** True when the exact MC lattice has at least one sample in a closed interval. */
FORCEINLINE bool LatticeAxisHasSampleInInterval(
float Min, float Max, float Origin, int32 Step)
{
if (Step <= 0 || !VoxelMath::IsFinite(Min) || !VoxelMath::IsFinite(Max)
|| !VoxelMath::IsFinite(Origin) || Min > Max)
{
return false;
}
const float InvStep = 1.0f / static_cast<float>(Step);
const int32 First = FMath::CeilToInt(
(Min - Origin) * InvStep - 1.0e-4f);
const int32 Last = FMath::FloorToInt(
(Max - Origin) * InvStep + 1.0e-4f);
return First <= Last;
}
FORCEINLINE bool OriginLandingRoomTouchesLattice(
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step,
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius,
float ReachScale = 1.0f)
{
if (!VoxelBox.IsValid) return false;
if (!VoxelMath::IsFinite(ReachScale) || ReachScale <= 0.0f)
{
// This is an invalid proof request, not an empty room. The manager treats a
// malformed scale as conservative/unknown before calling this helper.
return true;
}
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
if (!Geometry.bValid) return false;
const float Pad = LandingCarveBlendVoxels * ReachScale;
return LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.X, -Geometry.HalfWidth - Pad),
FMath::Min((float)VoxelBox.Max.X, Geometry.HalfWidth + Pad),
(float)LatticeOrigin.X, Step)
&& LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.Y, -Geometry.HalfWidth - Pad),
FMath::Min((float)VoxelBox.Max.Y, Geometry.HalfWidth + Pad),
(float)LatticeOrigin.Y, Step)
&& LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.Z, Geometry.FloorZ - Pad),
FMath::Min((float)VoxelBox.Max.Z, Geometry.CeilingZ + Pad),
(float)LatticeOrigin.Z, Step);
}
FORCEINLINE bool OriginLandingFloorTouchesLattice(
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step,
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
{
if (!VoxelBox.IsValid) return false;
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
if (!Geometry.bValid) return false;
constexpr float ProofPad = 1.0f;
const float FloorHalfWidth = FMath::Max(
Geometry.HalfWidth - MetersToVoxels(0.25f), 0.0f);
return LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.X, -FloorHalfWidth - ProofPad),
FMath::Min((float)VoxelBox.Max.X, FloorHalfWidth + ProofPad),
(float)LatticeOrigin.X, Step)
&& LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.Y, -FloorHalfWidth - ProofPad),
FMath::Min((float)VoxelBox.Max.Y, FloorHalfWidth + ProofPad),
(float)LatticeOrigin.Y, Step)
&& LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.Z,
Geometry.FloorZ - Geometry.FloorThickness - ProofPad),
FMath::Min((float)VoxelBox.Max.Z, Geometry.FloorZ + ProofPad),
(float)LatticeOrigin.Z, Step);
}
/** Conservative influence box for the origin room's air carve. */
FORCEINLINE bool OriginLandingRoomTouchesBox(
const FBox& VoxelBox,
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
{
if (!VoxelBox.IsValid) return false;
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
if (!Geometry.bValid) return false;
const float Pad = LandingCarveBlendVoxels;
return IntersectsClosedInterval(
VoxelBox.Min.X, VoxelBox.Max.X,
-Geometry.HalfWidth - Pad, Geometry.HalfWidth + Pad)
&& IntersectsClosedInterval(
VoxelBox.Min.Y, VoxelBox.Max.Y,
-Geometry.HalfWidth - Pad, Geometry.HalfWidth + Pad)
&& IntersectsClosedInterval(
VoxelBox.Min.Z, VoxelBox.Max.Z,
Geometry.FloorZ - Pad, Geometry.CeilingZ + Pad);
}
/** Conservative influence box for the origin room's guaranteed solid floor. */
FORCEINLINE bool OriginLandingFloorTouchesBox(
const FBox& VoxelBox,
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
{
if (!VoxelBox.IsValid) return false;
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
if (!Geometry.bValid) return false;
// One voxel is a proof margin, not a write margin. It keeps ClassifyTile conservative at
// an exact lattice edge and mirrors the existing passage-floor guard.
constexpr float ProofPad = 1.0f;
const float FloorHalfWidth = FMath::Max(
Geometry.HalfWidth - MetersToVoxels(0.25f), 0.0f);
return IntersectsClosedInterval(
VoxelBox.Min.X, VoxelBox.Max.X,
-FloorHalfWidth - ProofPad, FloorHalfWidth + ProofPad)
&& IntersectsClosedInterval(
VoxelBox.Min.Y, VoxelBox.Max.Y,
-FloorHalfWidth - ProofPad, FloorHalfWidth + ProofPad)
&& IntersectsClosedInterval(
VoxelBox.Min.Z, VoxelBox.Max.Z,
Geometry.FloorZ - Geometry.FloorThickness - ProofPad,
Geometry.FloorZ + ProofPad);
}
/** Signed distance for a rounded origin room with a hard flat floor. */
FORCEINLINE float OriginLandingRoomSDF(
const FVector& Position, const FOriginLandingGeometry& Geometry)
{
if (!Geometry.bValid || Geometry.CeilingZ <= Geometry.FloorZ)
{
return FLT_MAX;
}
const FVector Center(0.0f, 0.0f,
(Geometry.FloorZ + Geometry.CeilingZ) * 0.5f);
const float Rounding = RoomRoundingVoxels;
const float HalfZ = (Geometry.CeilingZ - Geometry.FloorZ) * 0.5f;
const FVector HalfExtent(
FMath::Max(Geometry.HalfWidth - Rounding, MetersToVoxels(0.0625f)),
FMath::Max(Geometry.HalfWidth - Rounding, MetersToVoxels(0.0625f)),
FMath::Max(HalfZ - Rounding, MetersToVoxels(0.0625f)));
const FVector D(
FMath::Abs(Position.X - Center.X) - HalfExtent.X,
FMath::Abs(Position.Y - Center.Y) - HalfExtent.Y,
FMath::Abs(Position.Z - Center.Z) - HalfExtent.Z);
const FVector Outside(
FMath::Max(D.X, 0.0f),
FMath::Max(D.Y, 0.0f),
FMath::Max(D.Z, 0.0f));
const float Inside = FMath::Min(
FMath::Max(D.X, FMath::Max(D.Y, D.Z)), 0.0f);
const float RoundedBoxSDF = Outside.Size() + Inside - Rounding;
// Below the floor is rock. Above it the rounded-box air volume is authoritative.
return FMath::Max(RoundedBoxSDF, Geometry.FloorZ - Position.Z);
}
}
@@ -0,0 +1,195 @@
#pragma once
#include "CoreMinimal.h"
#include "VoxelTypes.h"
class UVoxelGenerator;
#if WITH_EDITOR
/**
* Step 2d editor research field. It is intentionally separate from the gameplay evaluator and
* is selected only by the default-off voxel.ResearchRedefinedDensityField console variable.
*
* The field is a connected, archetype-sized cave lattice with an authored walkable floor at each
* passage landing. Its macro shape is evaluated directly in world space; bounded, continuous
* hash-value noise is added at every full-resolution sample. The brick classifier is permitted to
* skip only when a Lipschitz bound on the macro field plus the exact maximum detail amplitude
* proves that the final sign is uniform throughout the brick and its marching-cubes halo.
*/
namespace VoxelForgeRedefinedDensityField
{
/** Proof sample width shared by the research mesher and the region-evaluation harness. */
constexpr int32 ParameterizedBrickSamples = 4;
/** Uniform fill magnitude used after a 16 metre sign proof. */
constexpr float ParameterizedUniformDensity = 16.0f * 100.0f / VOXEL_SIZE;
/** Per-worker, per-brick state. The opaque plan pointer is valid until the layout changes. */
struct FBrickEvaluationContext
{
const void* PlanToken = nullptr;
bool bPlanResolved = false;
bool bMayContainPassage = true;
bool bChunkResolved = false;
bool bIsGapChunk = false;
bool bHasDiffMods = false;
bool bMacroXYPrecomputed = false;
FIntVector BrickOrigin = FIntVector::ZeroValue;
FIntVector BrickSize = FIntVector::ZeroValue;
FIntVector Chunk = FIntVector::ZeroValue;
TArray<float> FloorByXY;
TArray<float> GridAirByXY;
TArray<float> DetailFadeByXY;
TArray<float> LandingHorizontalByXY;
TArray<float> LandingApproachByXY;
TArray<float> LandingBridgeHorizontalByXY;
TArray<float> LandingBridgeFloorByXY;
TArray<int32> PassageCandidates;
};
/** Cell-level controls for the separate editor-only parameterized cave field. */
struct FGenerationParameters
{
float RoomPitchVoxels = 144.0f;
float MinimumRoomRadiusFraction = 0.30f;
float MaximumRoomRadiusFraction = 0.45f;
float RoomHeightVoxels = 192.0f;
float FloorReliefVoxels = 8.0f;
float TunnelRadiusVoxels = 28.0f;
float TunnelHalfHeightVoxels = 26.0f;
float LoopProbability = 0.20f;
float VerticalShaftProbability = 0.10f;
float ShaftRadiusVoxels = 18.0f;
float PillarProbability = 0.10f;
float LedgeProbability = 0.20f;
float FloatingMassProbability = 0.10f;
float SpireProbability = 0.25f;
float BasinProbability = 0.0f;
float BasinDepthVoxels = 0.0f;
float WaterLevelFraction = 0.5f;
};
/** Brick-local immutable room cells used by step 2e evaluation and its final-field proof. */
struct FParameterizedBrickContext
{
const void* PlanToken = nullptr;
const void* RoomCellCacheToken = nullptr;
bool bPlanResolved = false;
bool bChunkResolved = false;
bool bIsGapChunk = false;
bool bHasDiffMods = false;
bool bMayContainPassage = true;
FIntVector BrickOrigin = FIntVector::ZeroValue;
FIntVector BrickSize = FIntVector::ZeroValue;
FIntVector Chunk = FIntVector::ZeroValue;
FIntPoint RoomCellCacheMin = FIntPoint::ZeroValue;
FIntPoint RoomCellCacheSize = FIntPoint::ZeroValue;
TArray<int32> PassageCandidates;
};
enum class EBrickFallbackReason : uint8
{
None,
UnsupportedState,
ChunkOrStrateBoundary,
PassageBounds,
MixedMacroInterval
};
struct FBrickCounters
{
uint64 Tested = 0;
uint64 Skipped = 0;
uint64 EvaluatedSamples = 0;
};
VOXELFORGE_API bool IsEnabled();
VOXELFORGE_API void SetEnabled(bool bEnabled);
VOXELFORGE_API float GetDetailScale();
VOXELFORGE_API void SetDetailScale(float Scale);
/** The parameterized cave field is editor-only and default-off. -1 rolls per strate. */
VOXELFORGE_API bool IsParameterizedEnabled();
VOXELFORGE_API void SetParameterizedEnabled(bool bEnabled);
/** Select the frozen step 2e implementation while the step 2f audit measures its gates. */
VOXELFORGE_API void SetLegacyStep2eAuditMode(bool bEnabled);
VOXELFORGE_API void SetParameterSet(int32 ParameterSet);
VOXELFORGE_API int32 GetParameterSet();
VOXELFORGE_API int32 GetParameterSetCount();
VOXELFORGE_API bool GetParameterSetInfo(
int32 ParameterSet, const TCHAR*& OutName, FGenerationParameters& OutParameters);
/** Evaluate the separate parameterized room / tunnel network, including edits/passages. */
VOXELFORGE_API float EvaluateParameterized(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ,
float* OutPreEditDensity = nullptr, float DetailScale = 1.0f);
/** Evaluate only the parameterized room/tunnel macro, before passage overlays and seals. */
VOXELFORGE_API float EvaluateParameterizedMacroOnly(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ);
/** Describe the candidate room SDF values at a point for step 2f proof diagnosis. */
VOXELFORGE_API FString DescribeParameterizedMacroAt(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ);
/** Return the active parameterized detail signal for the band-limit research gate. */
VOXELFORGE_API float EvaluateParameterizedDetail(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ);
VOXELFORGE_API float EvaluateParameterizedWithDetailValue(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ,
float DetailValue, float* OutPreEditDensity = nullptr, float DetailScale = 1.0f,
const FParameterizedBrickContext* Context = nullptr);
/** Evaluate a Z column while reusing its XY-dependent room graph data. */
VOXELFORGE_API void EvaluateParameterizedColumnWithDetailValues(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZStart,
const float* DetailValues, int32 DetailStride, int32 Count, float* OutValues,
const FParameterizedBrickContext* Context = nullptr, float DetailScale = 1.0f,
int32 SampleStep = 1);
VOXELFORGE_API bool TryClassifyParameterizedBrick(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
bool& bOutAir, float DetailScale = 1.0f,
bool* bOutEvaluatedMacroCentre = nullptr,
EBrickFallbackReason* OutFallbackReason = nullptr,
int32 SampleStep = 1);
VOXELFORGE_API void BuildParameterizedBrickContext(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
FParameterizedBrickContext& OutContext, int32 SampleStep = 1);
/** Evaluate a mixed brick with one shared room/passage setup for all Z samples. */
VOXELFORGE_API void EvaluateParameterizedDensityBrick(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
float DetailScale, FParameterizedBrickContext& InOutContext,
TArray<float>& InOutDetail, TArray<float>& InOutColumnValues,
TArray<float>& OutValues, int32 SampleStep = 1);
/** Evaluate the new field, including existing passage carving and player diffs. */
VOXELFORGE_API float Evaluate(const UVoxelGenerator& Generator, float WorldX, float WorldY,
float WorldZ, float* OutPreEditDensity = nullptr,
float DetailScale = 1.0f);
/** Build mathematically identical multi-scale noise for one integer-lattice brick. */
VOXELFORGE_API void BuildDetailBrick(const UVoxelGenerator& Generator,
FIntVector OriginVoxels, FIntVector Size, float DetailScale,
TArray<float>& OutDetail, FBrickEvaluationContext* OutContext = nullptr,
bool bParameterized = false, int32 SampleStep = 1);
/** Evaluate with a detail value produced by BuildDetailBrick. */
VOXELFORGE_API float EvaluateWithDetailValue(const UVoxelGenerator& Generator,
float WorldX, float WorldY, float WorldZ, float DetailValue,
float* OutPreEditDensity = nullptr, float DetailScale = 1.0f,
const FBrickEvaluationContext* Context = nullptr);
/** Prove a world-aligned box has one final density sign; false means evaluate every point. */
VOXELFORGE_API bool TryClassifyBrick(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
bool& bOutAir, float DetailScale = 1.0f,
bool* bOutEvaluatedMacroCentre = nullptr,
EBrickFallbackReason* OutFallbackReason = nullptr);
/** Reset this thread's immutable plan cache; used by deterministic editor experiments. */
VOXELFORGE_API void ResetThreadCache();
}
#endif
@@ -0,0 +1,71 @@
// Cookable runtime carrier for one reviewed season manifest.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "VoxelSeasonAsset.generated.h"
struct FVoxelSeasonManifest;
class UVoxelStrateDefinition;
/**
* The JSON remains the reviewed/diffable source artifact. This asset embeds that exact text so
* Unreal's cooker can carry it into a packaged build, together with an independently stored digest
* used to detect an accidentally stale or edited asset.
*/
UCLASS(BlueprintType)
class VOXELFORGE_API UVoxelSeasonAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
virtual FPrimaryAssetId GetPrimaryAssetId() const override;
/** Parse and validate the embedded manifest, including both content-hash copies. */
bool LoadManifest(FVoxelSeasonManifest& OutManifest, FString& OutReport) const;
/** Runtime-safe import core used by the editor button and automation. */
bool SetManifestJson(const FString& InManifestJson, FString& OutReport);
int32 GetSeasonNumber() const { return SeasonNumber; }
int32 GetSeasonSeed() const { return SeasonSeed; }
float GetOriginSpineRadius() const { return OriginSpineRadius; }
float GetWorldRadiusVoxels() const { return WorldRadiusVoxels; }
/** Create the asset in Content Browser, choose a JSON file, then press this button. */
UFUNCTION(CallInEditor, Category = "Season")
void ImportSeasonManifestJson();
#if WITH_EDITORONLY_DATA
UPROPERTY(EditAnywhere, Category = "Season|Import", meta = (FilePathFilter = "json"))
FFilePath SourceManifestJson;
#endif
private:
UPROPERTY(VisibleAnywhere, Category = "Season")
int32 SchemaVersion = 0;
UPROPERTY(VisibleAnywhere, Category = "Season")
int32 SeasonNumber = 0;
UPROPERTY(VisibleAnywhere, Category = "Season")
int32 SeasonSeed = 0;
UPROPERTY(VisibleAnywhere, Category = "Season")
float OriginSpineRadius = 0.0f;
UPROPERTY(VisibleAnywhere, Category = "Season")
float WorldRadiusVoxels = 0.0f;
UPROPERTY(VisibleAnywhere, Category = "Season")
FString ManifestContentHash;
UPROPERTY(VisibleAnywhere, Category = "Season", meta = (MultiLine = true))
FString ManifestJson;
// Import-derived cook references for fixed/authored content bags named by the JSON. Generated
// strates legitimately have no source definition and use deterministic C++ defaults at runtime.
UPROPERTY(VisibleAnywhere, Category = "Season")
TArray<TSoftObjectPtr<UVoxelStrateDefinition>> ReferencedDefinitions;
};
@@ -0,0 +1,299 @@
// Offline Tier 4c composition plus the runtime-readable, diffable Tier A manifest.
#pragma once
#include "CoreMinimal.h"
#include "VoxelStrateComposer.h"
#include "VoxelStrateMeasure.h"
class UVoxelSettings;
/** Why the provisional season selector put one surviving candidate in a slot. */
enum class EVoxelSeasonSelectionReason : uint8
{
Grounded,
Outlier,
Fixed,
};
/**
* Explicit, reviewable selection knobs for one season.
*
* None of these values is a settled definition of quality. They are the first policy Jahni can
* change after walking the review page. Hard validation remains separate from this policy.
*/
struct VOXELFORGE_API FVoxelSeasonSelectionPolicy
{
// Placeholder mix only: ordinary strates give an outlier contrast. Revisit after the first
// season review; this is not a learned or final quality score.
float GroundedFraction = 0.75f;
float OutlierFraction = 0.25f;
// Below this measured distance two neighbours look too similar. The selector subtracts a
// penalty proportional to the gap; provisional because the useful contrast is visual.
double AdjacentSimilarityDistanceThreshold = 0.20;
double AdjacentSimilarityPenaltyWeight = 1.0;
// Rewards an unseen archetype/recipe in the already chosen prefix. These are variety
// tie-breakers, not claims that one archetype or recipe is better. Provisional review knobs.
double NewArchetypeBonus = 0.10;
double NewRecipeBonus = 0.05;
// GDD §6 says roughly every five levels, and a boss is intermediary rather than terminal.
// This interval is therefore an explicit provisional layout knob, not a quality score.
int32 BossSlotInterval = 5;
// Tier 2 hard gate: a surviving air component must retain at least this share. Provisional
// only in its threshold; the existence of the gate is non-negotiable for a shippable season.
float MinimumLargestComponentShare = 0.50f;
// Normalizers for the two unbounded measured dimensions in the adjacency distance. Fractions
// already live in [0,1]. These physical scales are provisional and visible in the manifest.
float FeatureScaleNormalizationVoxels = 256.0f;
float ClearanceNormalizationVoxels = 64.0f;
};
/** A caller-supplied fixed slot for tests/tools that already have a complete manifest record. */
struct VOXELFORGE_API FVoxelSeasonFixedStrate
{
int32 Seed = 0;
int32 HeightInChunks = 8;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams Params;
FVoxelOpStackRecipe Recipe;
FVoxelStrateRegionManifest Regions;
bool bUsesRecipe = true;
bool bUsesRegions = false;
FString SourceDefinitionPath;
};
/** Settings for the offline build-box pass. No member is consulted by normal runtime generation. */
struct VOXELFORGE_API FVoxelSeasonCompositionSettings
{
// Optional live-world settings. TotalStrates, spine/gap values, and FixedStrates are read
// from it when present. The pointer is borrowed for the duration of VF_ComposeSeason only.
const UVoxelSettings* WorldSettings = nullptr;
// Optional preloaded corpus. With nullptr the composer loads the project asset corpus on the
// build box, exactly like the existing offline composer tests.
const FVoxelStrateCorpus* Corpus = nullptr;
// 24 gives a useful minute-scale automation pass at the default coarse measurement window.
// Production can raise this to the overnight budget without changing the policy.
int32 CandidateCount = 24;
int32 SelectedStrateCount = 10;
// Generated candidates use one stable height. Fixed authored slots may provide their own
// height. World layout bounds are written into every selected parameter family below.
int32 StrateHeightInChunks = 8;
int32 InterStrateGapChunks = 0;
float OriginSpineRadius = 28.0f;
float WorldRadiusVoxels = 0.0f;
// Coarse measurement is deliberately bounded. Tests may reduce the window; production may
// spend the overnight budget on a finer pass after the selector has been judged.
FVoxelStrateMeasureSettings MeasureSettings;
// Player-fit is a separate, explicitly fine pass. SampleStep values other than one are
// refused by the metric, never interpreted as an approximate capsule answer.
FVoxelStrateMeasureSettings PlayerFitMeasureSettings;
FVoxelSeasonSelectionPolicy SelectionPolicy;
// Explicit composed fixed slots take precedence. When empty, the composer adapts the
// existing UVoxelSettings::FixedStrates map, preserving its absolute slot indices.
TMap<int32, FVoxelSeasonFixedStrate> FixedStrates;
// Empty means ProjectSavedDir()/VoxelForge/Seasons. Files are never written to the runtime
// content directory by this API.
FString OutputDirectory;
bool bWriteArtifacts = true;
bool bWriteReviewPage = true;
bool bIncludeCandidateAudit = true;
// Informational only; when WorldSettings is present CurrentSeason supplies the default.
int32 SeasonNumber = 0;
FVoxelSeasonCompositionSettings()
{
// The existing Tier 4b test uses step 4. Season composition defaults to step 8 so the
// default automation artifact is measured in minutes, while MaxCells remains bounded.
MeasureSettings.SampleStep = 8;
MeasureSettings.RadiusInVoxels = 512;
MeasureSettings.MaxCells = 8000000;
MeasureSettings.MaxRouteRetries = 16;
MeasureSettings.HeadroomCells = 2;
MeasureSettings.InteriorMarginVoxels = -1;
PlayerFitMeasureSettings.SampleStep = 1;
PlayerFitMeasureSettings.RadiusInVoxels = 128;
// A fitted mouth-to-mouth XY window across an 8-chunk (256-voxel) strate can approach
// four million cells. Keep the cap finite and below the old multi-gigabyte retry cost.
PlayerFitMeasureSettings.MaxCells = 4000000;
PlayerFitMeasureSettings.MaxRouteRetries = 16;
PlayerFitMeasureSettings.HeadroomCells = 2;
PlayerFitMeasureSettings.InteriorMarginVoxels = -1;
}
};
/** Compact audit row; the full vectors live only for selected strates. */
struct VOXELFORGE_API FVoxelSeasonCandidateAudit
{
int32 CandidateIndex = INDEX_NONE;
int32 Seed = 0;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
uint32 RecipeHash = 0;
double DistanceFromCorpusCentroid = 0.0;
bool bPassedHardGates = false;
bool bSelected = false;
FString RejectionReason;
};
/** Count of one explicit hard-gate or final-placement rejection reason. */
struct VOXELFORGE_API FVoxelSeasonRejectionCount
{
FString Reason;
int32 Count = 0;
};
/** One ordered Tier A spine entry. */
struct VOXELFORGE_API FVoxelSeasonStrate
{
// DepthIndex is the absolute world-layout slot. CandidateIndex is the offline attempt id.
int32 DepthIndex = INDEX_NONE;
int32 CandidateIndex = INDEX_NONE;
int32 Seed = 0;
int32 HeightInChunks = 0;
int32 TopWorldZ = 0;
int32 BottomWorldZ = 0;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelOpStackRecipe Recipe;
FVoxelStrateArchetypeParams Params;
// When true this is the complete lateral parent description. Params/Archetype remain the
// compatibility identity of region zero; runtime rebuilds use Regions and never blend params.
FVoxelStrateRegionManifest Regions;
FVoxelStrateMeasuredMetrics Metrics;
double DistanceFromCorpusCentroid = 0.0;
FString SelectionReason;
FString SourceDefinitionPath;
bool bGrounded = false;
bool bOutlier = false;
bool bBossSlot = false;
bool bFixed = false;
bool bUsesRecipe = true;
bool bUsesRegions = false;
bool bPassedNonVacuous = false;
bool bPassedLargestComponent = false;
bool bPassedPrimordialLaw = false;
EVoxelConnectivityResult PrimordialLawResult = EVoxelConnectivityResult::OutOfWindow;
};
/**
* The stored Tier A season artifact.
*
* Tier B is intentionally absent: its hash-lattice field remains an arithmetic/enumeration API,
* not a second stored table. Strates is ordered from the surface down and is the only finite spine
* persisted by this task.
*/
struct VOXELFORGE_API FVoxelSeasonManifest
{
static constexpr int32 CurrentSchemaVersion = 2;
bool bValid = false;
int32 SchemaVersion = CurrentSchemaVersion;
FString Error;
int32 Season = 0;
int32 Seed = 0;
uint32 InputCorpusHash = 0;
// SHA-1 of the canonical manifest JSON with this field omitted. This detects stale cooked
// season assets without requiring the editor-only input corpus on a player's machine.
FString ContentHash;
int32 CandidateCount = 0;
int32 SurvivorCount = 0;
int32 SelectedCount = 0;
int32 RejectedCount = 0;
int32 GroundedSelectedCount = 0;
int32 OutlierSelectedCount = 0;
int32 FixedSelectedCount = 0;
int32 TotalStrates = 0;
int32 InterStrateGapChunks = 0;
float OriginSpineRadius = 0.0f;
float WorldRadiusVoxels = 0.0f;
FVoxelSeasonSelectionPolicy SelectionPolicy;
FString PolicyNote;
TArray<FVoxelSeasonStrate> Strates;
TArray<FVoxelSeasonCandidateAudit> CandidateAudit;
TArray<FVoxelSeasonRejectionCount> RejectionCounts;
// Derived artifact locations; intentionally not serialized into the portable JSON.
FString ManifestPath;
FString ReviewPagePath;
FString SerializedJson;
bool IsUsable() const
{
if (!bValid || !Error.IsEmpty() || ContentHash.Len() != 40 || WorldRadiusVoxels != 0.0f
|| Strates.Num() != SelectedCount || SelectedCount <= 0)
{
return false;
}
for (const FVoxelSeasonStrate& Strate : Strates)
{
// Region composition is intentionally not a shippable season format yet. Keeping
// this rejection in the portable manifest contract prevents an editor experiment
// from becoming packaged terrain merely because its density evaluator exists.
if (Strate.bUsesRegions)
{
return false;
}
}
return true;
}
};
/** Compose, validate, select, serialize, and (by default) write one season. Editor/build-box only. */
#if WITH_EDITOR
VOXELFORGE_API FVoxelSeasonManifest VF_ComposeSeason(
int32 SeasonSeed, const FVoxelSeasonCompositionSettings& Settings);
/** Convenience overload that adapts the existing runtime settings asset. */
VOXELFORGE_API FVoxelSeasonManifest VF_ComposeSeason(
int32 SeasonSeed, const UVoxelSettings* WorldSettings);
#endif
/** Serialize/read the complete Tier A manifest as stable pretty JSON. */
VOXELFORGE_API FString VF_SerializeVoxelSeasonManifest(
const FVoxelSeasonManifest& Manifest);
/** Parse an in-memory/cooked JSON manifest and verify its embedded content hash. */
VOXELFORGE_API bool VF_DeserializeVoxelSeasonManifest(
const FString& ManifestJson, FVoxelSeasonManifest& OutManifest, FString& OutReport);
/** Stable SHA-1 over the canonical manifest payload (the content_hash field itself is omitted). */
VOXELFORGE_API FString VF_ComputeVoxelSeasonManifestContentHash(
const FVoxelSeasonManifest& Manifest);
VOXELFORGE_API bool VF_SaveVoxelSeasonManifest(
const FString& ManifestPath, const FVoxelSeasonManifest& Manifest, FString& OutReport);
VOXELFORGE_API bool VF_LoadVoxelSeasonManifest(
const FString& ManifestPath, FVoxelSeasonManifest& OutManifest, FString& OutReport);
/** Rebuild exactly the stack represented by one manifest entry. */
VOXELFORGE_API bool VF_BuildSeasonStrateStack(
const FVoxelSeasonStrate& Strate,
float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr);
/** Verbose alias for tests/tools that want the round-trip operation named explicitly. */
VOXELFORGE_API bool VF_RebuildVoxelSeasonStrate(
const FVoxelSeasonStrate& Strate,
float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr);
@@ -0,0 +1,436 @@
// VoxelSettings.h
// Settings du plugin VoxelForge.
//
// Data asset unique (assigné sur AVoxelWorld) qui regroupe tous les tuning
// du monde: seed, streaming, LOD, pool de strates, budgets de carving.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "VoxelStrateDefinition.h"
#include "VoxelSettings.generated.h"
// IWYU : pointeur seulement (VoxelMaterial). / Pointer-only use.
class UMaterialInterface;
class UVoxelSeasonAsset;
UCLASS(BlueprintType)
class UVoxelSettings : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
/** Season seed/radius replace the mutable authored values only when Season is assigned. */
int32 GetEffectiveWorldSeed() const;
float GetEffectiveOriginSpineRadius() const;
float GetEffectiveWorldRadiusVoxels() const;
int32 GetEffectiveSeasonNumber() const;
//=========================================================================
// STREAMING (distance de vue)
//=========================================================================
// En CHUNKS (CHUNK_SIZE=32). At 12.5 cm/voxel, each view-distance chunk covers 4 m.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 ViewDistanceXY = 32;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 ViewDistanceUp = 10;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 ViewDistanceDown = 10;
// Nombre max de tâches de génération/mesh en parallèle.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 MaxConcurrentTasks = 16;
// Optional hard cap for the generation worker budget. 0 keeps the safe automatic cap of
// logical cores minus two; a positive value makes that cap visible and authorable beside the
// task budget instead of hiding it in AVoxelWorld. The effective task count is still the
// minimum of this value and MaxConcurrentTasks.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0"))
int32 MaxGenerationWorkerCores = 0;
// Keep this many task slots available for collision-critical, edited, and level-0 terrain while
// coarse horizon work is in flight. A positive reserve prevents a full far-field queue from
// delaying a new near tile because running tasks cannot be preempted. The runtime clamps the
// value below the effective task budget; 4 is the conservative default for the ship settings.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0", ClampMax = "16"))
int32 NearTaskReserve = 4;
// Nombre max de meshes appliqués (upload GPU) par frame. Seuls les vrais applies
// comptent (chunks vides/périmés se vident gratuitement). À 32³ chaque apply est léger
// (~0.1 ms) — tunable en live sur l'asset : montez (8-16) si le remplissage traîne,
// baissez si l'apparition des chunks fait des à-coups.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 MaxMeshAppliesPerFrame = 4;
// Wall-clock cap for the complete game-thread result drain, including component teardown and
// collision configuration. The count cap remains the second guard. A zero value disables this
// cap; the default keeps one burst of chunk application below a small frame-time budget without
// changing worker resolution or mesh quality.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0.0"))
float MaxMeshApplyMilliseconds = 2.0f;
// Max tile teardowns (component destroy + content-actor Destroy) per frame. A fast traversal
// culls a whole shell of tiles at once; doing every destroy in one frame is a game-thread spike
// ("stuff torn down behind you"). This spreads it. The drain auto-scales up if the backlog grows
// (PendingUnload/4) so it never falls far behind — raise the floor if teardown lags at high speed.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 MaxUnloadsPerFrame = 6;
// Independent level-0 collision radius around the locally controlled pawn. Render tiles outside
// this cube remain visible but do not queue Chaos cooks; registered streaming anchors keep their
// own authored collision boxes, and the collision gate can extend a landing-support column.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "1", ClampMax = "4"))
int32 PawnCollisionRadiusChunks = 2;
// Strate-aware VERTICAL streaming: don't load chunks into a strate you can't see.
// Seals + the inter-strate bedrock gap are light-tight (§8.7), so the strate above/below
// the one you're in is fully occluded — loading it just wastes scene primitives (the
// game-thread cost). When ON, the vertical view is clamped to the player's strate ±
// StrateViewMarginChunks. In the bedrock GAP itself the view is NOT clamped (you can see
// both sides through the descent passage there), so the next strate streams in ahead.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
bool bClampViewToStrate = true;
// Chunks of vertical look-ahead PAST the strate seal (each side) when bClampViewToStrate
// is on: keeps the descent tunnel / immediate transition loaded and acts as the trailing
// buffer of the strate you're leaving (it sheds naturally as you descend out of view).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0"))
int32 StrateViewMarginChunks = 6;
// STRATE CONTENT CUT (F17 separation) — a level-L tile is 2^L chunks TALL and can straddle a
// strate boundary: at coarse Steps the thin seal/gap solid between two strates' airs falls
// between lattice points (⇒ holes into the neighbour strate at far LOD) and one tile mixes
// both strates' materials. From this clip level UP, the mesher only meshes cells inside the
// PLAYER's strate Z-band (the other strates are sealed/enclosed ⇒ invisible from here anyway);
// loaded coarse tiles re-queue automatically when the band changes (strate transition).
// Default 1 = cut LOD1 and coarser. LOD0 is deliberately never content-cut because it owns pawn
// collision: cutting the passage, gap, or next-strate support tile would deadlock a walk at the
// current strate boundary. The runtime clamps legacy/hand-authored values below 1 to 1 as well.
// Raise only if a broader near-field view needs full coarse tiles. 9 = off.
// At ultra-coarse levels where one CELL is taller than the band itself, the tile is skipped
// entirely (see LoadTile) — cell-granular cutting there could only render garbage.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "1", ClampMax = "9"))
int32 StrateContentCutMinLevel = 1;
/**
* Return the effective content-cut level used by runtime streaming.
*
* Existing data assets can contain a serialized legacy value of 0 even though the property now
* documents 1 as the minimum. Keep the safety rule in code, at the consumer boundary: level 0
* tiles are the collision tiles and must never receive a strate content band.
*/
int32 GetEffectiveStrateContentCutMinLevel() const
{
return FMath::Clamp(FMath::Max(StrateContentCutMinLevel, 1), 1, 9);
}
//=========================================================================
// CLIPMAP (chunked-LOD streaming — supersedes the ViewDistance box above)
//=========================================================================
// Streaming loads concentric shells of tiles: level 0 = full-res chunks near the player,
// each coarser level doubles tile size (and reach). Total tile/draw/gen count stays ~flat
// regardless of how far you see — so this is the "see to the horizon cheaply" system.
// Total reach ≈ ClipRadius × CHUNK_SIZE × 2^MaxClipLevel voxels.
// Near full-res reach ≈ ClipRadius × CHUNK_SIZE voxels.
// Half-extent (in tiles, per axis) of EACH level's shell. Bigger = more tiles per level
// (more detail / overlap) but more components. 3 → a 7×7×7 box per level (minus the inner
// hole filled by the finer level).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "1"))
int32 ClipRadius = 6;
// Coarsest LOD level. 0 = full-res only (no clipmap). Each level up doubles tile size &
// reach at ~constant cost — raise this to see much farther for cheap. (Level L step = 2^L.)
// Ship default: level 5, the measured horizon-distance balance at 1536 chunks.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0", ClampMax = "8"))
int32 MaxClipLevel = 5;
// How many near levels mesh at FULL resolution (CHUNK_SIZE cells). Levels at or above this
// use the cheaper CoarseTileCells → MUCH faster gen for the far field (it's far, so the
// extra blockiness is hidden, and skirts cover the seams). 2 = levels 0,1 crisp; set to 1
// for the fastest gen (slightly harder LOD0→1 transition).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "1"))
int32 FullResClipLevels = 2;
// Cells per axis for COARSE tiles (levels >= FullResClipLevels). Lower = cheaper gen, blockier
// far terrain. 16 = ~⅛ the gen samples of a full 32-cell tile; 8 = ~1/64. Tile EXTENT is
// unchanged (the shells still tile) — only the mesh resolution within a far tile drops.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "4", ClampMax = "32"))
int32 CoarseTileCells = 16;
// RENDER DISTANCE — custom horizontal reach, in CHUNKS (1 chunk = 4 m; 256 ≈ 1 km, 1536 ≈ 6 km).
// When > 0 and farther than the natural clipmap reach (ClipRadius × 2^MaxClipLevel chunks), the
// OUTERMOST shell keeps generating level-MaxClipLevel tiles outward until it covers this
// distance. Pick MaxClipLevel = the coarsest level that still renders strates correctly (one
// cell must fit inside a strate band — level ≥7 blanks via the too-coarse skip) and buy the
// remaining horizon here. Cost: the extra ring is all same-level tiles — tile/draw/gen count
// grows with (distance / 2^MaxClipLevel)², so each MaxClipLevel step down quadruples the ring.
// Ship default: 1536 chunks (about 6.1 km at 4 m/chunk). 0 = off (natural reach).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0"))
int32 RenderDistanceChunks = 1536;
// SKIRTS — seal the thin cracks where neighbouring clipmap shells (different resolutions) meet.
// A short wall is extruded into the solid from each surface edge on the tile's outer faces.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap")
bool bGenerateSkirts = true;
// Skirt depth in tile CELLS (× the tile's step). Four L0 cells preserve the former 0.5 m
// default depth at 12.5 cm per voxel. Raise if cracks still show; lower if skirts peek out.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0.5", ClampMax = "8.0"))
float SkirtCells = 4.0f;
// T2.b — LOD-aware octave reduction. Octaves DROPPED from per-voxel volumetric noise
// (roughness, worms displacement, slab/maze/shaft/island detail) per Step doubling on
// coarse tiles: a Step=4 tile drops 2×this. Sub-cell octaves can't shape a coarse
// isosurface — they only cost CPU — so 1 shaves 30-50% off far-tile gen for a sub-cell
// isosurface shift (skirts already stitch bigger LOD seams). LOD0 is NEVER affected.
// 0 = off (every LOD samples full octaves — byte-identical to before this setting).
// Réduction d'octaves sur les tuiles lointaines ; 0 = désactivé, LOD0 jamais touché.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0", ClampMax = "3"))
int32 LODOctaveDrop = 0;
//==========================================================================
// WORLD BOUNDS (bounded gigantic world in actor-space XY)
//==========================================================================
// Radius (voxels) of the bounded world in XY, measured from actor-space (0,0).
// 0 = unbounded (legacy behaviour, no edge seal).
//
// ⚠️ DEFAULTS TO 0 ON PURPOSE, and it is not the value the GDD wants.
// Existing DA_Settings / BP_VoxelSettings assets predate this property, so they do NOT serialize
// it — they silently inherit whatever C++ default is written here. A non-zero default would
// therefore wall in every existing world the next time the editor opens, with nothing in the
// asset to show why. The former 8192-voxel / 2.05 km target must be 16384 voxels at 12.5 cm.
// 8192 now means a 1.024 km radius.
// SET IT IN THE ASSET, deliberately, once.
//
// FR : 0 par défaut EXPRÈS — les assets existants n'ont pas cette propriété et héritent
// silencieusement de la valeur C++. La valeur de production visée (~8192) se règle dans l'asset.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|World Bounds", meta = (ClampMin = "0.0"))
float WorldRadiusVoxels = 0.0f;
// Solid ramp thickness at the radial world edge, in voxels.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|World Bounds", meta = (ClampMin = "0.0"))
float EdgeSealThickness = 128.0f;
//=========================================================================
// CONTENT — distance-based decoration grid (no LOD pop)
//=========================================================================
// Decorations are placed on a fixed WORLD XY cell grid (1 cell = 1 chunk footprint) and streamed
// by DISTANCE from the player, independent of which clipmap tile (LOD) currently meshes the ground
// under them. Each candidate column is ray-marched through the player's strate via the generator's
// density field and snapped to the real surface — so a given prop keeps the SAME world position at
// every LOD (no teleport/pop on tile swaps). Decorations exist only in the player's current strate.
// FAR-tier stream radius in cells (= chunks): how far FStrateDecoration entries set to EDecoStreamTier::Far
// (the default — trees, landmarks, rare props) stream out. Bigger = props visible farther + more
// spawn/march cost (but the far grid is COARSE — see DecorationFarSpacingVoxels — so far cost is cheap).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1"))
int32 DecorationRadiusChunks = 12;
// NEAR-tier stream radius in cells (= chunks): how far EDecoStreamTier::Near entries (dense groundcover
// like grass) stream out. Keep this SHORT — near entries use the FINE grid (DecorationSpacingVoxels), so
// their cost is the steep one; bounding their radius keeps the far-region HISM build + memory small.
// (Repurposes the old vestigial DecorationActorRadiusChunks; same default.)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1"))
int32 DecorationNearRadiusChunks = 6;
// Decoration cells are grouped into REGIONS of RxR cells, and ALL placements in a region share ONE
// HISM per mesh (instead of one HISM per cell per mesh). Regions load/unload as a unit, so clearing
// stays a plain DestroyComponent — no per-instance index remapping. This is the render-thread lever:
// component count (which InitViews walks every frame) drops by ~R^2. R=4 → ~16 regions in a radius-6
// disk vs 169 cells (~10x fewer components). Bigger R = fewer components but coarser pop-in + heavier
// one-shot cluster-tree build per region (apply is budgeted, so the build is amortised). MUST be >= 1.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "16"))
int32 DecorationRegionSizeCells = 8;
// NEAR-tier column spacing (in voxels) within a cell — the FINE grid. MUST divide CHUNK_SIZE (32):
// 4 → 8×8=64 columns/cell. Smaller = denser placement potential + more march cost. SpawnDensity rolls
// per column-crossing. Used by EDecoStreamTier::Near entries (and is the legacy single-grid spacing).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "32"))
int32 DecorationSpacingVoxels = 8;
// FAR-tier column spacing (in voxels) within a cell — the COARSE grid. MUST divide CHUNK_SIZE (32):
// 16 → 2×2=4 columns/cell (16× fewer worker ray-marches than a spacing-4 grid). This is the lever that
// makes a RARE prop visible at every distance cheap: the far grid samples sparsely, so supporting a
// low-SpawnDensity landmark across the full radius costs a fraction of the fine grid. DEFAULTS to the
// fine value (4) so existing worlds are byte-identical until you raise it; bump to 8–16 for cheap far props.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "32"))
int32 DecorationFarSpacingVoxels = 8;
// COARSE vertical march step (in voxels) when searching a column for surface crossings. The crossing
// Z is then bisection-refined, so accuracy is independent of this — raise it (4-8) to cut the scan
// cost (the column is ray-marched on a WORKER thread, but a smaller step still means more samples).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "8"))
int32 DecorationMarchStepVoxels = 4;
// Max surface crossings placed per column (caps cave columns that pierce many floors/ceilings;
// surface worlds have 1). Bounds worst-case march/spawn cost.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "16"))
int32 DecorationMaxCrossingsPerColumn = 4;
// Stop marching a column after this many voxels of CONTIGUOUS SOLID once it has already entered the
// open space — i.e. cap how far into bedrock BELOW the floor (or below a cavern) we keep scanning.
// The top cap/seal and the air above the ground are always marched first (this only trims the dead
// rock underneath), so surface worlds still get their ground and caves still find layered floors.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "8"))
int32 DecorationColumnDepthVoxels = 320;
// Budget: how many completed decoration REGIONS to APPLY (game-thread HISM build + SpawnActor) per
// frame. A region is applied once ALL its cells have finished marching, in one batched AddInstances
// per mesh; this throttles that burst. The expensive ray-march runs async on workers. (Named "Cells"
// for asset back-compat — the apply unit is now a region of DecorationRegionSizeCells^2 cells.)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "1"))
int32 MaxDecorationCellsPerFrame = 2;
// Max decoration ray-march tasks in flight at once. Keeps decoration marching from crowding the
// mesh-gen workers (which are the streaming bottleneck). 0 disables decorations entirely.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0"))
int32 MaxConcurrentDecorationTasks = 4;
//=========================================================================
// LIGHTING — DENSITY VOLUME (mini-sun raymarched shadows)
//=========================================================================
// A player-centred CLIPMAP of the density field, uploaded to the GPU so the terrain
// material can RAYMARCH it toward the "mini-sun" orbs → from-the-orb, crisp, dynamic
// shadows under FORWARD rendering (Lumen/DF off the table). The volume is the load-bearing
// prerequisite: density is CPU-only (GetDensityAt), so we stream it onto the GPU here.
// Concentric levels: level 0 = full-res near the player (step 1), each level up doubles the
// sampling step & reach (fine near / coarse far — exactly what shadow rays want). Filled on
// WORKER threads (re-evaluating GetDensityAt → deterministic, carves auto-picked-up), with
// toroidal incremental refill on movement and localized refill on carve. See VoxelDensityVolume.
// Master switch. OFF = no volume built, no fill tasks, no GPU cost (terrain unlit by orbs).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting")
bool bEnableDensityVolume = false;
// Per-axis resolution of EACH clip level (cells). Memory per level ≈ Res³ bytes (R8).
// 256 → ~16 MiB/level; 512 → ~128 MiB/level. Doubling Res preserves metre reach at 12.5 cm.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "32", ClampMax = "512"))
int32 DensityVolumeResolution = 256;
// Number of concentric clip levels. Level L samples every (1<<L) voxels and covers
// Res·(1<<L) voxels. At 12.5 cm, Res=512 → near 64 m (full-res) out to ~256 m (coarse).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "1", ClampMax = "5"))
int32 DensityVolumeLevels = 3;
// A fill box is split into Z-slabs of at most this many cells per task, so no single task is
// huge (a full level refill on startup/teleport fans out across workers). Lower = more, smaller
// tasks (better parallelism / latency); higher = fewer, fatter tasks.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "1", ClampMax = "64"))
int32 DensityVolumeFillSlabCells = 8;
// Per-pixel shadow-march step count toward the orb (the terrain material reads this). More = crisper
// occlusion at grazing angles but higher GPU cost. 64 is a sane start; tune against the look/cost.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "4", ClampMax = "256"))
int32 DensityVolumeMarchSteps = 64;
// Upload the clipmap to GPU R8 volume textures (so the terrain material can march it). OFF = the
// CPU volume still streams (debug-draw works) but nothing reaches the GPU — the safe fallback if
// the runtime Texture3D RHI path misbehaves on a given engine build.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting")
bool bDensityVolumeGPUUpload = true;
// DEBUG (step 1a verification, no GPU): draw small boxes for SOLID cells of level 0 within
// DensityVolumeDebugRadiusCells of the player, so you can confirm the volume holds terrain-shaped
// solidity, follows you, and updates on carve — BEFORE the GPU upload + material march land.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting|Debug")
bool bDebugDrawDensityVolume = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting|Debug", meta = (ClampMin = "1", ClampMax = "32"))
int32 DensityVolumeDebugRadiusCells = 12;
//=========================================================================
// RENDERING
//=========================================================================
// Matériau global par défaut (peut être override par strate).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Rendering")
UMaterialInterface* VoxelMaterial;
//=========================================================================
// STRATES
//=========================================================================
// Seed du monde. Pilote toute la génération procédurale.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates")
int32 Seed = 0;
// Optional reviewed/cooked season. When set, its manifest owns layout, seed, bounds and
// density recipes; the authored StratePool/FixedStrates path remains untouched when unset.
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Voxel|Strates")
TSoftObjectPtr<UVoxelSeasonAsset> Season;
// Numéro de saison (incrementé par ChangeSeed). Purement informatif côté gameplay.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates")
int32 CurrentSeason = 1;
// Pool de définitions de strates disponibles pour l'assignation aléatoire.
// Chaque définition a sa propre hauteur — les strates ne sont PAS uniformes.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates")
TArray<TSoftObjectPtr<UVoxelStrateDefinition>> StratePool;
// Strates fixées: {index → définition}. Ex: {5, CrystalCaverns} =
// la strate 5 est toujours CrystalCaverns. Les autres sont tirées du pool.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates")
TMap<int32, TSoftObjectPtr<UVoxelStrateDefinition>> FixedStrates;
// Nombre total de strates à générer vers le bas.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "1"))
int32 TotalStrates = 10;
// Vertical SEPARATION between consecutive strates, in chunks of solid bedrock.
// 0 = strates touch (just the seals between them). Higher = a thick rock layer
// between each biome that the player must dig through at (0,0), and that the
// auto-carved passages tunnel across.
// 0 → adjacent layers (default) · 1-2 → a real bedrock band · 4+ → deep separation
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "0", ClampMax = "16"))
int32 InterStrateGapChunks = 0;
//=========================================================================
// SPINE & INTER-STRATE CONNECTIONS (the (0,0) descent axis)
//=========================================================================
// Authored radius (voxels) of the guaranteed (0,0) landing room in EVERY strate, regardless
// of archetype. The §6.5 body-sized formulas turn it into a flat-floored room reserved for a
// future straight shaft of diameter 2 * this radius. At the default 28-voxel radius that is
// 56 voxels / 7 m, with a six-voxel / 0.75 m floor ledge around it. This setting does not bore the
// shaft today: lower seals remain solid until a progression passage is opened.
// 10-14 → compact landing room (default) · 20+ → broad landing room
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Spine", meta = (ClampMin = "0.0"))
float OriginSpineRadius = 28.0f;
// Auto-open the very top seal at (0,0) so the world starts with a hole to the
// surface (the entry shaft). Lower boundaries are NOT auto-opened — the player
// digs those. Disable for a fully sealed world the player must breach from above.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Spine")
bool bOpenSurfaceEntry = true;
// NOTE: inter-strate tunnel count + shape (style, width, length, placement, worming,
// spiral, cascade) are now PER-STRATE — see UVoxelStrateDefinition::PassageConfig.
// Each strate controls its own descent tunnels to the layer below it.
//=========================================================================
// CARVING (modifications du joueur)
//=========================================================================
// Nombre max d'opérations de modification par saison. 0 = illimité.
// 500 = édition légère, 2000 = mining modéré, 0 = créatif.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Carving",
meta = (ClampMin = "0"))
int32 MaxModifications = 0;
// Rayon max d'un brush (en voxels). Empêche les édits géants.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Carving",
meta = (ClampMin = "1.0"))
float MaxBrushRadius = 30.0f;
// Volume total maximum (somme des volumes de brush). 0 = illimité.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Carving",
meta = (ClampMin = "0.0"))
float MaxTotalVolume = 0.0f;
};
@@ -0,0 +1,65 @@
// Bounded, opt-in startup trace for the complete streaming path.
// The trace is deliberately independent of the shared Unreal log: callers provide an
// absolute output path and every record is one JSON object per line.
#pragma once
#include "CoreMinimal.h"
namespace VoxelForgeStartupTrace
{
struct VOXELFORGE_API FTileSample
{
int32 TileX = 0;
int32 TileY = 0;
int32 TileZ = 0;
int32 Level = -1;
int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (early path)
bool bSealedSolidProof = false;
bool bEmpty = true;
int32 Vertices = 0;
int32 Triangles = 0;
uint64 GeometryBytes = 0;
// This is worker-result -> game-thread mesh submission. Collision readiness is a later
// per-tile RMC completion event recorded separately as collision_ready.
double RequestToApplySeconds = 0.0;
double QueueWaitSeconds = 0.0;
double WorkerQueueSeconds = 0.0;
double ResultQueueSeconds = 0.0;
double GenerationSeconds = 0.0;
double ClassifySeconds = 0.0;
double MeshSeconds = 0.0;
double StreamSeconds = 0.0;
double ApplySeconds = 0.0;
};
/** Starts when -voxel.StartupTraceFile=<absolute path> is present. */
VOXELFORGE_API void BeginFromCommandLine();
VOXELFORGE_API bool IsActive();
/** FieldsJson is the contents after the type/timestamp object prefix, without braces. */
VOXELFORGE_API void RecordEvent(const TCHAR* Type, const FString& FieldsJson = FString());
VOXELFORGE_API void StageBegin(const TCHAR* Name);
VOXELFORGE_API void StageEnd(const TCHAR* Name);
VOXELFORGE_API void RecordTile(const FTileSample& Sample);
/** Writes the final bounded summary and closes the file. Safe to call more than once. */
VOXELFORGE_API void Finish(const TCHAR* Reason);
/** RAII helper for stages which have early-return failure paths. */
class VOXELFORGE_API FStageScope
{
public:
explicit FStageScope(const TCHAR* InName);
~FStageScope();
FStageScope(const FStageScope&) = delete;
FStageScope& operator=(const FStageScope&) = delete;
private:
FString Name;
bool bActive = false;
};
}
@@ -0,0 +1,27 @@
// VoxelStats.h
// Per-frame runtime counters for tile classification and meshing.
// Compteurs runtime par frame pour la classification et le meshing des tuiles.
#pragma once
#include "Stats/Stats.h"
DECLARE_STATS_GROUP(TEXT("VoxelForge"), STATGROUP_VoxelForge, STATCAT_Advanced);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Classified"), STAT_VoxelForgeTilesClassified, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Skipped All Solid"), STAT_VoxelForgeTilesSkippedAllSolid, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Skipped All Air"), STAT_VoxelForgeTilesSkippedAllAir, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Meshed"), STAT_VoxelForgeTilesMeshed, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Solid"), STAT_VoxelForgeTilesOpStackSolid, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Air"), STAT_VoxelForgeTilesOpStackAir, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Out Of Layout"), STAT_VoxelForgeCaveMixOutOfLayout, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Gap"), STAT_VoxelForgeCaveMixGap, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Surface World"), STAT_VoxelForgeCaveMixSurfaceWorld, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Two Cave Slots"), STAT_VoxelForgeCaveBailTwoCaveSlots, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Params"), STAT_VoxelForgeCaveBailParams, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Stack Verdict"), STAT_VoxelForgeCaveBailStackVerdict, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Fold Conflict"), STAT_VoxelForgeCaveBailFoldConflict, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Disturbance"), STAT_VoxelForgeCaveBailDisturbance, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail No Stack"), STAT_VoxelForgeCaveBailNoStack, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Column Memo Hits"), STAT_VoxelForgeColumnMemoHit, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Column Memo Misses"), STAT_VoxelForgeColumnMemoMiss, STATGROUP_VoxelForge, VOXELFORGE_API);
@@ -0,0 +1,821 @@
// Offline strate corpus and parameter composer.
//
// This is deliberately not part of the runtime generation path. The composer is a build-box
// tool/test API: it loads authored vectors, measures their spread, and makes deterministic
// candidates by blending those vectors before applying spread-relative (or season-zero bootstrap)
// jitter.
#pragma once
#include "CoreMinimal.h"
#include "VoxelStrateTypes.h"
#include "VoxelDensityOpStack.h"
#include "VoxelStrateMeasure.h"
#include "VoxelStrateComposer.generated.h"
class UVoxelSettings;
class UVoxelStrateDefinition;
class UVoxelStrateManager;
#if WITH_EDITOR
// The complete definitions live in VoxelSeasonManifest.h. Keeping this forward declaration here
// makes the Tier 4c entry point discoverable from the composer header without creating a circular
// include (the manifest header includes this one for the existing vector/recipe types).
struct FVoxelSeasonManifest;
struct FVoxelSeasonCompositionSettings;
VOXELFORGE_API FVoxelSeasonManifest VF_ComposeSeason(
int32 SeasonSeed, const FVoxelSeasonCompositionSettings& Settings);
#endif
/** The one-bit identity choice at the root of a rolled structure. */
UENUM(BlueprintType)
enum class EVoxelStrateRootPolarity : uint8
{
RockCarve = 0,
VoidFill = 1,
};
/** Native parameter family an op reads when a recipe is materialised. */
UENUM(BlueprintType)
enum class EVoxelStrateParamBlock : uint8
{
None = 0,
TunnelNetwork = 1,
Slab = 2,
Maze = 3,
Surface = 4,
VerticalShaft = 5,
FloatingIsland = 6,
};
/** Stable manifest id for every op the offline structure roller may place. */
UENUM(BlueprintType)
enum class EVoxelStrateOpClass : uint8
{
ConstantRockSource = 0,
ConstantVoidSource = 1,
RoomGraphSource = 2,
LatticeCorridorSource = 3,
ShaftFieldSource = 4,
IslandBlobSource = 5,
NoiseRibbonSource = 6,
SdfRoughnessMod = 7,
SdfCarve = 8,
SdfFill = 9,
GridColumnMod = 10,
CaveRoughnessMod = 11,
CaveTerraceMod = 12,
LayerLineMod = 13,
RibbingMod = 14,
CaveOverhangMod = 15,
CaveCliffMod = 16,
ScallopMod = 17,
CaveArchMod = 18,
RoomColumnMod = 19,
DomeMod = 20,
PinchMod = 21,
FloorBiasMod = 22,
WormFieldSource = 23,
ShaftLedgeMod = 24,
DensityNoiseCarveMod = 25,
DensityNoiseFillMod = 26,
};
/** One creative op in the serialisable structure manifest. */
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelOpRecipeEntry
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
EVoxelStrateOpClass OpClass = EVoxelStrateOpClass::ConstantRockSource;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
EVoxelStrateParamBlock ParamBlock = EVoxelStrateParamBlock::None;
};
/**
* Serialisable creative structure. Structural posts are intentionally absent: the recipe builder
* appends spine, vertical seal, passage carve, and XY edge seal in that fixed order every time.
*/
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelOpStackRecipe
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
EVoxelStrateRootPolarity RootPolarity = EVoxelStrateRootPolarity::RockCarve;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
FVoxelOpRecipeEntry Root;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
FVoxelOpRecipeEntry ShapeSource;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
FVoxelOpRecipeEntry Conversion;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
TArray<FVoxelOpRecipeEntry> Modifiers;
// Which native block supplies the mandatory structural post parameters.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
EVoxelStrateParamBlock StructuralParamBlock = EVoxelStrateParamBlock::None;
};
/** The kind of value represented by one entry of VF_STRATE_PARAM_FIELDS. */
enum class EVoxelStrateFieldKind : uint8
{
Continuous,
Integer,
Boolean,
Enum,
};
/** A field deliberately kept out of the parameter roll after the measured liveness audit. */
struct VOXELFORGE_API FVoxelStrateFieldExclusion
{
FString FieldName;
FString Reason;
};
/** Measured statistics for one field in one archetype's corpus group. */
struct VOXELFORGE_API FVoxelStrateFieldSpread
{
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FString ParamStructName;
FString FieldName;
EVoxelStrateFieldKind Kind = EVoxelStrateFieldKind::Continuous;
bool bExcluded = false;
bool bReflected = false;
// Actual authored corpus samples and synthetic per-operation default samples are kept
// separate in the report. SampleCount is their effective total used for the statistics.
int32 AuthoredSampleCount = 0;
int32 PromotedSampleCount = 0;
int32 DefaultSeedCount = 0;
int32 SampleCount = 0;
double Min = 0.0;
double Max = 0.0;
double Mean = 0.0;
double StdDev = 0.0;
// These are copied from UPROPERTY ClampMin/ClampMax when reflection metadata is available in
// the current build. They are safety limits, never the source of a roll range.
bool bHasClampMin = false;
bool bHasClampMax = false;
double ClampMin = 0.0;
double ClampMax = 0.0;
};
/** Where a member of the offline corpus came from. */
enum class EVoxelStrateCorpusProvenance : uint8
{
Project,
Default,
Promoted,
};
/** The finite, diffable metric summary stored with a promoted strate. */
struct VOXELFORGE_API FVoxelStrateMeasuredMetrics
{
bool bValid = false;
int64 NumSampled = 0;
int64 NumAir = 0;
int64 NumSolid = 0;
float AirFraction = 0.0f;
int32 NumAirComponents = 0;
float LargestComponentShare = 0.0f;
FVector LargestComponentPoint = FVector::ZeroVector;
int64 LargestComponentCells = 0;
int32 NumComponentsAtLeast1Pct = 0;
float WalkableFraction = 0.0f;
int64 WalkableFloorColumns = 0;
float WalkableFloorAreaFraction = 0.0f;
int32 NumWalkableSurfaceComponents = 0;
int64 LargestWalkableSurfaceColumns = 0;
float LargestWalkableSurfaceShare = 0.0f;
float MedianFeatureScale = 0.0f;
int32 MedianVerticalClearance = 0;
bool bPlayerFitResolved = false;
FString PlayerFitRefusalReason;
int64 NumPlayerFitCells = 0;
float PlayerFitFraction = 0.0f;
int32 NumTraversableComponents = 0;
int64 LargestTraversableComponentCells = 0;
float TraversableComponentShare = 0.0f;
float MinimumPlayerClearanceVoxels = 0.0f;
int32 ResolvedMarginVoxels = 0;
int32 SampledMinZ = 0;
int32 SampledMaxZ = 0;
int32 SampledNumX = 0;
int32 SampledNumY = 0;
int32 SampledNumZ = 0;
float SampledMinX = 0.0f;
float SampledMaxX = 0.0f;
float SampledMinY = 0.0f;
float SampledMaxY = 0.0f;
// This is the component-size audit trail, not a retained density grid. It is bounded by the
// number of discovered air components and is useful when reviewing a promoted record.
TArray<int64> AirComponentCells;
bool IsUsable() const;
};
/** Copy the measured facts that are safe and useful to persist; no sampler or grid is retained. */
VOXELFORGE_API FVoxelStrateMeasuredMetrics VF_SummarizeStrateMetrics(
const FVoxelStrateMetrics& Metrics);
/** Native parameter storage for every currently supported strate archetype. */
struct VOXELFORGE_API FVoxelStrateArchetypeParams
{
FStrateGenerationParams TunnelNetworkParams;
FSlabGenerationParams SlabParams;
FMazeGenerationParams MazeParams;
FSurfaceGenerationParams SurfaceParams;
FVerticalShaftParams VerticalShaftParams;
FFloatingIslandParams FloatingIslandParams;
};
/**
* One lateral region in a strate.
*
* This is deliberately a plain manifest record rather than a label on the strate. The recipe
* and the native vector belong to the region, and the runtime combines the resulting density
* fields. Structural posts are not region-owned.
*/
struct VOXELFORGE_API FVoxelStrateRegion
{
int32 RegionIndex = 0;
int32 Seed = 0;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams ArchetypeParams;
bool bUsesRecipe = false;
FVoxelOpStackRecipe Recipe;
};
/**
* Offline-composed lateral region manifest for one strate.
*
* `StrateIndex` is part of the partition identity. A candidate can be moved to another live
* slot, so the editor hand-off must call VF_RekeyStrateRegionManifest before installing it.
* `PartitionSeed` is retained in the manifest so the runtime never has to recreate an RNG stream.
*/
struct VOXELFORGE_API FVoxelStrateRegionManifest
{
bool bValid = false;
FString FailureReason;
int32 Seed = 0;
int32 StrateIndex = 0;
int32 RegionCount = 1;
uint32 PartitionSeed = 0;
// A 32-voxel chunk sees a tiny portion of this lattice. The values are manifest data, not
// hidden runtime tuning knobs; changing them changes the authored world and its hash.
float LatticeCellSize = 256.0f;
float BlendWidth = 24.0f;
// One global structural post configuration. Region recipes may use different native blocks,
// but no region is allowed to choose the spine/seal/passage/edge-seal values independently.
bool bHasGlobalStructuralParams = false;
EVoxelStrateParamBlock StructuralParamBlock = EVoxelStrateParamBlock::None;
float StrateTopWorldZ = 0.0f;
float StrateBottomWorldZ = 0.0f;
float BoundarySealThickness = 0.0f;
float BaseDensity = 8.0f;
TArray<FVoxelStrateRegion> Regions;
bool IsSingleRegion() const { return RegionCount == 1 && Regions.Num() == 1; }
bool IsValid() const
{
return bValid && RegionCount >= 1 && RegionCount <= 3
&& Regions.Num() == RegionCount
&& FMath::IsFinite(LatticeCellSize) && LatticeCellSize > 0.0f
&& FMath::IsFinite(BlendWidth) && BlendWidth >= 0.0f;
}
};
/** Result of the positional Voronoi query used by the lateral density combiner. */
struct VOXELFORGE_API FVoxelStrateRegionQuery
{
int32 PrimaryRegion = 0;
int32 NeighborRegion = INDEX_NONE;
float NeighborWeight = 0.0f;
float NearestDifferentRegionGap = FLT_MAX;
// False means the bounded lattice neighborhood could not prove that its different-region
// candidate is the nearest one. Density evaluation may still use a zero blend weight, but a
// ClassifyBox proof must refuse to call that an infinite boundary distance.
bool bNearestDifferentRegionKnown = false;
bool bInBlendBand = false;
};
/** Conservative XY proof used before a tile may be skipped. */
struct VOXELFORGE_API FVoxelStrateRegionBoxProof
{
bool bProvablySingleRegion = false;
bool bTouchesRegionBoundary = true;
bool bTouchesBlendBand = true;
};
/** One hash-lattice site retained by the per-chunk partition cache. */
struct VOXELFORGE_API FVoxelStrateRegionSite
{
int32 CellX = 0;
int32 CellY = 0;
int32 Region = 0;
float WorldX = 0.0f;
float WorldY = 0.0f;
};
/**
* Worker-local cache for the lateral partition.
*
* Hash/site discovery and the integer XY grid are prepared once for a chunk. The voxel path
* then does an O(1) lookup for the ordinary integer density samples; only fractional gradient
* probes use the small prepared site list. No seed lattice is searched or allocated per voxel.
*/
struct VOXELFORGE_API FVoxelStrateRegionPartitionCache
{
void PrepareForChunk(const FVoxelStrateRegionManifest& Manifest,
const FIntVector& ChunkCoord);
FVoxelStrateRegionQuery Query(float WorldX, float WorldY) const;
private:
int32 RegionCount = 1;
uint32 PartitionSeed = 0;
float LatticeCellSize = 256.0f;
float BlendWidth = 24.0f;
int32 BaseX = 0;
int32 BaseY = 0;
int32 Dim = 0;
TArray<FVoxelStrateRegionSite> Sites;
TArray<FVoxelStrateRegionQuery> IntegerSamples;
};
/** Set every native family’s runtime Z bounds to the same strate interval. */
VOXELFORGE_API void VF_SetStrateArchetypeRuntimeBounds(
FVoxelStrateArchetypeParams& Params, float TopWorldZ, float BottomWorldZ);
/** Stable identity helpers for the deterministic lateral partition. */
#if WITH_EDITOR
VOXELFORGE_API int32 VF_RollStrateRegionCount(int32 Seed, int32 StrateIndex);
#endif
VOXELFORGE_API uint32 VF_GetStrateRegionPartitionSeed(int32 Seed, int32 StrateIndex);
VOXELFORGE_API void VF_RekeyStrateRegionManifest(
FVoxelStrateRegionManifest& Manifest, int32 Seed, int32 StrateIndex);
/** Pure positional query; no retained state and no ordering-dependent RNG. */
VOXELFORGE_API FVoxelStrateRegionQuery VF_QueryStrateRegion(
const FVoxelStrateRegionManifest& Manifest, float WorldX, float WorldY);
/** Conservative proof for an XY projection of a voxel box. */
VOXELFORGE_API FVoxelStrateRegionBoxProof VF_AnalyzeStrateRegionBox(
const FVoxelStrateRegionManifest& Manifest, const FBox& VoxelBox);
/** One known-good authored vector and the archetype that gives it meaning. */
struct VOXELFORGE_API FVoxelStrateCorpusEntry
{
FString SourcePath;
FString SourceName;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
// Native storage is selected by Archetype. Keeping every family here makes the corpus
// type-safe without changing any runtime generation parameter struct.
FVoxelStrateArchetypeParams ArchetypeParams;
// Compatibility view for callers of the original Tier 4a API. It is meaningful for
// TunnelNetwork/Underwater entries and is the same value as ArchetypeParams.TunnelNetworkParams.
FStrateGenerationParams Params;
// The current asset format has no corpus weight field. Loaded hand-authored entries therefore
// use 1.0. The field remains explicit so a future offline corpus can assign weights without
// changing the roll algorithm.
float Weight = 1.0f;
EVoxelStrateCorpusProvenance Provenance = EVoxelStrateCorpusProvenance::Project;
// Promoted entries retain their recipe and re-measured facts so the next policy decision can
// compare measured geometry, not only parameter-space distance. Project/default entries leave
// these fields unset unless an offline simulation explicitly measures them.
bool bHasRecipe = false;
FVoxelOpStackRecipe Recipe;
bool bHasMeasuredMetrics = false;
FVoxelStrateMeasuredMetrics MeasuredMetrics;
FString PromotionRecordId;
int32 PromotionSeason = INDEX_NONE;
int32 PromotionSeed = 0;
int32 PromotionCandidateIndex = INDEX_NONE;
uint32 PromotionInputCorpusHash = 0;
};
/** A candidate record that is eligible to be considered for corpus promotion. */
struct VOXELFORGE_API FVoxelStratePromotableRecord
{
FString RecordId;
int32 Season = 0;
int32 Seed = 0;
int32 CandidateIndex = INDEX_NONE;
uint32 InputCorpusHash = 0;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams Params;
FVoxelOpStackRecipe Recipe;
FVoxelStrateMeasuredMetrics MeasuredMetrics;
// These are recorded evidence for review. The loader deliberately ignores the stored metric
// and gate values and asks its verifier to rebuild/re-measure the recipe instead.
bool bPassedNonVacuous = false;
bool bPassedLargestComponent = false;
bool bPassedPrimordialLaw = false;
};
/** Fresh validation output used by the safe promoted-record loader. */
struct VOXELFORGE_API FVoxelStratePromotionVerification
{
bool bPassedNonVacuous = false;
bool bPassedLargestComponent = false;
bool bPassedPrimordialLaw = false;
FVoxelStrateMeasuredMetrics MeasuredMetrics;
FString FailureReason;
bool PassedAllGates() const
{
return bPassedNonVacuous && bPassedLargestComponent && bPassedPrimordialLaw
&& MeasuredMetrics.IsUsable();
}
};
/**
* World-specific re-measurement seam for promoted records.
*
* A corpus store cannot know the active fixture, mouths, or validation window. Its caller must
* provide this verifier; a null verifier means promoted records are skipped rather than trusted.
*/
class VOXELFORGE_API IVoxelStratePromotionVerifier
{
public:
virtual ~IVoxelStratePromotionVerifier() = default;
virtual bool Verify(const FVoxelStratePromotableRecord& Record,
FVoxelStratePromotionVerification& OutVerification) const = 0;
// Optional companion audit for project/default members. A policy pass can then compare a
// candidate against the measured corpus rather than falling back to parameter identity. A
// world-specific verifier may return false when that fixture cannot measure the member.
virtual bool MeasureCorpusEntry(const FVoxelStrateCorpusEntry& Entry,
FVoxelStrateMeasuredMetrics& OutMetrics) const
{
(void)Entry;
(void)OutMetrics;
return false;
}
};
/** Explicit, provisional Tier 5 policy. The owner can change these after seeing the preview. */
struct VOXELFORGE_API FVoxelStratePromotionPolicy
{
// Five normalized measured dimensions are compared with Euclidean distance. 0.20 is large
// enough to reject close copies while still admitting genuinely different geometry; it is a
// provisional review threshold, not a claim that this is the owner's final definition of good.
double MinimumNormalizedMeasuredMetricDistance = 0.20;
int32 MaxPromotionsPerSeason = 6;
// Physical normalization for the two unbounded-in-principle metric dimensions. Air fraction,
// largest share, and walkable fraction already live in [0,1].
float FeatureScaleNormalizationVoxels = 256.0f;
float ClearanceNormalizationVoxels = 64.0f;
};
/** Counters and accepted records from one deterministic policy pass. */
struct VOXELFORGE_API FVoxelStratePromotionBatchResult
{
bool bPolicyValid = false;
TArray<FVoxelStratePromotableRecord> Promoted;
int32 NumCandidates = 0;
int32 NumValidationRejected = 0;
int32 NumCorpusHashRejected = 0;
int32 NumNearDuplicateRejected = 0;
int32 NumCapRejected = 0;
double MinimumAcceptedDistance = 0.0;
};
/** One line of the diffable season manifest written beside the cumulative record store. */
struct VOXELFORGE_API FVoxelStrateSeasonManifest
{
int32 Season = 0;
int32 Seed = 0;
uint32 InputCorpusHash = 0;
int32 CandidateCount = 0;
int32 SurvivorCount = 0;
int32 PromotedCount = 0;
int32 CorpusSize = 0;
int32 ProjectCount = 0;
int32 DefaultCount = 0;
int32 PromotedCorpusCount = 0;
float SurvivalRate = 0.0f;
double MeasuredSpread = 0.0;
};
/** Result of one deterministic roll, including provenance for the offline report. */
struct VOXELFORGE_API FVoxelStrateRollInfo
{
bool bValid = false;
FString FailureReason;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams ArchetypeParams;
// Compatibility view for the original tunnel-only API. For a non-tunnel roll, the native
// result is in ArchetypeParams and this member is not the active candidate vector.
FStrateGenerationParams Params;
TArray<int32> ParentEntryIndices;
TArray<float> ParentWeights;
int32 DominantParentPosition = INDEX_NONE;
// Provenance for the starved-corpus fallback. Names are keyed as "archetype:field" so a
// report can distinguish field descriptors from the number of times they were applied.
int32 BootstrapJitterFieldCount = 0;
TArray<FString> BootstrapJitterFieldNames;
};
#if WITH_EDITOR
/**
* One candidate materialised by the same deterministic calls used by the offline composer tests.
* This is an editor-only hand-off object: it is never a reflected asset and never part of the
* shipping generation contract.
*/
struct VOXELFORGE_API FVoxelStrateComposerCandidate
{
bool bValid = false;
FString FailureReason;
int32 Seed = 0;
int32 Index = 0;
bool bStructureRoll = false;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams ArchetypeParams;
FVoxelOpStackRecipe Recipe;
FVoxelStrateRegionManifest Regions;
// Kept so automation/editor callers can audit the exact six independent structure blocks
// without re-rolling them through a second implementation.
FVoxelStrateRollInfo ParameterRoll;
TArray<FVoxelStrateRollInfo> StructureBlockRolls;
};
#endif
/**
* Known-good strate vectors plus their measured field spread.
*
* `LoadFromAssetRegistry` enumerates every project UVoxelStrateDefinition through the Asset
* Registry, explicitly excluding Saved/Autosaves and Saved/Cooked copies. It also adds one
* known-good default vector for every archetype/family in VoxelStrateTypes.h, then optionally
* loads the cumulative promoted-record JSON store. Promoted records enter only after the caller's
* world-specific verifier rebuilds and remeasures them; with no verifier they are skipped safely.
* Parents are selected only inside the exact archetype group; no cross-archetype blend is attempted.
*/
class VOXELFORGE_API FVoxelStrateCorpus
{
public:
void Reset();
bool LoadFromAssetRegistry(
FString& OutReport,
const FString& PromotedStorePath = FString(),
const IVoxelStratePromotionVerifier* PromotionVerifier = nullptr);
bool LoadFromDefinitions(const TArray<UVoxelStrateDefinition*>& Definitions, FString& OutReport);
/** Add an already-resolved vector, primarily for offline tools and focused tests. */
bool AddEntry(const FString& SourcePath, const FString& SourceName,
ECaveGeneratorType Archetype, const FStrateGenerationParams& Params,
float Weight = 1.0f);
bool AddEntry(const FString& SourcePath, const FString& SourceName,
ECaveGeneratorType Archetype, const FVoxelStrateArchetypeParams& Params,
float Weight = 1.0f);
/** Load cumulative promoted records, re-verifying every record before it enters Entries. */
bool LoadPromotedRecords(const FString& StorePath,
const IVoxelStratePromotionVerifier* PromotionVerifier,
FString& OutReport);
/** Attach an offline measurement to an existing member without changing corpus identity. */
bool SetMeasuredMetrics(const FString& SourcePath,
const FVoxelStrateMeasuredMetrics& Metrics);
bool IsValid() const { return bSchemaValid && Entries.Num() > 0; }
bool IsSchemaValid() const { return bSchemaValid; }
const FString& GetSchemaError() const { return SchemaError; }
int32 Num() const { return Entries.Num(); }
const TArray<FVoxelStrateCorpusEntry>& GetEntries() const { return Entries; }
const TArray<FVoxelStrateFieldSpread>& GetFieldSpreads() const { return FieldSpreads; }
int32 NumForArchetype(ECaveGeneratorType Archetype) const;
int32 NumForProvenance(EVoxelStrateCorpusProvenance Provenance) const;
const FVoxelStrateFieldSpread* FindSpread(
ECaveGeneratorType Archetype, const FString& FieldName) const;
const FVoxelStrateFieldSpread* FindSpread(const FString& FieldName) const;
/** Stable hash of source paths, archetypes, weights, and every listed scalar corpus field. */
uint32 GetContentsHash() const;
/** Explicit exclusion list retained after the fixed-lattice terrain-detail liveness audit. */
static const TArray<FVoxelStrateFieldExclusion>& GetNonTunableFields();
/** True in an editor build where UPROPERTY metadata was available while spreads were built. */
bool AreClampMetadataAvailableAtBuild() const { return bClampMetadataAvailableAtBuild; }
/** False by policy: metadata is not promised to survive into a cooked runtime. */
bool AreClampMetadataAvailableAtRuntime() const { return false; }
private:
bool AddEntryInternal(const FString& SourcePath, const FString& SourceName,
ECaveGeneratorType Archetype,
const FVoxelStrateArchetypeParams& Params,
float Weight, EVoxelStrateCorpusProvenance Provenance,
const FVoxelOpStackRecipe* Recipe,
const FVoxelStrateMeasuredMetrics* Metrics,
const FString& PromotionRecordId,
int32 PromotionSeason,
int32 PromotionSeed,
int32 PromotionCandidateIndex,
uint32 PromotionInputCorpusHash,
bool bRebuild);
void RebuildSpreads();
TArray<FVoxelStrateCorpusEntry> Entries;
TArray<FVoxelStrateFieldSpread> FieldSpreads;
TArray<FString> SkippedDefinitions;
FString SchemaError;
bool bSchemaValid = true;
bool bClampMetadataAvailableAtBuild = false;
};
/** Human-readable name used by the offline report. */
VOXELFORGE_API const TCHAR* VF_GetStrateArchetypeName(ECaveGeneratorType Archetype);
VOXELFORGE_API const TCHAR* VF_GetStrateCorpusProvenanceName(
EVoxelStrateCorpusProvenance Provenance);
/** Full provenance result. Pure with respect to the corpus: no global or retained RNG state. */
VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailed(
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 Index);
/** Roll one exact native family, used to feed the independent blocks of a novel recipe. */
VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailedForArchetype(
const FVoxelStrateCorpus& Corpus, ECaveGeneratorType Archetype, int32 Seed, int32 Index);
/** Pure structure roll: no retained RNG state and no corpus/runtime dependency. */
VOXELFORGE_API FVoxelOpStackRecipe VF_RollStrateStructure(int32 Seed, int32 Index);
/** Roll one-to-three independent region recipes/vectors plus their deterministic partition. */
VOXELFORGE_API FVoxelStrateRegionManifest VF_RollStrateRegionManifest(
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 StrateIndex, bool bRollStructure);
/**
* ⛔ THE LATERAL-REGION GATE. Returns false, and must keep returning false until the seam law holds.
*
* Lateral regions (archetypes mixing inside one strate — "tunnel network leading to a big chamber")
* are BUILT and MEASURED but NOT SHIPPABLE. Measured 2026-09-05: `arrival -> departure` across a
* region seam passes **9 of 16 seeds**, against 11/16 for same-region controls in the same harness.
*
* Density blending alone does not guarantee a player can cross a seam. A strate that mixes archetypes
* and strands you at the boundary is strictly worse than one that does not mix — so this stays off.
*
* ⚠️ DO NOT flip this to true to "enable the feature". The fix is an explicit corridor/landing
* contract across seams (a design decision, deliberately not made unilaterally), after which the
* measured rate must be 16/16. `VoxelForge.Composer.LateralRegions` asserts that this gate is shut
* whenever the rate is imperfect, so flipping it without fixing the law turns the suite red.
*
* Safe today regardless: single-region output is bit-identical to legacy, and cross-boundary box
* verdicts returned 0 violations over 199,800 brute-forced samples (all correctly `Mixed`).
*/
VOXELFORGE_API bool VF_LateralRegionsAreShippable();
/**
* Provenance for a parameter block that does not read the authored corpus.
*
* NaiveUniform is the §3.3 control: every scalar is rolled independently inside the same
* finite code-declared envelope used by the constraint arm. ConstraintSampled draws the
* independent geometry quantities and derives the coupled quantities from generator equations.
* Neither mode reads a strate asset or a corpus entry.
*/
enum class EVoxelStrateCorpusFreeSamplingMode : uint8
{
NaiveUniform,
ConstraintSampled,
};
/** Roll one native parameter family without consulting FVoxelStrateCorpus. */
VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsCorpusFree(
ECaveGeneratorType Archetype,
int32 Seed,
int32 Index,
EVoxelStrateCorpusFreeSamplingMode Mode,
float StrateHeightInVoxels);
/** Validate the hard geometry relations used by ConstraintSampled and the corpus audit. */
VOXELFORGE_API bool VF_ValidateStrateCorpusFreeConstraints(
ECaveGeneratorType Archetype,
const FVoxelStrateArchetypeParams& Params,
float StrateHeightInVoxels,
FString& OutViolation);
/** Normalized Euclidean distance in the five policy metric dimensions. */
VOXELFORGE_API double VF_NormalizedMeasuredMetricDistance(
const FVoxelStrateMeasuredMetrics& A,
const FVoxelStrateMeasuredMetrics& B,
const FVoxelStratePromotionPolicy& Policy);
/** Select deterministic, validated, varied records for one season. */
VOXELFORGE_API FVoxelStratePromotionBatchResult VF_SelectStratePromotions(
const FVoxelStrateCorpus& Corpus,
const TArray<FVoxelStratePromotableRecord>& Candidates,
const FVoxelStratePromotionPolicy& Policy);
/** Write/readable cumulative JSON store; records are sorted by RecordId before writing. */
VOXELFORGE_API bool VF_SaveStratePromotedRecords(
const FString& StorePath,
const TArray<FVoxelStratePromotableRecord>& Records,
FString& OutReport);
/** Write the matching season summary beside the promoted-record store. */
VOXELFORGE_API bool VF_SaveStrateSeasonManifest(
const FString& ManifestPath,
const FVoxelStrateSeasonManifest& Manifest,
FString& OutReport);
#if WITH_EDITOR
/**
* Roll one candidate for the editor walk-through path. Parameter rolls call
* VF_RollStrateParamsDetailed; structure rolls call VF_RollStrateStructure and the exact six
* VF_RollStrateParamsDetailedForArchetype block rolls used by the structure test.
*/
VOXELFORGE_API FVoxelStrateComposerCandidate VF_RollStrateCandidate(
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 Index, bool bRollStructure);
/**
* Build a parameter-roll candidate through the same archetype-to-stack mapping used by production
* GetDensityAt/ClassifyTile. This is editor-only so the offline composer remains absent from a
* shipping build; the result is used only for the post-apply density sanity check.
*/
VOXELFORGE_API bool VF_BuildNativeStrateStackForCandidate(
ECaveGeneratorType Archetype,
const FVoxelStrateArchetypeParams& Params,
int32 Seed,
float SpineRadius,
float WorldRadiusVoxels,
float EdgeSealThickness,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext);
/** Build the persisted record from the exact editor candidate and its just-measured gate facts. */
VOXELFORGE_API FVoxelStratePromotableRecord VF_MakeStratePromotableRecord(
const FVoxelStrateComposerCandidate& Candidate,
const FVoxelStrateMetrics& Metrics,
int32 Season,
uint32 InputCorpusHash,
bool bPassedNonVacuous,
bool bPassedLargestComponent,
bool bPassedPrimordialLaw);
#endif
/** Stable compact representation and equality/hash helpers for manifests and reports. */
VOXELFORGE_API FString VF_FormatStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe);
VOXELFORGE_API uint32 VF_HashStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe);
VOXELFORGE_API bool VF_AreStrateStructureRecipesIdentical(
const FVoxelOpStackRecipe& A, const FVoxelOpStackRecipe& B);
/** Bitwise comparison over exactly the fields in FStrateGenerationParams::Lerp. */
VOXELFORGE_API bool VF_AreStrateParamsBitIdentical(
const FStrateGenerationParams& A, const FStrateGenerationParams& B);
/** Bitwise comparison of the native vector for one archetype family. */
VOXELFORGE_API bool VF_AreStrateArchetypeParamsBitIdentical(
const FVoxelStrateArchetypeParams& A,
const FVoxelStrateArchetypeParams& B,
ECaveGeneratorType Archetype);
/**
* Normalised distance from the measured centroid of one exact-archetype corpus group.
*
* This is an offline ordering aid for the preview only. Continuous/integer fields are measured
* in their corpus units; zero-spread fields use a conservative scale floor so the value remains
* finite without turning clamps into roll ranges. It never participates in generation.
*/
VOXELFORGE_API double VF_DistanceFromStrateCorpusCentroid(
const FVoxelStrateCorpus& Corpus,
const FVoxelStrateArchetypeParams& Params,
ECaveGeneratorType Archetype);
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff. Show more