fix(world): the voxel field is in ACTOR space -- make every entry point agree

The city's hole is not at world (0,0), so the AVoxelWorld actor has to move.
It couldn't: the plugin was only HALF actor-relative, and had been for a long
time without symptoms because the actor always sat at the origin.

Already correct: GetBiomeAtWorldLocation, GetVoxelSurfaceHeightAt,
UpdateDecorations, UpdateLandmarks, UpdateWater, UVoxelDensityVolume::Update,
the passage debug draw, the clipmap material.

Not correct: the streaming centre, the streaming anchors, all six carve/fill
entry points, GetStrateAtPosition, and UVoxelAtmosphereManager::UpdateForPlayer
-- whose own comment documented the assumption out loud ("actor at
origin/identity"). Carving would have landed offset; the strate lookup would
have been quietly wrong by the actor's Z.

The trap worth recording: because the content managers already convert
internally, converting PlayerLastPos once at the source would have DOUBLE-
converted four call sites. The fix has to be per-boundary. That was only
visible after reading each consumer -- the estimate before that read was wrong
in both directions.

WorldToLocalCm / WorldToLocalVoxel / LocalVoxelToWorld are now declared as the
only sanctioned boundary between Unreal coordinates and voxel coordinates, and
the real deliverable is the invariant next to them: no `/ VOXEL_SIZE` applied
to a parameter named `World*` outside those three functions. A grep can check
that. The risk was never a missed site among the ones found -- it is the next
one nobody notices.

Also adds a translation-only guard in BeginPlay. Tiles, ClassifyBox verdicts,
the clipmap window, culling and streaming distance all assume world-axis-
aligned boxes; a rotation or non-unit scale breaks that structurally rather
than gracefully, so it logs an Error naming what was set.

With the actor at the origin every conversion is identity, so this is a no-op
until the actor actually moves. Built and confirmed working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-08-23 17:47:23 +02:00
1 parent dfa1d5f5b1
commit 302cf16681
5 files changed
+608 -21

No files matched your search

+35
View File
@@ -318,6 +318,41 @@ public:
* @param WorldPosition - Position in world space (Unreal units)
* @return Strate index (0 = topmost), or -1 if outside all strates
*/
//=========================================================================
// ESPACE MONDE <-> ESPACE ACTEUR / WORLD <-> ACTOR SPACE
//=========================================================================
// Le champ de densite est defini en espace ACTEUR : (0,0) est l origine de CET acteur, pas
// celle du monde Unreal. Tout point venant d Unreal (position du pion, hit de trace, ancre de
// streaming) DOIT passer par ces helpers avant de toucher quoi que ce soit cote voxel.
//
// The density field is authored in ACTOR space: (0,0) is THIS actor s origin, not Unreal s
// world origin. Any point arriving from Unreal (pawn position, trace hit, streaming anchor)
// MUST go through these helpers before touching anything voxel-side.
//
// INVARIANT VERIFIABLE PAR GREP / GREP-CHECKABLE INVARIANT:
// aucun `/ VOXEL_SIZE` applique a un parametre nomme `World*` hors de ces fonctions.
// no `/ VOXEL_SIZE` applied to a parameter named `World*` outside these functions.
// C est la vraie garantie : le risque n est pas un site rate parmi ceux deja trouves, c est
// le SITE SUIVANT que personne ne verra. The risk is not a missed site among those already
// found - it is the NEXT one nobody notices.
//
// La racine est Static (voir BeginPlay) => la transform ne bouge pas en jeu, donc la relire a
// chaque appel est gratuit et ne demande aucune invalidation.
// The root is Static (see BeginPlay), so the transform cannot change during play: re-reading
// it per call is free and needs no invalidation.
/** World cm -> actor-local cm. */
UFUNCTION(BlueprintPure, Category = "Voxel World|Space")
FVector WorldToLocalCm(FVector WorldPos) const;
/** World cm -> actor-local VOXEL coords (what every density / diff-layer API expects). */
UFUNCTION(BlueprintPure, Category = "Voxel World|Space")
FVector WorldToLocalVoxel(FVector WorldPos) const;
/** Actor-local VOXEL coords -> world cm (placing actors, debug draws, UI markers). */
UFUNCTION(BlueprintPure, Category = "Voxel World|Space")
FVector LocalVoxelToWorld(FVector VoxelPos) const;
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
int32 GetStrateAtPosition(FVector WorldPosition) const;