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:
1 parent
a443eb8546
commit
05f62cff36
114 files changed
+68206
-3
No files matched your search
@@ -9,6 +9,12 @@
|
||||
!/Source/
|
||||
!/Source/**
|
||||
|
||||
# Portable CPU generator and D3D11 viewer.
|
||||
!/Standalone/
|
||||
!/Standalone/**
|
||||
/Standalone/Build/
|
||||
/Standalone/bin/
|
||||
|
||||
# The guarded single-entry test harness is source, not generated output.
|
||||
!/Tools/
|
||||
!/Tools/**
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "UObject/Package.h"
|
||||
#include "UObject/StrongObjectPtr.h"
|
||||
#include "UObject/UnrealType.h"
|
||||
#include "UObject/TextProperty.h"
|
||||
#include "UObject/UObjectGlobals.h"
|
||||
#include "VoxelCaveMorphology.h"
|
||||
#include "VoxelDiffLayer.h"
|
||||
@@ -38,11 +40,13 @@
|
||||
#include "VoxelNoise.h"
|
||||
#include "VoxelPassageGeometry.h"
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelSeasonAsset.h"
|
||||
#include "VoxelStackSampler.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelStrateManager.h"
|
||||
#include "VoxelStrateMeasure.h"
|
||||
#include "VoxelStrateComposer.h"
|
||||
#include "VoxelTerrainOpDefinition.h"
|
||||
#include "VoxelTypes.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -379,6 +383,7 @@ struct FExploreArguments
|
||||
float RoomMouthRiseBlend = 0.0f;
|
||||
bool bPassageFloorWritesOverride = false;
|
||||
int32 PassageFloorWrites = 1;
|
||||
bool bStandaloneWorldExport = false;
|
||||
bool bRenderCameraOverride = false;
|
||||
FVector RenderCamera = FVector::ZeroVector;
|
||||
bool bRenderTargetOverride = false;
|
||||
@@ -418,6 +423,9 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
FString OutText;
|
||||
|
||||
OutArguments.bSeedOverride = FParse::Value(*Params, TEXT("seed="), OutArguments.Seed);
|
||||
int32 StandaloneWorldExport = 0;
|
||||
FParse::Value(*Params, TEXT("standaloneworldexport="), StandaloneWorldExport);
|
||||
OutArguments.bStandaloneWorldExport = StandaloneWorldExport != 0;
|
||||
FParse::Value(*Params, TEXT("archetype="), ArchetypeText);
|
||||
FParse::Value(*Params, TEXT("slot="), OutArguments.Slot);
|
||||
FParse::Value(*Params, TEXT("strateref="), OutArguments.StrateReference);
|
||||
@@ -939,6 +947,13 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
"densityslice must be origin x,y,z, direction x,y,z, sample count, and positive step.");
|
||||
return false;
|
||||
}
|
||||
if (OutArguments.bStandaloneWorldExport
|
||||
&& (!OutArguments.bExport || OutArguments.SettingsReference.IsEmpty()
|
||||
|| !OutArguments.StrateReference.IsEmpty()))
|
||||
{
|
||||
OutError = TEXT("standaloneworldexport requires modes=export and settingsref, and must omit strateref.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (OutText.IsEmpty())
|
||||
{
|
||||
@@ -1572,6 +1587,7 @@ bool PrepareRegionEvalWorld(
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!World.Definitions.IsValidIndex(Arguments.Slot))
|
||||
{
|
||||
OutError = TEXT("Region evaluator target definition is missing.");
|
||||
@@ -12196,6 +12212,525 @@ bool RunOpBounds(
|
||||
return true;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonValue> StandaloneSerializeStruct(
|
||||
const UStruct* Type, const void* Data, TSet<const UObject*>& ActiveObjects,
|
||||
const UObject* Object = nullptr);
|
||||
|
||||
TSharedPtr<FJsonValue> StandaloneSerializeProperty(
|
||||
FProperty* Property, const void* Data, TSet<const UObject*>& ActiveObjects)
|
||||
{
|
||||
if (Property == nullptr || Data == nullptr) return MakeShared<FJsonValueNull>();
|
||||
if (const FBoolProperty* Bool = CastField<FBoolProperty>(Property))
|
||||
{
|
||||
return MakeShared<FJsonValueBoolean>(Bool->GetPropertyValue(Data));
|
||||
}
|
||||
if (const FEnumProperty* Enum = CastField<FEnumProperty>(Property))
|
||||
{
|
||||
return MakeShared<FJsonValueNumber>(static_cast<double>(
|
||||
Enum->GetUnderlyingProperty()->GetSignedIntPropertyValue(Data)));
|
||||
}
|
||||
if (const FNumericProperty* Numeric = CastField<FNumericProperty>(Property))
|
||||
{
|
||||
const double Value = Numeric->IsFloatingPoint()
|
||||
? Numeric->GetFloatingPointPropertyValue(Data)
|
||||
: static_cast<double>(Numeric->GetSignedIntPropertyValue(Data));
|
||||
return MakeShared<FJsonValueNumber>(Value);
|
||||
}
|
||||
if (const FStrProperty* String = CastField<FStrProperty>(Property))
|
||||
{
|
||||
return MakeShared<FJsonValueString>(String->GetPropertyValue(Data));
|
||||
}
|
||||
if (const FNameProperty* Name = CastField<FNameProperty>(Property))
|
||||
{
|
||||
return MakeShared<FJsonValueString>(Name->GetPropertyValue(Data).ToString());
|
||||
}
|
||||
if (const FTextProperty* Text = CastField<FTextProperty>(Property))
|
||||
{
|
||||
return MakeShared<FJsonValueString>(Text->GetPropertyValue(Data).ToString());
|
||||
}
|
||||
if (const FSoftObjectProperty* Soft = CastField<FSoftObjectProperty>(Property))
|
||||
{
|
||||
return MakeShared<FJsonValueString>(
|
||||
Soft->GetPropertyValue(Data).ToSoftObjectPath().ToString());
|
||||
}
|
||||
if (const FArrayProperty* Array = CastField<FArrayProperty>(Property))
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Values;
|
||||
FScriptArrayHelper Helper(Array, Data);
|
||||
Values.Reserve(Helper.Num());
|
||||
for (int32 Index = 0; Index < Helper.Num(); ++Index)
|
||||
{
|
||||
Values.Add(StandaloneSerializeProperty(
|
||||
Array->Inner, Helper.GetRawPtr(Index), ActiveObjects));
|
||||
}
|
||||
return MakeShared<FJsonValueArray>(MoveTemp(Values));
|
||||
}
|
||||
if (const FSetProperty* Set = CastField<FSetProperty>(Property))
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Values;
|
||||
FScriptSetHelper Helper(Set, Data);
|
||||
for (int32 Index = 0; Index < Helper.GetMaxIndex(); ++Index)
|
||||
{
|
||||
if (Helper.IsValidIndex(Index))
|
||||
{
|
||||
Values.Add(StandaloneSerializeProperty(
|
||||
Set->ElementProp, Helper.GetElementPtr(Index), ActiveObjects));
|
||||
}
|
||||
}
|
||||
return MakeShared<FJsonValueArray>(MoveTemp(Values));
|
||||
}
|
||||
if (const FMapProperty* Map = CastField<FMapProperty>(Property))
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Values;
|
||||
FScriptMapHelper Helper(Map, Data);
|
||||
for (int32 Index = 0; Index < Helper.GetMaxIndex(); ++Index)
|
||||
{
|
||||
if (!Helper.IsValidIndex(Index)) continue;
|
||||
TSharedRef<FJsonObject> Pair = MakeShared<FJsonObject>();
|
||||
Pair->SetField(TEXT("key"), StandaloneSerializeProperty(
|
||||
Map->KeyProp, Helper.GetKeyPtr(Index), ActiveObjects));
|
||||
Pair->SetField(TEXT("value"), StandaloneSerializeProperty(
|
||||
Map->ValueProp, Helper.GetValuePtr(Index), ActiveObjects));
|
||||
Values.Add(MakeShared<FJsonValueObject>(Pair));
|
||||
}
|
||||
return MakeShared<FJsonValueArray>(MoveTemp(Values));
|
||||
}
|
||||
if (const FStructProperty* Struct = CastField<FStructProperty>(Property))
|
||||
{
|
||||
return StandaloneSerializeStruct(Struct->Struct, Data, ActiveObjects);
|
||||
}
|
||||
if (const FObjectPropertyBase* ObjectProperty = CastField<FObjectPropertyBase>(Property))
|
||||
{
|
||||
const UObject* Object = ObjectProperty->GetObjectPropertyValue(Data);
|
||||
if (!Object) return MakeShared<FJsonValueNull>();
|
||||
if (Object->GetClass()->GetName().StartsWith(TEXT("Voxel"), ESearchCase::CaseSensitive))
|
||||
{
|
||||
return StandaloneSerializeStruct(Object->GetClass(), Object, ActiveObjects, Object);
|
||||
}
|
||||
return MakeShared<FJsonValueString>(Object->GetPathName());
|
||||
}
|
||||
return MakeShared<FJsonValueNull>();
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonValue> StandaloneSerializeStruct(
|
||||
const UStruct* Type, const void* Data, TSet<const UObject*>& ActiveObjects,
|
||||
const UObject* Object)
|
||||
{
|
||||
if (Type == nullptr || Data == nullptr) return MakeShared<FJsonValueNull>();
|
||||
if (Object && ActiveObjects.Contains(Object))
|
||||
{
|
||||
return MakeShared<FJsonValueString>(Object->GetPathName());
|
||||
}
|
||||
if (Object) ActiveObjects.Add(Object);
|
||||
|
||||
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
||||
for (TFieldIterator<FProperty> It(Type); It; ++It)
|
||||
{
|
||||
FProperty* Property = *It;
|
||||
Result->SetField(Property->GetName(), StandaloneSerializeProperty(
|
||||
Property, Property->ContainerPtrToValuePtr<void>(const_cast<void*>(Data)),
|
||||
ActiveObjects));
|
||||
}
|
||||
if (Object) ActiveObjects.Remove(Object);
|
||||
return MakeShared<FJsonValueObject>(Result);
|
||||
}
|
||||
|
||||
template<class TWriter>
|
||||
void StandaloneWriteJsonValue(
|
||||
TWriter& Writer, const TCHAR* Identifier, const TSharedPtr<FJsonValue>& Value)
|
||||
{
|
||||
if (Identifier) Writer.WriteIdentifierPrefix(Identifier);
|
||||
if (!Value.IsValid() || Value->Type == EJson::Null)
|
||||
{
|
||||
Writer.WriteNull();
|
||||
}
|
||||
else if (Value->Type == EJson::Boolean)
|
||||
{
|
||||
Writer.WriteValue(Value->AsBool());
|
||||
}
|
||||
else if (Value->Type == EJson::Number)
|
||||
{
|
||||
Writer.WriteValue(Value->AsNumber());
|
||||
}
|
||||
else if (Value->Type == EJson::String)
|
||||
{
|
||||
Writer.WriteValue(Value->AsString());
|
||||
}
|
||||
else if (Value->Type == EJson::Array)
|
||||
{
|
||||
Writer.WriteArrayStart();
|
||||
for (const TSharedPtr<FJsonValue>& Element : Value->AsArray())
|
||||
{
|
||||
StandaloneWriteJsonValue(Writer, nullptr, Element);
|
||||
}
|
||||
Writer.WriteArrayEnd();
|
||||
}
|
||||
else if (Value->Type == EJson::Object)
|
||||
{
|
||||
Writer.WriteObjectStart();
|
||||
for (const TPair<FString, TSharedPtr<FJsonValue>>& Field : Value->AsObject()->Values)
|
||||
{
|
||||
StandaloneWriteJsonValue(Writer, *Field.Key, Field.Value);
|
||||
}
|
||||
Writer.WriteObjectEnd();
|
||||
}
|
||||
else
|
||||
{
|
||||
Writer.WriteNull();
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> StandaloneVectorJson(const FVector& Value)
|
||||
{
|
||||
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
||||
Result->SetNumberField(TEXT("x"), Value.X);
|
||||
Result->SetNumberField(TEXT("y"), Value.Y);
|
||||
Result->SetNumberField(TEXT("z"), Value.Z);
|
||||
return Result;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> StandaloneLandingJson(const FVoxelPassageLanding& Landing)
|
||||
{
|
||||
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
||||
Result->SetObjectField(TEXT("standing_point"), StandaloneVectorJson(Landing.StandingPoint));
|
||||
Result->SetObjectField(TEXT("door_point"), StandaloneVectorJson(Landing.DoorPoint));
|
||||
Result->SetObjectField(TEXT("door_direction"), StandaloneVectorJson(Landing.DoorDirection));
|
||||
Result->SetNumberField(TEXT("floor_z"), Landing.FloorZ);
|
||||
Result->SetNumberField(TEXT("ceiling_z"), Landing.CeilingZ);
|
||||
Result->SetNumberField(TEXT("half_width"), Landing.HalfWidth);
|
||||
Result->SetNumberField(TEXT("floor_thickness"), Landing.FloorThickness);
|
||||
Result->SetBoolField(TEXT("source_player_fit"), Landing.bSourcePlayerFit);
|
||||
return Result;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> StandalonePassageJson(const FVoxelPassage& Passage)
|
||||
{
|
||||
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
||||
Result->SetNumberField(TEXT("upper_strate_index"), Passage.UpperStrateIndex);
|
||||
Result->SetNumberField(TEXT("lower_strate_index"), Passage.LowerStrateIndex);
|
||||
Result->SetObjectField(TEXT("upper_point"), StandaloneVectorJson(Passage.UpperPoint));
|
||||
Result->SetObjectField(TEXT("lower_point"), StandaloneVectorJson(Passage.LowerPoint));
|
||||
Result->SetObjectField(TEXT("requested_upper_point"), StandaloneVectorJson(Passage.RequestedUpperPoint));
|
||||
Result->SetObjectField(TEXT("requested_lower_point"), StandaloneVectorJson(Passage.RequestedLowerPoint));
|
||||
Result->SetNumberField(TEXT("radius"), Passage.Radius);
|
||||
Result->SetNumberField(TEXT("passage_type"), static_cast<int32>(Passage.PassageType));
|
||||
auto WriteVectors = [](const TCHAR* Key, const TArray<FVector>& Source,
|
||||
TSharedRef<FJsonObject> Out)
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Values;
|
||||
Values.Reserve(Source.Num());
|
||||
for (const FVector& Value : Source)
|
||||
{
|
||||
Values.Add(MakeShared<FJsonValueObject>(StandaloneVectorJson(Value)));
|
||||
}
|
||||
Out->SetArrayField(Key, MoveTemp(Values));
|
||||
};
|
||||
auto WriteFloats = [](const TCHAR* Key, const TArray<float>& Source,
|
||||
TSharedRef<FJsonObject> Out)
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Values;
|
||||
Values.Reserve(Source.Num());
|
||||
for (float Value : Source) Values.Add(MakeShared<FJsonValueNumber>(Value));
|
||||
Out->SetArrayField(Key, MoveTemp(Values));
|
||||
};
|
||||
WriteVectors(TEXT("control_points"), Passage.ControlPoints, Result);
|
||||
WriteFloats(TEXT("control_radii"), Passage.ControlRadii, Result);
|
||||
Result->SetObjectField(TEXT("upper_landing"), StandaloneLandingJson(Passage.UpperLanding));
|
||||
Result->SetObjectField(TEXT("lower_landing"), StandaloneLandingJson(Passage.LowerLanding));
|
||||
Result->SetBoolField(TEXT("walkable_tunnel_contract"), Passage.bWalkableTunnelContract);
|
||||
Result->SetBoolField(TEXT("native_floor_enabled"), Passage.bNativeFloorEnabled);
|
||||
WriteFloats(TEXT("native_floor_profile_z"), Passage.NativeFloorProfileZ, Result);
|
||||
WriteFloats(TEXT("native_floor_relief_scales"), Passage.NativeFloorReliefScales, Result);
|
||||
Result->SetNumberField(TEXT("native_floor_relief_strength"), Passage.NativeFloorReliefStrength);
|
||||
Result->SetNumberField(TEXT("native_floor_relief_frequency"), Passage.NativeFloorReliefFrequency);
|
||||
Result->SetNumberField(TEXT("native_floor_seed"),
|
||||
static_cast<int64>(Passage.NativeFloorSeed));
|
||||
Result->SetNumberField(TEXT("tunnel_max_gradient_degrees"), Passage.TunnelMaxGradientDegrees);
|
||||
Result->SetNumberField(TEXT("tunnel_vertical_drop_voxels"), Passage.TunnelVerticalDropVoxels);
|
||||
Result->SetNumberField(TEXT("tunnel_required_horizontal_run_voxels"), Passage.TunnelRequiredHorizontalRunVoxels);
|
||||
Result->SetNumberField(TEXT("tunnel_horizontal_path_length_voxels"), Passage.TunnelHorizontalPathLengthVoxels);
|
||||
Result->SetNumberField(TEXT("tunnel_minimum_clear_width_voxels"), Passage.TunnelMinimumClearWidthVoxels);
|
||||
Result->SetNumberField(TEXT("tunnel_clear_height_voxels"), Passage.TunnelClearHeightVoxels);
|
||||
Result->SetObjectField(TEXT("bound_center"), StandaloneVectorJson(Passage.BoundCenter));
|
||||
Result->SetNumberField(TEXT("bound_radius"), Passage.BoundRadius);
|
||||
Result->SetNumberField(TEXT("bound_radius_sq"), Passage.BoundRadiusSq);
|
||||
return Result;
|
||||
}
|
||||
|
||||
struct FStandaloneExportSample
|
||||
{
|
||||
FVector Position = FVector::ZeroVector;
|
||||
FString Kind;
|
||||
int32 StrateIndex = INDEX_NONE;
|
||||
float Density = 0.0f;
|
||||
};
|
||||
|
||||
bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutError, FString* OutJson)
|
||||
{
|
||||
UVoxelSettings* AuthoredSettings = LoadObject<UVoxelSettings>(
|
||||
nullptr, *Arguments.SettingsReference);
|
||||
if (!AuthoredSettings)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Could not load owner settings asset '%s'."),
|
||||
*Arguments.SettingsReference);
|
||||
return false;
|
||||
}
|
||||
UVoxelStrateManager* Manager = NewObject<UVoxelStrateManager>(
|
||||
GetTransientPackage(), NAME_None, RF_Transient);
|
||||
int32 Seed = AuthoredSettings->Seed;
|
||||
float OriginSpineRadius = AuthoredSettings->OriginSpineRadius;
|
||||
float WorldRadiusVoxels = AuthoredSettings->WorldRadiusVoxels;
|
||||
if (const UVoxelSeasonAsset* Season = AuthoredSettings->Season.LoadSynchronous())
|
||||
{
|
||||
Seed = Season->GetSeasonSeed();
|
||||
OriginSpineRadius = Season->GetOriginSpineRadius();
|
||||
WorldRadiusVoxels = Season->GetWorldRadiusVoxels();
|
||||
}
|
||||
if (!Manager->Initialize(AuthoredSettings, Seed))
|
||||
{
|
||||
OutError = TEXT("Owner settings did not produce a valid resolved strate layout.");
|
||||
return false;
|
||||
}
|
||||
Seed = Manager->GetWorldSeed();
|
||||
UVoxelGenerator* Generator = NewObject<UVoxelGenerator>(
|
||||
GetTransientPackage(), NAME_None, RF_Transient);
|
||||
Generator->InitializeSettings(AuthoredSettings);
|
||||
Generator->SetStrateManager(Manager);
|
||||
|
||||
TArray<FStandaloneExportSample> Samples;
|
||||
auto AddSample = [&Samples](const FVector& Position, const TCHAR* Kind, int32 StrateIndex)
|
||||
{
|
||||
if (!VoxelMath::IsFinite(Position.X) || !VoxelMath::IsFinite(Position.Y)
|
||||
|| !VoxelMath::IsFinite(Position.Z)) return;
|
||||
FStandaloneExportSample Sample;
|
||||
Sample.Position = Position;
|
||||
Sample.Kind = Kind;
|
||||
Sample.StrateIndex = StrateIndex;
|
||||
Samples.Add(MoveTemp(Sample));
|
||||
};
|
||||
|
||||
const int32 XYAnchors[] = { 0, -1, 1, -128, 128, -512, 512, -2048, 2048, -8192, 8192 };
|
||||
FRandomStream SampleRandom(Seed ^ 0x13579BDF);
|
||||
FVector Camera = FVector::ZeroVector;
|
||||
bool bCameraFound = false;
|
||||
for (const FStrateSlot& Slot : Manager->GetLayout())
|
||||
{
|
||||
const float TopZ = static_cast<float>(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||
const float MidZ = (TopZ + BottomZ) * 0.5f;
|
||||
const int32 MidChunkZ = FMath::FloorToInt(MidZ / CHUNK_SIZE);
|
||||
for (int32 ZOffset : { -2, -1, 0, 1, 2 })
|
||||
{
|
||||
for (int32 Y : XYAnchors)
|
||||
for (int32 X : XYAnchors)
|
||||
{
|
||||
AddSample(FVector(X, Y, TopZ + ZOffset), TEXT("strate_boundary"), Slot.StrateIndex);
|
||||
AddSample(FVector(X, Y, BottomZ + ZOffset), TEXT("strate_boundary"), Slot.StrateIndex);
|
||||
}
|
||||
}
|
||||
for (int32 Index = 0; Index < 384; ++Index)
|
||||
{
|
||||
const float X = static_cast<float>(SampleRandom.RandRange(-4096, 4096));
|
||||
const float Y = static_cast<float>(SampleRandom.RandRange(-4096, 4096));
|
||||
const float Z = BottomZ + SampleRandom.RandRange(0, FMath::Max(1, FMath::FloorToInt(TopZ - BottomZ)));
|
||||
AddSample(FVector(X, Y, Z), TEXT("varied_world"), Slot.StrateIndex);
|
||||
}
|
||||
|
||||
if (Slot.Definition)
|
||||
{
|
||||
const FStrateGenerationParams Params = Manager->GetGenerationParams(
|
||||
FIntVector(0, 0, MidChunkZ));
|
||||
FChunkSDFCache Cache;
|
||||
VoxelCaveMorphology::BuildChunkCache(
|
||||
Cache, -384.0f, -384.0f, 384.0f, 384.0f, Params,
|
||||
static_cast<uint32>(Seed), Slot.StrateIndex,
|
||||
&Slot.Definition->TerrainOperations);
|
||||
for (const FCachedRoom& Room : Cache.Rooms)
|
||||
{
|
||||
const FVector Candidate = Room.Center;
|
||||
const float Density = Generator->GetDensityAt(
|
||||
Candidate.X, Candidate.Y, Candidate.Z);
|
||||
if (Density >= 0.0f)
|
||||
{
|
||||
AddSample(Candidate, TEXT("room_interior"), Slot.StrateIndex);
|
||||
if (Slot.StrateIndex == 0 && !bCameraFound
|
||||
&& Generator->GetDensityAt(Candidate.X, Candidate.Y, Candidate.Z + 8.0f) >= 0.0f
|
||||
&& Generator->GetDensityAt(Candidate.X, Candidate.Y, Candidate.Z + 16.0f) >= 0.0f)
|
||||
{
|
||||
Camera = Candidate;
|
||||
bCameraFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const FCachedTunnel& Tunnel : Cache.Tunnels)
|
||||
{
|
||||
for (int32 PointIndex = 0; PointIndex < Tunnel.WorldControlPoints.Num(); ++PointIndex)
|
||||
{
|
||||
const FVector Candidate = Tunnel.WorldControlPoints[PointIndex];
|
||||
const float Density = Generator->GetDensityAt(
|
||||
Candidate.X, Candidate.Y, Candidate.Z);
|
||||
if (Density >= 0.0f)
|
||||
{
|
||||
AddSample(Candidate, TEXT("tunnel_interior"), Slot.StrateIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Slot.StrateIndex == 0)
|
||||
{
|
||||
// Prefer an open, body-height column in the first authored strate. This seed pose is
|
||||
// exported to the standalone viewer; it uses only the owner's live base field.
|
||||
for (int32 Radius = 0; Radius <= 128 && !bCameraFound; Radius += 8)
|
||||
for (int32 Y = -Radius; Y <= Radius && !bCameraFound; Y += 8)
|
||||
for (int32 X = -Radius; X <= Radius && !bCameraFound; X += 8)
|
||||
{
|
||||
const float Z = MidZ;
|
||||
if (Generator->GetDensityAt(static_cast<float>(X), static_cast<float>(Y), Z) >= 0.0f
|
||||
&& Generator->GetDensityAt(static_cast<float>(X), static_cast<float>(Y), Z + 8.0f) >= 0.0f
|
||||
&& Generator->GetDensityAt(static_cast<float>(X), static_cast<float>(Y), Z + 16.0f) >= 0.0f)
|
||||
{
|
||||
Camera = FVector(X, Y, Z + 2.0f);
|
||||
bCameraFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const FVoxelPassage& Passage : Manager->GetPassages())
|
||||
{
|
||||
for (const FVector& Point : Passage.ControlPoints)
|
||||
{
|
||||
AddSample(Point, TEXT("inter_strate_passage"), Passage.UpperStrateIndex);
|
||||
}
|
||||
AddSample(Passage.UpperLanding.StandingPoint, TEXT("passage_landing"), Passage.UpperStrateIndex);
|
||||
AddSample(Passage.LowerLanding.StandingPoint, TEXT("passage_landing"), Passage.LowerStrateIndex);
|
||||
}
|
||||
|
||||
for (FStandaloneExportSample& Sample : Samples)
|
||||
{
|
||||
Sample.Density = Generator->GetDensityAt(
|
||||
static_cast<float>(Sample.Position.X), static_cast<float>(Sample.Position.Y),
|
||||
static_cast<float>(Sample.Position.Z));
|
||||
}
|
||||
|
||||
if (!bCameraFound)
|
||||
{
|
||||
OutError = TEXT("Could not locate open camera space in the owner's first strate.");
|
||||
return false;
|
||||
}
|
||||
|
||||
FString Json;
|
||||
TSharedRef<FExploreJsonWriter> Writer =
|
||||
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
|
||||
Writer->WriteObjectStart();
|
||||
Writer->WriteValue(TEXT("schema_version"), 1);
|
||||
Writer->WriteValue(TEXT("source"), TEXT("VoxelForgeExplore standalone base-world export"));
|
||||
Writer->WriteValue(TEXT("voxel_size_cm"), VOXEL_SIZE);
|
||||
Writer->WriteValue(TEXT("seed"), Seed);
|
||||
Writer->WriteValue(TEXT("open_surface_entry"), AuthoredSettings->bOpenSurfaceEntry);
|
||||
Writer->WriteValue(TEXT("origin_spine_radius"), OriginSpineRadius);
|
||||
Writer->WriteValue(TEXT("world_radius_voxels"), WorldRadiusVoxels);
|
||||
Writer->WriteValue(TEXT("edge_seal_thickness"), AuthoredSettings->EdgeSealThickness);
|
||||
Writer->WriteValue(TEXT("total_strates"), Manager->GetNumStrates());
|
||||
Writer->WriteValue(TEXT("inter_strate_gap_chunks"), AuthoredSettings->InterStrateGapChunks);
|
||||
Writer->WriteValue(TEXT("clip_radius"), AuthoredSettings->ClipRadius);
|
||||
Writer->WriteValue(TEXT("max_clip_level"), AuthoredSettings->MaxClipLevel);
|
||||
Writer->WriteValue(TEXT("full_res_clip_levels"), AuthoredSettings->FullResClipLevels);
|
||||
Writer->WriteValue(TEXT("coarse_tile_cells"), AuthoredSettings->CoarseTileCells);
|
||||
Writer->WriteValue(TEXT("render_distance_chunks"), AuthoredSettings->RenderDistanceChunks);
|
||||
Writer->WriteValue(TEXT("generate_skirts"), AuthoredSettings->bGenerateSkirts);
|
||||
Writer->WriteValue(TEXT("skirt_cells"), AuthoredSettings->SkirtCells);
|
||||
Writer->WriteValue(TEXT("lod_octave_drop"), AuthoredSettings->LODOctaveDrop);
|
||||
Writer->WriteValue(TEXT("strate_content_cut_min_level"), AuthoredSettings->GetEffectiveStrateContentCutMinLevel());
|
||||
TSet<const UObject*> ActiveObjects;
|
||||
StandaloneWriteJsonValue(*Writer, TEXT("settings_asset"),
|
||||
StandaloneSerializeStruct(AuthoredSettings->GetClass(), AuthoredSettings,
|
||||
ActiveObjects, AuthoredSettings));
|
||||
|
||||
Writer->WriteArrayStart(TEXT("strates"));
|
||||
for (const FStrateSlot& Slot : Manager->GetLayout())
|
||||
{
|
||||
Writer->WriteObjectStart();
|
||||
Writer->WriteValue(TEXT("index"), Slot.StrateIndex);
|
||||
Writer->WriteValue(TEXT("top_chunk_z"), Slot.TopChunkZ);
|
||||
Writer->WriteValue(TEXT("bottom_chunk_z"), Slot.BottomChunkZ);
|
||||
Writer->WriteValue(TEXT("height_chunks"), Slot.HeightInChunks);
|
||||
StandaloneWriteJsonValue(*Writer, TEXT("asset"),
|
||||
StandaloneSerializeStruct(Slot.Definition->GetClass(), Slot.Definition,
|
||||
ActiveObjects, Slot.Definition));
|
||||
StandaloneWriteJsonValue(*Writer, TEXT("generation_params"),
|
||||
StandaloneSerializeStruct(FStrateGenerationParams::StaticStruct(),
|
||||
&Slot.Definition->GenerationParams, ActiveObjects));
|
||||
StandaloneWriteJsonValue(*Writer, TEXT("disturbances"),
|
||||
StandaloneSerializeStruct(FStrateDisturbanceParams::StaticStruct(),
|
||||
&Slot.Definition->Disturbances, ActiveObjects));
|
||||
StandaloneWriteJsonValue(*Writer, TEXT("passage_config"),
|
||||
StandaloneSerializeStruct(FStratePassageConfig::StaticStruct(),
|
||||
&Slot.Definition->PassageConfig, ActiveObjects));
|
||||
Writer->WriteArrayStart(TEXT("terrain_operation_assets"));
|
||||
for (const FStrateTerrainOpEntry& Entry : Slot.Definition->TerrainOperations)
|
||||
{
|
||||
Writer->WriteObjectStart();
|
||||
Writer->WriteValue(TEXT("weight"), Entry.Weight);
|
||||
Writer->WriteValue(TEXT("probability"), Entry.Probability);
|
||||
Writer->WriteValue(TEXT("asset_path"), Entry.Operation.ToString());
|
||||
if (const UVoxelTerrainOpDefinition* Op = Entry.Operation.LoadSynchronous())
|
||||
{
|
||||
StandaloneWriteJsonValue(*Writer, TEXT("asset"),
|
||||
StandaloneSerializeStruct(Op->GetClass(), Op, ActiveObjects, Op));
|
||||
}
|
||||
Writer->WriteObjectEnd();
|
||||
}
|
||||
Writer->WriteArrayEnd();
|
||||
Writer->WriteObjectEnd();
|
||||
}
|
||||
Writer->WriteArrayEnd();
|
||||
|
||||
Writer->WriteArrayStart(TEXT("passages"));
|
||||
for (const FVoxelPassage& Passage : Manager->GetPassages())
|
||||
{
|
||||
StandaloneWriteJsonValue(*Writer, nullptr,
|
||||
MakeShared<FJsonValueObject>(StandalonePassageJson(Passage)));
|
||||
}
|
||||
Writer->WriteArrayEnd();
|
||||
Writer->WriteArrayStart(TEXT("density_samples"));
|
||||
for (const FStandaloneExportSample& Sample : Samples)
|
||||
{
|
||||
Writer->WriteObjectStart();
|
||||
Writer->WriteValue(TEXT("x"), static_cast<double>(Sample.Position.X));
|
||||
Writer->WriteValue(TEXT("y"), static_cast<double>(Sample.Position.Y));
|
||||
Writer->WriteValue(TEXT("z"), static_cast<double>(Sample.Position.Z));
|
||||
Writer->WriteValue(TEXT("density"), static_cast<double>(Sample.Density));
|
||||
Writer->WriteValue(TEXT("kind"), Sample.Kind);
|
||||
Writer->WriteValue(TEXT("strate_index"), Sample.StrateIndex);
|
||||
Writer->WriteObjectEnd();
|
||||
}
|
||||
Writer->WriteArrayEnd();
|
||||
Writer->WriteObjectStart(TEXT("viewer_start"));
|
||||
Writer->WriteValue(TEXT("found_open_space"), bCameraFound);
|
||||
Writer->WriteValue(TEXT("x"), static_cast<double>(Camera.X));
|
||||
Writer->WriteValue(TEXT("y"), static_cast<double>(Camera.Y));
|
||||
Writer->WriteValue(TEXT("z"), static_cast<double>(Camera.Z));
|
||||
Writer->WriteObjectEnd();
|
||||
Writer->WriteObjectEnd();
|
||||
Writer->Close();
|
||||
|
||||
const FString Path = FPaths::Combine(Arguments.OutDirectory, TEXT("owner_world.json"));
|
||||
if (!FFileHelper::SaveStringToFile(
|
||||
Json, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Could not write standalone owner data to '%s'."), *Path);
|
||||
return false;
|
||||
}
|
||||
if (OutJson) *OutJson = Json;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeStandaloneExport] seed=%d voxel_size_cm=%.3f strates=%d passages=%d samples=%d camera_found=%d file=%s"),
|
||||
Seed, VOXEL_SIZE, Manager->GetNumStrates(), Manager->GetPassages().Num(),
|
||||
Samples.Num(), bCameraFound ? 1 : 0, *Path);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UVoxelForgeExploreCommandlet::UVoxelForgeExploreCommandlet()
|
||||
@@ -12239,6 +12774,16 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (Arguments.bStandaloneWorldExport)
|
||||
{
|
||||
if (!RunStandaloneWorldExport(Arguments, Error, OutJson))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] %s"), *Error);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
FExploreBudget Budget(
|
||||
MainStartSeconds,
|
||||
static_cast<double>(Arguments.BudgetMinutes) * 60.0);
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
#pragma once
|
||||
@@ -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};};
|
||||
@@ -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,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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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,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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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** |
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user