feat: SurfaceWorld complete — biome blending wired, guard removed

The two biome checks added last commit printed nothing on success, so a passing run
was indistinguishable from a block that never executed — the exact flaw I flagged
twice this session and then wrote myself. Both now report their coverage.

Step 2c closes SurfaceWorld:

- FSurfaceColumnSource takes per-biome params and an OWNED IVoxelBiomeField. Empty
  params leaves the original path bit-for-bit unchanged.
- The field is owned by the stack rather than borrowed: the adapter points at
  GetDensityAt's thread_local biome context and cache, and the stack is itself
  thread_local rebuilt in the same refetch block, so all three live and die together.
  Structural ownership beats a convention the next reader has to infer.
- The overhang amp blends across biomes — Lerp(Amp(PD), Amp(PN), W) with slope and
  threshold from the dominant only, as ComputeSurfaceColumn does. Interpolating the
  slope would be meaningless; it measures the terrain rather than configuring it.
- FGeneratorBiomeField lives in VoxelGenerator.cpp, on the side that knows the
  generator. The op sees a capability, never an owner — which is what lets it become
  an asset in Phase 3.
- The no-biome guard is removed from UsesOperatorStackForChunk.

Also: the two constructors now delegate to one body with one id counter. The first
draft had two competing counters, one tagged with a high bit to avoid collision,
which is a smell rather than a design.

5 of 8 archetypes ported: Maze, FlatPlain, CrystalChamber, SurfaceWorld.

UNVERIFIED: not compiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 16:51:18 +02:00
parent 1a3f6b6a72
commit c277931a08
6 changed files with 267 additions and 58 deletions
+14 -7
View File
@@ -29,6 +29,7 @@
#include "CoreMinimal.h"
#include "VoxelDensityOp.h"
#include "VoxelStrateTypes.h" // FMazeGenerationParams
#include "VoxelHeightOp.h" // IVoxelBiomeField — BuildSurfaceStack takes ownership of one
class UVoxelStrateManager;
@@ -200,16 +201,22 @@ namespace VoxelDensityOps
int32 Seed);
/**
* SurfaceWorld — ⚠️ PAS ENCORE COMPLET, et c'est délibéré. Équivaut exactement à
* `GetSurfaceDensity`, c.-à-d. la version **sans overhang** et **sans mélange de biomes** :
* • l'overhang a besoin d'une donnée par colonne que `GetSurfaceDensity` ne calcule pas
* (il passe `OverhangAmp = 0`) — sa référence est le chemin caché ;
* • le mélange de biomes est le combiner `Mask`, prototype de la Phase 3 (§5).
* Les deux arrivent à l'étape 2b. Ne pas brancher dans un monde à biomes avant.
* SurfaceWorld, COMPLET : colonne (sol + voûte) → densité, overhang 3D, post structurel, et le
* mélange de biomes quand `PerBiomeParams` est non vide.
*
* @param PerBiomeParams vide ⇒ pas de biomes, chemin d'origine strictement inchangé. Non vide
* ⇒ une pile de hauteur COMPLÈTE par biome, sol mélangé / voûte
* sélectionnée, amplitude d'overhang interpolée (§5, combiner `Mask`).
* @param BiomeField **transféré** à la pile, qui le possède. Doit répondre pour les mêmes
* indices que `PerBiomeParams`. `nullptr` avec des params non vides ⇒
* biome 0 partout (dégradation sûre, pas un crash).
*/
VOXELFORGE_API void BuildSurfaceStack(FVoxelOpStack& OutStack, const FSurfaceGenerationParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager);
const UVoxelStrateManager* StrateManager,
const TArray<FSurfaceGenerationParams>& PerBiomeParams =
TArray<FSurfaceGenerationParams>(),
TUniquePtr<IVoxelBiomeField> BiomeField = nullptr);
/**
* FlatPlain ET CrystalChamber — la même pile, **sans branchement sur le type** :