wip(assembler): M1 round 6 -- owner walk fixes: 25 cm geometry scale, one height datum, carve/fill sign, faster relief meshing

Owner saw content floating above native terrain, spiky triangles, reversed carve/fill and late relief.
Causes: shared mesher placed Core tiles on a 12.5 cm grid; content/atmosphere asked strate ranges in
12.5 cm voxels; Core edits add density where the legacy diff subtracts. Horizon LOD8 bridge edge
5.657 -> 2.265 cells. Relief mesh p95 60.8 -> 42.0 ms (LOD0). New BuildHost checks for scale, height,
blueprint carve/fill and per-kind streaming lag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-09 20:39:24 +02:00
1 parent f67b519e75
commit 61633cb492
18 files changed
+1708 -142

No files matched your search

+317 -39
View File
@@ -8,6 +8,7 @@
#include "VoxelBiomeDefinition.h"
#include "VoxelGenerator.h"
#include "VoxelSettings.h"
#include "VoxelWorld.h"
#include "VoxelCaveMorphology.h" // VoxelHash
#include "Components/StaticMeshComponent.h"
#include "Components/HierarchicalInstancedStaticMeshComponent.h"
@@ -18,6 +19,114 @@
#include "Tasks/Task.h"
#include "HAL/PlatformProcess.h"
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes
#include <VoxelForgeCore/VoxelForgeCore.h>
#include <cmath>
#include <string>
struct FCoreSurfaceSnapshot
{
VoxelForgeCore::WorldAssemblyRecipeV1 Recipe;
TMap<int32, VoxelForgeCore::WorldAssemblyStratumPlanV1> Plans;
const VoxelForgeCore::WorldAssemblyStratumPlanV1* FindPlan(int32 StrateIndex) const
{
return Plans.Find(StrateIndex);
}
bool GetHeights(int32 StrateIndex, float LegacyVoxelX, float LegacyVoxelY,
float& OutGroundLegacyVoxels, float& OutCapLegacyVoxels) const
{
const auto* Plan = FindPlan(StrateIndex);
if (!Plan || !VoxelMath::IsFinite(LegacyVoxelX) || !VoxelMath::IsFinite(LegacyVoxelY))
{
return false;
}
// One 12.5 cm legacy voxel equals 128 subvoxels, half a 25 cm Core voxel.
// Un voxel historique de 12,5 cm vaut 128 sous-voxels, soit la moitie d'un voxel Core de 25 cm.
constexpr double LegacyVoxelToSubvoxels = 128.0;
constexpr double SubvoxelsPerLegacyVoxel = LegacyVoxelToSubvoxels;
constexpr double MaxInput = static_cast<double>(MAX_int64) / LegacyVoxelToSubvoxels;
if (FMath::Abs(static_cast<double>(LegacyVoxelX)) > MaxInput
|| FMath::Abs(static_cast<double>(LegacyVoxelY)) > MaxInput)
{
return false;
}
const VoxelForgeCore::Heightfield2D Heightfield(Recipe, *Plan);
if (!Heightfield.isValid())
{
return false;
}
const int64 XSubvoxels = static_cast<int64>(std::llround(
static_cast<double>(LegacyVoxelX) * LegacyVoxelToSubvoxels));
const int64 YSubvoxels = static_cast<int64>(std::llround(
static_cast<double>(LegacyVoxelY) * LegacyVoxelToSubvoxels));
const auto Sample = Heightfield.point(
XSubvoxels, YSubvoxels, 0);
int64 SurfaceHeightSubvoxels = Sample.groundHeightSubvoxels;
int64 SearchTopSubvoxels = Sample.groundHeightSubvoxels;
for (const auto& Feature : Plan->features)
{
if ((Feature.type != 2 && Feature.type != 4)
|| XSubvoxels < Feature.minimumSubvoxels[0] - 4 * 256
|| XSubvoxels > Feature.maximumSubvoxels[0] + 4 * 256
|| YSubvoxels < Feature.minimumSubvoxels[1] - 4 * 256
|| YSubvoxels > Feature.maximumSubvoxels[1] + 4 * 256)
{
continue;
}
SearchTopSubvoxels = FMath::Max(
SearchTopSubvoxels, Feature.maximumSubvoxels[2]);
}
SearchTopSubvoxels = FMath::Min(
Sample.capHeightSubvoxels, SearchTopSubvoxels + 256);
if (SearchTopSubvoxels > Sample.groundHeightSubvoxels)
{
// Quarter Core voxel, bounded to relevant features. / Quart de voxel Core, borne aux volumes pertinents.
constexpr int64 ScanStepSubvoxels = 64;
int64 PreviousZ = SearchTopSubvoxels;
int64 PreviousDensity = Heightfield.point(XSubvoxels, YSubvoxels, PreviousZ).densitySubvoxels;
for (int64 Z = SearchTopSubvoxels - ScanStepSubvoxels;
Z >= Sample.groundHeightSubvoxels; Z -= ScanStepSubvoxels)
{
const int64 Density = Heightfield.point(XSubvoxels, YSubvoxels, Z).densitySubvoxels;
if (PreviousDensity >= 0 && Density < 0)
{
const double Denominator = static_cast<double>(PreviousDensity - Density);
const double Alpha = Denominator > 0.0
? static_cast<double>(PreviousDensity) / Denominator : 0.5;
SurfaceHeightSubvoxels = static_cast<int64>(std::llround(
static_cast<double>(PreviousZ) + (static_cast<double>(Z - PreviousZ) * Alpha)));
break;
}
PreviousZ = Z;
PreviousDensity = Density;
}
}
OutGroundLegacyVoxels = static_cast<float>(SurfaceHeightSubvoxels / SubvoxelsPerLegacyVoxel);
OutCapLegacyVoxels = static_cast<float>(Sample.capHeightSubvoxels / SubvoxelsPerLegacyVoxel);
return VoxelMath::IsFinite(OutGroundLegacyVoxels) && VoxelMath::IsFinite(OutCapLegacyVoxels);
}
bool GetWater(int32 StrateIndex, bool& OutHasPlan, bool& OutEnabled, float& OutLocalCm) const
{
OutHasPlan = false;
OutEnabled = false;
OutLocalCm = -FLT_MAX;
const auto* Plan = FindPlan(StrateIndex);
if (!Plan)
{
return false;
}
OutHasPlan = true;
OutEnabled = Plan->waterEnabled;
if (OutEnabled)
{
OutLocalCm = static_cast<float>(Plan->waterSurfaceSubvoxels) * (25.0f / 256.0f);
}
return true;
}
};
// Global safety cap on spawned decoration ACTORS per cell (across all entries).
// HISM instances are exempt — they're batched render data, capped per entry by MaxPerChunk.
@@ -64,6 +173,37 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
Generator = InGenerator;
Settings = InSettings;
Seed = InSeed;
NativeSurfaceSnapshot.Reset();
if (Settings)
{
FString ManifestJson, ManifestHash, ManifestError;
if (Settings->GetWorldAssemblyManifestJson(ManifestJson, ManifestHash, ManifestError)
&& !ManifestJson.IsEmpty())
{
FTCHARToUTF8 Utf8Json(*ManifestJson);
std::string CoreError;
const std::optional<VoxelForgeCore::WorldAssemblyManifest> Manifest =
VoxelForgeCore::WorldAssemblyManifest::FromJson(
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())), CoreError);
if (Manifest)
{
TSharedPtr<FCoreSurfaceSnapshot, ESPMode::ThreadSafe> Snapshot =
MakeShared<FCoreSurfaceSnapshot, ESPMode::ThreadSafe>();
Snapshot->Recipe = Manifest->recipe;
for (const auto& Plan : Manifest->plan.strates)
{
Snapshot->Plans.Add(Plan.strateIndex, Plan);
}
NativeSurfaceSnapshot = Snapshot;
}
else
{
UE_LOG(LogTemp, Warning, TEXT("[VoxelContentManager] native surface snapshot rejected: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
}
}
}
if (!PlaneMesh)
{
@@ -71,6 +211,39 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
}
}
bool UVoxelContentManager::QueryNativeSurfaceAnchors(
int32 StrateIndex, const FVector2D& LocalXYCm, float& OutGroundLocalZCm,
bool& bOutHasWater, float& OutWaterLocalZCm) const
{
OutGroundLocalZCm = 0.0f;
bOutHasWater = false;
OutWaterLocalZCm = -FLT_MAX;
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
if (VoxelWorld == nullptr || !VoxelWorld->bUseVoxelForgeCore
|| !NativeSurfaceSnapshot.IsValid())
{
return false;
}
float GroundLegacyVoxels = 0.0f;
float CapLegacyVoxels = 0.0f;
if (!NativeSurfaceSnapshot->GetHeights(StrateIndex,
LocalXYCm.X / VOXEL_SIZE, LocalXYCm.Y / VOXEL_SIZE,
GroundLegacyVoxels, CapLegacyVoxels))
{
return false;
}
OutGroundLocalZCm = GroundLegacyVoxels * VOXEL_SIZE;
bool bHasPlan = false;
if (!NativeSurfaceSnapshot->GetWater(StrateIndex, bHasPlan,
bOutHasWater, OutWaterLocalZCm)
|| !bHasPlan)
{
return false;
}
return true;
}
void UVoxelContentManager::BeginDestroy()
{
// Backstop: stop any worker from touching us. EndPlay → NotifyShutdown should have already drained.
@@ -185,11 +358,14 @@ void UVoxelContentManager::UpdateDecorations(const FVector& PlayerWorldPos)
// All strate/water/density queries are in actor-LOCAL space (StrateManager assumes actor origin;
// the mesher builds geometry from voxel*VOXEL_SIZE in local space). Bring the player local.
const FVector LocalPlayer = OwnerXf.InverseTransformPosition(PlayerWorldPos);
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(OwnerActor);
const float ManagerLocalZ = VoxelWorld
? VoxelWorld->ToStrateManagerLocalZ(LocalPlayer.Z) : LocalPlayer.Z;
// Player's strate band (actor-local cm). No strate (inter-strate gap / outside world) ⇒ no decos.
float TopZ, BotZ;
const bool bInStrate = StrateManager->GetStrateUnrealZRange(LocalPlayer.Z, TopZ, BotZ);
const int32 StrateIndex = bInStrate ? StrateManager->GetStrateIndex(LocalPlayer.Z) : INT32_MIN;
const bool bInStrate = StrateManager->GetStrateUnrealZRange(ManagerLocalZ, TopZ, BotZ);
const int32 StrateIndex = bInStrate ? StrateManager->GetStrateIndex(ManagerLocalZ) : INT32_MIN;
if (!bInStrate)
{
@@ -206,17 +382,39 @@ void UVoxelContentManager::UpdateDecorations(const FVector& PlayerWorldPos)
// Shared per-update strate context (a strate is a horizontal slab → same for every cell).
CurrentCtx = FDecoContext();
CurrentCtx.TopVoxelZ = TopZ / VOXEL_SIZE;
CurrentCtx.BottomVoxelZ = BotZ / VOXEL_SIZE;
CurrentCtx.StrateIndex = StrateIndex;
CurrentCtx.TopVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(TopZ) : TopZ) / VOXEL_SIZE;
CurrentCtx.BottomVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(BotZ) : BotZ) / VOXEL_SIZE;
CurrentCtx.RepChunkZ = FMath::FloorToInt(((TopZ + BotZ) * 0.5f / VOXEL_SIZE) / (float)CHUNK_SIZE);
const FIntVector RepChunk(PlayerCell.X, PlayerCell.Y, CurrentCtx.RepChunkZ);
CurrentCtx.Def = StrateManager->GetStrateForChunk(RepChunk);
CurrentCtx.bSurfaceWorld =
(StrateManager->GetGeneratorTypeForChunk(RepChunk) == ECaveGeneratorType::SurfaceWorld);
CurrentCtx.NativeSurface = NativeSurfaceSnapshot;
CurrentCtx.bNativeSurface = VoxelWorld != nullptr && VoxelWorld->bUseVoxelForgeCore
&& CurrentCtx.bSurfaceWorld
&& CurrentCtx.NativeSurface.IsValid()
&& CurrentCtx.NativeSurface->FindPlan(StrateIndex) != nullptr;
{
const float Wv = StrateManager->GetWaterLevelWorldZForChunk(RepChunk);
CurrentCtx.bHasWater = (Wv != -FLT_MAX);
CurrentCtx.WaterLocalZ = CurrentCtx.bHasWater ? Wv * VOXEL_SIZE : -FLT_MAX;
bool bHasNativePlan = false;
bool bNativeWaterEnabled = false;
float NativeWaterLocalZ = -FLT_MAX;
if (CurrentCtx.bNativeSurface
&& CurrentCtx.NativeSurface->GetWater(StrateIndex, bHasNativePlan,
bNativeWaterEnabled, NativeWaterLocalZ)
&& bHasNativePlan)
{
CurrentCtx.bHasWater = bNativeWaterEnabled;
CurrentCtx.WaterLocalZ = bNativeWaterEnabled ? NativeWaterLocalZ : -FLT_MAX;
}
else
{
const float Wv = StrateManager->GetWaterLevelWorldZForChunk(RepChunk);
CurrentCtx.bHasWater = (Wv != -FLT_MAX);
CurrentCtx.WaterLocalZ = CurrentCtx.bHasWater
? (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(Wv * VOXEL_SIZE) : Wv * VOXEL_SIZE)
: -FLT_MAX;
}
}
// Strate biome field (XY-global → resolved once; the worker picks the dominant biome per COLUMN).
CurrentCtx.BiomeCtx = StrateManager->GetBiomeContextForChunk(RepChunk);
@@ -513,6 +711,15 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
const bool bHasBiomes = Ctx.BiomeCtx.IsValid();
auto D = [&](float VX, float VY, float VZ) { return Gen->GetDensityAt(VX, VY, VZ); };
auto GetSurface = [&](float VX, float VY, float& OutGround, float& OutCap)
{
if (Ctx.bNativeSurface && Ctx.NativeSurface.IsValid())
{
return Ctx.NativeSurface->GetHeights(
Ctx.StrateIndex, VX, VY, OutGround, OutCap);
}
return Gen->GetSurfaceHeightAt(VX, VY, Ctx.RepChunkZ, OutGround, OutCap);
};
// Build a companion satellite's transform from its profile + per-instance hash at a snapped surface point
// (voxel XY/Z + outward normal). Shared by level-1 and level-2 companions.
@@ -756,14 +963,15 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
// columns carved away by passages / the (0,0) spine / player diffs (the oracle is the raw
// heightfield and doesn't know about carving).
float hC, cC, hXp, cXp, hXm, cXm, hYp, cYp, hYm, cYm;
if (!Gen->GetSurfaceHeightAt(VX, VY, Ctx.RepChunkZ, hC, cC)) continue;
Gen->GetSurfaceHeightAt(VX + 1.0f, VY, Ctx.RepChunkZ, hXp, cXp);
Gen->GetSurfaceHeightAt(VX - 1.0f, VY, Ctx.RepChunkZ, hXm, cXm);
Gen->GetSurfaceHeightAt(VX, VY + 1.0f, Ctx.RepChunkZ, hYp, cYp);
Gen->GetSurfaceHeightAt(VX, VY - 1.0f, Ctx.RepChunkZ, hYm, cYm);
if (!GetSurface(VX, VY, hC, cC)) continue;
GetSurface(VX + 1.0f, VY, hXp, cXp);
GetSurface(VX - 1.0f, VY, hXm, cXm);
GetSurface(VX, VY + 1.0f, hYp, cYp);
GetSurface(VX, VY - 1.0f, hYm, cYm);
// Floor (terrain top): outward normal = (-dH/dx, -dH/dy, 1).
if (hC >= Ctx.BottomVoxelZ && hC <= Ctx.TopVoxelZ && D(VX, VY, hC) <= 0.5f)
if (hC >= Ctx.BottomVoxelZ && hC <= Ctx.TopVoxelZ
&& (Ctx.bNativeSurface || D(VX, VY, hC) <= 0.5f))
{
const float dHdx = (hXp - hXm) * 0.5f;
const float dHdy = (hYp - hYm) * 0.5f;
@@ -773,7 +981,8 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
}
// Sky-cap ceiling underside: outward normal = (dC/dx, dC/dy, -1). Only if open space below.
if (cC > hC + 1.0f && cC <= Ctx.TopVoxelZ && D(VX, VY, cC) <= 0.5f)
if (cC > hC + 1.0f && cC <= Ctx.TopVoxelZ
&& (Ctx.bNativeSurface || D(VX, VY, cC) <= 0.5f))
{
const float dCdx = (cXp - cXm) * 0.5f;
const float dCdy = (cYp - cYm) * 0.5f;
@@ -1132,16 +1341,26 @@ bool UVoxelContentManager::FindLandmarkColumn(const UVoxelGenerator* Gen, const
if (Ctx.bSurfaceWorld)
{
float hC, cC;
if (!Gen->GetSurfaceHeightAt(VX, VY, Ctx.RepChunkZ, hC, cC)) return false;
auto GetSurface = [&](float SampleX, float SampleY, float& OutGround, float& OutCap)
{
if (Ctx.bNativeSurface && Ctx.NativeSurface.IsValid())
{
return Ctx.NativeSurface->GetHeights(
Ctx.StrateIndex, SampleX, SampleY, OutGround, OutCap);
}
return Gen->GetSurfaceHeightAt(
SampleX, SampleY, Ctx.RepChunkZ, OutGround, OutCap);
};
if (!GetSurface(VX, VY, hC, cC)) return false;
if (Surf == ESurfaceType::Ceiling)
{
if (!(cC > hC + 1.0f && cC <= Ctx.TopVoxelZ)) return false;
float d, cXp, cXm, cYp, cYm;
Gen->GetSurfaceHeightAt(VX + 1.0f, VY, Ctx.RepChunkZ, d, cXp);
Gen->GetSurfaceHeightAt(VX - 1.0f, VY, Ctx.RepChunkZ, d, cXm);
Gen->GetSurfaceHeightAt(VX, VY + 1.0f, Ctx.RepChunkZ, d, cYp);
Gen->GetSurfaceHeightAt(VX, VY - 1.0f, Ctx.RepChunkZ, d, cYm);
GetSurface(VX + 1.0f, VY, d, cXp);
GetSurface(VX - 1.0f, VY, d, cXm);
GetSurface(VX, VY + 1.0f, d, cYp);
GetSurface(VX, VY - 1.0f, d, cYm);
FVector N = OwnerXf.TransformVectorNoScale(
FVector((cXp - cXm) * 0.5f, (cYp - cYm) * 0.5f, -1.0f)).GetSafeNormal();
if (N.IsNearlyZero()) N = FVector::DownVector;
@@ -1149,12 +1368,12 @@ bool UVoxelContentManager::FindLandmarkColumn(const UVoxelGenerator* Gen, const
}
// Floor / Wall / Any → the terrain top.
if (!(hC >= Ctx.BottomVoxelZ && hC <= Ctx.TopVoxelZ)) return false;
if (Gen->GetDensityAt(VX, VY, hC) > 0.5f) return false; // carved away (passage/spine/diff)
if (!Ctx.bNativeSurface && Gen->GetDensityAt(VX, VY, hC) > 0.5f) return false;
float d, hXp, hXm, hYp, hYm;
Gen->GetSurfaceHeightAt(VX + 1.0f, VY, Ctx.RepChunkZ, hXp, d);
Gen->GetSurfaceHeightAt(VX - 1.0f, VY, Ctx.RepChunkZ, hXm, d);
Gen->GetSurfaceHeightAt(VX, VY + 1.0f, Ctx.RepChunkZ, hYp, d);
Gen->GetSurfaceHeightAt(VX, VY - 1.0f, Ctx.RepChunkZ, hYm, d);
GetSurface(VX + 1.0f, VY, hXp, d);
GetSurface(VX - 1.0f, VY, hXm, d);
GetSurface(VX, VY + 1.0f, hYp, d);
GetSurface(VX, VY - 1.0f, hYm, d);
FVector N = OwnerXf.TransformVectorNoScale(
FVector(-(hXp - hXm) * 0.5f, -(hYp - hYm) * 0.5f, 1.0f)).GetSafeNormal();
if (N.IsNearlyZero()) N = FVector::UpVector;
@@ -1382,10 +1601,13 @@ void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
const FTransform OwnerXf = OwnerActor->GetActorTransform();
const FVector LocalPlayer = OwnerXf.InverseTransformPosition(PlayerWorldPos);
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(OwnerActor);
const float ManagerLocalZ = VoxelWorld
? VoxelWorld->ToStrateManagerLocalZ(LocalPlayer.Z) : LocalPlayer.Z;
float TopZ, BotZ;
const bool bInStrate = StrateManager->GetStrateUnrealZRange(LocalPlayer.Z, TopZ, BotZ);
const int32 StrateIndex = bInStrate ? StrateManager->GetStrateIndex(LocalPlayer.Z) : INT32_MIN;
const bool bInStrate = StrateManager->GetStrateUnrealZRange(ManagerLocalZ, TopZ, BotZ);
const int32 StrateIndex = bInStrate ? StrateManager->GetStrateIndex(ManagerLocalZ) : INT32_MIN;
if (!bInStrate)
{
@@ -1403,8 +1625,9 @@ void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
// Shared strate context (a strate is a horizontal slab → same everywhere this update).
FDecoContext Ctx;
Ctx.TopVoxelZ = TopZ / VOXEL_SIZE;
Ctx.BottomVoxelZ = BotZ / VOXEL_SIZE;
Ctx.StrateIndex = StrateIndex;
Ctx.TopVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(TopZ) : TopZ) / VOXEL_SIZE;
Ctx.BottomVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(BotZ) : BotZ) / VOXEL_SIZE;
Ctx.RepChunkZ = FMath::FloorToInt(((TopZ + BotZ) * 0.5f / VOXEL_SIZE) / (float)CHUNK_SIZE);
const FIntVector RepChunk(FMath::FloorToInt(LocalPlayer.X / ChunkWorld),
FMath::FloorToInt(LocalPlayer.Y / ChunkWorld), Ctx.RepChunkZ);
@@ -1416,10 +1639,30 @@ void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
}
Ctx.Def = Def;
Ctx.bSurfaceWorld = (StrateManager->GetGeneratorTypeForChunk(RepChunk) == ECaveGeneratorType::SurfaceWorld);
Ctx.NativeSurface = NativeSurfaceSnapshot;
Ctx.bNativeSurface = VoxelWorld != nullptr && VoxelWorld->bUseVoxelForgeCore
&& Ctx.bSurfaceWorld && Ctx.NativeSurface.IsValid()
&& Ctx.NativeSurface->FindPlan(StrateIndex) != nullptr;
{
const float Wv = StrateManager->GetWaterLevelWorldZForChunk(RepChunk);
Ctx.bHasWater = (Wv != -FLT_MAX);
Ctx.WaterLocalZ = Ctx.bHasWater ? Wv * VOXEL_SIZE : -FLT_MAX;
bool bHasNativePlan = false;
bool bNativeWaterEnabled = false;
float NativeWaterLocalZ = -FLT_MAX;
if (Ctx.bNativeSurface
&& Ctx.NativeSurface->GetWater(StrateIndex, bHasNativePlan,
bNativeWaterEnabled, NativeWaterLocalZ)
&& bHasNativePlan)
{
Ctx.bHasWater = bNativeWaterEnabled;
Ctx.WaterLocalZ = bNativeWaterEnabled ? NativeWaterLocalZ : -FLT_MAX;
}
else
{
const float Wv = StrateManager->GetWaterLevelWorldZForChunk(RepChunk);
Ctx.bHasWater = (Wv != -FLT_MAX);
Ctx.WaterLocalZ = Ctx.bHasWater
? (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(Wv * VOXEL_SIZE) : Wv * VOXEL_SIZE)
: -FLT_MAX;
}
}
// Landmarks use the same per-column biome gates as decorations. Keep the complete POD biome
// context in the worker/game-thread placement context; a null/default context would silently
@@ -1585,16 +1828,51 @@ void UVoxelContentManager::UpdateWater(const FVector& PlayerWorldPos)
const FTransform Xf = OwnerActor->GetActorTransform();
const FVector LocalPos = Xf.InverseTransformPosition(PlayerWorldPos);
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(OwnerActor);
const float LocalToManagerScale = VoxelWorld
? VOXEL_SIZE / VoxelWorld->GetActiveVoxelSizeCm() : 1.0f;
FVector ManagerLocalPos = LocalPos * LocalToManagerScale;
if (VoxelWorld)
{
ManagerLocalPos.Z = VoxelWorld->ToStrateManagerLocalZ(LocalPos.Z);
}
// Player chunk (voxel→chunk) for the strate / water-level lookup.
const float ChunkWorld = (float)CHUNK_SIZE * VOXEL_SIZE; // 1 chunk footprint in cm
const FIntVector PlayerChunk(
FMath::FloorToInt(LocalPos.X / ChunkWorld),
FMath::FloorToInt(LocalPos.Y / ChunkWorld),
FMath::FloorToInt(LocalPos.Z / ChunkWorld));
FMath::FloorToInt(ManagerLocalPos.X / ChunkWorld),
FMath::FloorToInt(ManagerLocalPos.Y / ChunkWorld),
FMath::FloorToInt(ManagerLocalPos.Z / ChunkWorld));
const float WaterVoxelZ = StrateManager->GetWaterLevelWorldZForChunk(PlayerChunk);
if (WaterVoxelZ == -FLT_MAX) // current strate has no water → hide the plane
float WaterLocalZ = -FLT_MAX;
const int32 StrateIndex = StrateManager->GetStrateIndex(ManagerLocalPos.Z);
bool bHasNativePlan = false;
bool bNativeWaterEnabled = false;
float NativeWaterLocalZ = -FLT_MAX;
if (VoxelWorld != nullptr && VoxelWorld->bUseVoxelForgeCore
&& NativeSurfaceSnapshot.IsValid()
&& NativeSurfaceSnapshot->GetWater(StrateIndex, bHasNativePlan,
bNativeWaterEnabled, NativeWaterLocalZ)
&& bHasNativePlan)
{
if (bNativeWaterEnabled)
{
WaterLocalZ = NativeWaterLocalZ;
}
}
else
{
const float WaterVoxelZ = StrateManager->GetWaterLevelWorldZForChunk(PlayerChunk);
if (WaterVoxelZ != -FLT_MAX)
{
const float ManagerWaterLocalZ = WaterVoxelZ * VOXEL_SIZE;
WaterLocalZ = VoxelWorld
? VoxelWorld->FromStrateManagerLocalZ(ManagerWaterLocalZ) : ManagerWaterLocalZ;
}
}
// No water in this strate means the plane stays hidden. / Sans eau dans cette strate, le plan reste masque.
if (WaterLocalZ == -FLT_MAX)
{
if (WaterPlane) { WaterPlane->SetVisibility(false); }
LastWaterZ = -FLT_MAX;
@@ -1609,7 +1887,7 @@ void UVoxelContentManager::UpdateWater(const FVector& PlayerWorldPos)
FMath::FloorToInt(LocalPos.X / ChunkWorld),
FMath::FloorToInt(LocalPos.Y / ChunkWorld));
if (WaterPlane && WaterPlane->IsVisible() && WaterVoxelZ == LastWaterZ && Cell == LastWaterCell)
if (WaterPlane && WaterPlane->IsVisible() && WaterLocalZ == LastWaterZ && Cell == LastWaterCell)
return; // nothing changed
if (!WaterPlane)
@@ -1632,7 +1910,7 @@ void UVoxelContentManager::UpdateWater(const FVector& PlayerWorldPos)
const FVector LocalCenter(((float)Cell.X + 0.5f) * ChunkWorld,
((float)Cell.Y + 0.5f) * ChunkWorld,
WaterVoxelZ * VOXEL_SIZE);
WaterLocalZ);
WaterPlane->SetWorldLocation(Xf.TransformPosition(LocalCenter));
WaterPlane->SetVisibility(true);
@@ -1641,7 +1919,7 @@ void UVoxelContentManager::UpdateWater(const FVector& PlayerWorldPos)
if (Def->WaterMaterial) { WaterPlane->SetMaterial(0, Def->WaterMaterial); }
}
LastWaterZ = WaterVoxelZ;
LastWaterZ = WaterLocalZ;
LastWaterCell = Cell;
}