feat(passages): passages are D-tunnels now -- legacy support writer fires ZERO
The owner was right and said so plainly: "the tunnels should be a flat D with then a
bit of noise added, as it's most basic shape -- this alone should make all tunnels not
over X steepness to be walkable." Codex had reached the same conclusion from the other
direction ("the native fix is to author the inter-strate passage floor profile during
passage construction"); it had measured the current position while he described the
destination. This is that refactor.
Walkable inter-strate passages now bake their floor profile and bounded relief during
GeneratePassages. Evaluation composes a D-shaped SmoothMax floor, with final MC
ownership after the generic writers -- the same ordering fix that made the tunnel floor
native in 2260c4d.
native compositions 7,629
legacy support fires 0
floor-profile fallbacks 0
player-fit cells 20,804 -> 20,830 (+26)
walk-reachable 10,884 -> 10,909 (+25)
climb-reachable 10,911
connectivity arrival -> departure, Connected
largest component 10,909
⭐ Support-ON and support-OFF now produce IDENTICAL export geometry (hash 07C14005,
82,273 v / 153,346 t, mesh 2.981 s vs 2.960 s). The writer that previously cost
3,359 reachable cells when disabled is now inert -- the shape provides what it used to
patch. That is the ~17.43% of banded pixels from 4f8b4a9 addressed at the source.
The legacy writer stays only as a fail-closed fallback for a malformed/non-native
passage. It is no longer load-bearing.
PUSHBACK, and it is the right restraint: only passages carrying
`bWalkableTunnelContract` -- currently the authored `Straight` style -- receive the
guaranteed D-floor. Spiral, Cascading and Worm styles do NOT silently gain a
walkability promise without a separate gradient proof. Correct: a guarantee nobody
proved is worse than no guarantee.
ROUGHNESS -- the owner's report, measured properly this time and CLOSED
He confirmed twice that changing roughness type/frequency changed generation time from
startup. The earlier sweep was a single 128^3 export at LOD0 and could not have seen
it, so this one used the game path, fixed streaming centre, identical 850-tile workload:
skip rate LOD0 70.8% LOD1 57.3% LOD2 32.6% (Cellular 31.8%) LOD3 2.5% LOD4 0%
gen p95 s FBM .472/.369/.083/.087/.145 Ridged .473/.365/.084/.084/.143
Mixed .474/.358/.084/.090/.149 Cellular .479/.360/.083/.089/.161
FBM frequency 0.025-0.2: flat across every LOD
No classifier-bound collapse by type or frequency. The startup difference was
workload/streaming behaviour, not a type-specific bound defect. No unsafe bounds change
was made to chase it.
⚠️ Worth noting for later: the skip rate collapses with coarseness -- 70.8% at LOD0 to
0% at LOD4. That is expected (a LOD4 tile spans a huge volume and almost certainly
contains something) but it means the classifier does nothing for exactly the tiles that
cover the most ground.
VERIFIED BY ME: build 0 errors; export 1.418 / 1.445 / 1.458 s (4f8b4a9 was
1.353 / 1.430 / 1.432); 153,346 triangles. Peak game RSS ~2.05 GB.
Sources consulted: Epic's UE task-systems documentation for the worker/game-path
distinction, and Lorensen-Cline for the discrete isosurface context.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
1 parent
4f8b4a9a27
commit
adb34dffa9
6 files changed
+704
-28
No files matched your search
@@ -427,6 +427,8 @@ namespace VoxelDensityProfile
|
||||
case ECounter::TunnelFloorProfileLedges2To4Voxels:return TEXT("TunnelFloorProfileLedges2To4Voxels");
|
||||
case ECounter::TunnelFloorProfileLedges4To8Voxels:return TEXT("TunnelFloorProfileLedges4To8Voxels");
|
||||
case ECounter::TunnelFloorProfileLedges8PlusVoxels:return TEXT("TunnelFloorProfileLedges8PlusVoxels");
|
||||
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");
|
||||
|
||||
@@ -2151,7 +2151,6 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
StrateManager->ApplyPassageStructuralPostsMC(
|
||||
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
|
||||
CP_Dist.BoundarySealThickness, bProtectAuthoredTunnelFloor);
|
||||
|
||||
// Disturbance features are authored as a generic MC-space post and may add a ridge or
|
||||
// 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,
|
||||
@@ -2256,6 +2255,12 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
|
||||
}
|
||||
}
|
||||
// Inter-strate passages own the same final D-floor contract as graph tunnels. This is
|
||||
// composed from the immutable construction-time profile after generic writers and the
|
||||
// graph overlap, so the legacy passage support slab can be disabled without losing the
|
||||
// floor to a disturbance refill.
|
||||
StrateManager->ApplyPassageNativeFloorMC(
|
||||
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
|
||||
DensityStructuralPostsTimer.End();
|
||||
}
|
||||
else
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
#include "VoxelDensityProfile.h" // Opt-in targeted per-voxel attribution
|
||||
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
||||
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
|
||||
#include "Misc/CommandLine.h"
|
||||
#include "Misc/Crc.h"
|
||||
#include "Misc/Parse.h"
|
||||
#include "UObject/UObjectGlobals.h"
|
||||
|
||||
#include <atomic>
|
||||
@@ -19,6 +21,111 @@
|
||||
namespace
|
||||
{
|
||||
std::atomic<uint64> GNextStrateManagerLifetimeId { 0 };
|
||||
|
||||
struct FRuntimeRoughnessOverrides
|
||||
{
|
||||
bool bSurfaceRoughness = false;
|
||||
float SurfaceRoughness = 0.0f;
|
||||
bool bFrequency = false;
|
||||
float Frequency = 0.0f;
|
||||
bool bNoiseType = false;
|
||||
EVoxelNoiseType NoiseType = EVoxelNoiseType::FBM;
|
||||
};
|
||||
|
||||
const FRuntimeRoughnessOverrides& VF_GetRuntimeRoughnessOverrides()
|
||||
{
|
||||
// These are diagnostic command-line inputs, captured once per process. Keeping the
|
||||
// override at the params boundary makes every chunk, classifier, and mesher see the same
|
||||
// immutable values without putting a command-line parse in the voxel hot loop.
|
||||
static const FRuntimeRoughnessOverrides Overrides = []()
|
||||
{
|
||||
FRuntimeRoughnessOverrides Result;
|
||||
const FString CommandLine(FCommandLine::Get());
|
||||
|
||||
Result.bSurfaceRoughness = FParse::Value(
|
||||
*CommandLine, TEXT("voxel.surfaceroughness="), Result.SurfaceRoughness);
|
||||
if (Result.bSurfaceRoughness
|
||||
&& (!FMath::IsFinite(Result.SurfaceRoughness) || Result.SurfaceRoughness < 0.0f))
|
||||
{
|
||||
Result.bSurfaceRoughness = false;
|
||||
}
|
||||
|
||||
Result.bFrequency = FParse::Value(
|
||||
*CommandLine, TEXT("voxel.roughnessfrequency="), Result.Frequency);
|
||||
if (Result.bFrequency
|
||||
&& (!FMath::IsFinite(Result.Frequency) || Result.Frequency <= 0.0f))
|
||||
{
|
||||
Result.bFrequency = false;
|
||||
}
|
||||
|
||||
FString NoiseTypeText;
|
||||
if (FParse::Value(
|
||||
*CommandLine, TEXT("voxel.roughnesstype="), NoiseTypeText))
|
||||
{
|
||||
NoiseTypeText.TrimStartAndEndInline();
|
||||
NoiseTypeText.ToLowerInline();
|
||||
NoiseTypeText.ReplaceInline(TEXT("_"), TEXT(""));
|
||||
NoiseTypeText.ReplaceInline(TEXT("-"), TEXT(""));
|
||||
if (NoiseTypeText == TEXT("fbm") || NoiseTypeText == TEXT("fractal"))
|
||||
{
|
||||
Result.NoiseType = EVoxelNoiseType::FBM;
|
||||
Result.bNoiseType = true;
|
||||
}
|
||||
else if (NoiseTypeText == TEXT("ridged") || NoiseTypeText == TEXT("ridge"))
|
||||
{
|
||||
Result.NoiseType = EVoxelNoiseType::Ridged;
|
||||
Result.bNoiseType = true;
|
||||
}
|
||||
else if (NoiseTypeText == TEXT("mixed"))
|
||||
{
|
||||
Result.NoiseType = EVoxelNoiseType::Mixed;
|
||||
Result.bNoiseType = true;
|
||||
}
|
||||
else if (NoiseTypeText == TEXT("cellular") || NoiseTypeText == TEXT("worley"))
|
||||
{
|
||||
Result.NoiseType = EVoxelNoiseType::Cellular;
|
||||
Result.bNoiseType = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Result.bSurfaceRoughness || Result.bFrequency || Result.bNoiseType)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[StrateManager] Runtime roughness override: strength=%s%.3f frequency=%s%.6f type=%s"),
|
||||
Result.bSurfaceRoughness ? TEXT("") : TEXT("(asset) "),
|
||||
Result.SurfaceRoughness,
|
||||
Result.bFrequency ? TEXT("") : TEXT("(asset) "),
|
||||
Result.Frequency,
|
||||
Result.bNoiseType
|
||||
? (Result.NoiseType == EVoxelNoiseType::FBM ? TEXT("FBM")
|
||||
: Result.NoiseType == EVoxelNoiseType::Ridged ? TEXT("Ridged")
|
||||
: Result.NoiseType == EVoxelNoiseType::Mixed ? TEXT("Mixed")
|
||||
: TEXT("Cellular"))
|
||||
: TEXT("(asset)"));
|
||||
}
|
||||
return Result;
|
||||
}();
|
||||
return Overrides;
|
||||
}
|
||||
|
||||
static FStrateGenerationParams VF_ApplyRuntimeRoughnessOverrides(
|
||||
FStrateGenerationParams Params)
|
||||
{
|
||||
const FRuntimeRoughnessOverrides& Overrides = VF_GetRuntimeRoughnessOverrides();
|
||||
if (Overrides.bSurfaceRoughness)
|
||||
{
|
||||
Params.SurfaceRoughness = Overrides.SurfaceRoughness;
|
||||
}
|
||||
if (Overrides.bFrequency)
|
||||
{
|
||||
Params.RoughnessFrequency = Overrides.Frequency;
|
||||
}
|
||||
if (Overrides.bNoiseType)
|
||||
{
|
||||
Params.RoughnessNoiseType = Overrides.NoiseType;
|
||||
}
|
||||
return Params;
|
||||
}
|
||||
}
|
||||
|
||||
UVoxelStrateManager::UVoxelStrateManager()
|
||||
@@ -136,6 +243,399 @@ namespace
|
||||
}
|
||||
return Cache.Nearby;
|
||||
}
|
||||
|
||||
// Passage relief is a function of the immutable passage descriptor and the actual XY column.
|
||||
// Keep the cache fixed-size and thread-local, like the morphology floor cache: a long export
|
||||
// cannot retain one entry per tile, and collisions only lose a hit. Fractional normal
|
||||
// samples intentionally fall through to the exact same two-octave calculation.
|
||||
struct FVoxelPassageReliefCacheEntry
|
||||
{
|
||||
const UVoxelStrateManager* Owner = nullptr;
|
||||
uint64 OwnerLifetimeId = 0;
|
||||
uint32 LayoutVersion = 0;
|
||||
int32 PassageIndex = INDEX_NONE;
|
||||
int32 X = 0;
|
||||
int32 Y = 0;
|
||||
float Noise = 0.0f;
|
||||
bool bValid = false;
|
||||
};
|
||||
|
||||
struct FVoxelPassageReliefCache
|
||||
{
|
||||
static constexpr int32 Capacity = 16384;
|
||||
static_assert((Capacity & (Capacity - 1)) == 0,
|
||||
"passage relief cache must be a power of two");
|
||||
FVoxelPassageReliefCacheEntry Entries[Capacity];
|
||||
|
||||
static uint32 HashKey(
|
||||
const UVoxelStrateManager* Owner, uint64 LifetimeId, uint32 Version,
|
||||
int32 PassageIndex, int32 X, int32 Y)
|
||||
{
|
||||
uint32 Hash = static_cast<uint32>(reinterpret_cast<UPTRINT>(Owner));
|
||||
Hash ^= static_cast<uint32>(reinterpret_cast<UPTRINT>(Owner) >> 32);
|
||||
const auto Combine = [&Hash](uint32 Value)
|
||||
{
|
||||
Hash ^= Value + 0x9E3779B9u + (Hash << 6) + (Hash >> 2);
|
||||
};
|
||||
Combine(static_cast<uint32>(LifetimeId));
|
||||
Combine(static_cast<uint32>(LifetimeId >> 32));
|
||||
Combine(Version);
|
||||
Combine(static_cast<uint32>(PassageIndex));
|
||||
Combine(static_cast<uint32>(X));
|
||||
Combine(static_cast<uint32>(Y));
|
||||
return VoxelHash::Mix(Hash);
|
||||
}
|
||||
};
|
||||
|
||||
FVoxelPassageReliefCache& VF_GetPassageReliefCache()
|
||||
{
|
||||
thread_local FVoxelPassageReliefCache Cache;
|
||||
return Cache;
|
||||
}
|
||||
|
||||
FORCEINLINE float VF_ComputePassageReliefNoise(
|
||||
double X, double Y, uint32 FloorSeed, float Frequency)
|
||||
{
|
||||
const float SF = static_cast<float>(FloorSeed) * 0.00001f;
|
||||
float Noise = FMath::PerlinNoise2D(
|
||||
FVector2D(
|
||||
static_cast<float>(X) * Frequency + SF,
|
||||
static_cast<float>(Y) * Frequency + SF * 1.7f)) * 0.65f;
|
||||
Noise += FMath::PerlinNoise2D(
|
||||
FVector2D(
|
||||
static_cast<float>(X) * Frequency * 2.3f + SF * 3.1f,
|
||||
static_cast<float>(Y) * Frequency * 2.3f + SF * 5.3f)) * 0.35f;
|
||||
return Noise;
|
||||
}
|
||||
|
||||
FORCEINLINE float VF_PassageReliefNoise(
|
||||
const UVoxelStrateManager* Manager, int32 PassageIndex,
|
||||
const FVoxelPassage& Passage, double X, double Y)
|
||||
{
|
||||
if (!(Passage.NativeFloorReliefStrength > 0.0f)
|
||||
|| !FMath::IsFinite(Passage.NativeFloorReliefStrength)
|
||||
|| !FMath::IsFinite(Passage.NativeFloorReliefFrequency)
|
||||
|| !(FMath::Abs(Passage.NativeFloorReliefFrequency) > KINDA_SMALL_NUMBER))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const bool bInteger = FMath::IsFinite(X) && FMath::IsFinite(Y)
|
||||
&& FMath::FloorToDouble(X) == X && FMath::FloorToDouble(Y) == Y
|
||||
&& X >= static_cast<double>(MIN_int32) && X <= static_cast<double>(MAX_int32)
|
||||
&& Y >= static_cast<double>(MIN_int32) && Y <= static_cast<double>(MAX_int32);
|
||||
if (!bInteger || Manager == nullptr || PassageIndex == INDEX_NONE)
|
||||
{
|
||||
return VF_ComputePassageReliefNoise(
|
||||
X, Y, Passage.NativeFloorSeed, Passage.NativeFloorReliefFrequency);
|
||||
}
|
||||
|
||||
const int32 IX = FMath::FloorToInt(X);
|
||||
const int32 IY = FMath::FloorToInt(Y);
|
||||
FVoxelPassageReliefCache& Cache = VF_GetPassageReliefCache();
|
||||
const uint64 LifetimeId = Manager->GetCacheLifetimeId();
|
||||
const uint32 Version = Manager->GetLayoutVersion();
|
||||
FVoxelPassageReliefCacheEntry& Entry = Cache.Entries[
|
||||
FVoxelPassageReliefCache::HashKey(
|
||||
Manager, LifetimeId, Version, PassageIndex, IX, IY)
|
||||
& (FVoxelPassageReliefCache::Capacity - 1)];
|
||||
if (Entry.bValid
|
||||
&& Entry.Owner == Manager
|
||||
&& Entry.OwnerLifetimeId == LifetimeId
|
||||
&& Entry.LayoutVersion == Version
|
||||
&& Entry.PassageIndex == PassageIndex
|
||||
&& Entry.X == IX && Entry.Y == IY)
|
||||
{
|
||||
return Entry.Noise;
|
||||
}
|
||||
|
||||
const float Noise = VF_ComputePassageReliefNoise(
|
||||
X, Y, Passage.NativeFloorSeed, Passage.NativeFloorReliefFrequency);
|
||||
Entry.Owner = Manager;
|
||||
Entry.OwnerLifetimeId = LifetimeId;
|
||||
Entry.LayoutVersion = Version;
|
||||
Entry.PassageIndex = PassageIndex;
|
||||
Entry.X = IX;
|
||||
Entry.Y = IY;
|
||||
Entry.Noise = Noise;
|
||||
Entry.bValid = true;
|
||||
return Noise;
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// The graph-tunnel relief proof is intentionally repeated here instead of sharing a loose
|
||||
// "noise amplitude" constant. The passage uses the same weighted two-octave field and the
|
||||
// same conservative partial-derivative bound; a zero scale is the fail-closed result.
|
||||
constexpr float VF_PassagePerlin2DPartialAbsBound = 8.5f;
|
||||
constexpr float VF_PassageMaxReliefScale = 0.20f;
|
||||
|
||||
static float VF_BuildPassageReliefScale(
|
||||
const FVector& A, const FVector& B,
|
||||
float BaseGradient, int32 SegmentIndex, int32 NumSegments,
|
||||
float Strength, float Frequency)
|
||||
{
|
||||
if (!(Strength > 0.0f)
|
||||
|| !FMath::IsFinite(Strength)
|
||||
|| !FMath::IsFinite(Frequency)
|
||||
|| !(FMath::Abs(Frequency) > KINDA_SMALL_NUMBER)
|
||||
|| !FMath::IsFinite(BaseGradient)
|
||||
|| BaseGradient < 0.0f)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const float AvailableGradient =
|
||||
VoxelPassageGeometry::PlayerWalkableFloorMaxGradient - BaseGradient;
|
||||
if (!(AvailableGradient > 0.0f))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const float Amplitude = FMath::Abs(Strength) * VOXEL_NOISE_SCALE;
|
||||
const float OctaveFrequencyWeight = 0.65f + 0.35f * 2.3f;
|
||||
const float NoiseGradientBound =
|
||||
1.4142135623730951f * VF_PassagePerlin2DPartialAbsBound
|
||||
* OctaveFrequencyWeight * FMath::Abs(Frequency) * Amplitude;
|
||||
const int32 LandingEnvelopeCount = (SegmentIndex == 0 ? 1 : 0)
|
||||
+ (SegmentIndex + 1 == NumSegments ? 1 : 0);
|
||||
const float HorizontalRun = FVector2D(
|
||||
static_cast<float>(B.X - A.X),
|
||||
static_cast<float>(B.Y - A.Y)).Size();
|
||||
const float EnvelopeDerivativeBound = LandingEnvelopeCount > 0
|
||||
? (1.5f * static_cast<float>(LandingEnvelopeCount)
|
||||
/ VoxelPassageGeometry::WalkableTunnelLandingApronVoxels)
|
||||
* Amplitude
|
||||
: 0.0f;
|
||||
const float ReliefGradientBound =
|
||||
NoiseGradientBound + EnvelopeDerivativeBound;
|
||||
if (!(ReliefGradientBound > KINDA_SMALL_NUMBER)
|
||||
|| !FMath::IsFinite(ReliefGradientBound)
|
||||
|| !(HorizontalRun > KINDA_SMALL_NUMBER))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return FMath::Clamp(
|
||||
FMath::Min(
|
||||
VF_PassageMaxReliefScale,
|
||||
AvailableGradient / ReliefGradientBound),
|
||||
0.0f, VF_PassageMaxReliefScale);
|
||||
}
|
||||
|
||||
static bool VF_ProjectNativePassageFloor(
|
||||
const FVoxelPassage& Passage, const FVector& Position,
|
||||
float& OutFloorZ, float& OutSupportRadius,
|
||||
const UVoxelStrateManager* Manager = nullptr,
|
||||
int32 PassageIndex = INDEX_NONE)
|
||||
{
|
||||
if (!Passage.bNativeFloorEnabled
|
||||
|| Passage.ControlPoints.Num() < 2
|
||||
|| Passage.ControlRadii.Num() != Passage.ControlPoints.Num()
|
||||
|| Passage.NativeFloorProfileZ.Num() != Passage.ControlPoints.Num())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float BestDistanceSquared = FLT_MAX;
|
||||
int32 BestSegment = INDEX_NONE;
|
||||
float BestT = 0.0f;
|
||||
float BestSupportRadius = 0.0f;
|
||||
for (int32 SegmentIndex = 0;
|
||||
SegmentIndex + 1 < Passage.ControlPoints.Num();
|
||||
++SegmentIndex)
|
||||
{
|
||||
const FVector& A = Passage.ControlPoints[SegmentIndex];
|
||||
const FVector& B = Passage.ControlPoints[SegmentIndex + 1];
|
||||
const FVector2D Delta(
|
||||
static_cast<float>(B.X - A.X),
|
||||
static_cast<float>(B.Y - A.Y));
|
||||
const float LengthSquared = Delta.SizeSquared();
|
||||
if (!(LengthSquared > KINDA_SMALL_NUMBER))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FVector2D QueryXY(
|
||||
static_cast<float>(Position.X),
|
||||
static_cast<float>(Position.Y));
|
||||
const FVector2D AXY(
|
||||
static_cast<float>(A.X), static_cast<float>(A.Y));
|
||||
const float T = FMath::Clamp(
|
||||
FVector2D::DotProduct(QueryXY - AXY, Delta) / LengthSquared,
|
||||
0.0f, 1.0f);
|
||||
const FVector2D ClosestXY = AXY + Delta * T;
|
||||
const float DistanceSquared = (QueryXY - ClosestXY).SizeSquared();
|
||||
if (DistanceSquared >= BestDistanceSquared)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
BestDistanceSquared = DistanceSquared;
|
||||
BestSegment = SegmentIndex;
|
||||
BestT = T;
|
||||
BestSupportRadius = FMath::Max(
|
||||
FMath::Min(
|
||||
FMath::Abs(Passage.ControlRadii[SegmentIndex]),
|
||||
FMath::Abs(Passage.ControlRadii[SegmentIndex + 1])) - 0.5f,
|
||||
VoxelPassageGeometry::PlayerRadiusVoxels);
|
||||
}
|
||||
|
||||
if (BestSegment == INDEX_NONE
|
||||
|| BestDistanceSquared > FMath::Square(BestSupportRadius))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const float ClampedT = FMath::Clamp(BestT, 0.0f, 1.0f);
|
||||
OutFloorZ = FMath::Lerp(
|
||||
Passage.NativeFloorProfileZ[BestSegment],
|
||||
Passage.NativeFloorProfileZ[BestSegment + 1],
|
||||
ClampedT);
|
||||
|
||||
float ReliefScale = 0.0f;
|
||||
if (Passage.NativeFloorReliefScales.IsValidIndex(BestSegment))
|
||||
{
|
||||
ReliefScale = Passage.NativeFloorReliefScales[BestSegment];
|
||||
}
|
||||
float Envelope = 1.0f;
|
||||
const float HorizontalRun = FVector2D(
|
||||
static_cast<float>(Passage.ControlPoints[BestSegment + 1].X
|
||||
- Passage.ControlPoints[BestSegment].X),
|
||||
static_cast<float>(Passage.ControlPoints[BestSegment + 1].Y
|
||||
- Passage.ControlPoints[BestSegment].Y)).Size();
|
||||
if (BestSegment == 0)
|
||||
{
|
||||
Envelope *= SmoothStep01(FMath::Clamp(
|
||||
(ClampedT * HorizontalRun)
|
||||
/ VoxelPassageGeometry::WalkableTunnelLandingApronVoxels,
|
||||
0.0f, 1.0f));
|
||||
}
|
||||
if (BestSegment + 1 == Passage.ControlPoints.Num() - 1)
|
||||
{
|
||||
Envelope *= SmoothStep01(FMath::Clamp(
|
||||
((1.0f - ClampedT) * HorizontalRun)
|
||||
/ VoxelPassageGeometry::WalkableTunnelLandingApronVoxels,
|
||||
0.0f, 1.0f));
|
||||
}
|
||||
OutFloorZ += VF_PassageReliefNoise(
|
||||
Manager, PassageIndex, Passage,
|
||||
static_cast<double>(Position.X), static_cast<double>(Position.Y))
|
||||
* VOXEL_NOISE_SCALE * Passage.NativeFloorReliefStrength
|
||||
* Envelope * ReliefScale;
|
||||
OutSupportRadius = BestSupportRadius;
|
||||
return FMath::IsFinite(OutFloorZ)
|
||||
&& FMath::IsFinite(OutSupportRadius)
|
||||
&& OutSupportRadius > 0.0f;
|
||||
}
|
||||
|
||||
static bool VF_ProjectPassageFloor(
|
||||
const FVoxelPassage& Passage, const FVector& Position,
|
||||
float& OutFloorZ, float& OutSupportRadius,
|
||||
const UVoxelStrateManager* Manager = nullptr,
|
||||
int32 PassageIndex = INDEX_NONE)
|
||||
{
|
||||
if (VF_ProjectNativePassageFloor(
|
||||
Passage, Position, OutFloorZ, OutSupportRadius,
|
||||
Manager, PassageIndex))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!Passage.bWalkableTunnelContract)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return VoxelPassageGeometry::ProjectWalkableTunnelFloor(
|
||||
Passage.ControlPoints, Passage.ControlRadii, Position,
|
||||
OutFloorZ, OutSupportRadius);
|
||||
}
|
||||
}
|
||||
|
||||
static void VF_AuthorNativePassageFloor(
|
||||
FVoxelPassage& Passage,
|
||||
const UVoxelStrateDefinition* UpperDefinition,
|
||||
const UVoxelStrateDefinition* LowerDefinition,
|
||||
uint32 PassageSeed)
|
||||
{
|
||||
Passage.bNativeFloorEnabled = false;
|
||||
Passage.NativeFloorProfileZ.Reset();
|
||||
Passage.NativeFloorReliefScales.Reset();
|
||||
Passage.NativeFloorReliefStrength = 0.0f;
|
||||
Passage.NativeFloorReliefFrequency = 0.015f;
|
||||
Passage.NativeFloorSeed = 0;
|
||||
|
||||
if (!Passage.bWalkableTunnelContract
|
||||
|| Passage.ControlPoints.Num() < 2
|
||||
|| Passage.ControlRadii.Num() != Passage.ControlPoints.Num())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Inter-strate passages can connect to non-cave archetypes. Keep the native D profile for
|
||||
// the walkable contract in all cases, but only borrow relief from a cave strate that actually
|
||||
// owns the same room-floor field. A malformed/unknown parameter source fails closed to a
|
||||
// flat floor rather than inventing a second relief system.
|
||||
const FStrateGenerationParams* ReliefParams = nullptr;
|
||||
if (UpperDefinition != nullptr
|
||||
&& (UpperDefinition->GeneratorType == ECaveGeneratorType::TunnelNetwork
|
||||
|| UpperDefinition->GeneratorType == ECaveGeneratorType::Underwater))
|
||||
{
|
||||
ReliefParams = &UpperDefinition->GenerationParams;
|
||||
}
|
||||
else if (LowerDefinition != nullptr
|
||||
&& (LowerDefinition->GeneratorType == ECaveGeneratorType::TunnelNetwork
|
||||
|| LowerDefinition->GeneratorType == ECaveGeneratorType::Underwater))
|
||||
{
|
||||
ReliefParams = &LowerDefinition->GenerationParams;
|
||||
}
|
||||
if (ReliefParams != nullptr)
|
||||
{
|
||||
Passage.NativeFloorReliefStrength = FMath::Max(
|
||||
ReliefParams->FloorReliefStrength, 0.0f);
|
||||
Passage.NativeFloorReliefFrequency = ReliefParams->FloorReliefFrequency;
|
||||
}
|
||||
Passage.NativeFloorSeed = VoxelHash::Mix(PassageSeed ^ 0xD00DF10Eu);
|
||||
Passage.NativeFloorProfileZ.SetNum(Passage.ControlPoints.Num());
|
||||
for (int32 PointIndex = 0;
|
||||
PointIndex < Passage.ControlPoints.Num();
|
||||
++PointIndex)
|
||||
{
|
||||
const float Radius = FMath::Abs(Passage.ControlRadii[PointIndex]);
|
||||
const float FloorZ = VoxelPassageGeometry::TunnelFloorZ(
|
||||
Passage.ControlPoints[PointIndex], Radius);
|
||||
if (!FMath::IsFinite(FloorZ))
|
||||
{
|
||||
Passage.NativeFloorProfileZ.Reset();
|
||||
return;
|
||||
}
|
||||
Passage.NativeFloorProfileZ[PointIndex] = FloorZ;
|
||||
}
|
||||
|
||||
const int32 NumSegments = Passage.ControlPoints.Num() - 1;
|
||||
Passage.NativeFloorReliefScales.SetNumZeroed(NumSegments);
|
||||
for (int32 SegmentIndex = 0;
|
||||
SegmentIndex < NumSegments;
|
||||
++SegmentIndex)
|
||||
{
|
||||
const FVector& A = Passage.ControlPoints[SegmentIndex];
|
||||
const FVector& B = Passage.ControlPoints[SegmentIndex + 1];
|
||||
const float HorizontalRun = FVector2D(
|
||||
static_cast<float>(B.X - A.X),
|
||||
static_cast<float>(B.Y - A.Y)).Size();
|
||||
const float BaseGradient = HorizontalRun > KINDA_SMALL_NUMBER
|
||||
? FMath::Abs(
|
||||
Passage.NativeFloorProfileZ[SegmentIndex + 1]
|
||||
- Passage.NativeFloorProfileZ[SegmentIndex]) / HorizontalRun
|
||||
: FLT_MAX;
|
||||
Passage.NativeFloorReliefScales[SegmentIndex] =
|
||||
VF_BuildPassageReliefScale(
|
||||
A, B, BaseGradient, SegmentIndex, NumSegments,
|
||||
Passage.NativeFloorReliefStrength,
|
||||
Passage.NativeFloorReliefFrequency);
|
||||
}
|
||||
Passage.bNativeFloorEnabled =
|
||||
Passage.NativeFloorProfileZ.Num() == Passage.ControlPoints.Num();
|
||||
}
|
||||
|
||||
static float VF_BoundarySealThicknessForDefinition(
|
||||
@@ -172,7 +672,9 @@ static float VF_BoundarySealThicknessForDefinition(
|
||||
*/
|
||||
static bool VF_IsWalkableTunnelFloor(
|
||||
const FVoxelPassage& Passage,
|
||||
const FVector& Position)
|
||||
const FVector& Position,
|
||||
const UVoxelStrateManager* Manager = nullptr,
|
||||
int32 PassageIndex = INDEX_NONE)
|
||||
{
|
||||
if (!Passage.bWalkableTunnelContract
|
||||
|| Passage.ControlPoints.Num() < 2
|
||||
@@ -183,9 +685,8 @@ static bool VF_IsWalkableTunnelFloor(
|
||||
|
||||
float FloorZ = 0.0f;
|
||||
float SupportRadius = 0.0f;
|
||||
if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor(
|
||||
Passage.ControlPoints, Passage.ControlRadii, Position,
|
||||
FloorZ, SupportRadius))
|
||||
if (!VF_ProjectPassageFloor(
|
||||
Passage, Position, FloorZ, SupportRadius, Manager, PassageIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -196,7 +697,9 @@ static bool VF_IsWalkableTunnelFloor(
|
||||
|
||||
static bool VF_IsWalkableTunnelAir(
|
||||
const FVoxelPassage& Passage,
|
||||
const FVector& Position)
|
||||
const FVector& Position,
|
||||
const UVoxelStrateManager* Manager = nullptr,
|
||||
int32 PassageIndex = INDEX_NONE)
|
||||
{
|
||||
if (!Passage.bWalkableTunnelContract
|
||||
|| Passage.ControlPoints.Num() < 2
|
||||
@@ -207,9 +710,8 @@ static bool VF_IsWalkableTunnelAir(
|
||||
|
||||
float FloorZ = 0.0f;
|
||||
float SupportRadius = 0.0f;
|
||||
if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor(
|
||||
Passage.ControlPoints, Passage.ControlRadii, Position,
|
||||
FloorZ, SupportRadius))
|
||||
if (!VF_ProjectPassageFloor(
|
||||
Passage, Position, FloorZ, SupportRadius, Manager, PassageIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -253,13 +755,13 @@ static bool VF_FindWalkableTunnelAir(
|
||||
}
|
||||
const FVoxelPassage& Passage = Passages[PassageIndex];
|
||||
if (FVector::DistSquared(Position, Passage.BoundCenter) > Passage.BoundRadiusSq
|
||||
|| !VF_IsWalkableTunnelAir(Passage, Position))
|
||||
|| !VF_IsWalkableTunnelAir(Passage, Position, Manager, PassageIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
VoxelPassageGeometry::ProjectWalkableTunnelFloor(
|
||||
Passage.ControlPoints, Passage.ControlRadii, Position,
|
||||
OutFloorZ, OutSupportRadius);
|
||||
VF_ProjectPassageFloor(
|
||||
Passage, Position, OutFloorZ, OutSupportRadius,
|
||||
Manager, PassageIndex);
|
||||
OutPassageIndex = PassageIndex;
|
||||
return true;
|
||||
}
|
||||
@@ -291,7 +793,7 @@ static bool VF_IsAnyPassageFloorAt(
|
||||
&& (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
||||
|| (Manager->ArePassageSupportFloorWritesEnabledForDiagnostics()
|
||||
&& VF_IsWalkableTunnelFloor(Passage, Position))))
|
||||
&& VF_IsWalkableTunnelFloor(Passage, Position, Manager, PassageIndex))))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -1540,6 +2042,13 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
// Fallback / bounds if an invalid authored width produced no positive profile.
|
||||
Passage.Radius = FMath::Max(Passage.Radius, FMath::Max(Cfg.MouthRadius, Cfg.MidRadius));
|
||||
|
||||
// The passage now owns its D-section at construction time. Both landing floors,
|
||||
// the complete control chain, and the room-derived relief source are known here;
|
||||
// no voxel query needs to decide whether this particular passage deserves a floor.
|
||||
VF_AuthorNativePassageFloor(
|
||||
Passage, UpperDef, LowerDef,
|
||||
PassageSeed ^ VoxelHash::Cell(i, c, PassageSeed));
|
||||
|
||||
// Bounding sphere over the tube, both rooms, and both floor slabs (+ widest radius +
|
||||
// blend) for culling. Under-sizing this sphere would cull a real landing and leave a
|
||||
// sealed pocket, so the room's full box diagonal is included rather than treating the
|
||||
@@ -1737,6 +2246,26 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
|
||||
Pos, P.ControlPoints[j], P.ControlPoints[j + 1], rA, rB);
|
||||
PassageSDF = VoxelSDF::SmoothMin(PassageSDF, SegSDF, BlendK);
|
||||
}
|
||||
|
||||
// A walkable inter-strate passage is a D-shaped primitive, not a capsule followed
|
||||
// by a slab. The profile and its bounded relief were authored in GeneratePassages;
|
||||
// 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)
|
||||
{
|
||||
float NativeFloorZ = 0.0f;
|
||||
float NativeSupportRadius = 0.0f;
|
||||
if (VF_ProjectNativePassageFloor(
|
||||
P, Pos, NativeFloorZ, NativeSupportRadius,
|
||||
this, PIdx))
|
||||
{
|
||||
PassageSDF = VoxelSDF::SmoothMax(
|
||||
PassageSDF,
|
||||
NativeFloorZ - static_cast<float>(Pos.Z),
|
||||
BlendK * 0.35f);
|
||||
}
|
||||
}
|
||||
MinSDF = VoxelSDF::SmoothMin(MinSDF, PassageSDF, BlendK);
|
||||
}
|
||||
else
|
||||
@@ -1780,8 +2309,12 @@ void UVoxelStrateManager::ApplyPassageModifier(
|
||||
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker()
|
||||
&& (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
|
||||
&& VF_IsWalkableTunnelFloor(Passage, Position))))
|
||||
&& !Passage.bNativeFloorEnabled
|
||||
&& VF_IsWalkableTunnelFloor(Passage, Position, this, PassageIndex))))
|
||||
{
|
||||
// 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
|
||||
@@ -1920,7 +2453,8 @@ void UVoxelStrateManager::ApplyPassageLandingFloorMC(
|
||||
&& (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
||||
|| (bPassageSupportFloorWritesEnabled
|
||||
&& VF_IsWalkableTunnelFloor(Passage, Position))))
|
||||
&& !Passage.bNativeFloorEnabled
|
||||
&& VF_IsWalkableTunnelFloor(Passage, Position, this, PassageIndex))))
|
||||
{
|
||||
// Result is in MC convention here (negative = solid). This reassertion is the
|
||||
// structural floor backstop after the optional MC-space disturbance layer.
|
||||
@@ -1967,6 +2501,7 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
|
||||
const bool bSuppressFloor =
|
||||
VoxelPassageGeometry::VerticalShaftConnectorAirMarker();
|
||||
bool bAnyPassageFloor = false;
|
||||
bool bLegacyTunnelSupportFloor = false;
|
||||
bool bAnyRoomFloor = false;
|
||||
bool bWalkableAir = false;
|
||||
float MinLandingSDF = FLT_MAX;
|
||||
@@ -1979,14 +2514,21 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
|
||||
if (!Passages.IsValidIndex(PassageIndex)) continue;
|
||||
const FVoxelPassage& Passage = Passages[PassageIndex];
|
||||
|
||||
const bool bTunnelSupportFloor =
|
||||
bPassageSupportFloorWritesEnabled
|
||||
&& !Passage.bNativeFloorEnabled
|
||||
&& VF_IsWalkableTunnelFloor(Passage, Position, this, PassageIndex);
|
||||
if (!bSuppressFloor
|
||||
&& (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
||||
|| (bPassageSupportFloorWritesEnabled
|
||||
&& VF_IsWalkableTunnelFloor(Passage, Position))))
|
||||
|| bTunnelSupportFloor))
|
||||
{
|
||||
bAnyPassageFloor = true;
|
||||
}
|
||||
if (!bSuppressFloor && bTunnelSupportFloor)
|
||||
{
|
||||
bLegacyTunnelSupportFloor = true;
|
||||
}
|
||||
|
||||
if (!bSuppressFloor
|
||||
&& (VF_IsPassageRoomFloor(Position, Passage.UpperLanding)
|
||||
@@ -1997,7 +2539,7 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
|
||||
|
||||
if (!bWalkableAir
|
||||
&& FVector::DistSquared(Position, Passage.BoundCenter) <= Passage.BoundRadiusSq
|
||||
&& VF_IsWalkableTunnelAir(Passage, Position))
|
||||
&& VF_IsWalkableTunnelAir(Passage, Position, this, PassageIndex))
|
||||
{
|
||||
bWalkableAir = true;
|
||||
}
|
||||
@@ -2043,6 +2585,65 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
|
||||
{
|
||||
Density = FMath::Min(Density, -BaseDensity);
|
||||
}
|
||||
|
||||
if (bLegacyTunnelSupportFloor && VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::PassageSupportFloorBackstopFires);
|
||||
}
|
||||
}
|
||||
|
||||
void UVoxelStrateManager::ApplyPassageNativeFloorMC(
|
||||
float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity) const
|
||||
{
|
||||
const FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
|
||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
||||
if (Nearby.Num() == 0
|
||||
|| VoxelPassageGeometry::VerticalShaftConnectorAirMarker())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FVector Position(WorldX, WorldY, WorldZ);
|
||||
for (const int32 PassageIndex : Nearby)
|
||||
{
|
||||
if (!Passages.IsValidIndex(PassageIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const FVoxelPassage& Passage = Passages[PassageIndex];
|
||||
if (!Passage.bNativeFloorEnabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float FloorZ = 0.0f;
|
||||
float SupportRadius = 0.0f;
|
||||
if (!VF_ProjectNativePassageFloor(
|
||||
Passage, Position, FloorZ, SupportRadius,
|
||||
this, PassageIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (Position.Z <= FloorZ + KINDA_SMALL_NUMBER
|
||||
&& Position.Z > FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels)
|
||||
{
|
||||
// The old support slab is now disabled in the A/B run. This final write is the
|
||||
// D-shaped primitive's floor ownership, derived from its build-time profile; it is
|
||||
// intentionally not controlled by bPassageSupportFloorWritesEnabled.
|
||||
Density = FMath::Min(Density, -FMath::Max(BaseDensity, 1.0f));
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::PassageNativeFloorCompositions);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UVoxelStrateManager::ApplyPassageLandingRoomFloorMC(
|
||||
@@ -4622,7 +5223,7 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||
// A candidate is a hard replacement of this slot. Do not blend its rolled vector with an
|
||||
// authored neighbour at a boundary; this is also how the offline fixture measures it.
|
||||
return Result;
|
||||
return VF_ApplyRuntimeRoughnessOverrides(Result);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -4633,7 +5234,7 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||
// Season vectors are already final, measured slot records. Vertical structural posts own
|
||||
// their boundary; blending them with another selected recipe would describe neither one.
|
||||
return Result;
|
||||
return VF_ApplyRuntimeRoughnessOverrides(Result);
|
||||
}
|
||||
|
||||
FStrateGenerationParams BaseParams = BuildParamsFromDefinition(Slot.Definition);
|
||||
@@ -4728,7 +5329,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
float Alpha = 1.0f - ((float)DistFromBottom / (float)EffectiveBlend);
|
||||
Alpha = FMath::Clamp(Alpha, 0.0f, 1.0f);
|
||||
|
||||
return FStrateGenerationParams::Lerp(BaseParams, BelowParams, Alpha);
|
||||
return VF_ApplyRuntimeRoughnessOverrides(
|
||||
FStrateGenerationParams::Lerp(BaseParams, BelowParams, Alpha));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -4781,7 +5383,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
// Only blend if alpha > 0 (we're inside the warped transition zone)
|
||||
if (Alpha > 0.0f)
|
||||
{
|
||||
return FStrateGenerationParams::Lerp(BaseParams, BelowParams, Alpha);
|
||||
return VF_ApplyRuntimeRoughnessOverrides(
|
||||
FStrateGenerationParams::Lerp(BaseParams, BelowParams, Alpha));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -4822,7 +5425,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
float Alpha = 1.0f - ((float)DistFromTop / (float)EffectiveBlend);
|
||||
Alpha = FMath::Clamp(Alpha, 0.0f, 1.0f);
|
||||
|
||||
return FStrateGenerationParams::Lerp(BaseParams, AboveParams, Alpha);
|
||||
return VF_ApplyRuntimeRoughnessOverrides(
|
||||
FStrateGenerationParams::Lerp(BaseParams, AboveParams, Alpha));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -4851,7 +5455,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
|
||||
if (Alpha > 0.0f)
|
||||
{
|
||||
return FStrateGenerationParams::Lerp(BaseParams, AboveParams, Alpha);
|
||||
return VF_ApplyRuntimeRoughnessOverrides(
|
||||
FStrateGenerationParams::Lerp(BaseParams, AboveParams, Alpha));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -4860,7 +5465,7 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
}
|
||||
|
||||
// Not near any boundary (or Hard transition) — use this strate's params directly
|
||||
return BaseParams;
|
||||
return VF_ApplyRuntimeRoughnessOverrides(BaseParams);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
@@ -4886,6 +5491,17 @@ FStrateGenerationParams UVoxelStrateManager::BuildParamsFromDefinition(const UVo
|
||||
|
||||
uint64 UVoxelStrateManager::GetGenerationParamsFingerprint() const
|
||||
{
|
||||
// The diagnostic command-line override changes density inputs without changing an asset. Do
|
||||
// not let a verdict from the unoverridden layout survive into an overridden session (or vice
|
||||
// versa); a zero fingerprint conservatively disables persistence for that session.
|
||||
const FRuntimeRoughnessOverrides& RoughnessOverrides = VF_GetRuntimeRoughnessOverrides();
|
||||
if (RoughnessOverrides.bSurfaceRoughness
|
||||
|| RoughnessOverrides.bFrequency
|
||||
|| RoughnessOverrides.bNoiseType)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The session cache is deliberately conservative. These inputs are valid density inputs but
|
||||
// are not represented by the fixed-size hash below, so returning zero disables verdict reuse
|
||||
// instead of pretending that a partial key is complete.
|
||||
|
||||
@@ -60,6 +60,33 @@ namespace
|
||||
bool GVoxelForgeStreamingBudgetReported = false;
|
||||
bool GVoxelForgeGenerationCapHitReported = false;
|
||||
int32 GVoxelForgeMaxPendingTilesObserved = 0;
|
||||
bool GVoxelForgeTestStreamingCenterParsed = false;
|
||||
bool GVoxelForgeUseTestStreamingCenter = false;
|
||||
FIntVector GVoxelForgeTestStreamingCenter = FIntVector::ZeroValue;
|
||||
|
||||
void VF_ParseTestStreamingCenter()
|
||||
{
|
||||
if (GVoxelForgeTestStreamingCenterParsed) return;
|
||||
GVoxelForgeTestStreamingCenterParsed = true;
|
||||
|
||||
int32 X = 0, Y = 0, Z = 0;
|
||||
const TCHAR* CommandLine = FCommandLine::Get();
|
||||
const bool bHaveX = FParse::Value(CommandLine, TEXT("voxel.StreamingTestCenterX="), X);
|
||||
const bool bHaveY = FParse::Value(CommandLine, TEXT("voxel.StreamingTestCenterY="), Y);
|
||||
const bool bHaveZ = FParse::Value(CommandLine, TEXT("voxel.StreamingTestCenterZ="), Z);
|
||||
if (!bHaveX || !bHaveY || !bHaveZ)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GVoxelForgeTestStreamingCenter = FIntVector(X, Y, Z);
|
||||
GVoxelForgeUseTestStreamingCenter = true;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForge] diagnostic fixed streaming center: (%d,%d,%d)"),
|
||||
GVoxelForgeTestStreamingCenter.X,
|
||||
GVoxelForgeTestStreamingCenter.Y,
|
||||
GVoxelForgeTestStreamingCenter.Z);
|
||||
}
|
||||
}
|
||||
|
||||
AVoxelWorld::AVoxelWorld()
|
||||
@@ -1835,6 +1862,7 @@ int32 AVoxelWorld::GetMaxConcurrentTasks() const
|
||||
void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
|
||||
{
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_UpdateChunks);
|
||||
VF_ParseTestStreamingCenter();
|
||||
const int32 MaxTasks = GetMaxConcurrentTasks();
|
||||
if (GVoxelForgeProfileTileGeneration != 0
|
||||
&& !GVoxelForgeStreamingBudgetReported)
|
||||
@@ -1859,7 +1887,11 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
|
||||
GVoxelForgeStreamingBudgetReported = true;
|
||||
}
|
||||
|
||||
const FIntVector CenterChunk = WorldToChunkCoord(WorldToLocalCm(CenterPosition)); // player's level-0 tile
|
||||
FIntVector CenterChunk = WorldToChunkCoord(WorldToLocalCm(CenterPosition)); // player's level-0 tile
|
||||
if (GVoxelForgeUseTestStreamingCenter)
|
||||
{
|
||||
CenterChunk = GVoxelForgeTestStreamingCenter;
|
||||
}
|
||||
CurrentCenterChunk = CenterChunk;
|
||||
|
||||
// Streaming anchors (AI / remote players, §9.3): prune dead ones + detect chunk crossings so the
|
||||
|
||||
@@ -109,6 +109,8 @@ namespace VoxelDensityProfile
|
||||
TunnelFloorProfileLedges2To4Voxels,
|
||||
TunnelFloorProfileLedges4To8Voxels,
|
||||
TunnelFloorProfileLedges8PlusVoxels,
|
||||
PassageSupportFloorBackstopFires,
|
||||
PassageNativeFloorCompositions,
|
||||
PassageCandidates,
|
||||
PassageEvaluated,
|
||||
MesherBlockTests,
|
||||
|
||||
@@ -92,6 +92,15 @@ struct FVoxelPassage
|
||||
// Construction-time walkability facts for diagnostics and the commandlet report. These are
|
||||
// metadata, not a second runtime decision: the final-density sampler remains authoritative.
|
||||
bool bWalkableTunnelContract = false;
|
||||
// Passage floors are authored once, after the final control chain and both landing floors are
|
||||
// known. The evaluator uses this immutable profile to turn each tapered capsule into a D
|
||||
// section; it never invents a support plane from the current voxel.
|
||||
bool bNativeFloorEnabled = false;
|
||||
TArray<float> NativeFloorProfileZ;
|
||||
TArray<float> NativeFloorReliefScales;
|
||||
float NativeFloorReliefStrength = 0.0f;
|
||||
float NativeFloorReliefFrequency = 0.015f;
|
||||
uint32 NativeFloorSeed = 0;
|
||||
float TunnelMaxGradientDegrees = 0.0f;
|
||||
float TunnelVerticalDropVoxels = 0.0f;
|
||||
float TunnelRequiredHorizontalRunVoxels = 0.0f;
|
||||
@@ -423,6 +432,14 @@ public:
|
||||
float BaseDensity, float SealThickness,
|
||||
bool bProtectAuthoredTunnelFloor = false) const;
|
||||
|
||||
/**
|
||||
* Compose the build-authored passage D-floor after generic MC writers. This is the native
|
||||
* passage shape's final ownership step, not the diagnostic legacy support slab controlled by
|
||||
* bPassageSupportFloorWritesEnabled.
|
||||
*/
|
||||
void ApplyPassageNativeFloorMC(float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity) const;
|
||||
|
||||
/** Re-assert a landing floor after the landing air pass, still before the final XY seal. */
|
||||
void ApplyPassageLandingFloorMC(float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity) const;
|
||||
@@ -519,7 +536,9 @@ protected:
|
||||
// VoxelSettings::InterStrateGapChunks during Initialize().
|
||||
int32 InterStrateGapChunks = 0;
|
||||
|
||||
// Production is true. This diagnostic switch never changes the passage carve or air contract.
|
||||
// Production keeps the legacy writer available only as a fail-closed fallback for a
|
||||
// non-native/malformed passage; native D-floors bypass it. This switch never changes the
|
||||
// passage carve or air contract.
|
||||
bool bPassageSupportFloorWritesEnabled = true;
|
||||
|
||||
// Per-passage tunnel shape now lives on each UVoxelStrateDefinition::PassageConfig
|
||||
|
||||
Reference in new issue
Block a user