feat: Phase 1 — Maze ported to the operator stack, OFF the hot path
Maze decomposes into seven ops with no contortion:
ConstantRockSource -> LatticeCorridorSource -> SdfRoughnessMod -> SdfCarve
-> OriginSpine -> BoundarySeal -> PassageCarve
That is the answer to Phase 1's actual question (OPSTACK-PLAN section 4's
stop-trigger: "does the source/modifier split fall out naturally?"). It does.
Three of those ops are already shared: ConstantRockSource is the first line of
TunnelNetwork, Maze AND VerticalShafts; SdfCarve is the same six lines in all
three; the structural post is identical across all six density functions.
GetDensityAt and ClassifyTile are NOT touched. The archetype switch is still the
only path feeding the game, so nothing in a running world can change. The port
is validated instead by VoxelForge.OpStack.MazeEquivalence, which compares the
stack against GetMazeDensity over 20k points, re-checks purity across worker
threads, and brute-forces every box verdict the stack emits.
Two contract decisions, delegated and taken:
1. Eval is now two-channel (FVoxelOpSample { Density, Sdf }). Maze forces it:
its roughness perturbs the SDF, not the density, and on density the same
noise scales with the local gradient and is a visibly different effect. It is
also what lets two different sources SmoothMin together later, which is the
difference between a composed idea belonging somewhere and being punched into
it.
2. The stack's density channel is INTERNAL convention (positive = solid),
negated once by the caller. This REVERSES what the header said yesterday.
Every archetype body is already written that way, so each port becomes a
literal transcription instead of a sign-flip of every line -- on the plugin's
documented #1 source of confusion. The SDF channel keeps standard SDF
convention, so min() means opposite things on the two channels; the header
says so loudly.
Also extracts spine/seal/passage from VoxelGenerator.cpp into
Public/VoxelDensityPrimitives.h so the generator and the ops share ONE copy of
three world invariants. Forwarders keep the local names, so not one of the ~20
call sites changes; bodies are byte-identical.
One thing found while writing the seal's ClassifyBox and NOT silently fixed: at
the inner edge of a seal band, 1 - Dist/Thickness can round to exactly 0.0f, so
SealFactor*BaseDensity is 0, internal density lands on 0, and the mesher counts
that as AIR. Claiming AllSolid there would be a hole. The new op keeps a 1-voxel
safety margin before it forces. Today's ClassifyTile has no such margin -- the
window is hairline and needs the archetype to produce air at exactly that z, but
it is real. Reported rather than patched, since Phase 1 does not touch that path.
UNVERIFIED: not compiled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3e4ee198fe
commit
4c53d3bbed
6 files changed
+1188
-79
No files matched your search
@@ -13,6 +13,7 @@
|
||||
#include "VoxelDiffLayer.h"
|
||||
#include "VoxelBiomeDefinition.h"
|
||||
#include "VoxelNoise.h" // T2.a: float, SIMD-batched gradient-noise core
|
||||
#include "VoxelDensityPrimitives.h" // spine / seal / passage — shared with the operator stack
|
||||
|
||||
//=============================================================================
|
||||
// SURFACE COLUMN CACHE (T1.a) — kill the per-Z heightfield redundancy
|
||||
@@ -230,80 +231,34 @@ static float CellularNoise3D(const FVector& Position)
|
||||
// DENSITY PIPELINE HELPERS (partagés entre TunnelNetwork et Slab)
|
||||
//=============================================================================
|
||||
|
||||
// Seal solide aux bords haut et bas de la strate. Fade smoothstep sur
|
||||
// `Thickness` voxels depuis chaque bord. N'AJOUTE que de la densité
|
||||
// (FMath::Max), jamais en enlève → le joueur ne peut jamais percer le seal
|
||||
// "par accident", seulement via les passages.
|
||||
static void ApplyBoundarySeal(float& Density, float WorldZ,
|
||||
float StrateTopZ, float StrateBottomZ,
|
||||
float Thickness, float BaseDensity)
|
||||
// Les CORPS de ces trois helpers ont déménagé dans Public/VoxelDensityPrimitives.h : la pile
|
||||
// d'opérateurs (VoxelDensityOpStack) a besoin exactement des mêmes, et deux copies de trois
|
||||
// INVARIANTS de monde (descente possible, seals qui tiennent, passages qui percent) finiraient par
|
||||
// diverger. Ces trois lignes gardent les noms locaux pour que les ~20 sites d'appel ci-dessous ne
|
||||
// bougent pas d'un caractère — le déplacement ne change AUCUN comportement.
|
||||
//
|
||||
// The BODIES moved to Public/VoxelDensityPrimitives.h; the operator stack needs the same three, and
|
||||
// two copies of three world invariants would eventually drift. These forwarders keep the local names
|
||||
// so not one of the ~20 call sites below changes. No behavioural change.
|
||||
//
|
||||
// Convention INTERNE ici : positif = SOLIDE. La négation vers MC se fait sur le `return`.
|
||||
static FORCEINLINE void ApplyBoundarySeal(float& Density, float WorldZ,
|
||||
float StrateTopZ, float StrateBottomZ, float Thickness, float BaseDensity)
|
||||
{
|
||||
if (Thickness <= 0.0f) return;
|
||||
|
||||
const float DistTop = StrateTopZ - WorldZ; // + si on est sous le plafond
|
||||
const float DistBot = WorldZ - StrateBottomZ; // + si on est au-dessus du sol
|
||||
|
||||
if (DistTop >= 0.0f && DistTop < Thickness)
|
||||
{
|
||||
float SealFactor = 1.0f - (DistTop / Thickness);
|
||||
SealFactor = SmoothStep01(SealFactor);
|
||||
Density = FMath::Max(Density, SealFactor * BaseDensity);
|
||||
}
|
||||
if (DistBot >= 0.0f && DistBot < Thickness)
|
||||
{
|
||||
float SealFactor = 1.0f - (DistBot / Thickness);
|
||||
SealFactor = SmoothStep01(SealFactor);
|
||||
Density = FMath::Max(Density, SealFactor * BaseDensity);
|
||||
}
|
||||
VF_ApplyBoundarySeal(Density, WorldZ, StrateTopZ, StrateBottomZ, Thickness, BaseDensity);
|
||||
}
|
||||
|
||||
// Creuse un passage inter-strates. Évalué APRÈS le seal pour que les passages
|
||||
// puissent percer à travers le bouchon solide.
|
||||
// Le rayon de blend hard-codé à 4.0f correspond à l'ancienne valeur —
|
||||
// à exposer via UVoxelSettings si on veut pouvoir le tweaker.
|
||||
static void ApplyPassageCarving(float& Density, float ModSDF,
|
||||
static FORCEINLINE void ApplyPassageCarving(float& Density, float ModSDF,
|
||||
float BaseDensity, float SealThickness)
|
||||
{
|
||||
constexpr float PASSAGE_BLEND_RADIUS = 4.0f;
|
||||
if (ModSDF >= PASSAGE_BLEND_RADIUS) return;
|
||||
|
||||
float CarveFactor = FMath::Clamp(
|
||||
(PASSAGE_BLEND_RADIUS - ModSDF) / (PASSAGE_BLEND_RADIUS * 2.0f),
|
||||
0.0f, 1.0f);
|
||||
CarveFactor = SmoothStep01(CarveFactor);
|
||||
|
||||
// FORCE the density toward guaranteed AIR so the passage punches through ANYTHING in
|
||||
// its path (seals, columns, surface roughness, terrain ops). A plain subtraction can
|
||||
// be out-paced by stacked density additions, leaving solid plugs mid-tunnel — which is
|
||||
// why the shaft "didn't go all the way through". Lerp toward a strongly negative target
|
||||
// and take the min so we only ever make it MORE air (never refill an existing cave).
|
||||
const float AirTarget = -(BaseDensity * 2.0f + SealThickness + 4.0f);
|
||||
Density = FMath::Min(Density, FMath::Lerp(Density, AirTarget, CarveFactor));
|
||||
VF_ApplyPassageCarving(Density, ModSDF, BaseDensity, SealThickness);
|
||||
}
|
||||
|
||||
// (0,0) DESCENT SPINE — carve a guaranteed open vertical column at world XY (0,0)
|
||||
// inside the strate INTERIOR (between the top and bottom seals). The seals are left
|
||||
// intact so the player still has to dig through them to descend — this just makes a
|
||||
// clean, archetype-independent landing space aligned across every strate.
|
||||
static void ApplyOriginSpine(float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
static FORCEINLINE void ApplyOriginSpine(float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
|
||||
{
|
||||
if (Radius <= 0.0f) return;
|
||||
|
||||
// Stay within the interior — never touch the seal bands.
|
||||
const float InnerTop = StrateTopZ - SealThickness;
|
||||
const float InnerBot = StrateBottomZ + SealThickness;
|
||||
if (WorldZ <= InnerBot || WorldZ >= InnerTop) return;
|
||||
|
||||
const float DistXY = FMath::Sqrt(WorldX * WorldX + WorldY * WorldY);
|
||||
const float SDF = DistXY - Radius; // < 0 inside the column
|
||||
const float Blend = 3.0f;
|
||||
if (SDF < Blend)
|
||||
{
|
||||
float Carve = FMath::Clamp((Blend - SDF) / (Blend * 2.0f), 0.0f, 1.0f);
|
||||
Carve = SmoothStep01(Carve);
|
||||
Density -= Carve * (BaseDensity * 2.0f + SealThickness);
|
||||
}
|
||||
VF_ApplyOriginSpine(Density, WorldX, WorldY, WorldZ,
|
||||
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
|
||||
}
|
||||
|
||||
// DISTURBANCE LAYER — the "wow" post-process. Operates on the FINAL MC density
|
||||
|
||||
Reference in new issue
Block a user