feat(opstack B1): port TunnelNetwork surface roughness (STEP 4b) + share the cellular noise

STAGE B of TunnelNetwork, group 1 of 5. The stack is now
  ConstantRock -> RoomGraph -> SdfCarve -> CaveRoughness(4b) -> Worms -> [structural x3]
8 ops (was 7). Still NOT wired: UsesOperatorStackForChunk returns false for TunnelNetwork.

TRANSCRIBED
- FCaveRoughnessMod (VoxelDensityOpStack.cpp): STEP 4b literally -- two octave sets
  (main + fine x3 with the +2000/+2500/+3000 offsets), the optional domain warp, the four-way
  noise switch, the min(TotalRough, 0) anti-fill clamp inside definite air, and the quadratic
  fade by distance from surface. Reads EffectiveZ, not WorldZ.
- VoxelNoise::Cellular3D moved verbatim from VoxelGenerator.cpp's `static CellularNoise3D` into
  VoxelCaveMorphology.h; the generator keeps a one-line forwarder, exactly as FractalNoise3D and
  RidgedNoise3D already do since T2.a. It lands in the cave header rather than VoxelNoise.h
  because it needs VoxelHash, and VoxelNoise.h must not depend on the cave header. Forking a pure
  function is how AUDIT C1 happened.
- HRidged3D added next to HFractal3D (same FVector round-trip, deliberately).

TWO THINGS THAT LOOKED WRONG AND WERE PORTED AS-IS
- The domain warp computes ONE offset and adds it to BOTH noise positions, so the main and fine
  octave sets are warped identically. Two independent warps would be tidier and a different world.
- Roughness reads the STRATE params, NOT the per-room override. The original's
  `const FStrateGenerationParams& Params = LocalTerrainParams;` shadow is declared INSIDE the
  `if (bNearCaveSurface)` block that begins after step 4b. So eleven of the twelve detail
  modifiers read the room copy and this one does not -- flagged in the op so C1 does not
  "uniformise" it. (The handoff's "all 13 modifiers read the shadowed copy" is off on both counts:
  it is twelve modifiers, and one of them is outside the shadow.)

STAGE B5 DECIDED HERE, WITH REASONS (VF_NearCaveSurface)
The gate is a repeated early-out per op, NOT a scoping container: the stack is a flat list that
ClassifyBox folds op by op, a container would have to re-implement VF_FoldOp and would hide its
children from the fold, and an op that only exists inside a container is not a Phase-3 asset.
Cost stated rather than hidden: the original tests once and skips twelve, the stack tests twelve
times. Measured before optimised -- that is the C10 lesson.

TEST (same commit)
- Op count 7 -> 8.
- SurfaceRoughness/DomainWarp moved from DisableStageBModifiers into EnableTunnelFeatures.
- NEW check 1b, group coverage: rebuild the stack with the group OFF and count moved samples.
  Zero is an ERROR, not a warning -- a ported group that never executes is exactly how a bad
  transcription survives a whole green run (the PitDensity lesson). This diff-of-stacks is sound
  here although it lied for the op pool, because these are params and the params CRC IS in the
  SDF cache key.
- NEW check 1c: all 4 noise types x {warp off, warp on} compared to the original over 1500 points.
  The main equivalence only ever takes the FBM branch; three of four cases were untested.

IF THIS GROUP IS WRONG, what breaks first: the main equivalence reports diffs concentrated in
open cave and within |SDF| < SurfaceRoughness*2; if instead only check 1c fails, the fault is
inside one branch of the noise switch and nothing else is implicated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-07-28 03:18:21 +02:00
1 parent 7a87cdda14
commit 6e29cbfe4c
5 files changed
+535 -92

No files matched your search

+7 -57
View File
@@ -212,65 +212,15 @@ static float RidgedNoise3D(const FVector& Position, int32 Octaves = 4,
// Using F2-F1 (difference of two closest distances) gives smooth cell
// boundaries with ridges between cells — more interesting than raw distance.
// Le CORPS a déménagé dans Public/VoxelCaveMorphology.h (namespace VoxelNoise), au plus bas point
// qui voit déjà `VoxelHash` : la pile d'opérateurs a besoin exactement du même bruit pour la
// rugosité (4b, type Cellular) et pour les festons (4f), et deux copies d'une fonction pure finissent
// par diverger — c'est littéralement `AUDIT §C1`. Ce forwarder garde les ~3 sites d'appel ci-dessous
// inchangés, comme l'ont fait FractalNoise3D et RidgedNoise3D lors de T2.a. Aucun changement de
// comportement : corps identique, mêmes doubles de `FVector`, même ordre d'opérations.
static float CellularNoise3D(const FVector& Position)
{
// Integer cell coordinates
int32 CellX = FMath::FloorToInt(Position.X);
int32 CellY = FMath::FloorToInt(Position.Y);
int32 CellZ = FMath::FloorToInt(Position.Z);
// Fractional position within cell
float FracX = Position.X - CellX;
float FracY = Position.Y - CellY;
float FracZ = Position.Z - CellZ;
float F1 = FLT_MAX; // Distance to nearest feature point
float F2 = FLT_MAX; // Distance to 2nd nearest
// Search 3x3x3 neighborhood
for (int32 DZ = -1; DZ <= 1; DZ++)
{
for (int32 DY = -1; DY <= 1; DY++)
{
for (int32 DX = -1; DX <= 1; DX++)
{
int32 NX = CellX + DX;
int32 NY = CellY + DY;
int32 NZ = CellZ + DZ;
// Hash the neighbor cell to get a feature point position [0,1)
// Using three different hash mixes for X, Y, Z offsets
uint32 H = VoxelHash::Mix(
(uint32)(NX + 0x7FFFFFFF)
^ VoxelHash::Mix((uint32)(NY + 0x7FFFFFFF) * 2654435761u)
^ VoxelHash::Mix((uint32)(NZ + 0x7FFFFFFF) * 374761393u)
);
float FPX = (float)DX + VoxelHash::ToFloat01(H) - FracX;
float FPY = (float)DY + VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x12345678u)) - FracY;
float FPZ = (float)DZ + VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x9ABCDEF0u)) - FracZ;
float DistSq = FPX * FPX + FPY * FPY + FPZ * FPZ;
// Track closest two distances
if (DistSq < F1)
{
F2 = F1;
F1 = DistSq;
}
else if (DistSq < F2)
{
F2 = DistSq;
}
}
}
}
// F2 - F1: smooth cell boundaries with ridges between cells
// Sqrt for actual distance, then normalize to ~[-1, 1]
float Result = FMath::Sqrt(F2) - FMath::Sqrt(F1);
// Result is in [0, ~1.0]. Map to [-1, 1] for compatibility with other noise types.
return Result * 2.0f - 1.0f;
return VoxelNoise::Cellular3D(Position);
}
//=============================================================================