measure(perf): per-component ablation of the TunnelNetwork path; exact worms restored
Round (Codex; reviewed by me against PERF-SAMPLED.md). Revert: voxel.WormNoiseMode / voxel.WormLatticeStep, their storage and their switches are REMOVED. VoxelWormField has one path again, exact Perlin with the N1 short circuit. Kept: bEnableWorms (asset), WormsForceOff (measurement only), the exact block skip (default 0). Ablation instrument: 14 development-only, world-changing force-off bits (voxel.TunnelAblate*), applied on the fused, op-stack/classifier, cached SDF/core and modifier/floor paths. They resolve once per process, are hashed into the generation fingerprint, and are compiled to zero in UE_BUILD_SHIPPING (VoxelDensityAblation.cpp). They are a measurement instrument, never a gameplay or multiplayer switch. Defaults exact: canonical export OBJ b3e5f4c3..9b377, worms-region 311CA42C / 75,482 triangles, capability 20,830 / 10,909, and 841/841 keyed tiles identical to NextHeadStaticTrace_20260913. Static worker seconds saved by removing ONE component (two off runs vs the interleaved all-on 33.06 s; baseline noise 0.27%): tunnel SDF 11.4%, cave warp 10.3%, passage carving 10.1%, landing posts 9.9%, passage structural posts 5.6%, worms 2.4%, native floor 2.2%, origin spine 1.6%, room SDF 1.6%, everything else <= 1.1% Moving (2+2 runs each) confirms the top five at 6.0-11.6%. Tunnel SDF off cuts LOD0 generation p95 104 -> 83 ms: it owns the slow tail. The positive savings sum to 58% of the static total; the other 42% is shared. The rows are not additive. Unexplained: tunnel-core OFF made the static total 2.9x SLOWER (95.5 s) while LOD0 generation got faster (p50 64.7 -> 60.6 ms) and triangles fell. The report blames the changed field, which does not fit; more likely a slow fallback path in coarse tiles. That row is not a cost measurement. Ledger: every Unreal process exited 0 and verified the loaded DLL; two PowerShell parser errors happened before any process started. No crashes or retries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y
This commit is contained in:
1 parent
c132243b30
commit
6ccf3718ff
12 files changed
+833
-695
No files matched your search
@@ -30,6 +30,7 @@
|
||||
#include "VoxelCaveMorphology.h"
|
||||
#include "VoxelDensityProfile.h"
|
||||
#include "VoxelDensityPrimitives.h"
|
||||
#include "VoxelDensityAblation.h"
|
||||
#include "VoxelStrateMeasure.h"
|
||||
#include "VoxelTypes.h" // Pour VOXEL_NOISE_SCALE, SmoothStep01
|
||||
#include "VoxelStrateTypes.h"
|
||||
@@ -3166,6 +3167,10 @@ namespace
|
||||
const FStrateGenerationParams& Params,
|
||||
uint32 Seed)
|
||||
{
|
||||
if (VoxelDensityAblation::IsCaveWarpOff())
|
||||
{
|
||||
return WorldPoint;
|
||||
}
|
||||
float EffectiveZ = WorldPoint.Z;
|
||||
if (Params.VerticalScale > 0.0f && Params.VerticalScale != 1.0f)
|
||||
{
|
||||
@@ -7286,7 +7291,9 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
CR.RoomOp->ApplyTo(OpParams, CR.RoomOpWeight);
|
||||
|
||||
// PITS — downward shafts anchored in the room's lower half.
|
||||
BakeRoomFeature(CR, /*Max*/2, OpParams.PitDensity,
|
||||
if (!VoxelDensityAblation::IsPitChimneySDFOff())
|
||||
{
|
||||
BakeRoomFeature(CR, /*Max*/2, OpParams.PitDensity,
|
||||
0xDE1A7Eu, 6271u, 0xABCDu, 0x5EEDu,
|
||||
/*XYScale*/0.6f, OpParams.PitMinRadius, OpParams.PitMaxRadius,
|
||||
[&](float PX, float PY, float PitRadius, uint32 PH3)
|
||||
@@ -7308,8 +7315,8 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
OutCache.Pits.Add(Pit);
|
||||
});
|
||||
|
||||
// CHIMNEYS — mirror of pits: upward tubes anchored in the room's upper half.
|
||||
BakeRoomFeature(CR, /*Max*/2, OpParams.ChimneyDensity,
|
||||
// CHIMNEYS — mirror of pits: upward tubes anchored in the room's upper half.
|
||||
BakeRoomFeature(CR, /*Max*/2, OpParams.ChimneyDensity,
|
||||
0xC4F007u, 7919u, 0x1337u, 0xCAFEu,
|
||||
/*XYScale*/0.6f, OpParams.ChimneyMinRadius, OpParams.ChimneyMaxRadius,
|
||||
[&](float CX, float CY, float ChmRadius, uint32 CH3)
|
||||
@@ -7330,6 +7337,7 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
Chim.BoundXYRadiusSq = MaxXYR * MaxXYR;
|
||||
OutCache.Chimneys.Add(Chim);
|
||||
});
|
||||
}
|
||||
|
||||
// COLUMNS — full-height solid cylinders (no Z anchor, no flare).
|
||||
BakeRoomFeature(CR, /*Max*/4, OpParams.ColumnDensity,
|
||||
@@ -7481,9 +7489,13 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
||||
MinSDF = VoxelSDF::SmoothMin(MinSDF, RoomSDF, BlendK);
|
||||
};
|
||||
|
||||
const int32 RoomCandidateCount = VF_ForEachSpatialCandidate(
|
||||
Cache.RoomSpatialIndex, Cache.Rooms.Num(), WorldX, WorldY, EvaluateRoom,
|
||||
bUseSpatialIndex);
|
||||
int32 RoomCandidateCount = 0;
|
||||
if (!VoxelDensityAblation::IsRoomSDFOff())
|
||||
{
|
||||
RoomCandidateCount = VF_ForEachSpatialCandidate(
|
||||
Cache.RoomSpatialIndex, Cache.Rooms.Num(), WorldX, WorldY, EvaluateRoom,
|
||||
bUseSpatialIndex);
|
||||
}
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
@@ -7512,9 +7524,12 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
||||
const float JoinSDF = FMath::Max(HorizontalSDF, VerticalSDF);
|
||||
MinSDF = VoxelSDF::SmoothMin(MinSDF, JoinSDF, BlendK);
|
||||
};
|
||||
VF_ForEachSpatialCandidate(
|
||||
Cache.RoomFloorJoinSpatialIndex, Cache.RoomFloorJoins.Num(),
|
||||
WorldX, WorldY, EvaluateJoin, bUseSpatialIndex);
|
||||
if (!VoxelDensityAblation::IsRoomSDFOff())
|
||||
{
|
||||
VF_ForEachSpatialCandidate(
|
||||
Cache.RoomFloorJoinSpatialIndex, Cache.RoomFloorJoins.Num(),
|
||||
WorldX, WorldY, EvaluateJoin, bUseSpatialIndex);
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
// Tunnel SDFs
|
||||
@@ -7553,9 +7568,13 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
||||
MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK);
|
||||
};
|
||||
|
||||
const int32 TunnelCandidateCount = VF_ForEachSpatialCandidate(
|
||||
Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel,
|
||||
bUseSpatialIndex);
|
||||
int32 TunnelCandidateCount = 0;
|
||||
if (!VoxelDensityAblation::IsTunnelSDFOff())
|
||||
{
|
||||
TunnelCandidateCount = VF_ForEachSpatialCandidate(
|
||||
Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel,
|
||||
bUseSpatialIndex);
|
||||
}
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
@@ -7577,6 +7596,10 @@ float VoxelCaveMorphology::EvaluateTunnelCoreSDF(
|
||||
const FChunkSDFCache& Cache,
|
||||
bool bUseSpatialIndex)
|
||||
{
|
||||
if (VoxelDensityAblation::IsTunnelCoreOff())
|
||||
{
|
||||
return FLT_MAX;
|
||||
}
|
||||
const FVector Pos(WorldX, WorldY, WorldZ);
|
||||
float MinSDF = FLT_MAX;
|
||||
const FCachedTunnel* TunnelData = Cache.Tunnels.GetData();
|
||||
@@ -7618,6 +7641,10 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
||||
const FTunnelSupportFloorColumn* SupportColumn,
|
||||
bool bUseSpatialIndex)
|
||||
{
|
||||
if (VoxelDensityAblation::IsTunnelCoreOff())
|
||||
{
|
||||
return FTunnelCoreWorldEvaluation();
|
||||
}
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::TunnelCoreWorld);
|
||||
const FVector Pos(WorldX, WorldY, WorldZ);
|
||||
@@ -7756,6 +7783,10 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
const FChunkSDFCache& Cache,
|
||||
bool bUseSpatialIndex)
|
||||
{
|
||||
if (VoxelDensityAblation::IsTunnelCoreOff())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const FVector Pos(WorldX, WorldY, WorldZ);
|
||||
const FCachedTunnel* TunnelData = Cache.Tunnels.GetData();
|
||||
|
||||
@@ -7875,6 +7906,11 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
||||
FTunnelSupportFloorColumn& OutColumn,
|
||||
bool bUseSpatialIndex)
|
||||
{
|
||||
if (VoxelDensityAblation::IsTunnelCoreOff())
|
||||
{
|
||||
OutColumn.Reset();
|
||||
return;
|
||||
}
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::TunnelCoreSupport);
|
||||
OutColumn.Reset();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "VoxelStrateComposer.h"
|
||||
|
||||
#include "VoxelDensityPrimitives.h" // VF_ApplyOriginSpine / seals / PassageCarving
|
||||
#include "VoxelDensityAblation.h" // fingerprinted development-only stage measurements
|
||||
#include "VoxelCaveMorphology.h" // VoxelSDF::Capsule, VoxelHash
|
||||
#include "VoxelGenerator.h" // VoxelGenLOD::Eff
|
||||
#include "VoxelHeightOp.h" // FVoxelHeightStack — SurfaceWorld's two height stacks
|
||||
@@ -1933,6 +1934,10 @@ namespace
|
||||
EVoxelOpEffect EffectOverBox(const FBox& VoxelBox,
|
||||
const FVoxelOpContext& Ctx) const override
|
||||
{
|
||||
if (VoxelDensityAblation::IsOriginSpineOff())
|
||||
{
|
||||
return EVoxelOpEffect::Identity;
|
||||
}
|
||||
const bool bRoomTouches = Ctx.bUseLatticeProof
|
||||
? VoxelPassageGeometry::OriginLandingRoomTouchesLattice(
|
||||
VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step,
|
||||
@@ -2018,6 +2023,10 @@ namespace
|
||||
*/
|
||||
EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext&) const override
|
||||
{
|
||||
if (VoxelDensityAblation::IsBoundarySealOff())
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
if (Thickness <= 0.0f || Base <= 0.0f) { return EVoxelTileClass::Mixed; }
|
||||
|
||||
constexpr float SafetyMargin = 1.0f;
|
||||
@@ -2039,6 +2048,10 @@ namespace
|
||||
// Hors de sa bande, le seal ne fait rien du tout ; à cheval, il ne peut qu'ajouter du solide.
|
||||
EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext&) const override
|
||||
{
|
||||
if (VoxelDensityAblation::IsBoundarySealOff())
|
||||
{
|
||||
return EVoxelOpEffect::Identity;
|
||||
}
|
||||
if (Thickness <= 0.0f) { return EVoxelOpEffect::Identity; }
|
||||
const bool bTouchesTopBand = ((float)VoxelBox.Max.Z >= TopZ - Thickness) && ((float)VoxelBox.Min.Z <= TopZ);
|
||||
const bool bTouchesBotBand = ((float)VoxelBox.Min.Z <= BotZ + Thickness) && ((float)VoxelBox.Max.Z >= BotZ);
|
||||
@@ -2100,6 +2113,10 @@ namespace
|
||||
*/
|
||||
EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override
|
||||
{
|
||||
if (VoxelDensityAblation::IsXYEdgeSealOff())
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
return VF_XYEdgeSealBoxIsForcedSolid(
|
||||
VoxelBox, Ctx.WorldRadiusVoxels, Ctx.EdgeSealThickness, Base)
|
||||
? EVoxelTileClass::AllSolid : EVoxelTileClass::Mixed;
|
||||
@@ -2114,6 +2131,10 @@ namespace
|
||||
// In the inner world it is Identity; a box that reaches the radial band can only gain rock.
|
||||
EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override
|
||||
{
|
||||
if (VoxelDensityAblation::IsXYEdgeSealOff())
|
||||
{
|
||||
return EVoxelOpEffect::Identity;
|
||||
}
|
||||
return VF_XYEdgeSealBoxTouchesBand(
|
||||
VoxelBox, Ctx.WorldRadiusVoxels, Ctx.EdgeSealThickness)
|
||||
? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity;
|
||||
@@ -2177,29 +2198,39 @@ namespace
|
||||
EVoxelOpEffect EffectOverBox(const FBox& VoxelBox,
|
||||
const FVoxelOpContext& Ctx) const override
|
||||
{
|
||||
if (VoxelDensityAblation::IsPassageCarvingOff()
|
||||
&& VoxelDensityAblation::IsLandingPostsOff()
|
||||
&& VoxelDensityAblation::IsPassageStructuralPostsOff()
|
||||
&& VoxelDensityAblation::IsOriginSpineOff())
|
||||
{
|
||||
return EVoxelOpEffect::Identity;
|
||||
}
|
||||
const UVoxelStrateManager* LiveManager = Manager.Get();
|
||||
if (!LiveManager) { return EVoxelOpEffect::Identity; }
|
||||
const bool bOriginFloor = Ctx.bUseLatticeProof
|
||||
? VoxelPassageGeometry::OriginLandingFloorTouchesLattice(
|
||||
VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step,
|
||||
TopZ, BotZ, Seal, SpineRadius)
|
||||
: VoxelPassageGeometry::OriginLandingFloorTouchesBox(
|
||||
VoxelBox, TopZ, BotZ, Seal, SpineRadius);
|
||||
const bool bPassageFloor = Ctx.bUseLatticeProof
|
||||
? LiveManager->AnyPassageLandingFloorNearLattice(
|
||||
VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step)
|
||||
: LiveManager->AnyPassageLandingFloorNearBox(
|
||||
VoxelBox.Min, VoxelBox.Max);
|
||||
const bool bOriginFloor = !VoxelDensityAblation::IsLandingPostsOff()
|
||||
&& (Ctx.bUseLatticeProof
|
||||
? VoxelPassageGeometry::OriginLandingFloorTouchesLattice(
|
||||
VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step,
|
||||
TopZ, BotZ, Seal, SpineRadius)
|
||||
: VoxelPassageGeometry::OriginLandingFloorTouchesBox(
|
||||
VoxelBox, TopZ, BotZ, Seal, SpineRadius));
|
||||
const bool bPassageFloor = !VoxelDensityAblation::IsLandingPostsOff()
|
||||
&& (Ctx.bUseLatticeProof
|
||||
? LiveManager->AnyPassageLandingFloorNearLattice(
|
||||
VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step)
|
||||
: LiveManager->AnyPassageLandingFloorNearBox(
|
||||
VoxelBox.Min, VoxelBox.Max));
|
||||
if (bOriginFloor || bPassageFloor)
|
||||
{
|
||||
// The room carves air, while its support slab force-writes solid. Both
|
||||
// hypotheses must therefore be killed for a box touching that slab.
|
||||
return EVoxelOpEffect::Both;
|
||||
}
|
||||
const bool bPassageCarve = Ctx.bUseLatticeProof
|
||||
? LiveManager->AnyPassageNearLattice(
|
||||
VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step)
|
||||
: LiveManager->AnyPassageNearBox(VoxelBox.Min, VoxelBox.Max);
|
||||
const bool bPassageCarve = !VoxelDensityAblation::IsPassageCarvingOff()
|
||||
&& (Ctx.bUseLatticeProof
|
||||
? LiveManager->AnyPassageNearLattice(
|
||||
VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step)
|
||||
: LiveManager->AnyPassageNearBox(VoxelBox.Min, VoxelBox.Max));
|
||||
return bPassageCarve
|
||||
? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity;
|
||||
}
|
||||
@@ -2212,6 +2243,10 @@ namespace
|
||||
float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
|
||||
const FVoxelBoxHypotheses& H) const override
|
||||
{
|
||||
if (VoxelDensityAblation::IsPassageCarvingOff())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
const UVoxelStrateManager* LiveManager = Manager.Get();
|
||||
if (!LiveManager) { return 0.0f; }
|
||||
if (!Ctx.bUseLatticeProof)
|
||||
@@ -3917,6 +3952,10 @@ namespace
|
||||
bool TryGetLastTunnelCoreWorldEvaluation(
|
||||
FTunnelCoreWorldEvaluation& OutEvaluation) const override
|
||||
{
|
||||
if (VoxelDensityAblation::IsTunnelCoreOff())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FState& S = State();
|
||||
if (!S.bLastTunnelCoreWorldEvaluationValid
|
||||
|| S.LastTunnelCoreEvaluationPosition != S.LastWorldPosition)
|
||||
@@ -4078,7 +4117,7 @@ namespace
|
||||
// un « frame » : sa portée est exactement UN opérateur, donc elle appartient à cet
|
||||
// opérateur.
|
||||
float WarpedX = WorldX, WarpedY = WorldY, WarpedZ = EffectiveZ;
|
||||
if (P.CaveWarpStrength > 0.0f)
|
||||
if (!VoxelDensityAblation::IsCaveWarpOff() && P.CaveWarpStrength > 0.0f)
|
||||
{
|
||||
const float WF = P.CaveWarpFrequency;
|
||||
const float WS = P.CaveWarpStrength;
|
||||
@@ -4308,68 +4347,77 @@ namespace
|
||||
// mais à des coordonnées NON warpées. Sous un modèle de frames il aurait fallu les sortir
|
||||
// du frame tout en gardant le canal — exprimable, mais tordu. Dans un opérateur unique la
|
||||
// difficulté disparaît : le warp est une variable locale, pas un contexte hérité.
|
||||
auto EvaluatePit = [&](int32 PitIndex)
|
||||
if (!VoxelDensityAblation::IsPitChimneySDFOff())
|
||||
{
|
||||
const FCachedPit& Pit = GetCache().Pits[PitIndex];
|
||||
const float DZ = WorldZ - Pit.TopZ;
|
||||
if (DZ >= Pit.BlendK) { return; }
|
||||
if (-DZ > Pit.Depth + Pit.BlendK) { return; }
|
||||
|
||||
const float DX = WorldX - Pit.CenterX;
|
||||
const float DY = WorldY - Pit.CenterY;
|
||||
const float XYDistSq = DX * DX + DY * DY;
|
||||
if (XYDistSq > Pit.BoundXYRadiusSq) { return; }
|
||||
|
||||
float PitSDF;
|
||||
if (DZ <= 0.0f)
|
||||
auto EvaluatePit = [&](int32 PitIndex)
|
||||
{
|
||||
const float DepthBelow = -DZ;
|
||||
float FlareFactor = FMath::Clamp(1.0f - DepthBelow / Pit.FlareDist, 0.0f, 1.0f);
|
||||
FlareFactor = FlareFactor * FlareFactor;
|
||||
const float EffRadius = Pit.Radius + Pit.FlareExtra * FlareFactor;
|
||||
PitSDF = FMath::Sqrt(XYDistSq) - EffRadius;
|
||||
}
|
||||
else
|
||||
{
|
||||
PitSDF = FMath::Sqrt(XYDistSq) - (Pit.Radius + Pit.FlareExtra);
|
||||
}
|
||||
const FCachedPit& Pit = GetCache().Pits[PitIndex];
|
||||
const float DZ = WorldZ - Pit.TopZ;
|
||||
if (DZ >= Pit.BlendK) { return; }
|
||||
if (-DZ > Pit.Depth + Pit.BlendK) { return; }
|
||||
|
||||
CaveSDF = VoxelSDF::SmoothMin(CaveSDF, PitSDF, Pit.BlendK);
|
||||
};
|
||||
VF_ForEachChunkSDFSpatialCandidate(
|
||||
GetCache().PitSpatialIndex, GetCache().Pits.Num(),
|
||||
WorldX, WorldY, EvaluatePit, bUseSpatialIndex);
|
||||
const float DX = WorldX - Pit.CenterX;
|
||||
const float DY = WorldY - Pit.CenterY;
|
||||
const float XYDistSq = DX * DX + DY * DY;
|
||||
if (XYDistSq > Pit.BoundXYRadiusSq) { return; }
|
||||
|
||||
auto EvaluateChimney = [&](int32 ChimneyIndex)
|
||||
float PitSDF;
|
||||
if (DZ <= 0.0f)
|
||||
{
|
||||
const float DepthBelow = -DZ;
|
||||
float FlareFactor = FMath::Clamp(1.0f - DepthBelow / Pit.FlareDist, 0.0f, 1.0f);
|
||||
FlareFactor = FlareFactor * FlareFactor;
|
||||
const float EffRadius = Pit.Radius + Pit.FlareExtra * FlareFactor;
|
||||
PitSDF = FMath::Sqrt(XYDistSq) - EffRadius;
|
||||
}
|
||||
else
|
||||
{
|
||||
PitSDF = FMath::Sqrt(XYDistSq) - (Pit.Radius + Pit.FlareExtra);
|
||||
}
|
||||
|
||||
CaveSDF = VoxelSDF::SmoothMin(CaveSDF, PitSDF, Pit.BlendK);
|
||||
};
|
||||
if (!VoxelDensityAblation::IsPitChimneySDFOff())
|
||||
{
|
||||
const FCachedChimney& Chim = GetCache().Chimneys[ChimneyIndex];
|
||||
const float DZ = WorldZ - Chim.BottomZ;
|
||||
if (-DZ >= Chim.BlendK) { return; }
|
||||
if (DZ > Chim.Height + Chim.BlendK) { return; }
|
||||
VF_ForEachChunkSDFSpatialCandidate(
|
||||
GetCache().PitSpatialIndex, GetCache().Pits.Num(),
|
||||
WorldX, WorldY, EvaluatePit, bUseSpatialIndex);
|
||||
}
|
||||
|
||||
const float DX = WorldX - Chim.CenterX;
|
||||
const float DY = WorldY - Chim.CenterY;
|
||||
const float XYDistSq = DX * DX + DY * DY;
|
||||
if (XYDistSq > Chim.BoundXYRadiusSq) { return; }
|
||||
|
||||
float ChmSDF;
|
||||
if (DZ >= 0.0f)
|
||||
auto EvaluateChimney = [&](int32 ChimneyIndex)
|
||||
{
|
||||
float FlareFactor = FMath::Clamp(1.0f - DZ / Chim.FlareDist, 0.0f, 1.0f);
|
||||
FlareFactor = FlareFactor * FlareFactor;
|
||||
const float EffRadius = Chim.Radius + Chim.FlareExtra * FlareFactor;
|
||||
ChmSDF = FMath::Sqrt(XYDistSq) - EffRadius;
|
||||
}
|
||||
else
|
||||
{
|
||||
ChmSDF = FMath::Sqrt(XYDistSq) - (Chim.Radius + Chim.FlareExtra);
|
||||
}
|
||||
const FCachedChimney& Chim = GetCache().Chimneys[ChimneyIndex];
|
||||
const float DZ = WorldZ - Chim.BottomZ;
|
||||
if (-DZ >= Chim.BlendK) { return; }
|
||||
if (DZ > Chim.Height + Chim.BlendK) { return; }
|
||||
|
||||
CaveSDF = VoxelSDF::SmoothMin(CaveSDF, ChmSDF, Chim.BlendK);
|
||||
};
|
||||
VF_ForEachChunkSDFSpatialCandidate(
|
||||
GetCache().ChimneySpatialIndex, GetCache().Chimneys.Num(),
|
||||
WorldX, WorldY, EvaluateChimney, bUseSpatialIndex);
|
||||
const float DX = WorldX - Chim.CenterX;
|
||||
const float DY = WorldY - Chim.CenterY;
|
||||
const float XYDistSq = DX * DX + DY * DY;
|
||||
if (XYDistSq > Chim.BoundXYRadiusSq) { return; }
|
||||
|
||||
float ChmSDF;
|
||||
if (DZ >= 0.0f)
|
||||
{
|
||||
float FlareFactor = FMath::Clamp(1.0f - DZ / Chim.FlareDist, 0.0f, 1.0f);
|
||||
FlareFactor = FlareFactor * FlareFactor;
|
||||
const float EffRadius = Chim.Radius + Chim.FlareExtra * FlareFactor;
|
||||
ChmSDF = FMath::Sqrt(XYDistSq) - EffRadius;
|
||||
}
|
||||
else
|
||||
{
|
||||
ChmSDF = FMath::Sqrt(XYDistSq) - (Chim.Radius + Chim.FlareExtra);
|
||||
}
|
||||
|
||||
CaveSDF = VoxelSDF::SmoothMin(CaveSDF, ChmSDF, Chim.BlendK);
|
||||
};
|
||||
if (!VoxelDensityAblation::IsPitChimneySDFOff())
|
||||
{
|
||||
VF_ForEachChunkSDFSpatialCandidate(
|
||||
GetCache().ChimneySpatialIndex, GetCache().Chimneys.Num(),
|
||||
WorldX, WorldY, EvaluateChimney, bUseSpatialIndex);
|
||||
}
|
||||
}
|
||||
|
||||
InOut.Sdf = CaveSDF;
|
||||
|
||||
@@ -4378,40 +4426,46 @@ namespace
|
||||
// build-time ownership now so the stack can preserve the authored support through its
|
||||
// remaining modifiers without a second floor-writing pass. TryGet... below reuses the
|
||||
// exact same cached result for the generator's post-disturbance hand-off.
|
||||
const FTunnelSupportFloorColumn* SupportColumn = GetSupportColumn(
|
||||
S.LastWorldPosition);
|
||||
S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
||||
WorldX, WorldY, WorldZ, GetCache(), SupportColumn,
|
||||
bUseSpatialIndex);
|
||||
S.LastTunnelCoreEvaluationPosition = S.LastWorldPosition;
|
||||
S.bLastTunnelCoreWorldEvaluationValid = true;
|
||||
if (S.LastTunnelCoreWorldEvaluation.bSupportFloor
|
||||
|| S.LastTunnelCoreWorldEvaluation.bRoomFloor)
|
||||
if (!VoxelDensityAblation::IsTunnelCoreOff())
|
||||
{
|
||||
const float StructuralFloorMinimumDensity =
|
||||
FMath::Max(P.BaseDensity * 2.0f, 1.0f);
|
||||
if (VoxelMath::IsFinite(StructuralFloorMinimumDensity)
|
||||
&& StructuralFloorMinimumDensity > 0.0f)
|
||||
const FTunnelSupportFloorColumn* SupportColumn = GetSupportColumn(
|
||||
S.LastWorldPosition);
|
||||
S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
||||
WorldX, WorldY, WorldZ, GetCache(), SupportColumn,
|
||||
bUseSpatialIndex);
|
||||
S.LastTunnelCoreEvaluationPosition = S.LastWorldPosition;
|
||||
S.bLastTunnelCoreWorldEvaluationValid = true;
|
||||
if (S.LastTunnelCoreWorldEvaluation.bSupportFloor
|
||||
|| S.LastTunnelCoreWorldEvaluation.bRoomFloor)
|
||||
{
|
||||
InOut.bProtectedStructuralFloor = true;
|
||||
InOut.StructuralFloorMinimumDensity = StructuralFloorMinimumDensity;
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
const float StructuralFloorMinimumDensity =
|
||||
FMath::Max(P.BaseDensity * 2.0f, 1.0f);
|
||||
if (VoxelMath::IsFinite(StructuralFloorMinimumDensity)
|
||||
&& StructuralFloorMinimumDensity > 0.0f)
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelAuthoredFloorSamples);
|
||||
InOut.bProtectedStructuralFloor = true;
|
||||
InOut.StructuralFloorMinimumDensity = StructuralFloorMinimumDensity;
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelAuthoredFloorSamples);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// These are the per-sample hand-offs needed by an op-major block. The scalar path
|
||||
// continues to use FState directly; publishing them here only makes the dependency
|
||||
// explicit for a later modifier and does not alter Density/Sdf.
|
||||
InOut.bHasTunnelCoreWorldEvaluation = true;
|
||||
InOut.TunnelCoreWorldSDF = S.LastTunnelCoreWorldEvaluation.SDF;
|
||||
InOut.bTunnelCoreSupportFloor = S.LastTunnelCoreWorldEvaluation.bSupportFloor;
|
||||
InOut.bTunnelCoreRoomFloor = S.LastTunnelCoreWorldEvaluation.bRoomFloor;
|
||||
}
|
||||
|
||||
// These are the per-sample hand-offs needed by an op-major block. The scalar path
|
||||
// continues to use FState directly; publishing them here only makes the dependency
|
||||
// explicit for a later modifier and does not alter Density/Sdf.
|
||||
// These cache hand-offs remain valid when tunnel core is off; detail operators still
|
||||
// need the room graph and nearest-room ownership.
|
||||
InOut.RoomCache = &GetCache();
|
||||
InOut.NearestRoomIndex = S.NearestRoom;
|
||||
InOut.bHasTunnelCoreWorldEvaluation = true;
|
||||
InOut.TunnelCoreWorldSDF = S.LastTunnelCoreWorldEvaluation.SDF;
|
||||
InOut.bTunnelCoreSupportFloor = S.LastTunnelCoreWorldEvaluation.bSupportFloor;
|
||||
InOut.bTunnelCoreRoomFloor = S.LastTunnelCoreWorldEvaluation.bRoomFloor;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
@@ -5418,7 +5472,7 @@ namespace
|
||||
&& P.VerticalScale != 1.0f)
|
||||
? WorldZ / P.VerticalScale : WorldZ;
|
||||
FVector Query(WorldX, WorldY, EffectiveZ);
|
||||
if (P.CaveWarpStrength > 0.0f)
|
||||
if (!VoxelDensityAblation::IsCaveWarpOff() && P.CaveWarpStrength > 0.0f)
|
||||
{
|
||||
const float Frequency = P.CaveWarpFrequency;
|
||||
Query.X += VoxelNoise::Perlin3D(
|
||||
@@ -8543,9 +8597,6 @@ namespace
|
||||
FWormFieldSource(const FStrateGenerationParams& InP, int32 Seed)
|
||||
: P(InP)
|
||||
, SeedU((uint32)Seed)
|
||||
, WormLatticeStep(
|
||||
InP.WormStrength > 0.0f && InP.WormThreshold > 0.0f
|
||||
? VoxelWormField::GetLatticeStep() : 0)
|
||||
{}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
||||
@@ -8585,7 +8636,7 @@ namespace
|
||||
SeedU};
|
||||
const float WormValue = VoxelWormField::Evaluate(
|
||||
WorldX, WorldY, WorldZ, P.WormThreshold,
|
||||
WormParameters, WormLatticeStep);
|
||||
WormParameters);
|
||||
if (WormValue < P.WormThreshold)
|
||||
{
|
||||
const float t = 1.0f - (WormValue / P.WormThreshold);
|
||||
@@ -8656,9 +8707,7 @@ namespace
|
||||
// explicit count cap keeps this check bounded. The network mask still uses the SDF
|
||||
// interval's worst case, so this never assumes a room value that the preceding source
|
||||
// did not prove.
|
||||
if (VoxelWormField::GetNoiseMode() == 0
|
||||
&& WormLatticeStep <= 1
|
||||
&& Ctx.bUseLatticeProof && Ctx.bTightenWarpProof && Ctx.Step >= 1
|
||||
if (Ctx.bUseLatticeProof && Ctx.bTightenWarpProof && Ctx.Step >= 1
|
||||
&& VoxelMath::IsFinite(P.WormFrequency)
|
||||
&& VoxelMath::IsFinite(P.WormHorizontalBias)
|
||||
&& VoxelMath::IsFinite(P.WormThreshold)
|
||||
@@ -8778,7 +8827,6 @@ namespace
|
||||
private:
|
||||
FStrateGenerationParams P;
|
||||
uint32 SeedU;
|
||||
int32 WormLatticeStep = 0;
|
||||
};
|
||||
|
||||
#if WITH_EDITOR
|
||||
@@ -9722,6 +9770,8 @@ namespace VoxelDensityOps
|
||||
// `Sdf < SDFBlendRadius·3` via VF_NearCaveSurface. Voir la note de l'étape B5 là-bas.
|
||||
// L'ORDRE EST CELUI DE L'ORIGINAL et il compte : chacun lit la densité que le précédent
|
||||
// a laissée (le biais de sol est un ajout indépendant appliqué à cette accumulation).
|
||||
if (!VoxelDensityAblation::IsDetailOpsOff())
|
||||
{
|
||||
OutStack.Add(MakeUnique<FCaveRoughnessMod>(P, Seed)); // 4b
|
||||
OutStack.Add(MakeUnique<FCaveTerraceMod>(P, Seed, RoomPtr)); // 4c — terrasses
|
||||
OutStack.Add(MakeUnique<FLayerLineMod>(P, RoomPtr)); // 4c — lignes de strates
|
||||
@@ -9734,6 +9784,7 @@ namespace VoxelDensityOps
|
||||
OutStack.Add(MakeUnique<FDomeMod>(P, RoomPtr)); // 4g — dômes
|
||||
OutStack.Add(MakeUnique<FPinchMod>(P, RoomPtr)); // 4h — pincement
|
||||
OutStack.Add(MakeUnique<FFloorBiasMod>(P, RoomPtr)); // fin 4h — biais de sol
|
||||
}
|
||||
if (P.WormStrength > 0.0f && P.WormThreshold > 0.0f)
|
||||
{
|
||||
OutStack.Add(MakeUnique<FWormFieldSource>(P, Seed));
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "VoxelBiomeDefinition.h"
|
||||
#include "VoxelNoise.h" // T2.a: float, SIMD-batched gradient-noise core
|
||||
#include "VoxelWormField.h"
|
||||
#include "VoxelDensityAblation.h" // fingerprinted development-only stage measurements
|
||||
#include "VoxelDensityPrimitives.h" // spine / seals / passage — shared with the operator stack
|
||||
#include "VoxelDensityOpStack.h" // OPSTACK Phase 1: the opt-in per-strate operator stack
|
||||
#include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it
|
||||
@@ -985,6 +986,10 @@ static void ApplyDisturbances(float& MC, float X, float Y, float Z,
|
||||
const FStrateDisturbanceParams& D, uint32 Seed, bool bProtectVerticalShaftAir,
|
||||
bool bProtectAuthoredTunnelFloor)
|
||||
{
|
||||
if (VoxelDensityAblation::IsDisturbancesOff())
|
||||
{
|
||||
return;
|
||||
}
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::ApplyDisturbances);
|
||||
const float InnerTop = D.StrateTopWorldZ - D.BoundarySealThickness;
|
||||
@@ -2592,7 +2597,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
// Le seul point d'entrée de la pile dans le chemin de production. Elle rend la convention
|
||||
// MC (négatif = solide) comme les fonctions d'archétype, donc les disturbances et la couche
|
||||
// de diff qui suivent ne voient aucune différence.
|
||||
if (CP_UseOpStack)
|
||||
if (!VoxelDensityAblation::IsTunnelCoreOff() && CP_UseOpStack)
|
||||
{
|
||||
const bool bCanUseFusedEvaluator =
|
||||
GVoxelForgeUseFusedEvaluator != 0
|
||||
@@ -2751,7 +2756,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
PreDisturbanceTunnelCore);
|
||||
}
|
||||
}
|
||||
if (!bHavePreDisturbanceTunnelCore && ActiveTunnelCoreCache->bValid)
|
||||
if (!VoxelDensityAblation::IsTunnelCoreOff()
|
||||
&& !bHavePreDisturbanceTunnelCore && ActiveTunnelCoreCache->bValid)
|
||||
{
|
||||
const FTunnelSupportFloorColumn* SupportColumn = nullptr;
|
||||
FTunnelSupportFloorColumn EmptySupportColumn;
|
||||
@@ -2891,7 +2897,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
// bridge over a graph tunnel. Reassert the native cached tunnel core here, after every
|
||||
// solid floor writer but before the global XY seal. This cache is built once per chunk,
|
||||
// never once per voxel.
|
||||
if (ActiveTunnelCoreCache->bValid || CP_UseOpStack)
|
||||
if (!VoxelDensityAblation::IsTunnelCoreOff()
|
||||
&& (ActiveTunnelCoreCache->bValid || CP_UseOpStack))
|
||||
{
|
||||
FTunnelCoreWorldEvaluation TunnelCore;
|
||||
bool bHaveTunnelCore = bHavePreDisturbanceTunnelCore;
|
||||
@@ -3181,7 +3188,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
||||
float WarpedY = WorldY;
|
||||
float WarpedZ = EffectiveZ;
|
||||
|
||||
if (Params.CaveWarpStrength > 0.0f)
|
||||
if (!VoxelDensityAblation::IsCaveWarpOff() && Params.CaveWarpStrength > 0.0f)
|
||||
{
|
||||
const float WF = Params.CaveWarpFrequency;
|
||||
const float WS = Params.CaveWarpStrength;
|
||||
@@ -3472,74 +3479,77 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
||||
// transition as tunnel-to-room (no hard seam at PitTopZ)
|
||||
// - bNearCaveSurface becomes true inside the shaft — roughness clamp
|
||||
// prevents fill-back, so this is safe
|
||||
auto EvaluatePit = [&](int32 PitIndex)
|
||||
if (!VoxelDensityAblation::IsPitChimneySDFOff())
|
||||
{
|
||||
const FCachedPit& Pit = SDFCache.Pits[PitIndex];
|
||||
const float DZ = WorldZ - Pit.TopZ; // Negative = below anchor (shaft)
|
||||
if (DZ >= Pit.BlendK) return; // Above even the blend fringe
|
||||
if (-DZ > Pit.Depth + Pit.BlendK) return;
|
||||
|
||||
const float DX = WorldX - Pit.CenterX;
|
||||
const float DY = WorldY - Pit.CenterY;
|
||||
const float XYDistSq = DX * DX + DY * DY;
|
||||
if (XYDistSq > Pit.BoundXYRadiusSq) return;
|
||||
|
||||
float PitSDF;
|
||||
if (DZ <= 0.0f)
|
||||
auto EvaluatePit = [&](int32 PitIndex)
|
||||
{
|
||||
// Shaft: tapered cylinder with flared opening
|
||||
float DepthBelow = -DZ;
|
||||
float FlareFactor = FMath::Clamp(1.0f - DepthBelow / Pit.FlareDist, 0.0f, 1.0f);
|
||||
FlareFactor = FlareFactor * FlareFactor;
|
||||
float EffRadius = Pit.Radius + Pit.FlareExtra * FlareFactor;
|
||||
PitSDF = FMath::Sqrt(XYDistSq) - EffRadius;
|
||||
}
|
||||
else
|
||||
const FCachedPit& Pit = SDFCache.Pits[PitIndex];
|
||||
const float DZ = WorldZ - Pit.TopZ; // Negative = below anchor (shaft)
|
||||
if (DZ >= Pit.BlendK) return; // Above even the blend fringe
|
||||
if (-DZ > Pit.Depth + Pit.BlendK) return;
|
||||
|
||||
const float DX = WorldX - Pit.CenterX;
|
||||
const float DY = WorldY - Pit.CenterY;
|
||||
const float XYDistSq = DX * DX + DY * DY;
|
||||
if (XYDistSq > Pit.BoundXYRadiusSq) return;
|
||||
|
||||
float PitSDF;
|
||||
if (DZ <= 0.0f)
|
||||
{
|
||||
// Shaft: tapered cylinder with flared opening
|
||||
float DepthBelow = -DZ;
|
||||
float FlareFactor = FMath::Clamp(1.0f - DepthBelow / Pit.FlareDist, 0.0f, 1.0f);
|
||||
FlareFactor = FlareFactor * FlareFactor;
|
||||
float EffRadius = Pit.Radius + Pit.FlareExtra * FlareFactor;
|
||||
PitSDF = FMath::Sqrt(XYDistSq) - EffRadius;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Above anchor: only the blend fringe matters here.
|
||||
// We still pass a SDF so SmoothMin can soften the rim from above.
|
||||
// Treat this zone as a flat disc at TopZ (XY cylinder, no Z factor)
|
||||
// so the blend fades horizontally into the room floor.
|
||||
PitSDF = FMath::Sqrt(XYDistSq) - (Pit.Radius + Pit.FlareExtra);
|
||||
}
|
||||
|
||||
CaveSDF = VoxelSDF::SmoothMin(CaveSDF, PitSDF, Pit.BlendK);
|
||||
};
|
||||
VF_ForEachChunkSDFSpatialCandidate(
|
||||
SDFCache.PitSpatialIndex, SDFCache.Pits.Num(), WorldX, WorldY, EvaluatePit,
|
||||
bUseSpatialIndex);
|
||||
|
||||
auto EvaluateChimney = [&](int32 ChimneyIndex)
|
||||
{
|
||||
// Above anchor: only the blend fringe matters here.
|
||||
// We still pass a SDF so SmoothMin can soften the rim from above.
|
||||
// Treat this zone as a flat disc at TopZ (XY cylinder, no Z factor)
|
||||
// so the blend fades horizontally into the room floor.
|
||||
PitSDF = FMath::Sqrt(XYDistSq) - (Pit.Radius + Pit.FlareExtra);
|
||||
}
|
||||
const FCachedChimney& Chim = SDFCache.Chimneys[ChimneyIndex];
|
||||
const float DZ = WorldZ - Chim.BottomZ; // Positive = above anchor (shaft)
|
||||
if (-DZ >= Chim.BlendK) return; // Below even the blend fringe
|
||||
if (DZ > Chim.Height + Chim.BlendK) return;
|
||||
|
||||
CaveSDF = VoxelSDF::SmoothMin(CaveSDF, PitSDF, Pit.BlendK);
|
||||
};
|
||||
VF_ForEachChunkSDFSpatialCandidate(
|
||||
SDFCache.PitSpatialIndex, SDFCache.Pits.Num(), WorldX, WorldY, EvaluatePit,
|
||||
bUseSpatialIndex);
|
||||
const float DX = WorldX - Chim.CenterX;
|
||||
const float DY = WorldY - Chim.CenterY;
|
||||
const float XYDistSq = DX * DX + DY * DY;
|
||||
if (XYDistSq > Chim.BoundXYRadiusSq) return;
|
||||
|
||||
auto EvaluateChimney = [&](int32 ChimneyIndex)
|
||||
{
|
||||
const FCachedChimney& Chim = SDFCache.Chimneys[ChimneyIndex];
|
||||
const float DZ = WorldZ - Chim.BottomZ; // Positive = above anchor (shaft)
|
||||
if (-DZ >= Chim.BlendK) return; // Below even the blend fringe
|
||||
if (DZ > Chim.Height + Chim.BlendK) return;
|
||||
float ChmSDF;
|
||||
if (DZ >= 0.0f)
|
||||
{
|
||||
float FlareFactor = FMath::Clamp(1.0f - DZ / Chim.FlareDist, 0.0f, 1.0f);
|
||||
FlareFactor = FlareFactor * FlareFactor;
|
||||
float EffRadius = Chim.Radius + Chim.FlareExtra * FlareFactor;
|
||||
ChmSDF = FMath::Sqrt(XYDistSq) - EffRadius;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Below anchor: flat disc blend into room ceiling
|
||||
ChmSDF = FMath::Sqrt(XYDistSq) - (Chim.Radius + Chim.FlareExtra);
|
||||
}
|
||||
|
||||
const float DX = WorldX - Chim.CenterX;
|
||||
const float DY = WorldY - Chim.CenterY;
|
||||
const float XYDistSq = DX * DX + DY * DY;
|
||||
if (XYDistSq > Chim.BoundXYRadiusSq) return;
|
||||
|
||||
float ChmSDF;
|
||||
if (DZ >= 0.0f)
|
||||
{
|
||||
float FlareFactor = FMath::Clamp(1.0f - DZ / Chim.FlareDist, 0.0f, 1.0f);
|
||||
FlareFactor = FlareFactor * FlareFactor;
|
||||
float EffRadius = Chim.Radius + Chim.FlareExtra * FlareFactor;
|
||||
ChmSDF = FMath::Sqrt(XYDistSq) - EffRadius;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Below anchor: flat disc blend into room ceiling
|
||||
ChmSDF = FMath::Sqrt(XYDistSq) - (Chim.Radius + Chim.FlareExtra);
|
||||
}
|
||||
|
||||
CaveSDF = VoxelSDF::SmoothMin(CaveSDF, ChmSDF, Chim.BlendK);
|
||||
};
|
||||
VF_ForEachChunkSDFSpatialCandidate(
|
||||
SDFCache.ChimneySpatialIndex, SDFCache.Chimneys.Num(),
|
||||
WorldX, WorldY, EvaluateChimney, bUseSpatialIndex);
|
||||
CaveSDF = VoxelSDF::SmoothMin(CaveSDF, ChmSDF, Chim.BlendK);
|
||||
};
|
||||
VF_ForEachChunkSDFSpatialCandidate(
|
||||
SDFCache.ChimneySpatialIndex, SDFCache.Chimneys.Num(),
|
||||
WorldX, WorldY, EvaluateChimney, bUseSpatialIndex);
|
||||
}
|
||||
|
||||
// Convert SDF to density carving:
|
||||
// CaveSDF < 0 means we're inside a room/tunnel/pit → carve to air
|
||||
@@ -3563,6 +3573,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
||||
// post-disturbance tail can consume the result without allocating an FVoxelOpSample or
|
||||
// invoking the interpreted stack a second time for this sample.
|
||||
if (OutTunnelCore != nullptr
|
||||
&& !VoxelDensityAblation::IsTunnelCoreOff()
|
||||
&& Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f)
|
||||
{
|
||||
// The no-column form is the exact scalar reference path: it performs the world-bound and
|
||||
@@ -3585,10 +3596,11 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
||||
// seal / modifiers, so we jump to Step 5 instead of returning early.
|
||||
const float DetailThreshold = Params.SDFBlendRadius * 3.0f;
|
||||
const bool bNearCaveSurface = (CaveSDF < DetailThreshold) && (CaveSDF < FLT_MAX);
|
||||
const bool bRunDetail = !VoxelDensityAblation::IsDetailOpsOff() && bNearCaveSurface;
|
||||
if (bCollectFusedDiagnostics && VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
bNearCaveSurface
|
||||
bRunDetail
|
||||
? VoxelDensityProfile::ECounter::FusedTunnelDetailSamples
|
||||
: VoxelDensityProfile::ECounter::FusedTunnelDetailSkipped);
|
||||
}
|
||||
@@ -3606,7 +3618,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
||||
// The noise type (fBM, Ridged, Mixed) and optional domain warping are
|
||||
// per-strate settings, so each strate can have fundamentally different
|
||||
// wall character — smooth lava tubes vs craggy erosion cliffs.
|
||||
if (bNearCaveSurface && Params.SurfaceRoughness > 0.0f)
|
||||
if (bRunDetail && Params.SurfaceRoughness > 0.0f)
|
||||
{
|
||||
float RoughnessDepth = Params.SurfaceRoughness * 2.0f;
|
||||
float DistFromSurface = FMath::Abs(CaveSDF);
|
||||
@@ -3729,7 +3741,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
||||
//=========================================================================
|
||||
// All terrain ops only affect density near cave walls.
|
||||
// Deep-solid voxels skip this entire block (bNearCaveSurface = false).
|
||||
if (bNearCaveSurface)
|
||||
if (bRunDetail)
|
||||
{
|
||||
//=========================================================================
|
||||
// PER-ROOM TERRAIN PARAMS
|
||||
@@ -4276,10 +4288,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
||||
}
|
||||
|
||||
bool bWormBlockProofComputed = false;
|
||||
const int32 WormLatticeStep = VoxelWormField::GetLatticeStep();
|
||||
const bool bSkipWormBlock = VoxelWormField::GetNoiseMode() == 0
|
||||
&& WormLatticeStep <= 1
|
||||
&& GVoxelForgeWormBlockSkip != 0
|
||||
const bool bSkipWormBlock = GVoxelForgeWormBlockSkip != 0
|
||||
&& VF_TryGetWormBlockSkip(
|
||||
DensityCacheOwnerId, SeedU, ParamsFingerprint, LayoutVersion,
|
||||
WorldX, WorldY, WorldZ, Params, bWormBlockProofComputed);
|
||||
@@ -4305,7 +4314,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
||||
SeedU};
|
||||
const float WormValue = VoxelWormField::Evaluate(
|
||||
WorldX, WorldY, WorldZ, Params.WormThreshold,
|
||||
WormParameters, WormLatticeStep);
|
||||
WormParameters);
|
||||
|
||||
if (WormValue < Params.WormThreshold)
|
||||
{
|
||||
@@ -4370,8 +4379,10 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
||||
// the graph's own tunnel floor/core are structural route geometry. Direct callers of
|
||||
// GetDensityWithParams retain this historical tail; GetDensityAt skips it and applies the
|
||||
// same predicates once in its common MC-space post section below both generation paths.
|
||||
const bool bTunnelSupportFloor = VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
WorldX, WorldY, WorldZ, SDFCache, bUseSpatialIndex);
|
||||
const bool bTunnelCoreEnabled = !VoxelDensityAblation::IsTunnelCoreOff();
|
||||
const bool bTunnelSupportFloor = bTunnelCoreEnabled
|
||||
&& VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
WorldX, WorldY, WorldZ, SDFCache, bUseSpatialIndex);
|
||||
// Keep the direct legacy entry point capability-safe for callers outside the canonical
|
||||
// operator-stack path; its support band follows the same relieved swept floor.
|
||||
if (bTunnelSupportFloor)
|
||||
@@ -4388,7 +4399,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
||||
}
|
||||
Density = FMath::Max(Density, StructuralSolidDensity);
|
||||
}
|
||||
const float TunnelCoreSDF = (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f)
|
||||
const float TunnelCoreSDF = bTunnelCoreEnabled
|
||||
&& (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f)
|
||||
? VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
|
||||
WorldX, WorldY, WorldZ, SDFCache, bUseSpatialIndex)
|
||||
: FLT_MAX;
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
#include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord
|
||||
#include "VoxelCaveMorphology.h" // For VoxelSDF and VoxelHash
|
||||
#include "VoxelDensityPrimitives.h" // Shared passage carve polarity/strength
|
||||
#include "VoxelDensityAblation.h" // Fingerprinted development-only stage overrides
|
||||
#include "VoxelDensityProfile.h" // Opt-in targeted per-voxel attribution
|
||||
#include "VoxelStartupTrace.h"
|
||||
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
||||
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
|
||||
#include "VoxelWormField.h" // World-changing worm evaluator mode/lattice identity
|
||||
#include "HAL/IConsoleManager.h"
|
||||
#include "Misc/CommandLine.h"
|
||||
#include "Misc/Crc.h"
|
||||
@@ -2487,6 +2487,11 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
|
||||
float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float WorldZ) const
|
||||
{
|
||||
if (VoxelDensityAblation::IsPassageCarvingOff())
|
||||
{
|
||||
return FLT_MAX;
|
||||
}
|
||||
|
||||
const FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||
@@ -2552,7 +2557,7 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
|
||||
// projecting once here selects the nearest immutable floor segment for this sample.
|
||||
// Unknown/malformed descriptors deliberately retain the bare capsule and the legacy
|
||||
// diagnostic backstop remains available.
|
||||
if (P.bNativeFloorEnabled)
|
||||
if (P.bNativeFloorEnabled && !VoxelDensityAblation::IsNativeFloorOff())
|
||||
{
|
||||
float NativeFloorZ = 0.0f;
|
||||
float NativeSupportRadius = 0.0f;
|
||||
@@ -2593,8 +2598,11 @@ void UVoxelStrateManager::ApplyPassageModifier(
|
||||
{
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::PassageModifier);
|
||||
const float ModSDF = EvaluateModifierSDF(WorldX, WorldY, WorldZ);
|
||||
VF_ApplyPassageCarving(Density, ModSDF, BaseDensity, SealThickness);
|
||||
if (!VoxelDensityAblation::IsPassageCarvingOff())
|
||||
{
|
||||
const float ModSDF = EvaluateModifierSDF(WorldX, WorldY, WorldZ);
|
||||
VF_ApplyPassageCarving(Density, ModSDF, BaseDensity, SealThickness);
|
||||
}
|
||||
ApplyPassageLandingAir(Density, WorldX, WorldY, WorldZ, BaseDensity, SealThickness);
|
||||
|
||||
const FIntVector ChunkCoord(
|
||||
@@ -2607,15 +2615,15 @@ void UVoxelStrateManager::ApplyPassageModifier(
|
||||
for (const int32 PassageIndex : Nearby)
|
||||
{
|
||||
const FVoxelPassage& Passage = PassageData[PassageIndex];
|
||||
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker()
|
||||
const bool bLandingFloor = !VoxelDensityAblation::IsLandingPostsOff()
|
||||
&& (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
||||
// A valid native profile owns its D-floor in the shape and in the final MC
|
||||
// composition. Keep the old support slab only as a fail-closed fallback for a
|
||||
// malformed/non-native descriptor; it must not patch a floor the shape already owns.
|
||||
|| (bPassageSupportFloorWritesEnabled
|
||||
&& !Passage.bNativeFloorEnabled
|
||||
&& VF_IsWalkableTunnelFloor(Passage, Position, this, PassageIndex))))
|
||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding));
|
||||
const bool bLegacySupportFloor = !VoxelDensityAblation::IsPassageStructuralPostsOff()
|
||||
&& bPassageSupportFloorWritesEnabled
|
||||
&& !Passage.bNativeFloorEnabled
|
||||
&& VF_IsWalkableTunnelFloor(Passage, Position, this, PassageIndex);
|
||||
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker()
|
||||
&& (bLandingFloor || bLegacySupportFloor))
|
||||
{
|
||||
// This is the one bidirectional part of PassageCarveOp: a floor is a proved solid
|
||||
// support slab. It is deliberately applied after the air carve so a tube can never
|
||||
@@ -2636,6 +2644,10 @@ void UVoxelStrateManager::ApplyPassageCarvingOnly(
|
||||
float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity, float SealThickness) const
|
||||
{
|
||||
if (VoxelDensityAblation::IsPassageCarvingOff())
|
||||
{
|
||||
return;
|
||||
}
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::PassageModifier);
|
||||
const float ModSDF = EvaluateModifierSDF(WorldX, WorldY, WorldZ);
|
||||
@@ -2646,6 +2658,10 @@ void UVoxelStrateManager::ApplyPassageLandingAir(
|
||||
float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity, float SealThickness) const
|
||||
{
|
||||
if (VoxelDensityAblation::IsLandingPostsOff())
|
||||
{
|
||||
return;
|
||||
}
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::PassageLandingAir);
|
||||
const FIntVector ChunkCoord(
|
||||
@@ -2710,6 +2726,10 @@ void UVoxelStrateManager::ApplyPassageTunnelAir(
|
||||
float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity, float SealThickness) const
|
||||
{
|
||||
if (VoxelDensityAblation::IsPassageStructuralPostsOff())
|
||||
{
|
||||
return;
|
||||
}
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::PassageTunnelAir);
|
||||
const FVector Position(WorldX, WorldY, WorldZ);
|
||||
@@ -2740,6 +2760,11 @@ void UVoxelStrateManager::ApplyPassageTunnelAirMC(
|
||||
void UVoxelStrateManager::ApplyPassageLandingFloorMC(
|
||||
float& Density, float WorldX, float WorldY, float WorldZ, float BaseDensity) const
|
||||
{
|
||||
if (VoxelDensityAblation::IsLandingPostsOff()
|
||||
&& VoxelDensityAblation::IsPassageStructuralPostsOff())
|
||||
{
|
||||
return;
|
||||
}
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::PassageLandingFloor);
|
||||
const FIntVector ChunkCoord(
|
||||
@@ -2752,12 +2777,15 @@ void UVoxelStrateManager::ApplyPassageLandingFloorMC(
|
||||
for (const int32 PassageIndex : Nearby)
|
||||
{
|
||||
const FVoxelPassage& Passage = PassageData[PassageIndex];
|
||||
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker()
|
||||
const bool bLandingFloor = !VoxelDensityAblation::IsLandingPostsOff()
|
||||
&& (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
||||
|| (bPassageSupportFloorWritesEnabled
|
||||
&& !Passage.bNativeFloorEnabled
|
||||
&& VF_IsWalkableTunnelFloor(Passage, Position, this, PassageIndex))))
|
||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding));
|
||||
const bool bLegacySupportFloor = !VoxelDensityAblation::IsPassageStructuralPostsOff()
|
||||
&& bPassageSupportFloorWritesEnabled
|
||||
&& !Passage.bNativeFloorEnabled
|
||||
&& VF_IsWalkableTunnelFloor(Passage, Position, this, PassageIndex);
|
||||
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker()
|
||||
&& (bLandingFloor || bLegacySupportFloor))
|
||||
{
|
||||
// Result is in MC convention here (negative = solid). This reassertion is the
|
||||
// structural floor backstop after the optional MC-space disturbance layer.
|
||||
@@ -2790,6 +2818,13 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
|
||||
float BaseDensity, float SealThickness,
|
||||
bool bProtectAuthoredTunnelFloor) const
|
||||
{
|
||||
const bool bDisableStructuralPosts = VoxelDensityAblation::IsPassageStructuralPostsOff();
|
||||
const bool bDisableLandingPosts = VoxelDensityAblation::IsLandingPostsOff();
|
||||
if (bDisableStructuralPosts && bDisableLandingPosts)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::PassageStructuralPosts);
|
||||
|
||||
@@ -2804,7 +2839,7 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
|
||||
const FVector Position(WorldX, WorldY, WorldZ);
|
||||
const bool bSuppressFloor =
|
||||
VoxelPassageGeometry::VerticalShaftConnectorAirMarker();
|
||||
bool bAnyPassageFloor = false;
|
||||
bool bAnyLandingFloor = false;
|
||||
bool bLegacyTunnelSupportFloor = false;
|
||||
bool bAnyRoomFloor = false;
|
||||
bool bWalkableAir = false;
|
||||
@@ -2819,36 +2854,37 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
|
||||
const FVoxelPassage& Passage = PassageData[PassageIndex];
|
||||
|
||||
const bool bTunnelSupportFloor =
|
||||
bPassageSupportFloorWritesEnabled
|
||||
!bDisableStructuralPosts
|
||||
&& bPassageSupportFloorWritesEnabled
|
||||
&& !Passage.bNativeFloorEnabled
|
||||
&& VF_IsWalkableTunnelFloor(Passage, Position, this, PassageIndex);
|
||||
if (!bSuppressFloor
|
||||
if (!bSuppressFloor && !bDisableLandingPosts
|
||||
&& (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
||||
|| bTunnelSupportFloor))
|
||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)))
|
||||
{
|
||||
bAnyPassageFloor = true;
|
||||
bAnyLandingFloor = true;
|
||||
}
|
||||
if (!bSuppressFloor && bTunnelSupportFloor)
|
||||
if (!bSuppressFloor && !bDisableStructuralPosts && bTunnelSupportFloor)
|
||||
{
|
||||
bLegacyTunnelSupportFloor = true;
|
||||
}
|
||||
|
||||
if (!bSuppressFloor
|
||||
if (!bSuppressFloor && !bDisableLandingPosts
|
||||
&& (VF_IsPassageRoomFloor(Position, Passage.UpperLanding)
|
||||
|| VF_IsPassageRoomFloor(Position, Passage.LowerLanding)))
|
||||
{
|
||||
bAnyRoomFloor = true;
|
||||
}
|
||||
|
||||
if (!bWalkableAir
|
||||
if (!bDisableStructuralPosts && !bWalkableAir
|
||||
&& FVector::DistSquared(Position, Passage.BoundCenter) <= Passage.BoundRadiusSq
|
||||
&& VF_IsWalkableTunnelAir(Passage, Position, this, PassageIndex))
|
||||
{
|
||||
bWalkableAir = true;
|
||||
}
|
||||
|
||||
if (FVector::DistSquared(Position, Passage.BoundCenter) <= Passage.BoundRadiusSq)
|
||||
if (!bDisableLandingPosts
|
||||
&& FVector::DistSquared(Position, Passage.BoundCenter) <= Passage.BoundRadiusSq)
|
||||
{
|
||||
const FVoxelPassageLanding* Landings[] = {
|
||||
&Passage.UpperLanding, &Passage.LowerLanding };
|
||||
@@ -2865,8 +2901,8 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
|
||||
// budget. A neighbouring inter-strate landing must not reopen that finite support band; its
|
||||
// carve remains valid everywhere else. Legacy callers leave this false and retain the old
|
||||
// passage-owned behavior.
|
||||
if (!bProtectAuthoredTunnelFloor
|
||||
&& !bAnyPassageFloor && MinLandingSDF < FLT_MAX)
|
||||
if (!bDisableLandingPosts && !bProtectAuthoredTunnelFloor
|
||||
&& !bAnyLandingFloor && !bLegacyTunnelSupportFloor && MinLandingSDF < FLT_MAX)
|
||||
{
|
||||
float InternalDensity = -Density;
|
||||
VF_ApplyPassageLandingCarving(
|
||||
@@ -2874,7 +2910,7 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
|
||||
Density = -InternalDensity;
|
||||
}
|
||||
|
||||
if (bAnyPassageFloor)
|
||||
if (bAnyLandingFloor || bLegacyTunnelSupportFloor)
|
||||
{
|
||||
Density = FMath::Min(Density, -BaseDensity);
|
||||
}
|
||||
@@ -2901,6 +2937,10 @@ void UVoxelStrateManager::ApplyPassageNativeFloorMC(
|
||||
float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity) const
|
||||
{
|
||||
if (VoxelDensityAblation::IsNativeFloorOff())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||
@@ -2955,6 +2995,10 @@ void UVoxelStrateManager::ApplyPassageLandingRoomFloorMC(
|
||||
float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity) const
|
||||
{
|
||||
if (VoxelDensityAblation::IsLandingPostsOff())
|
||||
{
|
||||
return;
|
||||
}
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::PassageLandingRoomFloor);
|
||||
const FIntVector ChunkCoord(
|
||||
@@ -2979,6 +3023,10 @@ void UVoxelStrateManager::ApplyPassageLandingRoomFloorMC(
|
||||
|
||||
bool UVoxelStrateManager::AnyPassageNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const
|
||||
{
|
||||
if (VoxelDensityAblation::IsPassageCarvingOff())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Le carve d'un passage atteint ModSDF < PASSAGE_BLEND_RADIUS (4, VoxelGenerator.cpp) au-delà de
|
||||
// sa surface ; BoundRadius inclut déjà rayon + blend, on re-pad par sécurité (conservatif).
|
||||
constexpr float CarvePad = 4.0f;
|
||||
@@ -5530,7 +5578,12 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
{
|
||||
Result.WormStrength = 0.0f;
|
||||
}
|
||||
return VF_ApplyRuntimeRoughnessOverrides(Result);
|
||||
Result = VF_ApplyRuntimeRoughnessOverrides(Result);
|
||||
if (VoxelDensityAblation::IsCaveWarpOff())
|
||||
{
|
||||
Result.CaveWarpStrength = 0.0f;
|
||||
}
|
||||
return Result;
|
||||
};
|
||||
|
||||
#if WITH_EDITOR
|
||||
@@ -5885,11 +5938,9 @@ uint64 UVoxelStrateManager::GetGenerationParamsFingerprint() const
|
||||
HashValue(Slot.Definition->bUseOperatorStack);
|
||||
HashValue(Slot.Definition->bEnableWorms);
|
||||
HashValue(VF_WormsForceOff());
|
||||
// The selected evaluator is a world input even though it is deliberately not part of
|
||||
// FStrateGenerationParams (the fixed 83-field memo key). Keep persistent/verdict keys
|
||||
// from crossing an explicit WormNoiseMode/WormLatticeStep experiment.
|
||||
HashValue(VoxelWormField::GetNoiseMode());
|
||||
HashValue(VoxelWormField::GetLatticeStep());
|
||||
// Development-only stage ablations change the field and therefore belong in every
|
||||
// persistent/verdict key even though they are not authored generation parameters.
|
||||
HashValue(VoxelDensityAblation::GetResolvedMask());
|
||||
HashValue(Slot.Definition->StrateHeightInChunks);
|
||||
HashValue(Slot.Definition->TransitionType);
|
||||
HashValue(Slot.Definition->TransitionBlendChunks);
|
||||
|
||||
@@ -1,98 +1,14 @@
|
||||
// VoxelWormField.cpp
|
||||
// Shared deterministic worm-field evaluation and the measured lattice experiment.
|
||||
// Shared deterministic exact worm-field evaluation.
|
||||
|
||||
#include "VoxelWormField.h"
|
||||
|
||||
#include "VoxelCaveMorphology.h" // VoxelHash::SeedOffset
|
||||
#include "VoxelGenerator.h" // per-worker canonical tile window
|
||||
#include "VoxelNoise.h"
|
||||
#include "VoxelTypes.h" // VOXEL_NOISE_SCALE
|
||||
|
||||
#include "HAL/IConsoleManager.h"
|
||||
#include "Misc/CommandLine.h"
|
||||
#include "Misc/Parse.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cfloat>
|
||||
#include <cstring>
|
||||
|
||||
namespace
|
||||
{
|
||||
// The selected production mode is resolved once, before a generation stack is allowed to
|
||||
// observe it, so all workers in a process use one world definition. Mode 0 remains the exact
|
||||
// legacy control for measurements and compatibility investigations.
|
||||
int32 GVoxelForgeWormLatticeStep = 0;
|
||||
std::atomic<int32> GResolvedWormLatticeStep(-1);
|
||||
int32 GVoxelForgeWormNoiseMode = 1;
|
||||
std::atomic<int32> GResolvedWormNoiseMode(-1);
|
||||
|
||||
// The value field has the same threshold/strength contract as legacy Perlin. This small
|
||||
// amplitude retune keeps the authored TunnelNetwork player-fit margin after replacing the
|
||||
// gradient evaluation with the cheaper scalar hash. It is a code default, not a CVar, so a
|
||||
// client cannot silently choose a different world.
|
||||
constexpr float CheapWormNoiseAmplitude = 1.0f;
|
||||
|
||||
FAutoConsoleVariableRef CVarVoxelForgeWormLatticeStep(
|
||||
TEXT("voxel.WormLatticeStep"),
|
||||
GVoxelForgeWormLatticeStep,
|
||||
TEXT("WORLD-CHANGING development experiment. 0=exact, 2/4=coarse worm lattice with "
|
||||
"trilinear interpolation. All multiplayer peers and regenerated worlds must use "
|
||||
"the same value. Default 0 until measured."),
|
||||
ECVF_Default);
|
||||
|
||||
FAutoConsoleVariableRef CVarVoxelForgeWormNoiseMode(
|
||||
TEXT("voxel.WormNoiseMode"),
|
||||
GVoxelForgeWormNoiseMode,
|
||||
TEXT("WORLD-CHANGING development experiment. 0=legacy gradient Perlin, 1=cheap "
|
||||
"deterministic value-noise worm fields. All multiplayer peers and regenerated "
|
||||
"worlds must use the same value. Default 1 (the measured production mode)."),
|
||||
ECVF_Default);
|
||||
|
||||
int32 ResolveLatticeStep()
|
||||
{
|
||||
int32 Existing = GResolvedWormLatticeStep.load(std::memory_order_acquire);
|
||||
if (Existing >= 0)
|
||||
{
|
||||
return Existing;
|
||||
}
|
||||
|
||||
int32 CommandLineValue = GVoxelForgeWormLatticeStep;
|
||||
FParse::Value(
|
||||
FCommandLine::Get(), TEXT("voxel.WormLatticeStep="), CommandLineValue);
|
||||
const int32 Resolved = CommandLineValue == 2 || CommandLineValue == 4
|
||||
? CommandLineValue : 0;
|
||||
int32 Expected = -1;
|
||||
if (!GResolvedWormLatticeStep.compare_exchange_strong(
|
||||
Expected, Resolved, std::memory_order_release, std::memory_order_acquire))
|
||||
{
|
||||
return Expected;
|
||||
}
|
||||
GVoxelForgeWormLatticeStep = Resolved;
|
||||
return Resolved;
|
||||
}
|
||||
|
||||
int32 ResolveNoiseMode()
|
||||
{
|
||||
int32 Existing = GResolvedWormNoiseMode.load(std::memory_order_acquire);
|
||||
if (Existing >= 0)
|
||||
{
|
||||
return Existing;
|
||||
}
|
||||
|
||||
int32 CommandLineValue = GVoxelForgeWormNoiseMode;
|
||||
FParse::Value(
|
||||
FCommandLine::Get(), TEXT("voxel.WormNoiseMode="), CommandLineValue);
|
||||
const int32 Resolved = CommandLineValue == 1 ? 1 : 0;
|
||||
int32 Expected = -1;
|
||||
if (!GResolvedWormNoiseMode.compare_exchange_strong(
|
||||
Expected, Resolved, std::memory_order_release, std::memory_order_acquire))
|
||||
{
|
||||
return Expected;
|
||||
}
|
||||
GVoxelForgeWormNoiseMode = Resolved;
|
||||
return Resolved;
|
||||
}
|
||||
|
||||
FORCEINLINE FVector3f MakeWormNoiseBase(
|
||||
float WorldX,
|
||||
float WorldY,
|
||||
@@ -117,16 +33,11 @@ namespace
|
||||
float WorldY,
|
||||
float WorldZ,
|
||||
const VoxelWormField::FParameters& Parameters,
|
||||
int32 NoiseMode,
|
||||
float NoiseScale)
|
||||
{
|
||||
const FVector3f NoiseBase = MakeWormNoiseBase(
|
||||
WorldX, WorldY, WorldZ, Parameters);
|
||||
return FMath::Abs(
|
||||
(NoiseMode == 0
|
||||
? VoxelNoise::Perlin3D(NoiseBase)
|
||||
: VoxelNoise::ValueNoise3D(NoiseBase, Parameters.Seed, 0x4D3A2F19u))
|
||||
* NoiseScale);
|
||||
return FMath::Abs(VoxelNoise::Perlin3D(NoiseBase) * NoiseScale);
|
||||
}
|
||||
|
||||
FORCEINLINE float EvaluateN2Field(
|
||||
@@ -134,17 +45,12 @@ namespace
|
||||
float WorldY,
|
||||
float WorldZ,
|
||||
const VoxelWormField::FParameters& Parameters,
|
||||
int32 NoiseMode,
|
||||
float NoiseScale)
|
||||
{
|
||||
const FVector3f NoiseBase = MakeWormNoiseBase(
|
||||
WorldX, WorldY, WorldZ, Parameters);
|
||||
const FVector3f N2Position = NoiseBase + FVector3f(137.0f, 259.0f, 431.0f);
|
||||
return FMath::Abs(
|
||||
(NoiseMode == 0
|
||||
? VoxelNoise::Perlin3D(N2Position)
|
||||
: VoxelNoise::ValueNoise3D(N2Position, Parameters.Seed, 0xA7C15E2Du))
|
||||
* NoiseScale);
|
||||
return FMath::Abs(VoxelNoise::Perlin3D(N2Position) * NoiseScale);
|
||||
}
|
||||
|
||||
FORCEINLINE float EvaluateExactField(
|
||||
@@ -154,288 +60,22 @@ namespace
|
||||
float Threshold,
|
||||
const VoxelWormField::FParameters& Parameters)
|
||||
{
|
||||
const int32 NoiseMode = ResolveNoiseMode();
|
||||
const float NoiseScale = VOXEL_NOISE_SCALE
|
||||
* (NoiseMode == 1 ? CheapWormNoiseAmplitude : 1.0f);
|
||||
const float NoiseScale = VOXEL_NOISE_SCALE;
|
||||
const float N1 = EvaluateN1Field(
|
||||
WorldX, WorldY, WorldZ, Parameters, NoiseMode, NoiseScale);
|
||||
WorldX, WorldY, WorldZ, Parameters, NoiseScale);
|
||||
|
||||
// N2 is non-negative, so this preserves the old exact evaluator's short-circuit.
|
||||
// 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, NoiseMode, NoiseScale);
|
||||
}
|
||||
|
||||
struct FLatticeNode
|
||||
{
|
||||
int32 X = 0;
|
||||
int32 Y = 0;
|
||||
int32 Z = 0;
|
||||
float Value = 0.0f;
|
||||
bool bValid = false;
|
||||
};
|
||||
|
||||
// The shared density grid is traversed z/y/x. Retaining two adjacent lattice planes is
|
||||
// necessary for the z transition; 2048 entries evicted the previous plane and made the
|
||||
// experiment slower than exact evaluation. This is fixed storage, not a heap allocation, and
|
||||
// remains deterministic on every worker.
|
||||
constexpr uint32 LatticeCacheSize = 32768;
|
||||
static_assert((LatticeCacheSize & (LatticeCacheSize - 1u)) == 0,
|
||||
"The lattice cache size must remain a power of two for the mask lookup.");
|
||||
constexpr int32 TileLatticeAxis = 32;
|
||||
static_assert(static_cast<uint32>(TileLatticeAxis * TileLatticeAxis * TileLatticeAxis)
|
||||
== LatticeCacheSize,
|
||||
"The direct tile lattice must use the fixed cache storage exactly.");
|
||||
|
||||
struct FLatticeCache
|
||||
{
|
||||
uint32 Seed = 0;
|
||||
uint32 FrequencyBits = 0;
|
||||
uint32 HorizontalBiasBits = 0;
|
||||
uint32 VerticalScaleBits = 0;
|
||||
int32 Step = 0;
|
||||
bool bUseTileWindow = false;
|
||||
FIntVector TileBaseGrid = FIntVector::ZeroValue;
|
||||
int32 TileSampleStep = 0;
|
||||
int32 TileCellsPerAxis = 0;
|
||||
bool bContextValid = false;
|
||||
FLatticeNode Nodes[LatticeCacheSize];
|
||||
|
||||
void SetContext(
|
||||
const VoxelWormField::FParameters& Parameters,
|
||||
int32 InStep,
|
||||
bool InUseTileWindow,
|
||||
const FIntVector& InTileOrigin,
|
||||
int32 InTileSampleStep,
|
||||
int32 InTileCellsPerAxis)
|
||||
{
|
||||
uint32 FrequencyBitsIn = 0;
|
||||
uint32 HorizontalBiasBitsIn = 0;
|
||||
uint32 VerticalScaleBitsIn = 0;
|
||||
FMemory::Memcpy(&FrequencyBitsIn, &Parameters.Frequency, sizeof(uint32));
|
||||
FMemory::Memcpy(&HorizontalBiasBitsIn, &Parameters.HorizontalBias, sizeof(uint32));
|
||||
FMemory::Memcpy(&VerticalScaleBitsIn, &Parameters.VerticalScale, sizeof(uint32));
|
||||
const double WormStepD = static_cast<double>(InStep);
|
||||
const FIntVector InTileBaseGrid(
|
||||
FMath::FloorToInt(static_cast<double>(InTileOrigin.X) / WormStepD) - 2,
|
||||
FMath::FloorToInt(static_cast<double>(InTileOrigin.Y) / WormStepD) - 2,
|
||||
FMath::FloorToInt(static_cast<double>(InTileOrigin.Z) / WormStepD) - 2);
|
||||
if (bContextValid
|
||||
&& Seed == Parameters.Seed
|
||||
&& FrequencyBits == FrequencyBitsIn
|
||||
&& HorizontalBiasBits == HorizontalBiasBitsIn
|
||||
&& VerticalScaleBits == VerticalScaleBitsIn
|
||||
&& Step == InStep
|
||||
&& bUseTileWindow == InUseTileWindow
|
||||
&& TileBaseGrid == InTileBaseGrid
|
||||
&& TileSampleStep == InTileSampleStep
|
||||
&& TileCellsPerAxis == InTileCellsPerAxis)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Seed = Parameters.Seed;
|
||||
FrequencyBits = FrequencyBitsIn;
|
||||
HorizontalBiasBits = HorizontalBiasBitsIn;
|
||||
VerticalScaleBits = VerticalScaleBitsIn;
|
||||
Step = InStep;
|
||||
bUseTileWindow = InUseTileWindow;
|
||||
TileBaseGrid = InTileBaseGrid;
|
||||
TileSampleStep = InTileSampleStep;
|
||||
TileCellsPerAxis = InTileCellsPerAxis;
|
||||
bContextValid = true;
|
||||
for (FLatticeNode& Node : Nodes)
|
||||
{
|
||||
Node.bValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
static uint32 Hash(int32 X, int32 Y, int32 Z)
|
||||
{
|
||||
uint32 H = static_cast<uint32>(X) * 0x9E3779B1u;
|
||||
H ^= static_cast<uint32>(Y) * 0x85EBCA77u;
|
||||
H ^= static_cast<uint32>(Z) * 0xC2B2AE3Du;
|
||||
H ^= H >> 16;
|
||||
H *= 0x7FEB352Du;
|
||||
H ^= H >> 15;
|
||||
return H;
|
||||
}
|
||||
|
||||
float Get(int32 X, int32 Y, int32 Z,
|
||||
const VoxelWormField::FParameters& Parameters,
|
||||
int32 NoiseMode,
|
||||
float NoiseScale)
|
||||
{
|
||||
const auto EvaluateNode = [this, &Parameters, NoiseMode, NoiseScale](FLatticeNode& Node,
|
||||
int32 NodeX, int32 NodeY,
|
||||
int32 NodeZ) -> float
|
||||
{
|
||||
if (Node.bValid && Node.X == NodeX && Node.Y == NodeY && Node.Z == NodeZ)
|
||||
{
|
||||
return Node.Value;
|
||||
}
|
||||
Node.X = NodeX;
|
||||
Node.Y = NodeY;
|
||||
Node.Z = NodeZ;
|
||||
const double StepD = static_cast<double>(Step);
|
||||
const float WorldX = static_cast<float>(static_cast<double>(NodeX) * StepD);
|
||||
const float WorldY = static_cast<float>(static_cast<double>(NodeY) * StepD);
|
||||
const float WorldZ = static_cast<float>(static_cast<double>(NodeZ) * StepD);
|
||||
// The lattice stores N1 only. The requested voxel uses this interpolated N1 for
|
||||
// the threshold decision; N2 remains an exact point query only in the carve
|
||||
// region. This removes the first noise call on the hot above-threshold majority
|
||||
// without making the second noise unconditional.
|
||||
Node.Value = EvaluateN1Field(
|
||||
WorldX, WorldY, WorldZ, Parameters, NoiseMode, NoiseScale);
|
||||
Node.bValid = true;
|
||||
return Node.Value;
|
||||
};
|
||||
|
||||
if (bUseTileWindow)
|
||||
{
|
||||
const int32 LocalX = X - TileBaseGrid.X;
|
||||
const int32 LocalY = Y - TileBaseGrid.Y;
|
||||
const int32 LocalZ = Z - TileBaseGrid.Z;
|
||||
if (LocalX >= 0 && LocalX < TileLatticeAxis
|
||||
&& LocalY >= 0 && LocalY < TileLatticeAxis
|
||||
&& LocalZ >= 0 && LocalZ < TileLatticeAxis)
|
||||
{
|
||||
FLatticeNode& Node = Nodes[
|
||||
(LocalZ * TileLatticeAxis + LocalY) * TileLatticeAxis + LocalX];
|
||||
return EvaluateNode(Node, X, Y, Z);
|
||||
}
|
||||
|
||||
// A malformed or unusually large tile window must not alter the field's finite
|
||||
// fallback semantics. It is outside the fixed direct table, so evaluate N1 once
|
||||
// for this sample rather than attempting an unbounded allocation.
|
||||
const double StepD = static_cast<double>(Step);
|
||||
return EvaluateN1Field(
|
||||
static_cast<float>(static_cast<double>(X) * StepD),
|
||||
static_cast<float>(static_cast<double>(Y) * StepD),
|
||||
static_cast<float>(static_cast<double>(Z) * StepD),
|
||||
Parameters, NoiseMode, NoiseScale);
|
||||
}
|
||||
|
||||
const uint32 Start = Hash(X, Y, Z) & (LatticeCacheSize - 1u);
|
||||
for (uint32 Probe = 0; Probe < LatticeCacheSize; ++Probe)
|
||||
{
|
||||
FLatticeNode& Node = Nodes[(Start + Probe) & (LatticeCacheSize - 1u)];
|
||||
if (Node.bValid)
|
||||
{
|
||||
if (Node.X == X && Node.Y == Y && Node.Z == Z)
|
||||
{
|
||||
return Node.Value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Node.X = X;
|
||||
Node.Y = Y;
|
||||
Node.Z = Z;
|
||||
return EvaluateNode(Node, X, Y, Z);
|
||||
}
|
||||
|
||||
// A saturated cache is exceptionally unlikely for one density block. Falling back to
|
||||
// a direct N1 evaluation keeps the result bounded if a caller exceeds it.
|
||||
const double StepD = static_cast<double>(Step);
|
||||
return EvaluateN1Field(
|
||||
static_cast<float>(static_cast<double>(X) * StepD),
|
||||
static_cast<float>(static_cast<double>(Y) * StepD),
|
||||
static_cast<float>(static_cast<double>(Z) * StepD),
|
||||
Parameters, NoiseMode, NoiseScale);
|
||||
}
|
||||
};
|
||||
|
||||
thread_local FLatticeCache GLatticeCache;
|
||||
|
||||
FORCEINLINE bool CanUseLattice(float WorldX, float WorldY, float WorldZ, int32 Step)
|
||||
{
|
||||
if (Step <= 1 || !FMath::IsFinite(WorldX) || !FMath::IsFinite(WorldY)
|
||||
|| !FMath::IsFinite(WorldZ))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The integer grid indices must survive the conversion back to a finite float world
|
||||
// coordinate. Normal VoxelForge worlds are far inside this envelope; pathological input
|
||||
// takes the exact path rather than changing overflow behaviour.
|
||||
constexpr double MaxGridCoordinate = 1000000000.0;
|
||||
return FMath::Abs(static_cast<double>(WorldX) / static_cast<double>(Step))
|
||||
<= MaxGridCoordinate
|
||||
&& FMath::Abs(static_cast<double>(WorldY) / static_cast<double>(Step))
|
||||
<= MaxGridCoordinate
|
||||
&& FMath::Abs(static_cast<double>(WorldZ) / static_cast<double>(Step))
|
||||
<= MaxGridCoordinate;
|
||||
}
|
||||
|
||||
FORCEINLINE float Lerp(float A, float B, float Alpha)
|
||||
{
|
||||
return A + (B - A) * Alpha;
|
||||
}
|
||||
|
||||
float EvaluateLattice(
|
||||
float WorldX,
|
||||
float WorldY,
|
||||
float WorldZ,
|
||||
const VoxelWormField::FParameters& Parameters,
|
||||
int32 Step,
|
||||
float NoiseScale)
|
||||
{
|
||||
const int32 NoiseMode = ResolveNoiseMode();
|
||||
const bool bUseTileWindow = VoxelGenLOD::TileCellsPerAxis > 0
|
||||
&& VoxelGenLOD::TileCellsPerAxis <= CHUNK_SIZE
|
||||
&& VoxelGenLOD::SampleStep > 0;
|
||||
GLatticeCache.SetContext(
|
||||
Parameters,
|
||||
Step,
|
||||
bUseTileWindow,
|
||||
VoxelGenLOD::TileOriginVoxels,
|
||||
VoxelGenLOD::SampleStep,
|
||||
VoxelGenLOD::TileCellsPerAxis);
|
||||
|
||||
const double StepD = static_cast<double>(Step);
|
||||
const double GX = static_cast<double>(WorldX) / StepD;
|
||||
const double GY = static_cast<double>(WorldY) / StepD;
|
||||
const double GZ = static_cast<double>(WorldZ) / StepD;
|
||||
const int32 X0 = FMath::FloorToInt(GX);
|
||||
const int32 Y0 = FMath::FloorToInt(GY);
|
||||
const int32 Z0 = FMath::FloorToInt(GZ);
|
||||
const float Tx = static_cast<float>(GX - static_cast<double>(X0));
|
||||
const float Ty = static_cast<float>(GY - static_cast<double>(Y0));
|
||||
const float Tz = static_cast<float>(GZ - static_cast<double>(Z0));
|
||||
|
||||
const float V000 = GLatticeCache.Get(X0, Y0, Z0, Parameters, NoiseMode, NoiseScale);
|
||||
const float V100 = GLatticeCache.Get(X0 + 1, Y0, Z0, Parameters, NoiseMode, NoiseScale);
|
||||
const float V010 = GLatticeCache.Get(X0, Y0 + 1, Z0, Parameters, NoiseMode, NoiseScale);
|
||||
const float V110 = GLatticeCache.Get(X0 + 1, Y0 + 1, Z0, Parameters, NoiseMode, NoiseScale);
|
||||
const float V001 = GLatticeCache.Get(X0, Y0, Z0 + 1, Parameters, NoiseMode, NoiseScale);
|
||||
const float V101 = GLatticeCache.Get(X0 + 1, Y0, Z0 + 1, Parameters, NoiseMode, NoiseScale);
|
||||
const float V011 = GLatticeCache.Get(X0, Y0 + 1, Z0 + 1, Parameters, NoiseMode, NoiseScale);
|
||||
const float V111 = GLatticeCache.Get(X0 + 1, Y0 + 1, Z0 + 1, Parameters, NoiseMode, NoiseScale);
|
||||
|
||||
const float X00 = Lerp(V000, V100, Tx);
|
||||
const float X10 = Lerp(V010, V110, Tx);
|
||||
const float X01 = Lerp(V001, V101, Tx);
|
||||
const float X11 = Lerp(V011, V111, Tx);
|
||||
return Lerp(Lerp(X00, X10, Ty), Lerp(X01, X11, Ty), Tz);
|
||||
WorldX, WorldY, WorldZ, Parameters, NoiseScale);
|
||||
}
|
||||
}
|
||||
|
||||
namespace VoxelWormField
|
||||
{
|
||||
int32 GetLatticeStep()
|
||||
{
|
||||
return ResolveLatticeStep();
|
||||
}
|
||||
|
||||
int32 GetNoiseMode()
|
||||
{
|
||||
return ResolveNoiseMode();
|
||||
}
|
||||
|
||||
float EvaluateExact(
|
||||
float WorldX,
|
||||
float WorldY,
|
||||
@@ -451,21 +91,8 @@ namespace VoxelWormField
|
||||
float WorldY,
|
||||
float WorldZ,
|
||||
float Threshold,
|
||||
const FParameters& Parameters,
|
||||
int32 LatticeStep)
|
||||
const FParameters& Parameters)
|
||||
{
|
||||
const int32 NoiseMode = ResolveNoiseMode();
|
||||
const bool bUseLattice = CanUseLattice(WorldX, WorldY, WorldZ, LatticeStep);
|
||||
const float NoiseScale = VOXEL_NOISE_SCALE
|
||||
* (NoiseMode == 1 ? CheapWormNoiseAmplitude : 1.0f);
|
||||
const float N1 = bUseLattice
|
||||
? EvaluateLattice(WorldX, WorldY, WorldZ, Parameters, LatticeStep, NoiseScale)
|
||||
: EvaluateN1Field(WorldX, WorldY, WorldZ, Parameters, NoiseMode, NoiseScale);
|
||||
if (N1 >= Threshold)
|
||||
{
|
||||
return N1;
|
||||
}
|
||||
return N1 + EvaluateN2Field(
|
||||
WorldX, WorldY, WorldZ, Parameters, NoiseMode, NoiseScale);
|
||||
return EvaluateExactField(WorldX, WorldY, WorldZ, Threshold, Parameters);
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "CoreMinimal.h"
|
||||
#include "VoxelTypes.h" // SmoothStep01
|
||||
#include "VoxelPassageGeometry.h" // shared body-sized landing/tunnel geometry
|
||||
#include "VoxelDensityAblation.h" // development-only world-changing measurements
|
||||
|
||||
//=============================================================================
|
||||
// SEAL DE FRONTIÈRE / BOUNDARY SEAL
|
||||
@@ -43,7 +44,7 @@ FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
|
||||
float StrateTopZ, float StrateBottomZ,
|
||||
float Thickness, float BaseDensity)
|
||||
{
|
||||
if (Thickness <= 0.0f) return;
|
||||
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
|
||||
@@ -71,6 +72,7 @@ FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
|
||||
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;
|
||||
|
||||
@@ -121,7 +123,7 @@ FORCEINLINE void VF_ApplyPassageLandingCarving(float& Density, float LandingSDF,
|
||||
FORCEINLINE void VF_ApplyOriginSpine(float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
|
||||
{
|
||||
if (Radius <= 0.0f) return;
|
||||
if (VoxelDensityAblation::IsOriginSpineOff() || Radius <= 0.0f) return;
|
||||
|
||||
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
||||
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
||||
@@ -153,7 +155,7 @@ FORCEINLINE void VF_ApplyOriginLandingAir(float& Density,
|
||||
float WorldX, float WorldY, float WorldZ,
|
||||
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
|
||||
{
|
||||
if (Radius <= 0.0f) return;
|
||||
if (VoxelDensityAblation::IsLandingPostsOff() || Radius <= 0.0f) return;
|
||||
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
||||
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
||||
StrateTopZ, StrateBottomZ, SealThickness, Radius);
|
||||
@@ -169,7 +171,7 @@ FORCEINLINE void VF_ApplyOriginLandingFloor(float& Density,
|
||||
float WorldX, float WorldY, float WorldZ,
|
||||
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
|
||||
{
|
||||
if (Radius <= 0.0f) return;
|
||||
if (VoxelDensityAblation::IsLandingPostsOff() || Radius <= 0.0f) return;
|
||||
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
||||
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
||||
StrateTopZ, StrateBottomZ, SealThickness, Radius);
|
||||
@@ -221,7 +223,8 @@ FORCEINLINE void VF_ApplyOriginLandingFloorMC(float& Density,
|
||||
FORCEINLINE void VF_ApplyXYEdgeSeal(float& Density, float WorldX, float WorldY,
|
||||
float WorldRadiusVoxels, float Thickness, float BaseDensity)
|
||||
{
|
||||
if (!(WorldRadiusVoxels > 0.0f) || !(Thickness > 0.0f) || !(BaseDensity > 0.0f)) return;
|
||||
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;
|
||||
@@ -256,7 +259,8 @@ FORCEINLINE void VF_ApplyXYEdgeSeal(float& Density, float WorldX, float WorldY,
|
||||
FORCEINLINE void VF_ApplyXYEdgeSealMC(float& Density, float WorldX, float WorldY,
|
||||
float WorldRadiusVoxels, float Thickness, float BaseDensity)
|
||||
{
|
||||
if (!(WorldRadiusVoxels > 0.0f) || !(Thickness > 0.0f)) return;
|
||||
if (VoxelDensityAblation::IsXYEdgeSealOff()
|
||||
|| !(WorldRadiusVoxels > 0.0f) || !(Thickness > 0.0f)) return;
|
||||
|
||||
float InternalDensity = -Density;
|
||||
VF_ApplyXYEdgeSeal(InternalDensity, WorldX, WorldY,
|
||||
|
||||
@@ -142,56 +142,6 @@ FORCEINLINE float Perlin3D(const FVector3f& P)
|
||||
return Perlin3D(P.X, P.Y, P.Z);
|
||||
}
|
||||
|
||||
// A smooth, deterministic value-noise sample for the worm-only cost experiment. It keeps the
|
||||
// same unit lattice and quintic interpolation as Perlin3D, but uses one seeded scalar hash per
|
||||
// corner instead of the eight gradient selections and dot products. It is intentionally not used
|
||||
// by the terrain's other noise fields; WormNoiseMode gates it as an explicit world-changing test.
|
||||
FORCEINLINE uint32 ValueNoiseHash(int32 X, int32 Y, int32 Z, uint32 Seed, uint32 Salt)
|
||||
{
|
||||
uint32 H = Detail::HashCorner(X, Y, Z) ^ Seed ^ Salt;
|
||||
H ^= H >> 16;
|
||||
H *= 0x7FEB352Du;
|
||||
H ^= H >> 15;
|
||||
return H;
|
||||
}
|
||||
|
||||
FORCEINLINE float ValueNoise3D(const FVector3f& P, uint32 Seed, uint32 Salt)
|
||||
{
|
||||
const float XFloor = FMath::FloorToFloat(P.X);
|
||||
const float YFloor = FMath::FloorToFloat(P.Y);
|
||||
const float ZFloor = FMath::FloorToFloat(P.Z);
|
||||
const int32 X = static_cast<int32>(XFloor);
|
||||
const int32 Y = static_cast<int32>(YFloor);
|
||||
const int32 Z = static_cast<int32>(ZFloor);
|
||||
const float Fx = P.X - XFloor;
|
||||
const float Fy = P.Y - YFloor;
|
||||
const float Fz = P.Z - ZFloor;
|
||||
const float Sx = Detail::Fade(Fx);
|
||||
const float Sy = Detail::Fade(Fy);
|
||||
const float Sz = Detail::Fade(Fz);
|
||||
constexpr float HashToSigned = 2.0f / 16777216.0f;
|
||||
const auto Sample = [Seed, Salt, HashToSigned](int32 IX, int32 IY, int32 IZ) -> float
|
||||
{
|
||||
return static_cast<float>(ValueNoiseHash(IX, IY, IZ, Seed, Salt) & 0x00FFFFFFu)
|
||||
* HashToSigned - 1.0f;
|
||||
};
|
||||
const float V000 = Sample(X, Y, Z);
|
||||
const float V100 = Sample(X + 1, Y, Z);
|
||||
const float V010 = Sample(X, Y + 1, Z);
|
||||
const float V110 = Sample(X + 1, Y + 1, Z);
|
||||
const float V001 = Sample(X, Y, Z + 1);
|
||||
const float V101 = Sample(X + 1, Y, Z + 1);
|
||||
const float V011 = Sample(X, Y + 1, Z + 1);
|
||||
const float V111 = Sample(X + 1, Y + 1, Z + 1);
|
||||
const float X00 = Detail::Lerp(V000, V100, Sx);
|
||||
const float X10 = Detail::Lerp(V010, V110, Sx);
|
||||
const float X01 = Detail::Lerp(V001, V101, Sx);
|
||||
const float X11 = Detail::Lerp(V011, V111, Sx);
|
||||
const float Y0 = Detail::Lerp(X00, X10, Sy);
|
||||
const float Y1 = Detail::Lerp(X01, X11, Sy);
|
||||
return Detail::Lerp(Y0, Y1, Sz);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// LIPSCHITZ BOUND FOR THE ACTUAL 3D FIELD
|
||||
//=============================================================================
|
||||
|
||||
@@ -16,16 +16,6 @@ namespace VoxelWormField
|
||||
uint32 Seed = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the selected development experiment mode.
|
||||
*
|
||||
* 0 is the exact field. 2 and 4 evaluate the exact field on that many-voxel lattice and
|
||||
* trilinearly interpolate it. This is intentionally a WORLD-CHANGING development switch:
|
||||
* it must not be changed between multiplayer peers or for an already generated world.
|
||||
*/
|
||||
VOXELFORGE_API int32 GetLatticeStep();
|
||||
VOXELFORGE_API int32 GetNoiseMode();
|
||||
|
||||
/** Evaluate the exact two-noise field, retaining the N1 threshold short-circuit. */
|
||||
VOXELFORGE_API float EvaluateExact(
|
||||
float WorldX,
|
||||
@@ -34,12 +24,11 @@ namespace VoxelWormField
|
||||
float Threshold,
|
||||
const FParameters& Parameters);
|
||||
|
||||
/** Evaluate the exact field or its selected cached lattice approximation. */
|
||||
/** Evaluate the exact two-noise field. */
|
||||
VOXELFORGE_API float Evaluate(
|
||||
float WorldX,
|
||||
float WorldY,
|
||||
float WorldZ,
|
||||
float Threshold,
|
||||
const FParameters& Parameters,
|
||||
int32 LatticeStep);
|
||||
const FParameters& Parameters);
|
||||
}
|
||||
@@ -29,11 +29,11 @@
|
||||
#include "VoxelCaveMorphology.h"
|
||||
#include "VoxelDiffLayer.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
#include "VoxelDensityAblation.h"
|
||||
#include "VoxelDensityProfile.h"
|
||||
#include "VoxelGenerator.h"
|
||||
#include "VoxelMarchingCubesMesher.h"
|
||||
#include "VoxelNoise.h"
|
||||
#include "VoxelWormField.h"
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelStackSampler.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
@@ -4202,8 +4202,8 @@ FString BuildManifestJson(
|
||||
{
|
||||
Writer->WriteValue(TEXT("worm_strength"), static_cast<double>(Arguments.WormStrength));
|
||||
}
|
||||
Writer->WriteValue(TEXT("worm_lattice_step"), VoxelWormField::GetLatticeStep());
|
||||
Writer->WriteValue(TEXT("worm_noise_mode"), VoxelWormField::GetNoiseMode());
|
||||
Writer->WriteValue(TEXT("tunnel_ablation_mask"),
|
||||
static_cast<int64>(VoxelDensityAblation::GetResolvedMask()));
|
||||
WriteTunnelFloorOverrides(*Writer, Arguments);
|
||||
Writer->WriteValue(TEXT("room_mouth_rise_override"), Arguments.bRoomMouthRiseOverride);
|
||||
if (Arguments.bRoomMouthRiseOverride)
|
||||
@@ -4478,8 +4478,8 @@ FString BuildExploreJson(
|
||||
{
|
||||
Writer->WriteValue(TEXT("worm_strength"), static_cast<double>(Arguments.WormStrength));
|
||||
}
|
||||
Writer->WriteValue(TEXT("worm_lattice_step"), VoxelWormField::GetLatticeStep());
|
||||
Writer->WriteValue(TEXT("worm_noise_mode"), VoxelWormField::GetNoiseMode());
|
||||
Writer->WriteValue(TEXT("tunnel_ablation_mask"),
|
||||
static_cast<int64>(VoxelDensityAblation::GetResolvedMask()));
|
||||
WriteTunnelFloorOverrides(*Writer, Arguments);
|
||||
Writer->WriteValue(TEXT("room_mouth_rise_override"), Arguments.bRoomMouthRiseOverride);
|
||||
if (Arguments.bRoomMouthRiseOverride)
|
||||
|
||||
Reference in new issue
Block a user