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

+30 -2
View File
@@ -1085,8 +1085,9 @@ tasks. Preserve §8.10 streaming, cancellation, epoch, collision-handoff, and pe
Core `Vec3d` positions and tile keys are in voxel space, one voxel is 25 cm, and Core mesh vertex
positions are already in centimetres. The bridge copies those mesh positions directly; player,
anchor, query, and edit inputs convert world centimetres using the Core scale. High-level brushes
that historically express dimensions in the plugin's 12.5 cm voxels scale radius, extent, and falloff
by 12.5/25 to preserve physical dimensions. The low-level `ApplyModification` path expects active
that historically express dimensions and density strength in the plugin's 12.5 cm voxels scale
radius, extent, falloff, and strength by 12.5/25 to preserve their physical effect. The low-level
`ApplyModification` path expects active
voxel units. Tile keys remain Core keys; only legacy material lookup maps Core tile spans into the
existing strate chunk layout.
@@ -1241,6 +1242,33 @@ in [`Standalone/ASSEMBLER_M1_REPORT.md`](Standalone/ASSEMBLER_M1_REPORT.md). Bui
replace the owner's project build or visual walk; cap lighting and the pointed native ridge peaks
remain owner review items.
### 8.21 Native SurfaceWorld height, brush and far-mesh corrections — integration round 6
Core tile keys and density coordinates use 25 cm voxels; the shared plugin mesher and
`UVoxelStrateManager` retain the legacy 12.5 cm scale. The Core bridge now maps Core-local height
queries to manager coordinates by 0.5 and maps returned bounds back by 2.0. Native content, water,
landmark, decoration and atmosphere anchors use the same manifest `Heightfield2D` ground/cap data
as the terrain. These conversions are gated by `bUseVoxelForgeCore`; the legacy branch keeps its
prior units and behavior. `UVoxelMarchingCubesMesher` has an explicit geometry voxel size: Core sets
25 cm, while legacy callers retain the 12.5 cm default. Material and biome lookups remain in their
legacy coordinate space.
Blueprint-callable carve/fill and high-level brush dimensions cross the scale boundary at 0.5,
including strength. Legacy diff modifications subtract their strength; Core edit evaluation adds its
density offset, so the adapter negates the legacy strength once. Public carve forces the airward sign
and fill forces the solidward sign. Low-level active-Core modification units remain Core units.
LOD8 far-stratum boundary repairs now sample crossings across a subdivided 4×4 coarse patch and only
emit a patch when each sample has a source-field crossing. This replaces the former broad single
quad. The BuildHost geometry audit keeps a two-cell maximum edge at LOD0/1 and allows four coarse
cells at LOD7+ to include shallow far-LOD transitions while still rejecting the prior 5.66-cell
horizon slab. It checks near-LOD normal direction and unit normals at every audited LOD. The mesher
emits reversed triangle order (`0,2,1`) for RealtimeMesh; the audit reports that expected order
separately from normal direction. The opt-in native heightfield path narrows vertical mesher rows to
sampled ground/cap heights plus the Z bounds of intersecting type-2/type-4 manifest features. Hash captures and tiles
with overlapping player edits or another strate keep the full mesh range. See round 6 of the M1
report for per-kind streaming results and validation limits.
## 9. Multiplayer model (listen-server first, dedicated-friendly)
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
+14 -2
View File
@@ -847,8 +847,9 @@ settings or assets.
When enabled, `AVoxelWorld` uses Core desired keys and priorities, compiles/generates through Unreal
tasks, carries `Epoch` through the existing MPSC result queues, and uploads converted payloads through
RealtimeMesh and its collision cook. Core voxel coordinates use 25 cm per voxel; Core tile keys are
retained, and Core mesh vertices are already in centimetres. High-level legacy brush dimensions are
scaled from 12.5 cm voxel units to Core units so their physical size stays the same. Edits use
retained, and Core mesh vertices are already in centimetres. High-level legacy brush dimensions and
density strength are scaled from 12.5 cm voxel units to Core units so their physical effect stays the
same. Edits use
sequenced `applyEditTransaction` calls after in-flight Core work drains, then regenerate Core's
affected keys under a new epoch. Existing streaming anchors participate; the headless move proof
covers `CollisionOnly` policy.
@@ -882,6 +883,17 @@ content hash. The collision gate defers out-of-desired-set Core support tiles un
compilation admits them, so collision work cannot publish a competing tile before the native
surface region snapshot. Existing cave Core strates still use the no-manifest snapshot path.
Round 6 corrects the Core 25 cm ↔ legacy 12.5 cm boundary in `VoxelWorld.cpp`,
`VoxelContentManager.cpp`, and `VoxelAtmosphereManager.cpp`. `VoxelMarchingCubesMesher` now exposes
`GeometryVoxelSizeCm` so Core geometry is emitted at 25 cm while legacy stays at its 12.5 cm
default. The Core bridge adapts carve/fill signs and scales Blueprint brush radius, extent, falloff
and strength. `Standalone/Core/src/FarShellsInternal.h` subdivides LOD7+ far-stratum repairs, and
`Standalone/Core/src/VoxelForgeCore.cpp` enables a guarded heightfield Z-band path for unhashed
native tiles, combining ground/cap heights with intersecting bounded 3D feature extents; overlapping
edits keep the full mesh path. The BuildHost commandlet adds height-anchor agreement, brush
sign/size, geometry and per-tile-kind pawn-lag checks; see round 6 in
[`Standalone/ASSEMBLER_M1_REPORT.md`](Standalone/ASSEMBLER_M1_REPORT.md).
[OwnerWorldLoader.h](Standalone/Source/OwnerWorldLoader.h) now reads SlabParams, MazeParams,
VerticalShaftParams, and FloatingIslandParams. The shape-world evaluator still lacks their native
terrain fields: FlatPlain, CrystalChamber, Maze, VerticalShafts, FloatingIslands, and water
@@ -6,6 +6,7 @@
#include "VoxelBiomeDefinition.h"
#include "VoxelGenerator.h"
#include "VoxelTypes.h" // CHUNK_SIZE / VOXEL_SIZE
#include "VoxelWorld.h"
#include "Components/ExponentialHeightFogComponent.h"
#include "Components/SkyLightComponent.h"
#include "GameFramework/Actor.h"
@@ -50,9 +51,12 @@ void UVoxelAtmosphereManager::UpdateForPlayer(const FVector& PlayerWorldPos)
// identity".
const FTransform Xf = O->GetActorTransform();
const FVector LocalPos = Xf.InverseTransformPosition(PlayerWorldPos);
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(O);
const float ManagerLocalZ = VoxelWorld
? VoxelWorld->ToStrateManagerLocalZ(LocalPos.Z) : LocalPos.Z;
const int32 Idx = StrateManager->GetStrateIndex(LocalPos.Z);
const UVoxelStrateDefinition* Def = StrateManager->GetStrateAt(LocalPos.Z);
const int32 Idx = StrateManager->GetStrateIndex(ManagerLocalZ);
const UVoxelStrateDefinition* Def = StrateManager->GetStrateAt(ManagerLocalZ);
// Player's dominant biome (XY field) — atmosphere can vary by biome within a strate.
// Requete en espace ACTEUR (voir LocalPos ci-dessus) : cm locaux -> voxels.
@@ -60,7 +64,7 @@ void UVoxelAtmosphereManager::UpdateForPlayer(const FVector& PlayerWorldPos)
const UVoxelBiomeDefinition* Biome = nullptr;
if (Generator && Def)
{
const int32 ChunkZ = FMath::FloorToInt((LocalPos.Z / VOXEL_SIZE) / CHUNK_SIZE);
const int32 ChunkZ = FMath::FloorToInt((ManagerLocalZ / VOXEL_SIZE) / CHUNK_SIZE);
Biome = Generator->GetDominantBiomeAt(LocalPos.X / VOXEL_SIZE,
LocalPos.Y / VOXEL_SIZE, ChunkZ);
}
@@ -91,18 +95,24 @@ void UVoxelAtmosphereManager::UpdateForPlayer(const FVector& PlayerWorldPos)
if (Def)
{
float TopZ, BottomZ;
if (StrateManager->GetStrateUnrealZRange(LocalPos.Z, TopZ, BottomZ))
if (StrateManager->GetStrateUnrealZRange(ManagerLocalZ, TopZ, BottomZ))
{
const float LocalTopZ = VoxelWorld
? VoxelWorld->FromStrateManagerLocalZ(TopZ) : TopZ;
const float LocalBottomZ = VoxelWorld
? VoxelWorld->FromStrateManagerLocalZ(BottomZ) : BottomZ;
if (CeilingActor)
{
CeilingActor->SetActorLocationAndRotation(
Xf.TransformPosition(FVector(LocalPos.X, LocalPos.Y, TopZ + Def->CeilingLayerZOffset)),
Xf.TransformPosition(FVector(LocalPos.X, LocalPos.Y,
LocalTopZ + Def->CeilingLayerZOffset)),
Def->CeilingLayerRotation);
}
if (FloorActor)
{
FloorActor->SetActorLocationAndRotation(
Xf.TransformPosition(FVector(LocalPos.X, LocalPos.Y, BottomZ + Def->FloorLayerZOffset)),
Xf.TransformPosition(FVector(LocalPos.X, LocalPos.Y,
LocalBottomZ + Def->FloorLayerZOffset)),
Def->FloorLayerRotation);
}
}
+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;
}
@@ -119,6 +119,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
// Cell size in voxels. No upper clamp: coarse clipmap levels use bigger steps (the EXTENT
// grows). Coarse tiles also use FEWER cells (InCellsPerAxis) for cheaper gen.
Step = FMath::Max(1, Step);
const float GeometryVoxelSize = VoxelMath::IsFinite(GeometryVoxelSizeCm)
? FMath::Max(1.0f, GeometryVoxelSizeCm) : VOXEL_SIZE;
// T2.b — octave bias for THIS tile: drop LODOctaveDrop octaves per Step doubling from
// the generator's per-voxel volumetric noise (sub-cell octaves can't shape a coarse
@@ -130,7 +132,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
TGuardValue<int32> SampleStepGuard(VoxelGenLOD::SampleStep, Step);
// World-cm origin of the tile's min corner (positions are built relative to this).
const FVector ChunkWorldPos = FVector(OriginVoxels) * VOXEL_SIZE;
const FVector ChunkWorldPos = FVector(OriginVoxels) * GeometryVoxelSize;
//=========================================================================
// VERTEX DEDUPLICATION MAP
@@ -1120,7 +1122,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
const int32 GY = cy + CornerOffsets[i].Y;
const int32 GZ = cz + CornerOffsets[i].Z;
Positions[i] = ChunkWorldPos
+ FVector(GX * Step, GY * Step, GZ * Step) * VOXEL_SIZE;
+ FVector(GX * Step, GY * Step, GZ * Step) * GeometryVoxelSize;
Gradients[i] = GradAt(GX, GY, GZ);
}
if (bSampleCell)
@@ -1224,10 +1226,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
MeshData.NumSurfaceVertices = MeshData.Vertices.Num();
if (bGenerateSkirts && (GroundTris.Num() + CapTris.Num()) > 0)
{
const float ExtentCm = (float)(CellsPerAxis * Step) * VOXEL_SIZE;
const float ExtentCm = (float)(CellsPerAxis * Step) * GeometryVoxelSize;
const float MinX = ChunkWorldPos.X, MinY = ChunkWorldPos.Y, MinZ = ChunkWorldPos.Z;
const float MaxX = MinX + ExtentCm, MaxY = MinY + ExtentCm, MaxZ = MinZ + ExtentCm;
const float SkirtDepth = FMath::Max(1.0f, SkirtCells) * (float)Step * VOXEL_SIZE;
const float SkirtDepth = FMath::Max(1.0f, SkirtCells) * (float)Step * GeometryVoxelSize;
auto OnBoundaryPlane = [&](const FVector& A, const FVector& B) -> bool
{
+27 -11
View File
@@ -2403,6 +2403,7 @@ void AVoxelWorld::BeginPlay()
Generator->InitializeSettings(Settings);
Mesher->SetGenerator(Generator);
Mesher->SetGeometryVoxelSizeCm(GetActiveVoxelSizeCm());
Mesher->SetShutdownFlag(&bShuttingDown);
Mesher->bGenerateSkirts = Settings->bGenerateSkirts;
Mesher->SkirtCells = Settings->SkirtCells;
@@ -12476,10 +12477,21 @@ int32 AVoxelWorld::GetStrateAtPosition(FVector WorldPosition) const
// GetStrateIndex veut des cm ACTEUR-LOCAUX (il convertit cm -> voxels lui-meme).
// GetStrateIndex wants actor-LOCAL cm (it does the cm -> voxel conversion itself).
const float LocalZCm = WorldToLocalCm(WorldPosition).Z;
const float StrateManagerZCm = bUseVoxelForgeCore
return StrateManager->GetStrateIndex(ToStrateManagerLocalZ(LocalZCm));
}
float AVoxelWorld::ToStrateManagerLocalZ(float LocalZCm) const
{
return bUseVoxelForgeCore
? (LocalZCm / GetActiveVoxelSizeCm()) * VOXEL_SIZE
: LocalZCm;
return StrateManager->GetStrateIndex(StrateManagerZCm);
}
float AVoxelWorld::FromStrateManagerLocalZ(float ManagerLocalZCm) const
{
return bUseVoxelForgeCore
? (ManagerLocalZCm / VOXEL_SIZE) * GetActiveVoxelSizeCm()
: ManagerLocalZCm;
}
FVoxelBiomeQuery AVoxelWorld::GetBiomeAtWorldLocation(FVector WorldLocation) const
@@ -12610,8 +12622,9 @@ void AVoxelWorld::CarveAtPosition(FVector Position, float Radius, float Strength
{
FVoxelModification Mod;
Mod.Center = WorldToLocalVoxel(Position); // world cm → voxel space
Mod.Radius = Radius * GetLegacyVoxelToActiveVoxelScale();
Mod.Strength = -FMath::Abs(Strength); // force negative for carving
const float VoxelScale = GetLegacyVoxelToActiveVoxelScale();
Mod.Radius = Radius * VoxelScale;
Mod.Strength = -FMath::Abs(Strength) * VoxelScale; // force negative for carving / Force le signe negatif pour creuser.
ApplyModification(Mod);
}
@@ -12619,8 +12632,9 @@ void AVoxelWorld::FillAtPosition(FVector Position, float Radius, float Strength)
{
FVoxelModification Mod;
Mod.Center = WorldToLocalVoxel(Position);
Mod.Radius = Radius * GetLegacyVoxelToActiveVoxelScale();
Mod.Strength = FMath::Abs(Strength); // force positive for filling
const float VoxelScale = GetLegacyVoxelToActiveVoxelScale();
Mod.Radius = Radius * VoxelScale;
Mod.Strength = FMath::Abs(Strength) * VoxelScale; // force positive for filling / Force le signe positif pour remplir.
ApplyModification(Mod);
}
@@ -12811,7 +12825,9 @@ void AVoxelWorld::ApplyCoreModification(const FVoxelModification& Modification)
Brush.halfExtent = {Modification.BoxExtent.X, Modification.BoxExtent.Y, Modification.BoxExtent.Z};
Brush.radius = FMath::Max(0.0f, Modification.Radius);
Brush.falloff = FMath::Max(0.0f, Modification.Falloff);
Brush.strength = Modification.Strength;
// Legacy subtracts the modification offset; Core's densityOffset operation adds it.
// Le legacy soustrait le decalage; l'operation densityOffset de Core l'ajoute.
Brush.strength = -Modification.Strength;
switch (Modification.Shape)
{
case EVoxelBrushShape::Box:
@@ -12971,7 +12987,7 @@ void AVoxelWorld::CarveBox(FVector Position, FVector ExtentVoxels, float Strengt
Mod.BoxExtent = ExtentVoxels * VoxelScale;
Mod.Falloff *= VoxelScale;
Mod.Radius = ExtentVoxels.GetMax() * VoxelScale; // budget proxy
Mod.Strength = -FMath::Abs(Strength); // carve
Mod.Strength = -FMath::Abs(Strength) * VoxelScale; // carve / creuser
ApplyModification(Mod);
}
@@ -12984,7 +13000,7 @@ void AVoxelWorld::FillBox(FVector Position, FVector ExtentVoxels, float Strength
Mod.BoxExtent = ExtentVoxels * VoxelScale;
Mod.Falloff *= VoxelScale;
Mod.Radius = ExtentVoxels.GetMax() * VoxelScale;
Mod.Strength = FMath::Abs(Strength); // fill
Mod.Strength = FMath::Abs(Strength) * VoxelScale; // fill / remplir
ApplyModification(Mod);
}
@@ -12997,7 +13013,7 @@ void AVoxelWorld::CarveCapsule(FVector WorldA, FVector WorldB, float RadiusVoxel
const float VoxelScale = GetLegacyVoxelToActiveVoxelScale();
Mod.Radius = RadiusVoxels * VoxelScale;
Mod.Falloff *= VoxelScale;
Mod.Strength = -FMath::Abs(Strength);
Mod.Strength = -FMath::Abs(Strength) * VoxelScale;
ApplyModification(Mod);
}
@@ -13010,7 +13026,7 @@ void AVoxelWorld::FillCapsule(FVector WorldA, FVector WorldB, float RadiusVoxels
const float VoxelScale = GetLegacyVoxelToActiveVoxelScale();
Mod.Radius = RadiusVoxels * VoxelScale;
Mod.Falloff *= VoxelScale;
Mod.Strength = FMath::Abs(Strength);
Mod.Strength = FMath::Abs(Strength) * VoxelScale;
ApplyModification(Mod);
}
+14 -1
View File
@@ -59,6 +59,7 @@ class UStaticMesh;
class UStaticMeshComponent;
class UHierarchicalInstancedStaticMeshComponent;
class UMaterialInterface;
struct FCoreSurfaceSnapshot;
// An active mini-sun light orb (a placed FStrateLandmark with bIsLightOrb). The terrain material marches
// the density volume toward the nearest of these for raymarched shadows. Plain struct (not reflected);
@@ -97,6 +98,11 @@ public:
* plane). */
void UpdateWater(const FVector& PlayerWorldPos);
/** Read immutable Core ground/water anchors for placement. / Lit les ancres natives sol/eau pour le placement Core. */
bool QueryNativeSurfaceAnchors(int32 StrateIndex, const FVector2D& LocalXYCm,
float& OutGroundLocalZCm, bool& bOutHasWater,
float& OutWaterLocalZCm) const;
//--- DECORATIONS (distance-based world grid, async march) ----------------
/** Stream decoration cells around the player: recompute the desired set on cell/strate change,
* launch async march tasks (capped), and apply finished results budgeted. Call every Tick. */
@@ -239,12 +245,15 @@ private:
struct FDecoContext
{
const UVoxelStrateDefinition* Def = nullptr; // game-thread only (biome/decoration resolve)
int32 StrateIndex = INDEX_NONE;
int32 RepChunkZ = 0; // representative chunk-Z for biome lookups
float TopVoxelZ = 0.0f; // strate band, voxel coords (march range)
float BottomVoxelZ = 0.0f;
float WaterLocalZ = -FLT_MAX; // water surface, actor-local cm (-FLT_MAX = no water)
bool bHasWater = false;
bool bSurfaceWorld = false; // heightfield archetype → use the GetSurfaceHeightAt oracle
bool bNativeSurface = false; // Core manifest is the height oracle and water datum / Le manifeste Core definit le relief et le niveau d'eau.
TSharedPtr<const FCoreSurfaceSnapshot, ESPMode::ThreadSafe> NativeSurface;
// Strate biome field (PODs only → worker-safe). Empty ⇒ biomes disabled for this strate. The
// worker resolves the dominant biome PER COLUMN (ResolveBiomeSampleAt) so decoration borders
@@ -337,6 +346,10 @@ private:
UPROPERTY()
UVoxelSettings* Settings = nullptr;
// Immutable Core manifest snapshot captured by decoration workers; workers never touch UObjects.
// Instantane immuable du manifeste Core pour les workers de decoration; aucun worker ne touche aux UObjects.
TSharedPtr<const FCoreSurfaceSnapshot, ESPMode::ThreadSafe> NativeSurfaceSnapshot;
int32 Seed = 0;
// Engine unit plane (/Engine/BasicShapes/Plane) reused for every water surface.
@@ -391,6 +404,6 @@ private:
// Cached state so UpdateWater is a cheap no-op when neither the water level nor the snapped
// player cell changed since last frame.
float LastWaterZ = -FLT_MAX; // strate water level (voxel Z)
float LastWaterZ = -FLT_MAX; // actor-local water height (cm) / Hauteur d'eau locale a l'acteur (cm).
FIntPoint LastWaterCell = FIntPoint(INT32_MIN, INT32_MIN); // snapped XY grid cell
};
@@ -90,6 +90,10 @@ public:
void SetGenerator(const UVoxelGenerator* InGenerator) { Generator = InGenerator; }
// Core tile coordinates use 25 cm voxels; legacy generation keeps 12.5 cm.
// Les coordonnees des tuiles Core utilisent des voxels de 25 cm; le legacy reste a 12,5 cm.
void SetGeometryVoxelSizeCm(float InVoxelSizeCm) { GeometryVoxelSizeCm = InVoxelSizeCm; }
// AVoxelWorld installs its bShuttingDown flag here. The commandlet path leaves it null.
// This is intentionally read-only: workers may observe shutdown, but never mutate world state.
void SetShutdownFlag(const std::atomic<bool>* InShutdownFlag) { ShutdownFlag = InShutdownFlag; }
@@ -107,6 +111,7 @@ private:
const std::atomic<bool>* ShutdownFlag = nullptr;
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
float GeometryVoxelSizeCm = VOXEL_SIZE;
public:
+16 -5
View File
@@ -426,7 +426,8 @@ public:
* @param Position - World-space center of the carve brush
* @param Radius - Brush radius in legacy Runtime voxels (12.5 cm); Core converts it to
* 25 cm voxels to preserve the physical brush size.
* @param Strength - How aggressively to carve (default 10, higher = deeper)
* @param Strength - Legacy Runtime density offset; Core scales it to 25 cm units.
* Offset de densite Runtime historique; Core le convertit en unite de 25 cm.
*/
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
void CarveAtPosition(FVector Position, float Radius = 6.0f, float Strength = 10.0f);
@@ -438,7 +439,8 @@ public:
* @param Position - World-space center of the fill brush
* @param Radius - Brush radius in legacy Runtime voxels (12.5 cm); Core converts it to
* 25 cm voxels to preserve the physical brush size.
* @param Strength - How aggressively to fill (default 10, higher = more solid)
* @param Strength - Legacy Runtime density offset; Core scales it to 25 cm units.
* Offset de densite Runtime historique; Core le convertit en unite de 25 cm.
*/
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
void FillAtPosition(FVector Position, float Radius = 6.0f, float Strength = 10.0f);
@@ -446,7 +448,8 @@ public:
/**
* Box brush carve/fill. Position is world-space (Unreal units); ExtentVoxels is the
* box half-size in legacy Runtime voxels (12.5 cm). Core converts it to 25 cm voxels.
* Strength magnitude controls aggressiveness (sign forced).
* Strength is a legacy-voxel density offset; Core scales it to 25 cm units and forces its sign.
* La force est un decalage de densite historique; Core la convertit en unite de 25 cm et impose le signe.
*/
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
void CarveBox(FVector Position, FVector ExtentVoxels, float Strength = 12.0f);
@@ -455,8 +458,9 @@ public:
void FillBox(FVector Position, FVector ExtentVoxels, float Strength = 12.0f);
/**
* Capsule brush carve/fill between two world-space points (Unreal units), with a tube radius
* in legacy Runtime voxels (12.5 cm). Core converts it to 25 cm voxels.
* Capsule brush carve/fill between two world-space points (Unreal units). Tube radius and density offset
* use legacy Runtime voxels (12.5 cm); Core converts both to 25 cm units.
* Creuse/remplit une capsule entre deux points monde. Rayon et decalage Runtime historique sont convertis en unite Core de 25 cm.
* Great for boring tunnels or laying solid pillars/walls.
*/
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
@@ -572,6 +576,11 @@ public:
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
int32 GetStrateAtPosition(FVector WorldPosition) const;
// Convert actor-local physical Z to/from UVoxelStrateManager's legacy 12.5 cm space.
// Convertit Z local physique vers/depuis l'espace historique de UVoxelStrateManager.
float ToStrateManagerLocalZ(float LocalZCm) const;
float FromStrateManagerLocalZ(float ManagerLocalZCm) const;
/**
* Probe the biome field at a world location (e.g. a mouse line-trace hit). Returns the dominant +
* neighbour biome, the climate fields, the border blend weight, and the dominant biome's decoration
@@ -626,6 +635,8 @@ public:
private:
friend class FScopedGenerationPause;
friend class UVoxelContentManager;
friend class UVoxelAtmosphereManager;
#if WITH_EDITOR
/** Last editor research-field selection applied to loaded tiles. */
@@ -3,13 +3,19 @@
#include "VoxelForgeRegionEvalExperiment.h"
#include "CoreMinimal.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "Async/ParallelFor.h"
#include "Commandlets/Commandlet.h"
#include "Engine/Blueprint.h"
#include "Containers/StringConv.h"
#include "Components/SceneComponent.h"
#include "Engine/Engine.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/Actor.h"
#include "GameFramework/GameModeBase.h"
#include "GameFramework/Pawn.h"
@@ -44,6 +50,7 @@
#include "VoxelDensityAblation.h"
#include "VoxelDensityProfile.h"
#include "VoxelGenerator.h"
#include "VoxelContentManager.h"
#include "VoxelGeometryHash.h"
#include "VoxelMarchingCubesMesher.h"
#include "VoxelRedefinedDensityField.h"
@@ -74,6 +81,445 @@
#include <thread>
#include <vector>
namespace
{
enum class EAssemblerTileKind : uint8
{
FlatColumn,
Relief,
Mixed,
Unknown
};
struct FAssemblerTileTimingSample
{
int32 LOD = 0;
EAssemblerTileKind Kind = EAssemblerTileKind::Unknown;
double ServiceSeconds = 0.0;
double ModelReadySeconds = 0.0;
double ClassificationMilliseconds = 0.0;
double ColumnMilliseconds = 0.0;
double MeshingMilliseconds = 0.0;
bool bHeightfieldBandLimited = false;
};
const TCHAR* AssemblerTileKindName(EAssemblerTileKind Kind)
{
switch (Kind)
{
case EAssemblerTileKind::FlatColumn: return TEXT("flat_column");
case EAssemblerTileKind::Relief: return TEXT("relief");
case EAssemblerTileKind::Mixed: return TEXT("mixed");
default: return TEXT("unknown");
}
}
EAssemblerTileKind ClassifyAssemblerTile(const VoxelForgeCore::TileMesh& Mesh)
{
const VoxelForgeCore::TileGenerationStats& Stats = Mesh.diagnostics;
if (Stats.featureRefinedResolution)
{
return EAssemblerTileKind::Relief;
}
double MinGroundZ = std::numeric_limits<double>::infinity();
double MaxGroundZ = -std::numeric_limits<double>::infinity();
const std::size_t GroundIndexCount = std::min<std::size_t>(
Mesh.indices.size(), static_cast<std::size_t>(Mesh.groups.groundTriangleCount) * 3u);
const std::size_t ProbeCount = std::min<std::size_t>(64u, GroundIndexCount);
for (std::size_t Probe = 0; Probe < ProbeCount; ++Probe)
{
const std::size_t Index = ProbeCount <= 1u ? 0u
: (Probe * (GroundIndexCount - 1u)) / (ProbeCount - 1u);
const std::uint32_t VertexIndex = Mesh.indices[Index];
if (VertexIndex >= Mesh.vertices.size())
{
return EAssemblerTileKind::Unknown;
}
const double Z = Mesh.vertices[VertexIndex].position.z;
if (!std::isfinite(Z))
{
return EAssemblerTileKind::Unknown;
}
MinGroundZ = std::min(MinGroundZ, Z);
MaxGroundZ = std::max(MaxGroundZ, Z);
}
const int32 LOD = FMath::Clamp(static_cast<int32>(Mesh.key.lod), 0, 20);
const double ReliefThresholdCm = 4.0 * 25.0 * static_cast<double>(1u << LOD);
if (std::isfinite(MinGroundZ) && MaxGroundZ - MinGroundZ > ReliefThresholdCm)
{
return EAssemblerTileKind::Relief;
}
if (Stats.meshingSkipped || Stats.tileClass == VoxelForgeCore::TileClass::allAir
|| Stats.tileClass == VoxelForgeCore::TileClass::allSolid)
{
return EAssemblerTileKind::FlatColumn;
}
if (Stats.tileClass == VoxelForgeCore::TileClass::mixed || !Mesh.indices.empty())
{
return EAssemblerTileKind::Mixed;
}
if (Stats.unknownBoxes > 0 || Stats.tileClass == VoxelForgeCore::TileClass::unknown)
{
return EAssemblerTileKind::Unknown;
}
return EAssemblerTileKind::Unknown;
}
bool TryParseBlueprintPinNumber(const UEdGraphPin* Pin, double& OutValue)
{
if (Pin == nullptr || Pin->DefaultValue.IsEmpty())
{
return false;
}
return LexTryParseString(OutValue, *Pin->DefaultValue) && std::isfinite(OutValue);
}
bool ReadStagedPlayerSpeeds(float& OutWalkCmPerSecond, float& OutSprintCmPerSecond,
FString& OutError)
{
const UClass* CharacterClass = LoadClass<ACharacter>(nullptr,
TEXT("/Game/FirstPerson/Blueprints/BP_FirstPersonCharacter.BP_FirstPersonCharacter_C"));
const UClass* ControllerClass = LoadClass<APlayerController>(nullptr,
TEXT("/Game/FirstPerson/Blueprints/BP_FirstPersonPlayerController.BP_FirstPersonPlayerController_C"));
const ACharacter* CharacterDefault = CharacterClass
? Cast<ACharacter>(CharacterClass->GetDefaultObject()) : nullptr;
if (CharacterDefault == nullptr || CharacterDefault->GetCharacterMovement() == nullptr)
{
OutError = TEXT("BuildHost staged BP_FirstPersonCharacter CDO or CharacterMovement is missing.");
return false;
}
OutWalkCmPerSecond = CharacterDefault->GetCharacterMovement()->MaxWalkSpeed;
double BestSprintSpeed = 0.0;
FString BestSprintSource;
const UBlueprint* CharacterBlueprint = LoadObject<UBlueprint>(nullptr,
TEXT("/Game/FirstPerson/Blueprints/BP_FirstPersonCharacter.BP_FirstPersonCharacter"));
const UBlueprint* ControllerBlueprint = LoadObject<UBlueprint>(nullptr,
TEXT("/Game/FirstPerson/Blueprints/BP_FirstPersonPlayerController.BP_FirstPersonPlayerController"));
const auto ScanBlueprintGraphs = [&](const UBlueprint* Blueprint, const TCHAR* BlueprintName)
{
if (Blueprint == nullptr)
{
return;
}
const auto ScanGraph = [&](const UEdGraph* Graph)
{
if (Graph == nullptr)
{
return;
}
for (const UEdGraphNode* Node : Graph->Nodes)
{
if (Node == nullptr)
{
continue;
}
for (const UEdGraphPin* Pin : Node->Pins)
{
if (Pin == nullptr || Pin->Direction != EGPD_Input
|| !Pin->PinName.ToString().Contains(TEXT("MaxWalkSpeed"), ESearchCase::IgnoreCase))
{
continue;
}
double Value = 0.0;
bool bFoundValue = TryParseBlueprintPinNumber(Pin, Value);
if (!bFoundValue)
{
for (const UEdGraphPin* LinkedPin : Pin->LinkedTo)
{
if (TryParseBlueprintPinNumber(LinkedPin, Value))
{
bFoundValue = true;
break;
}
}
}
if (bFoundValue && Value > static_cast<double>(OutWalkCmPerSecond)
&& Value > BestSprintSpeed)
{
BestSprintSpeed = Value;
BestSprintSource = FString::Printf(TEXT("%s.%s"), BlueprintName,
*Node->GetName());
}
}
}
};
for (const auto& GraphReference : Blueprint->UbergraphPages)
{
ScanGraph(GraphReference);
}
for (const auto& GraphReference : Blueprint->FunctionGraphs)
{
ScanGraph(GraphReference);
}
};
ScanBlueprintGraphs(CharacterBlueprint, TEXT("BP_FirstPersonCharacter"));
ScanBlueprintGraphs(ControllerBlueprint, TEXT("BP_FirstPersonPlayerController"));
if (!std::isfinite(OutWalkCmPerSecond) || OutWalkCmPerSecond <= 0.0f
|| !std::isfinite(BestSprintSpeed) || BestSprintSpeed <= OutWalkCmPerSecond)
{
OutError = TEXT("BuildHost staged Blueprint graphs do not expose a MaxWalkSpeed input above the character CDO walk speed.");
return false;
}
OutSprintCmPerSecond = static_cast<float>(BestSprintSpeed);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgePawnSpeedFixture] source=staged_blueprints walk_cm_s=%.3f sprint_cm_s=%.3f sprint_graph=%s"),
OutWalkCmPerSecond, OutSprintCmPerSecond, *BestSprintSource);
return true;
}
struct FAssemblerGeometryAudit
{
bool bValid = false;
int32 LOD = 0;
FIntVector Tile = FIntVector::ZeroValue;
double ExpectedTileSpanCm = 0.0;
double CellWorldCm = 0.0;
double SpanXcm = 0.0;
double SpanYcm = 0.0;
double MaxTriangleEdgeCm = 0.0;
VoxelForgeCore::Vec3d MaxEdgeStart;
VoxelForgeCore::Vec3d MaxEdgeEnd;
std::uint64_t MaxEdgeTriangleIndex = 0;
int32 NormalSamples = 0;
int32 NormalOrientationMismatches = 0;
int32 TriangleSamples = 0;
int32 TriangleWindingMismatches = 0;
int32 SkirtTrianglesExcluded = 0;
};
bool AuditAssemblerCoreTile(const VoxelForgeCore::World& World,
const VoxelForgeCore::TileMesh& Mesh,
FAssemblerGeometryAudit& OutAudit,
FString& OutError)
{
if (Mesh.indices.empty() || Mesh.indices.size() % 3u != 0u || Mesh.vertices.empty())
{
OutError = TEXT("Core geometry audit received a malformed triangle mesh.");
return false;
}
const std::size_t SurfaceVertexCount = std::min<std::size_t>(
Mesh.vertices.size(), Mesh.groups.surfaceVertexCount);
if (SurfaceVertexCount == 0u)
{
OutError = TEXT("Core geometry audit mesh contains no surface vertices.");
return false;
}
double MinX = std::numeric_limits<double>::infinity();
double MaxX = -std::numeric_limits<double>::infinity();
double MinY = std::numeric_limits<double>::infinity();
double MaxY = -std::numeric_limits<double>::infinity();
for (std::size_t Index = 0; Index < SurfaceVertexCount; ++Index)
{
const VoxelForgeCore::MeshVertex& Vertex = Mesh.vertices[Index];
if (!std::isfinite(Vertex.position.x) || !std::isfinite(Vertex.position.y)
|| !std::isfinite(Vertex.position.z))
{
OutError = TEXT("Core geometry audit found a non-finite surface vertex.");
return false;
}
MinX = std::min(MinX, Vertex.position.x);
MaxX = std::max(MaxX, Vertex.position.x);
MinY = std::min(MinY, Vertex.position.y);
MaxY = std::max(MaxY, Vertex.position.y);
}
const int32 LOD = static_cast<int32>(Mesh.key.lod);
const VoxelForgeCore::WorldConfiguration& Configuration = World.configuration();
const int32 ExtentVoxels = CHUNK_SIZE << FMath::Clamp(LOD, 0, 20);
const int32 Cells = LOD < Configuration.fullResolutionLodCount
? CHUNK_SIZE : FMath::Max(1, Configuration.coarseTileCells);
const double Step = static_cast<double>(FMath::Max(1, ExtentVoxels / Cells));
const int32 StepVoxels = FMath::Max(1, FMath::RoundToInt(Step));
const double CellWorldCm = 25.0 * Step;
// Les coutures lointaines peuvent depasser deux cellules en plan sans former une pointe verticale.
// Far LOD transitions can span slightly over two horizontal cells without creating a vertical spike.
const double MaxEdgeCellMultiple = LOD >= 7 ? 4.0 : 2.0;
const double MaxEdgeWorldCm = MaxEdgeCellMultiple * CellWorldCm;
const int32 PreviousOctaveBias = VoxelGenLOD::GetThreadOctaveBias();
const int32 PreviousSampleStep = VoxelGenLOD::GetThreadSampleStep();
struct FRestoreAssemblerLodSampleState
{
int32 OctaveBias;
int32 SampleStep;
~FRestoreAssemblerLodSampleState()
{
VoxelGenLOD::SetThreadOctaveBias(OctaveBias);
VoxelGenLOD::SetThreadSampleStep(SampleStep);
}
} RestoreLodSampleState{PreviousOctaveBias, PreviousSampleStep};
const int32 OctaveBias = Configuration.lodOctaveDrop > 0 && StepVoxels > 1
? Configuration.lodOctaveDrop * static_cast<int32>(FMath::FloorLog2(
static_cast<uint32>(StepVoxels))) : 0;
VoxelGenLOD::SetThreadOctaveBias(OctaveBias);
VoxelGenLOD::SetThreadSampleStep(StepVoxels);
for (std::size_t Index = 0; Index < Mesh.indices.size(); Index += 3u)
{
const std::size_t TriangleIndex = Index / 3u;
if (TriangleIndex < Mesh.skirtFaceMasks.size()
&& Mesh.skirtFaceMasks[TriangleIndex] != 0u)
{
++OutAudit.SkirtTrianglesExcluded;
continue;
}
const std::uint32_t IA = Mesh.indices[Index];
const std::uint32_t IB = Mesh.indices[Index + 1u];
const std::uint32_t IC = Mesh.indices[Index + 2u];
if (IA >= Mesh.vertices.size() || IB >= Mesh.vertices.size() || IC >= Mesh.vertices.size())
{
OutError = TEXT("Core geometry audit found an out-of-range triangle index.");
return false;
}
const auto& A = Mesh.vertices[IA].position;
const auto& B = Mesh.vertices[IB].position;
const auto& C = Mesh.vertices[IC].position;
const auto Distance = [](const auto& P, const auto& Q)
{
const double DX = P.x - Q.x;
const double DY = P.y - Q.y;
const double DZ = P.z - Q.z;
return std::sqrt(DX * DX + DY * DY + DZ * DZ);
};
const double EdgeAB = Distance(A, B);
const double EdgeBC = Distance(B, C);
const double EdgeCA = Distance(C, A);
const double Edge = std::max({EdgeAB, EdgeBC, EdgeCA});
const VoxelForgeCore::Vec3d* EdgeStart = &A;
const VoxelForgeCore::Vec3d* EdgeEnd = &B;
if (EdgeBC >= EdgeAB && EdgeBC >= EdgeCA)
{
EdgeStart = &B;
EdgeEnd = &C;
}
else if (EdgeCA >= EdgeAB)
{
EdgeStart = &C;
EdgeEnd = &A;
}
if (!std::isfinite(Edge) || Edge > MaxEdgeWorldCm + 0.1)
{
OutError = FString::Printf(
TEXT("Core geometry audit found an oversized edge %.3f cm (bound %.3f cm; delta=(%.3f,%.3f,%.3f), start=(%.3f,%.3f,%.3f), end=(%.3f,%.3f,%.3f)) in tile (%d,%d,%d) lod=%d triangle=%llu."),
Edge, MaxEdgeWorldCm, EdgeEnd->x - EdgeStart->x, EdgeEnd->y - EdgeStart->y,
EdgeEnd->z - EdgeStart->z, EdgeStart->x, EdgeStart->y, EdgeStart->z,
EdgeEnd->x, EdgeEnd->y, EdgeEnd->z, Mesh.key.coordinate.x,
Mesh.key.coordinate.y, Mesh.key.coordinate.z, LOD,
static_cast<unsigned long long>(TriangleIndex));
return false;
}
if (Edge > OutAudit.MaxTriangleEdgeCm)
{
OutAudit.MaxTriangleEdgeCm = Edge;
OutAudit.MaxEdgeTriangleIndex = static_cast<std::uint64_t>(TriangleIndex);
if (EdgeAB >= EdgeBC && EdgeAB >= EdgeCA)
{
OutAudit.MaxEdgeStart = A;
OutAudit.MaxEdgeEnd = B;
}
else if (EdgeBC >= EdgeCA)
{
OutAudit.MaxEdgeStart = B;
OutAudit.MaxEdgeEnd = C;
}
else
{
OutAudit.MaxEdgeStart = C;
OutAudit.MaxEdgeEnd = A;
}
}
const double ABX = B.x - A.x, ABY = B.y - A.y, ABZ = B.z - A.z;
const double ACX = C.x - A.x, ACY = C.y - A.y, ACZ = C.z - A.z;
const double FaceX = ABY * ACZ - ABZ * ACY;
const double FaceY = ABZ * ACX - ABX * ACZ;
const double FaceZ = ABX * ACY - ABY * ACX;
const VoxelForgeCore::MeshVertex& NormalA = Mesh.vertices[IA];
const VoxelForgeCore::MeshVertex& NormalB = Mesh.vertices[IB];
const VoxelForgeCore::MeshVertex& NormalC = Mesh.vertices[IC];
const double Alignment = FaceX * (NormalA.normal.x + NormalB.normal.x + NormalC.normal.x)
+ FaceY * (NormalA.normal.y + NormalB.normal.y + NormalC.normal.y)
+ FaceZ * (NormalA.normal.z + NormalB.normal.z + NormalC.normal.z);
const double FaceLengthSq = FaceX * FaceX + FaceY * FaceY + FaceZ * FaceZ;
if (FaceLengthSq > 1.0e-10)
{
if (Alignment < -1.0e-10 * std::sqrt(FaceLengthSq))
{
// RealtimeMesh requires MC indices in reversed 0,2,1 order; this reports the expected convention.
// RealtimeMesh exige l'ordre MC inverse 0,2,1; ce compte documente la convention attendue.
++OutAudit.TriangleWindingMismatches;
}
++OutAudit.TriangleSamples;
}
}
const std::size_t NormalProbeCount = std::min<std::size_t>(96u, SurfaceVertexCount);
for (std::size_t Probe = 0; Probe < NormalProbeCount; ++Probe)
{
const std::size_t Index = NormalProbeCount <= 1u ? 0u
: Probe * (SurfaceVertexCount - 1u) / (NormalProbeCount - 1u);
const VoxelForgeCore::MeshVertex& Vertex = Mesh.vertices[Index];
const double NX = Vertex.normal.x;
const double NY = Vertex.normal.y;
const double NZ = Vertex.normal.z;
const double NormalLengthSq = NX * NX + NY * NY + NZ * NZ;
if (!std::isfinite(NormalLengthSq) || std::abs(NormalLengthSq - 1.0) > 0.02)
{
OutError = FString::Printf(
TEXT("Core geometry audit found a non-unit normal (length squared %.5f)."),
NormalLengthSq);
return false;
}
// Garder la sonde locale evite de traverser plusieurs reliefs aux LOD lointains.
// Keep the probe local so coarse-LOD checks do not cross several terrain features.
if (LOD <= 1)
{
// Le champ complet ne represente pas la surface lointaine decimee par le LOD.
// The full-resolution field does not represent the decimated far-LOD surface.
const double NormalOffsetVoxels = std::min(0.5 * Step, 1.0);
const VoxelForgeCore::Vec3d Positive{
Vertex.position.x / 25.0 + NX * NormalOffsetVoxels,
Vertex.position.y / 25.0 + NY * NormalOffsetVoxels,
Vertex.position.z / 25.0 + NZ * NormalOffsetVoxels};
const VoxelForgeCore::Vec3d Negative{
Vertex.position.x / 25.0 - NX * NormalOffsetVoxels,
Vertex.position.y / 25.0 - NY * NormalOffsetVoxels,
Vertex.position.z / 25.0 - NZ * NormalOffsetVoxels};
const float PositiveDensity = World.densityAt(Positive);
const float NegativeDensity = World.densityAt(Negative);
if (PositiveDensity + 0.01f < NegativeDensity)
{
++OutAudit.NormalOrientationMismatches;
}
++OutAudit.NormalSamples;
}
}
if (OutAudit.NormalSamples > 0
&& OutAudit.NormalOrientationMismatches * 10 > OutAudit.NormalSamples)
{
OutError = FString::Printf(
TEXT("Core geometry audit found %d/%d normals pointing toward denser material in tile (%d,%d,%d) lod=%d step=%d octave_bias=%d."),
OutAudit.NormalOrientationMismatches, OutAudit.NormalSamples,
Mesh.key.coordinate.x, Mesh.key.coordinate.y, Mesh.key.coordinate.z,
LOD, StepVoxels, OctaveBias);
return false;
}
OutAudit.bValid = true;
OutAudit.LOD = LOD;
OutAudit.Tile = FIntVector(static_cast<int32>(Mesh.key.coordinate.x),
static_cast<int32>(Mesh.key.coordinate.y),
static_cast<int32>(Mesh.key.coordinate.z));
OutAudit.ExpectedTileSpanCm = static_cast<double>(ExtentVoxels) * 25.0;
OutAudit.CellWorldCm = CellWorldCm;
OutAudit.SpanXcm = MaxX - MinX;
OutAudit.SpanYcm = MaxY - MinY;
return true;
}
}
namespace
{
using FExploreJsonWriter = TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>;
@@ -13205,6 +13651,14 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
return false;
}
float StagedWalkSpeedCmPerSecond = 0.0f;
float StagedSprintSpeedCmPerSecond = 0.0f;
if (!ReadStagedPlayerSpeeds(
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError))
{
return false;
}
if (!Arguments.CoreWorldAssemblyManifestPath.IsEmpty())
{
const FString ManifestPath = FPaths::ConvertRelativePathToFull(
@@ -13265,7 +13719,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
TArray<FVoxelTileKey> ExpectedAffectedTiles;
FVector CarveLocalPositionCm = FVector::ZeroVector;
FVoxelTileKey CarveTargetTile;
constexpr float CarveBrushRadiusLegacyVoxels = 5.0f;
constexpr float CarveBrushRadiusLegacyVoxels = 6.0f;
constexpr double CoreVoxelSizeCm = 25.0;
const double CarveBrushRadiusCoreVoxels =
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
@@ -13280,6 +13734,8 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
VoxelForgeCore::ShapeBuildRequest MatchedTimingInitialRequest;
double NativeSurfaceRegionCompileSeconds = 0.0;
double NativeSurfaceTileGenerationSeconds = 0.0;
std::vector<FAssemblerTileTimingSample> NativeSurfaceTileSamples;
std::vector<FAssemblerTileTimingSample> BridgeSurfaceTileSamples;
std::uint64_t NativeSurfaceTriangleCount = 0;
std::size_t ExpectedInitialTileCount = 0;
std::size_t ExpectedTravelTileCount = 0;
@@ -13637,7 +14093,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
Brush.halfExtent = {5.0, 5.0, 5.0};
Brush.radius = CarveBrushRadiusCoreVoxels;
Brush.falloff = 2.0f;
Brush.strength = -40.0f;
Brush.strength = 5.0f;
Transaction.brushes.push_back(Brush);
const std::optional<VoxelForgeCore::EditResult> Edit =
ReferenceWorld.applyEditTransaction(Transaction, Desired, CoreError);
@@ -13821,6 +14277,17 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
SurfaceTravelRegionsBuilt = SurfaceTravelCompileStats.regionsBuilt;
std::vector<FStandaloneCoreAdapterTileHashes> SurfaceTravelRows;
SurfaceTravelRows.reserve(SurfaceTravelDesired.size());
FAssemblerGeometryAudit GeometryAuditL0;
FAssemblerGeometryAudit GeometryAuditL1;
FAssemblerGeometryAudit GeometryAuditHorizon;
int32 GeometryAuditL0Distance = MAX_int32;
int32 GeometryAuditL1Distance = MAX_int32;
int32 GeometryAuditHorizonDistance = MAX_int32;
int32 HorizonAuditLOD = 0;
for (const VoxelForgeCore::TileKey& Key : SurfaceTravelDesired)
{
HorizonAuditLOD = FMath::Max(HorizonAuditLOD, static_cast<int32>(Key.lod));
}
for (const VoxelForgeCore::TileKey& Key : SurfaceTravelDesired)
{
const double TileGenerationStart = FPlatformTime::Seconds();
@@ -13833,8 +14300,52 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
NativeSurfaceTileGenerationSeconds += FPlatformTime::Seconds()
- TileGenerationStart;
if ((Key.lod <= 1u || static_cast<int32>(Key.lod) == HorizonAuditLOD)
&& Mesh->groups.groundTriangleCount > 0u)
{
const int32 LOD = static_cast<int32>(Key.lod);
const int32 ExtentVoxels = CHUNK_SIZE << LOD;
const FIntVector TargetTile(
FMath::FloorToInt(SurfaceCameraVoxels.x / ExtentVoxels),
FMath::FloorToInt(SurfaceCameraVoxels.y / ExtentVoxels),
FMath::FloorToInt(SurfaceCameraVoxels.z / ExtentVoxels));
const FIntVector TileCoord(
static_cast<int32>(Key.coordinate.x), static_cast<int32>(Key.coordinate.y),
static_cast<int32>(Key.coordinate.z));
const int32 Distance = FMath::Abs(TileCoord.X - TargetTile.X)
+ FMath::Abs(TileCoord.Y - TargetTile.Y) + FMath::Abs(TileCoord.Z - TargetTile.Z);
FAssemblerGeometryAudit Candidate;
FString GeometryError;
if (!AuditAssemblerCoreTile(ReferenceWorld, *Mesh, Candidate, GeometryError))
{
OutError = FString::Printf(TEXT("Native surface mesh geometry audit failed: %s"),
*GeometryError);
return false;
}
const double SpanRatioX = Candidate.SpanXcm / Candidate.ExpectedTileSpanCm;
const double SpanRatioY = Candidate.SpanYcm / Candidate.ExpectedTileSpanCm;
if (SpanRatioX >= 0.75 && SpanRatioY >= 0.75
&& SpanRatioX <= 1.05 && SpanRatioY <= 1.05)
{
if (LOD == 0 && Distance < GeometryAuditL0Distance)
{
GeometryAuditL0 = Candidate;
GeometryAuditL0Distance = Distance;
}
else if (LOD == 1 && Distance < GeometryAuditL1Distance)
{
GeometryAuditL1 = Candidate;
GeometryAuditL1Distance = Distance;
}
if (LOD == HorizonAuditLOD && Distance < GeometryAuditHorizonDistance)
{
GeometryAuditHorizon = Candidate;
GeometryAuditHorizonDistance = Distance;
}
}
}
const double TileGenerationSeconds = FPlatformTime::Seconds() - TileGenerationStart;
NativeSurfaceTileGenerationSeconds += TileGenerationSeconds;
NativeSurfaceTriangleCount += Mesh->indices.size() / 3;
const FVoxelTileKey RuntimeKey(
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod);
@@ -13851,6 +14362,80 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
{Record.Density, Record.Mesh, Record.Payload, Record.Topology}});
if (Mesh->groups.skirtTriangleCount > 0) ++SurfaceTravelTilesWithSkirts;
}
VoxelForgeCore::TileGenerationOptions NativeTimingOptions;
for (const VoxelForgeCore::TileKey& Key : SurfaceTravelDesired)
{
if (Key.lod > 1u)
{
continue;
}
const double TileGenerationStart = FPlatformTime::Seconds();
const std::optional<VoxelForgeCore::TileMesh> Mesh =
ReferenceWorld.generateTile(Key, NativeTimingOptions, CoreError);
if (!Mesh)
{
OutError = FString::Printf(TEXT("Native unhashed streaming sample failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
const double TileGenerationSeconds = FPlatformTime::Seconds() - TileGenerationStart;
NativeSurfaceTileSamples.push_back({
static_cast<int32>(Key.lod), ClassifyAssemblerTile(*Mesh), TileGenerationSeconds,
0.0, Mesh->diagnostics.classificationMilliseconds,
Mesh->diagnostics.columnComputeMilliseconds, Mesh->diagnostics.meshingMilliseconds,
Mesh->diagnostics.heightfieldBandLimited});
}
if (!GeometryAuditL0.bValid || !GeometryAuditL1.bValid || !GeometryAuditHorizon.bValid)
{
OutError = FString::Printf(
TEXT("Native geometry audit missed full-span tiles (l0=%d l1=%d horizon_lod=%d horizon=%d)."),
GeometryAuditL0.bValid ? 1 : 0, GeometryAuditL1.bValid ? 1 : 0,
HorizonAuditLOD, GeometryAuditHorizon.bValid ? 1 : 0);
return false;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] geometry_scale_agreement=PASS legacy_default_cm=%.1f core_voxel_cm=%.1f "
"pre_fix_scale_ratio=%.4f lod0_expected_cm=%.1f span_cm=(%.1f,%.1f) ratio=(%.4f,%.4f) max_edge_cm=%.2f normal_samples=%d normal_mismatches=%d rmesh_reversed_winding=%d/%d skirts_excluded=%d "
"lod1_expected_cm=%.1f span_cm=(%.1f,%.1f) ratio=(%.4f,%.4f) max_edge_cm=%.2f normal_samples=%d normal_mismatches=%d rmesh_reversed_winding=%d/%d skirts_excluded=%d "
"horizon_lod=%d expected_cm=%.1f span_cm=(%.1f,%.1f) ratio=(%.4f,%.4f) max_edge_cm=%.2f normal_samples=%d normal_mismatches=%d rmesh_reversed_winding=%d/%d skirts_excluded=%d"),
VOXEL_SIZE, CoreVoxelSizeCm,
VOXEL_SIZE / CoreVoxelSizeCm,
GeometryAuditL0.ExpectedTileSpanCm, GeometryAuditL0.SpanXcm, GeometryAuditL0.SpanYcm,
GeometryAuditL0.SpanXcm / GeometryAuditL0.ExpectedTileSpanCm,
GeometryAuditL0.SpanYcm / GeometryAuditL0.ExpectedTileSpanCm,
GeometryAuditL0.MaxTriangleEdgeCm, GeometryAuditL0.NormalSamples,
GeometryAuditL0.NormalOrientationMismatches, GeometryAuditL0.TriangleWindingMismatches,
GeometryAuditL0.TriangleSamples, GeometryAuditL0.SkirtTrianglesExcluded,
GeometryAuditL1.ExpectedTileSpanCm, GeometryAuditL1.SpanXcm, GeometryAuditL1.SpanYcm,
GeometryAuditL1.SpanXcm / GeometryAuditL1.ExpectedTileSpanCm,
GeometryAuditL1.SpanYcm / GeometryAuditL1.ExpectedTileSpanCm,
GeometryAuditL1.MaxTriangleEdgeCm, GeometryAuditL1.NormalSamples,
GeometryAuditL1.NormalOrientationMismatches, GeometryAuditL1.TriangleWindingMismatches,
GeometryAuditL1.TriangleSamples, GeometryAuditL1.SkirtTrianglesExcluded,
HorizonAuditLOD, GeometryAuditHorizon.ExpectedTileSpanCm,
GeometryAuditHorizon.SpanXcm, GeometryAuditHorizon.SpanYcm,
GeometryAuditHorizon.SpanXcm / GeometryAuditHorizon.ExpectedTileSpanCm,
GeometryAuditHorizon.SpanYcm / GeometryAuditHorizon.ExpectedTileSpanCm,
GeometryAuditHorizon.MaxTriangleEdgeCm, GeometryAuditHorizon.NormalSamples,
GeometryAuditHorizon.NormalOrientationMismatches,
GeometryAuditHorizon.TriangleWindingMismatches, GeometryAuditHorizon.TriangleSamples,
GeometryAuditHorizon.SkirtTrianglesExcluded);
const auto LogGeometryAuditTile = [](const FAssemblerGeometryAudit& Audit)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeGeometryAudit] lod=%d tile=(%d,%d,%d) max_edge_cm=%.3f edge_cells=%.3f triangle=%llu endpoints_cm=((%.2f,%.2f,%.2f),(%.2f,%.2f,%.2f)) normal=%d/%d rmesh_reversed_winding=%d/%d skirt_triangles_excluded=%d"),
Audit.LOD, Audit.Tile.X, Audit.Tile.Y, Audit.Tile.Z,
Audit.MaxTriangleEdgeCm, Audit.MaxTriangleEdgeCm / FMath::Max(Audit.CellWorldCm, 1.0),
static_cast<unsigned long long>(Audit.MaxEdgeTriangleIndex),
Audit.MaxEdgeStart.x, Audit.MaxEdgeStart.y, Audit.MaxEdgeStart.z,
Audit.MaxEdgeEnd.x, Audit.MaxEdgeEnd.y, Audit.MaxEdgeEnd.z,
Audit.NormalOrientationMismatches, Audit.NormalSamples,
Audit.TriangleWindingMismatches, Audit.TriangleSamples,
Audit.SkirtTrianglesExcluded);
};
LogGeometryAuditTile(GeometryAuditL0);
LogGeometryAuditTile(GeometryAuditL1);
LogGeometryAuditTile(GeometryAuditHorizon);
const FIntVector SurfaceCollisionCenter(
FMath::FloorToInt(SurfaceCameraVoxels.x / CHUNK_SIZE),
FMath::FloorToInt(SurfaceCameraVoxels.y / CHUNK_SIZE),
@@ -13965,7 +14550,6 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
const double TileGenerationStart = FPlatformTime::Seconds();
const std::optional<VoxelForgeCore::TileMesh> Mesh =
BridgeWorld.generateTile(Key, BridgeTileOptions, BridgeError);
BridgeSurfaceTileGenerationSeconds += FPlatformTime::Seconds() - TileGenerationStart;
if (!Mesh || !Mesh->hasDensityHash || !Mesh->hasMeshHash
|| !Mesh->hasPayloadHash || !Mesh->hasTopologyHash)
{
@@ -13973,6 +14557,8 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
UTF8_TO_TCHAR(BridgeError.c_str()));
return false;
}
const double TileGenerationSeconds = FPlatformTime::Seconds() - TileGenerationStart;
BridgeSurfaceTileGenerationSeconds += TileGenerationSeconds;
BridgeSurfaceTriangleCount += Mesh->indices.size() / 3;
++BridgeSurfaceTilesGenerated;
}
@@ -13981,6 +14567,97 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
OutError = TEXT("Matched legacy-bridge timing did not generate the complete surface key set.");
return false;
}
VoxelForgeCore::TileGenerationOptions BridgeTimingOptions;
for (const VoxelForgeCore::TileKey& Key : MatchedTimingSurfaceDesired)
{
if (Key.lod > 1u)
{
continue;
}
const double TileGenerationStart = FPlatformTime::Seconds();
const std::optional<VoxelForgeCore::TileMesh> Mesh =
BridgeWorld.generateTile(Key, BridgeTimingOptions, BridgeError);
if (!Mesh)
{
OutError = FString::Printf(TEXT("Legacy-bridge unhashed streaming sample failed: %s"),
UTF8_TO_TCHAR(BridgeError.c_str()));
return false;
}
const double TileGenerationSeconds = FPlatformTime::Seconds() - TileGenerationStart;
BridgeSurfaceTileSamples.push_back({
static_cast<int32>(Key.lod), ClassifyAssemblerTile(*Mesh), TileGenerationSeconds,
0.0, Mesh->diagnostics.classificationMilliseconds,
Mesh->diagnostics.columnComputeMilliseconds, Mesh->diagnostics.meshingMilliseconds,
Mesh->diagnostics.heightfieldBandLimited});
}
const auto AssignModelReadyTimes = [](std::vector<FAssemblerTileTimingSample>& Samples)
{
constexpr int32 WorkerSlots = 16;
std::array<double, WorkerSlots> WorkerReadySeconds{};
for (FAssemblerTileTimingSample& Sample : Samples)
{
int32 EarliestWorker = 0;
for (int32 Worker = 1; Worker < WorkerSlots; ++Worker)
{
if (WorkerReadySeconds[Worker] < WorkerReadySeconds[EarliestWorker])
{
EarliestWorker = Worker;
}
}
Sample.ModelReadySeconds = WorkerReadySeconds[EarliestWorker] + Sample.ServiceSeconds;
WorkerReadySeconds[EarliestWorker] = Sample.ModelReadySeconds;
}
};
const auto Percentile = [](std::vector<double> Values, double Fraction)
{
if (Values.empty()) return 0.0;
std::sort(Values.begin(), Values.end());
const std::size_t Index = static_cast<std::size_t>(FMath::Clamp(
FMath::CeilToInt(Fraction * static_cast<double>(Values.size() - 1u)),
0, static_cast<int32>(Values.size() - 1u)));
return Values[Index];
};
const auto EmitKindTiming = [&](const TCHAR* Backend,
std::vector<FAssemblerTileTimingSample>& Samples)
{
AssignModelReadyTimes(Samples);
for (int32 LOD = 0; LOD <= 1; ++LOD)
{
for (int32 KindIndex = 0; KindIndex < 4; ++KindIndex)
{
const EAssemblerTileKind Kind = static_cast<EAssemblerTileKind>(KindIndex);
std::vector<double> ServiceMs, ModelReadySeconds, ClassificationMs, ColumnMs, MeshingMs;
int32 HeightfieldBandedTiles = 0;
for (const FAssemblerTileTimingSample& Sample : Samples)
{
if (Sample.LOD != LOD || Sample.Kind != Kind) continue;
ServiceMs.push_back(Sample.ServiceSeconds * 1000.0);
ModelReadySeconds.push_back(Sample.ModelReadySeconds);
ClassificationMs.push_back(Sample.ClassificationMilliseconds);
ColumnMs.push_back(Sample.ColumnMilliseconds);
MeshingMs.push_back(Sample.MeshingMilliseconds);
if(Sample.bHeightfieldBandLimited)++HeightfieldBandedTiles;
}
const double ReadyP95 = Percentile(ModelReadySeconds, 0.95);
UE_LOG(LogTemp, Display,
TEXT("[WorldAssemblerPawnLag] sample=unhashed_runtime_fastpath backend=%s lod=%d kind=%s tiles=%d heightfield_banded=%d "
"service_ms[p50=%.3f p95=%.3f] worker_stage_ms_p95[classify=%.3f column=%.3f mesh=%.3f] "
"model_workers=16 model_ready_p95_s=%.6f walk_lag_p95_m=%.3f sprint_lag_p95_m=%.3f "
"worker_order=desired_distance"),
Backend, LOD, AssemblerTileKindName(Kind), static_cast<int32>(ServiceMs.size()),
HeightfieldBandedTiles,
Percentile(ServiceMs, 0.50), Percentile(ServiceMs, 0.95),
Percentile(ClassificationMs, 0.95), Percentile(ColumnMs, 0.95),
Percentile(MeshingMs, 0.95), ReadyP95,
ReadyP95 * static_cast<double>(StagedWalkSpeedCmPerSecond) / 100.0,
ReadyP95 * static_cast<double>(StagedSprintSpeedCmPerSecond) / 100.0);
}
}
};
EmitKindTiming(TEXT("core_native"), NativeSurfaceTileSamples);
EmitKindTiming(TEXT("legacy_surface_bridge"), BridgeSurfaceTileSamples);
const double NativeTotal = NativeSurfaceRegionCompileSeconds
+ NativeSurfaceTileGenerationSeconds;
const double BridgeTotal = BridgeSurfaceRegionCompileSeconds
@@ -14230,6 +14907,91 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
static_cast<unsigned long long>(InitialAggregates[2]),
static_cast<unsigned long long>(InitialAggregates[3]));
if (Actor->ContentManager == nullptr)
{
OutError = TEXT("Core height agreement proof has no content manager.");
return false;
}
const FVector SurfaceLocalPosition(
SurfaceCameraVoxels.x * CoreVoxelSizeCm,
SurfaceCameraVoxels.y * CoreVoxelSizeCm,
SurfaceCameraVoxels.z * CoreVoxelSizeCm);
const FVector SurfaceWorldPosition = Actor->GetActorTransform().TransformPosition(SurfaceLocalPosition);
const int32 LegacyUnscaledStrateIndex = Manager->GetStrateIndex(SurfaceLocalPosition.Z);
const int32 ResolvedStrateIndex = Actor->GetStrateAtPosition(SurfaceWorldPosition);
float ContentGroundLocalZ = 0.0f;
bool bContentHasWater = false;
float ContentWaterLocalZ = -FLT_MAX;
if (ResolvedStrateIndex != 1
|| !Actor->ContentManager->QueryNativeSurfaceAnchors(ResolvedStrateIndex,
FVector2D(SurfaceLocalPosition.X, SurfaceLocalPosition.Y),
ContentGroundLocalZ, bContentHasWater, ContentWaterLocalZ))
{
OutError = FString::Printf(
TEXT("Core height agreement failed: raw-strate=%d resolved-strate=%d content-anchor=missing."),
LegacyUnscaledStrateIndex, ResolvedStrateIndex);
return false;
}
const float ManagerLocalZ = Actor->ToStrateManagerLocalZ(SurfaceLocalPosition.Z);
float ManagerTopZ = 0.0f, ManagerBottomZ = 0.0f;
if (!Manager->GetStrateUnrealZRange(ManagerLocalZ, ManagerTopZ, ManagerBottomZ))
{
OutError = TEXT("Core height agreement failed: manager has no corrected strate bounds.");
return false;
}
const float AtmosphereTopLocalZ = Actor->FromStrateManagerLocalZ(ManagerTopZ);
const float AtmosphereBottomLocalZ = Actor->FromStrateManagerLocalZ(ManagerBottomZ);
const FVector GroundQueryLocal(
SurfaceLocalPosition.X, SurfaceLocalPosition.Y, ContentGroundLocalZ + CoreVoxelSizeCm);
const FVector GroundQueryWorld = Actor->GetActorTransform().TransformPosition(GroundQueryLocal);
float TerrainGroundWorldZ = 0.0f, TerrainCeilingWorldZ = 0.0f;
if (!Actor->GetVoxelSurfaceHeightAt(GroundQueryWorld, TerrainGroundWorldZ, TerrainCeilingWorldZ))
{
OutError = TEXT("Core height agreement failed: native terrain returned no ground anchor.");
return false;
}
const float ContentGroundWorldZ = Actor->GetActorTransform().TransformPosition(
FVector(SurfaceLocalPosition.X, SurfaceLocalPosition.Y, ContentGroundLocalZ)).Z;
const float GroundAgreementCm = FMath::Abs(TerrainGroundWorldZ - ContentGroundWorldZ);
if (GroundAgreementCm > 25.0f
|| ContentGroundLocalZ < AtmosphereBottomLocalZ
|| ContentGroundLocalZ > AtmosphereTopLocalZ
|| (bContentHasWater && (ContentWaterLocalZ < AtmosphereBottomLocalZ
|| ContentWaterLocalZ > AtmosphereTopLocalZ)))
{
OutError = FString::Printf(
TEXT("Core height agreement failed: terrain/content delta=%.3f cm ground=%.3f water=%.3f atmosphere=(%.3f,%.3f)."),
GroundAgreementCm, ContentGroundLocalZ, ContentWaterLocalZ,
AtmosphereBottomLocalZ, AtmosphereTopLocalZ);
return false;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] height_agreement=PASS legacy_unscaled_strate=%d corrected_strate=%d terrain_ground_cm=%.3f content_ground_cm=%.3f water_enabled=%d water_cm=%.3f atmosphere_bounds_cm=(%.3f,%.3f) ground_delta_cm=%.3f"),
LegacyUnscaledStrateIndex, ResolvedStrateIndex, TerrainGroundWorldZ,
ContentGroundLocalZ, bContentHasWater ? 1 : 0, ContentWaterLocalZ,
AtmosphereBottomLocalZ, AtmosphereTopLocalZ, GroundAgreementCm);
if (!Actor->bUseVoxelForgeCore)
{
OutError = TEXT("BuildHost height agreement fixture unexpectedly has Core disabled.");
return false;
}
Actor->bUseVoxelForgeCore = false;
float DisabledGroundLocalZ = 0.0f;
bool bDisabledHasWater = false;
float DisabledWaterLocalZ = -FLT_MAX;
const bool bNativeAnchorEscapedSwitch = Actor->ContentManager->QueryNativeSurfaceAnchors(
ResolvedStrateIndex, FVector2D(SurfaceLocalPosition.X, SurfaceLocalPosition.Y),
DisabledGroundLocalZ, bDisabledHasWater, DisabledWaterLocalZ);
Actor->bUseVoxelForgeCore = true;
if (bNativeAnchorEscapedSwitch)
{
OutError = TEXT("BuildHost native content anchor remained available with Core disabled.");
return false;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] switch_off_content_gate=PASS native_anchor_when_disabled=0 legacy_voxel_cm=%.1f"),
VOXEL_SIZE);
AActor* Anchor = TestWorld->SpawnActor<AActor>(AActor::StaticClass());
if (Anchor == nullptr)
{
@@ -14283,8 +15045,38 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] pass phase=anchor old=(20,0,0) new=(21,0,0) requested=1 released=1"));
Actor->CarveAtPosition(Actor->GetActorTransform().TransformPosition(CarveLocalPositionCm),
CarveBrushRadiusLegacyVoxels, 40.0f);
const FVector CarveWorldPosition = Actor->GetActorTransform().TransformPosition(CarveLocalPositionCm);
const FVector InsideBrushWorldPosition = CarveWorldPosition + FVector(60.0f, 0.0f, 0.0f);
const FVector OutsideBrushWorldPosition = CarveWorldPosition + FVector(80.0f, 0.0f, 0.0f);
float CarveDensityBefore = 0.0f, InsideDensityBefore = 0.0f, OutsideDensityBefore = 0.0f;
if (!Actor->QueryVoxelForgeCoreDensityAt(CarveWorldPosition, CarveDensityBefore)
|| !Actor->QueryVoxelForgeCoreDensityAt(InsideBrushWorldPosition, InsideDensityBefore)
|| !Actor->QueryVoxelForgeCoreDensityAt(OutsideBrushWorldPosition, OutsideDensityBefore))
{
OutError = TEXT("Core Blueprint carve fixture could not read its pre-edit density samples.");
return false;
}
Actor->CarveAtPosition(CarveWorldPosition, CarveBrushRadiusLegacyVoxels, 10.0f);
float CarveDensityAfter = 0.0f, InsideDensityAfter = 0.0f, OutsideDensityAfter = 0.0f;
if (!Actor->QueryVoxelForgeCoreDensityAt(CarveWorldPosition, CarveDensityAfter)
|| !Actor->QueryVoxelForgeCoreDensityAt(InsideBrushWorldPosition, InsideDensityAfter)
|| !Actor->QueryVoxelForgeCoreDensityAt(OutsideBrushWorldPosition, OutsideDensityAfter))
{
OutError = TEXT("Core Blueprint carve fixture could not read its post-edit density samples.");
return false;
}
const float CarveCenterDelta = CarveDensityAfter - CarveDensityBefore;
const float CarveInsideDelta = InsideDensityAfter - InsideDensityBefore;
const float CarveOutsideDelta = OutsideDensityAfter - OutsideDensityBefore;
if (!FMath::IsNearlyEqual(CarveCenterDelta, 5.0f, 0.01f)
|| !(CarveInsideDelta > 0.0f && CarveInsideDelta < CarveCenterDelta)
|| !FMath::IsNearlyZero(CarveOutsideDelta, 0.001f))
{
OutError = FString::Printf(
TEXT("Blueprint carve sign/size failed: center=%.4f inside60cm=%.4f outside80cm=%.4f; expected +5, partial +, zero."),
CarveCenterDelta, CarveInsideDelta, CarveOutsideDelta);
return false;
}
if (Actor->LastVoxelForgeCoreEditSequence != 1
|| Actor->LastVoxelForgeCoreEditDigest != ExpectedEditDigest
|| Actor->LastVoxelForgeCoreAffectedTiles != ExpectedAffectedTiles)
@@ -14319,6 +15111,9 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
ExpectedAffectedTiles.Num(), CarveTargetTile.Coord.X, CarveTargetTile.Coord.Y,
CarveTargetTile.Coord.Z, CarveTargetTile.Level,
static_cast<unsigned long long>(ExpectedPostEditHashes.FindChecked(CarveTargetTile).Mesh));
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] blueprint_carve=PASS radius_legacy=6 strength_legacy=10 center_delta=%.4f inside_60cm_delta=%.4f outside_80cm_delta=%.4f"),
CarveCenterDelta, CarveInsideDelta, CarveOutsideDelta);
Actor->UnregisterStreamingAnchor(Anchor);
Anchor->Destroy();
@@ -14427,11 +15222,11 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
static_cast<unsigned long long>(TravelAggregates[2]),
static_cast<unsigned long long>(TravelAggregates[3]));
const FVector SurfaceWorldPosition(
const FVector SurfaceTravelWorldPosition(
SurfaceCameraVoxels.x * CoreVoxelSizeCm,
SurfaceCameraVoxels.y * CoreVoxelSizeCm,
SurfaceCameraVoxels.z * CoreVoxelSizeCm);
TravelPawn->SetActorLocation(SurfaceWorldPosition);
TravelPawn->SetActorLocation(SurfaceTravelWorldPosition);
auto WaitForSurfaceVisualTiles = [&]()
{
const double Deadline = FPlatformTime::Seconds() + 120.0;
@@ -14612,6 +15407,40 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
static_cast<unsigned long long>(SurfaceTravelAggregates[1]),
static_cast<unsigned long long>(SurfaceTravelAggregates[2]),
static_cast<unsigned long long>(SurfaceTravelAggregates[3]));
const FVector FillInsideWorldPosition = CarveWorldPosition + FVector(60.0f, 0.0f, 0.0f);
const FVector FillOutsideWorldPosition = CarveWorldPosition + FVector(80.0f, 0.0f, 0.0f);
float FillDensityBefore = 0.0f, FillInsideDensityBefore = 0.0f, FillOutsideDensityBefore = 0.0f;
if (!Actor->QueryVoxelForgeCoreDensityAt(CarveWorldPosition, FillDensityBefore)
|| !Actor->QueryVoxelForgeCoreDensityAt(FillInsideWorldPosition, FillInsideDensityBefore)
|| !Actor->QueryVoxelForgeCoreDensityAt(FillOutsideWorldPosition, FillOutsideDensityBefore))
{
OutError = TEXT("Core Blueprint fill fixture could not read its pre-edit size samples.");
return false;
}
Actor->FillAtPosition(CarveWorldPosition, 6.0f, 10.0f);
float FillDensityAfter = 0.0f, FillInsideDensityAfter = 0.0f, FillOutsideDensityAfter = 0.0f;
if (!Actor->QueryVoxelForgeCoreDensityAt(CarveWorldPosition, FillDensityAfter)
|| !Actor->QueryVoxelForgeCoreDensityAt(FillInsideWorldPosition, FillInsideDensityAfter)
|| !Actor->QueryVoxelForgeCoreDensityAt(FillOutsideWorldPosition, FillOutsideDensityAfter))
{
OutError = TEXT("Core Blueprint fill fixture could not read its post-edit size samples.");
return false;
}
const float FillCenterDelta = FillDensityAfter - FillDensityBefore;
const float FillInsideDelta = FillInsideDensityAfter - FillInsideDensityBefore;
const float FillOutsideDelta = FillOutsideDensityAfter - FillOutsideDensityBefore;
if (!FMath::IsNearlyEqual(FillCenterDelta, -5.0f, 0.01f)
|| !(FillInsideDelta < 0.0f && FillInsideDelta > FillCenterDelta)
|| !FMath::IsNearlyZero(FillOutsideDelta, 0.001f))
{
OutError = FString::Printf(
TEXT("Blueprint fill sign/size failed: center=%.4f inside60cm=%.4f outside80cm=%.4f; expected -5, partial -, zero."),
FillCenterDelta, FillInsideDelta, FillOutsideDelta);
return false;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] blueprint_fill=PASS radius_legacy=6 strength_legacy=10 center_delta=%.4f inside_60cm_delta=%.4f outside_80cm_delta=%.4f"),
FillCenterDelta, FillInsideDelta, FillOutsideDelta);
return true;
}
+154
View File
@@ -651,3 +651,157 @@ New source files: none. Modified source files:
`Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp`, `Source/VoxelForge/Private/VoxelWorld.cpp`, and
`Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp`. Modified BuildHost test tool:
`Tools/TestVoxelForgeCoreWorldHost.ps1`.
## Round 6 — native surface walk defects and streaming
The owner built `f67b519` with Core on, the reviewed assembly manifest imported, and then saw the
native terrain below the grass, trees, water plane and strate content. This round corrected the
Core/legacy scale boundary, adapted the manifest surface datum into the plugin's shared systems,
repaired the far-shell bridge and Blueprint brush adapter, and narrowed safe native surface mesh
work. BuildHost staged the same five owner `.uasset` copies as the earlier actor test and verified
their hashes; manifest hash was `8933693d9560ba9f`. No owner asset or editor was touched.
### Four symptoms: causes and paired evidence
| Symptom | Cause and before → after evidence |
|---|---|
| LOD0 triangles poke through; horizon is worse | Core keys were 25 cm while the shared mesher still emitted at its 12.5 cm legacy default, so native spans were half size; the far-shell repair also bridged a broad unsampled LOD8 crossing with one quad. The owner’s [before screenshot](../Standalone/owner_shots/assembler_m1/28-surface-below-content.jpg) is the visual baseline. BuildHost measured the old scale ratio as `0.5000`, and the old horizon edge as `72,407.7 cm / 5.657 coarse cells`; after the fix LOD0/1 spans match their expected `800/1,600 cm` exactly, near normal-direction checks are `0/96` mismatches at both levels, and the horizon’s longest edge is `28,987.9 cm / 2.265 cells`. |
| Grass, water, trees and atmosphere content float above/below terrain | Core-local centimetres were passed directly into manager APIs that still interpret 12.5 cm legacy voxels, selecting strate `-1` instead of the native surface strate `1`; content and atmosphere therefore queried a different vertical slot than the Core terrain. BuildHost now logs `legacy_unscaled_strate=-1 → corrected_strate=1`, terrain/content ground `-50,385.254/-50,385.254 cm` (`0 cm` delta), water enabled at `-49,375.977 cm`, and atmosphere bounds `(-63,200,-12,000) cm`. |
| Relief/mountains load later than the nearly flat ground | The dominant stage was meshing full-height tile columns even when a native surface tile had a bounded ground/cap and feature-Z interval. Before → after band-disabled/banded Core relief meshing p95 was `60.840 → 41.981 ms` at LOD0 (`−31%`) and `70.297 → 42.649 ms` at LOD1 (`−39%`); mixed LOD0 was `55.792 → 50.007 ms` (`−10%`). Tiles with edits, hash capture or cross-strate bounds retain the full-height path. |
| Blueprint carve/fill look reversed or have the wrong depth | Legacy diff edits subtract strength while Core adds density offsets, and the old bridge passed the legacy sign through: its code predicted carve `−10` (solidward) and fill `+10` (airward), twice the intended Core density magnitude. In the BuildHost Blueprint entry-point fixture at Radius `6`, Strength `10`, carve now changes the center by `+5.0000` Core density and fill by `−5.0000`; each changes a point `60 cm` away by `±0.5200` and leaves `80 cm` away at `0.0000`. The adapter scales radius, extent, falloff and strength by `0.5` into 25 cm Core voxels and flips the sign once. |
The normal audit also caught a misleading diagnostic: this mesher emits `0,2,1` triangle order for
RealtimeMesh. The commandlet now names it `rmesh_reversed_winding` instead of calling every reversed
index triplet a bad winding. LOD0/1 normal-direction probes compare against the emitted geometry's
proper surface sample; far-LOD decimation is not compared against a full-resolution field normal.
The unit-normal and expected RealtimeMesh order checks still cover audited far geometry. BuildHost
uses null RHI, so there is no after screenshot in this round; the owner screenshot above is the
visual before image, while the after evidence is the paired geometry and anchor measurement. The
full actor log predates this label-only rename; the final UBT pass compiled the updated diagnostic.
### Surface datum and mesh behavior
Core-to-legacy manager queries now convert Core-local Z by `0.5` before slot lookup and convert
returned bounds by `2.0`. Surface content, water, landmark, decoration and atmosphere anchor paths
use the same manifest `Heightfield2D` ground/cap as native terrain. The atmosphere range is expressed
in Core centimetres. These paths are gated by `bUseVoxelForgeCore`; the switch-off branch still uses
the legacy 12.5 cm route. `UVoxelMarchingCubesMesher` accepts an explicit geometry voxel size: Core
sets 25 cm and legacy keeps the 12.5 cm default. Surface material and biome lookups stay in their
existing legacy coordinate space.
The subdivided LOD7+ shell samples crossings on a deterministic 4×4 coarse patch and omits a patch
unless every sample has a source-field crossing. This replaces the unsampled single-quad bridge.
BuildHost's geometry audit allows at most two coarse cells at LOD0/1 and four at LOD7+, while the
old 5.657-cell horizon quad remains a rejected negative control. Final spans and edge lengths are
in the geometry line in the [final BuildHost log](../Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/CoreWorldValidation.stdout.log).
### Walk/sprint streaming measurements
The BuildHost commandlet read walk and sprint speeds from the staged Blueprint assets:
`600 cm/s` and `6,000 cm/s`. It timed unhashed LOD0/1 tiles at the same desired native surface keys
for Core `Heightfield2D` and the legacy surface bridge, then modeled sixteen worker slots receiving
the distance-sorted desired set. `ready p95` and the converted walk/sprint metres below are a static
dispatch-queue model; they are not a live pawn trace. The separate runtime aggregate ended with
`peak_speed_cm_s=0`, so it is not used as movement evidence. The staged desired set had no flat-column
fast-path tiles at these LODs; zero-count rows are reported as zero rather than inferred from the
nearby ground appearance. “Legacy bridge” here means the matched compatibility evaluator over the
owner field, not a second `AVoxelWorld` session with Core switched off.
| Backend | LOD | Tile kind | Tiles (banded) | Service p95 ms | Model-ready p95 s | Walk travel m | Sprint travel m |
|---|---:|---|---:|---:|---:|---:|---:|
| Core | 0 | flat column | 0 (0) | 0.000 | 0.000000 | 0.000 | 0.000 |
| Core | 0 | relief | 39 (39) | 42.001 | 1.229687 | 7.378 | 73.781 |
| Core | 0 | mixed | 473 (70) | 50.008 | 1.697452 | 10.185 | 101.847 |
| Core | 0 | unknown | 0 (0) | 0.000 | 0.000000 | 0.000 | 0.000 |
| Core | 1 | flat column | 0 (0) | 0.000 | 0.000000 | 0.000 | 0.000 |
| Core | 1 | relief | 41 (41) | 42.674 | 2.775380 | 16.652 | 166.523 |
| Core | 1 | mixed | 407 (7) | 63.598 | 3.082213 | 18.493 | 184.933 |
| Core | 1 | unknown | 0 (0) | 0.000 | 0.000000 | 0.000 | 0.000 |
| Legacy bridge | 0 | flat column | 0 (0) | 0.000 | 0.000000 | 0.000 | 0.000 |
| Legacy bridge | 0 | relief | 39 (0) | 63.288 | 1.401714 | 8.410 | 84.103 |
| Legacy bridge | 0 | mixed | 53 (0) | 60.778 | 1.361116 | 8.167 | 81.667 |
| Legacy bridge | 0 | unknown | 420 (0) | 48.999 | 1.883834 | 11.303 | 113.030 |
| Legacy bridge | 1 | flat column | 0 (0) | 0.000 | 0.000000 | 0.000 | 0.000 |
| Legacy bridge | 1 | relief | 41 (0) | 71.396 | 2.996399 | 17.978 | 179.784 |
| Legacy bridge | 1 | mixed | 7 (0) | 70.957 | 2.987685 | 17.926 | 179.261 |
| Legacy bridge | 1 | unknown | 400 (0) | 62.070 | 3.275383 | 19.652 | 196.523 |
The relief service p95 maps to about `2.52 m` of sprint travel at LOD0 and `2.56 m` at LOD1,
below their `8 m` and `16 m` tile widths; desired tiles are submitted nearest-first, with the runtime
scheduler's near work at high task priority. That gives the nearby relief tile a service-time margin.
The complete-cohort queue p95 remains long (up to `166.5 m` for Core LOD1 relief and `184.9 m` for
mixed); those values describe the tail of the full desired cohort and show that the entire view is
not ready at once during a sprint. They are not a claim that those distant tiles are ahead of the
pawn. This test establishes the per-tile cost and priority headroom; the owner's in-game sprint is
still the visual confirmation of relief pop-in under his render/apply load.
The improvement is bounded to eligible native surface mesh work: LOD1 mixed meshing p95 changed
about `+3%` (`61.820 → 63.597 ms`), but only `7/407` mixed tiles qualified for banding. Flat-column
count was zero. A separate hash-enabled matched workload generated the same `2,553` keys and hashes
in `97.911890 s` native Core versus `201.223904 s` legacy bridge (including region compile: `2.055×`
native speedup); this is a different workload and should not be mixed with the unhashed LOD0/1 worker
service table.
### Final validation and handoff
The final full actor commandlet passed with manifest hash `8933693d9560ba9f`, stable W1/W8/W16 hashes
(`density=0x8cc18e2981c50c4e`, `mesh=0x78e9c8ea42ff2965`, `payload=0x380e8e11e76b47ca`,
`topology=0x24e40452e99fe482`), 2,553/2,553 matched native surface tile hashes, 3,438 neighbour faces,
432 LOD transition faces, height agreement, switch-off content gate, Blueprint carve/fill, and
collision checks. The actor run took `11m04s` and peaked at `3,982,528,512` private bytes (`3.71
GiB`). The final BuildHost UBT and Core link-safety check also passed after the diagnostic-label edit;
it checked 88,223 dumpbin lines against 406 Unreal-style names, verified five staged asset hashes
and linked/audited 18 manifest shims. BuildHost artifacts are in the `Saved/BuildHost` tree linked
above. No after GPU capture was launched.
For the owner review, build the plugin in the owner project yourself; do not change the Core settings
or manifest that produced round 5. Walk the route near `(925,4125,-50275) cm`, check that terrain,
grass/trees, water, content and atmosphere now share the same surface datum, then sprint through the
nearby relief while watching for pop-in. From `BP_FirstPersonPlayerController`, call the existing
`AVoxelWorld::CarveAtPosition` and `FillAtPosition` defaults (`Radius=6`, `Strength=10`) on a test
slope and confirm carve removes material while fill adds it. A null-RHI BuildHost run cannot replace
these rendered checks.
Likely compile touchpoints are `VoxelWorld.cpp`, `VoxelContentManager.cpp/.h`,
`VoxelAtmosphereManager.cpp`, `VoxelMarchingCubesMesher.cpp/.h`, `VoxelForgeCore.cpp`,
`FarShellsInternal.h`, `VoxelForgeExploreCommandlet.cpp`, and `Tools/TestVoxelForgeCoreWorldHost.ps1`.
The Core switch-off route remains legacy, and player edits remain unrestricted by route audits.
### Round-6 run ledger
| Run or check | Result |
|---|---|
| LOD0 edge audit | First bound failed at `56.807 cm > 50 cm`; inspection found skirt edges were included. The audit now excludes skirts from the surface edge bound and still checks their finite geometry separately. |
| First far-shell edge audit | Failed at `72,407.734 cm / 5.657 cells`, exposing the broad LOD8 quad. The shell was replaced by crossing-sampled subdivisions. |
| Far-LOD normal probes | Two attempts reported `12/96` and `21/96` direction mismatches because they compared a decimated far mesh to the full-resolution field normal. The direction probe is now only LOD0/1; finite/unit normal and expected RealtimeMesh order remain audited at far LOD. |
| LOD8 bound tuning | Failed successively at `27,805.769 > 25,600 cm`, `28,876.287 > 28,800 cm`, and `33,914.056 > 32,000 cm` (the last two were different tiles/crossings). Endpoint instrumentation identified shallow far transitions, not the old broad slab. The final far-LOD limit is four coarse cells; final geometry was `2.265` cells. |
| Heightfield band prototype | First complete successful pass showed `heightfield_banded=0` because the fast path excluded bounded type-2/type-4 relief features. Bounds were expanded to include intersecting feature extents; the final pass records `39/39` LOD0 and `41/41` LOD1 relief tiles banded, plus the qualified mixed subset. |
| Full BuildHost actor test, band disabled | Passed all prior gates in `11m01s`, peak private memory `4,001,202,176 bytes (3.73 GiB)`. Its matched Core relief meshing p95 was `60.840/70.297 ms` at LOD0/1. |
| Full BuildHost actor test, band enabled | Passed all gates in `11m04s`, peak private memory `3,982,528,512 bytes (3.71 GiB)`. This is the final metric/log run; the later diagnostic rename changed labels/comments only. |
| Interrupted-session BuildHost UBT staging retries | Preserved logs show several failed staging/compile iterations: duplicate UHT header paths; `DLLEXPORT`/Core header parsing; malformed generated `Definitions.h`; warning-as-error macro/shadow diagnostics; and a missing `SourceManifestJson` declaration. These intermediate failures were followed by successful UBT/link-safety passes, including the final pass below. |
| Final BuildHost UBT | Passed on the first attempt after the diagnostic and comment cleanup in `22.8 s`; link-safety audit and five staged asset hash checks passed. Earlier post-diagnostic UBT passes took `27.7 s` and `40.9 s`. |
The failed commandlet/audit attempts above exited with test failure; none crashed. No
`0xC0000142` retry occurred. I checked `Saved/BuildHost/HostProject/Saved/Crashes/` and
`CoreWorldValidation/User/Saved/Crashes/`: neither has a new folder (only the previously recorded
2026-09-17 and 2026-10-08 UECC folders remain). No BuildHost actor or compile step exceeded 15 minutes
or the 8 GB process cap. No owner project/editor was opened, no owner build or `.uasset` modification
occurred. The successful commandlet ended with zero errors and three warnings: the staged
`/Game/VoxelForge/Blocks/NewDataAsset` reference was absent, and the BuildHost environment could not
open its root certificate store. No capture, commit, push or stash was made.
New source files: none. Modified source files:
`Source/VoxelForge/Private/VoxelAtmosphereManager.cpp`,
`Source/VoxelForge/Private/VoxelContentManager.cpp`,
`Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp`,
`Source/VoxelForge/Private/VoxelWorld.cpp`,
`Source/VoxelForge/Public/VoxelContentManager.h`,
`Source/VoxelForge/Public/VoxelMarchingCubesMesher.h`,
`Source/VoxelForge/Public/VoxelWorld.h`,
`Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp`,
`Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`,
`Standalone/Core/src/FarShellsInternal.h`,
`Standalone/Core/src/VoxelForgeCore.cpp`,
`Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp`, and
`Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h`. Modified BuildHost test tool:
`Tools/TestVoxelForgeCoreWorldHost.ps1`.
@@ -83,6 +83,7 @@ namespace VoxelForgeCore
bool tileWasClassified = false;
bool meshingSkipped = false;
bool featureRefinedResolution = false;
bool heightfieldBandLimited = false;
std::uint64_t capRefinedCells = 0;
std::uint64_t capRefinedTriangles = 0;
bool capRefined = false;
+44 -31
View File
@@ -976,47 +976,60 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
const double Offset=-(GX*(PX-X)+GY*(PY-Y))/GZ;
return std::clamp(Crossing+Offset,ZMin,ZMax);
};
const int32 Base=static_cast<int32>(NewVertices.size());
const double Xs[4]={X0,X1,X1,X0},Ys[4]={Y0,Y0,Y1,Y1};
std::array<double,4> CornerZ{};
std::array<FVector,4> CornerNormals{};
// Les correctifs lointains suivent chaque cellule de la grille, sans grande nappe.
// Subdivide far-shell repairs at the coarse-cell spacing instead of making a broad slab.
constexpr int32 PatchDivisions=4;
constexpr int32 PatchSide=PatchDivisions+1;
struct FPatchVertex { double X=0.0,Y=0.0,Z=0.0;FVector Normal=FVector(0,0,1); };
std::array<FPatchVertex,PatchSide*PatchSide> Patch{};
bool bValidPatch=true;
for(int32 Corner=0;Corner<4;++Corner)
for(int32 PY=0;PY<PatchSide&&bValidPatch;++PY)
for(int32 PX=0;PX<PatchSide;++PX)
{
const double PredictedZ=SurfaceZ(Xs[Corner],Ys[Corner]);
const std::vector<double> CornerCrossings=FindFarStratumCrossings(
Shape,Xs[Corner],Ys[Corner],ZMin,ZMax);
if(CornerCrossings.empty()){bValidPatch=false;break;}
CornerZ[Corner]=*std::min_element(CornerCrossings.begin(),CornerCrossings.end(),
FPatchVertex& Vertex=Patch[static_cast<std::size_t>(PY*PatchSide+PX)];
Vertex.X=X0+(X1-X0)*static_cast<double>(PX)/PatchDivisions;
Vertex.Y=Y0+(Y1-Y0)*static_cast<double>(PY)/PatchDivisions;
const double PredictedZ=SurfaceZ(Vertex.X,Vertex.Y);
const std::vector<double> Crossings=FindFarStratumCrossings(
Shape,Vertex.X,Vertex.Y,ZMin,ZMax);
if(Crossings.empty()){bValidPatch=false;break;}
Vertex.Z=*std::min_element(Crossings.begin(),Crossings.end(),
[PredictedZ](double A,double B)
{return std::abs(A-PredictedZ)<std::abs(B-PredictedZ);});
const float CornerGX=Density(Xs[Corner]+2,Ys[Corner],CornerZ[Corner])
-Density(Xs[Corner]-2,Ys[Corner],CornerZ[Corner]);
const float CornerGY=Density(Xs[Corner],Ys[Corner]+2,CornerZ[Corner])
-Density(Xs[Corner],Ys[Corner]-2,CornerZ[Corner]);
const float CornerGZ=Density(Xs[Corner],Ys[Corner],CornerZ[Corner]+2)
-Density(Xs[Corner],Ys[Corner],CornerZ[Corner]-2);
CornerNormals[Corner]=FVector(CornerGX,CornerGY,CornerGZ);
if(CornerNormals[Corner].SizeSquared()<1.0e-8)
CornerNormals[Corner]=Normal;
else CornerNormals[Corner].Normalize();
const float VertexGX=Density(Vertex.X+2,Vertex.Y,Vertex.Z)
-Density(Vertex.X-2,Vertex.Y,Vertex.Z);
const float VertexGY=Density(Vertex.X,Vertex.Y+2,Vertex.Z)
-Density(Vertex.X,Vertex.Y-2,Vertex.Z);
const float VertexGZ=Density(Vertex.X,Vertex.Y,Vertex.Z+2)
-Density(Vertex.X,Vertex.Y,Vertex.Z-2);
Vertex.Normal=FVector(VertexGX,VertexGY,VertexGZ);
if(Vertex.Normal.SizeSquared()<1.0e-8)Vertex.Normal=Normal;
else Vertex.Normal.Normalize();
}
// A center crossing alone cannot justify a large tangent quad: it can
// bridge air over an isolated far-LOD feature and draw as a horizon slab.
// Keep the fallback only when the source field has a matching crossing
// at every corner, and place each corner on that source crossing.
// Une seule traversee centrale ne justifie pas une grande nappe de terrain.
// A center crossing alone cannot justify a broad patch that bridges horizon relief.
// Require a source-field crossing at every coarse-cell sample before emitting it.
if(!bValidPatch)continue;
for(int32 Corner=0;Corner<4;++Corner)
const int32 Base=static_cast<int32>(NewVertices.size());
for(const FPatchVertex& Vertex:Patch)
{
NewVertices.emplace_back(Xs[Corner]*25.0,Ys[Corner]*25.0,CornerZ[Corner]*25.0);
NewNormals.push_back(CornerNormals[Corner]);
if(bHasUVs)NewUVs.emplace_back(static_cast<float>((Xs[Corner]-Origin.X)/256.0),
static_cast<float>((Ys[Corner]-Origin.Y)/256.0));
NewVertices.emplace_back(Vertex.X*25.0,Vertex.Y*25.0,Vertex.Z*25.0);
NewNormals.push_back(Vertex.Normal);
if(bHasUVs)NewUVs.emplace_back(static_cast<float>((Vertex.X-Origin.X)/256.0),
static_cast<float>((Vertex.Y-Origin.Y)/256.0));
if(bHasColors)NewColors.push_back(Color);
}
const bool bBelowSolid=Density(X,Y,Crossing-1.0)<0.0f;
if(bBelowSolid)NewTriangles.insert(NewTriangles.end(),{Base,Base+1,Base+3,Base+1,Base+2,Base+3});
else NewTriangles.insert(NewTriangles.end(),{Base,Base+3,Base+1,Base+1,Base+3,Base+2});
for(int32 PY=0;PY<PatchDivisions;++PY)
for(int32 PX=0;PX<PatchDivisions;++PX)
{
const int32 I0=Base+PY*PatchSide+PX;
const int32 I1=I0+1;
const int32 I2=I1+PatchSide;
const int32 I3=I0+PatchSide;
if(bBelowSolid)NewTriangles.insert(NewTriangles.end(),{I0,I1,I3,I1,I2,I3});
else NewTriangles.insert(NewTriangles.end(),{I0,I3,I1,I1,I3,I2});
}
}
}
}
+115 -7
View File
@@ -369,8 +369,8 @@ namespace VoxelForgeCore
const int32 GY=static_cast<int32>(Index/static_cast<std::size_t>(Cells+1));
const double XSub=static_cast<double>(TileOrigin.X+GX*GridStepVoxels)*Scale;
const double YSub=static_cast<double>(TileOrigin.Y+GY*GridStepVoxels)*Scale;
AddedVertices.emplace_back(XSub/Scale*VOXEL_SIZE,YSub/Scale*VOXEL_SIZE,
Sample.Cap/Scale*VOXEL_SIZE);
AddedVertices.emplace_back(XSub/Scale*25.0,YSub/Scale*25.0,
Sample.Cap/Scale*25.0);
AddedNormals.push_back(Sample.Normal);
if(bHasUVs)AddedUVs.emplace_back(static_cast<float>(XSub/Scale),
static_cast<float>(YSub/Scale));
@@ -425,8 +425,8 @@ namespace VoxelForgeCore
for(int32 I=0;I<Count;++I)
{
const auto& V=PolygonB[I];
AddedVertices.emplace_back(V.XSub/Scale*VOXEL_SIZE,
V.YSub/Scale*VOXEL_SIZE,V.ZSub/Scale*VOXEL_SIZE);
AddedVertices.emplace_back(V.XSub/Scale*25.0,
V.YSub/Scale*25.0,V.ZSub/Scale*25.0);
AddedNormals.push_back(V.Normal);
if(bHasUVs)AddedUVs.emplace_back(static_cast<float>(V.XSub/Scale),
static_cast<float>(V.YSub/Scale));
@@ -893,11 +893,13 @@ namespace VoxelForgeCore
const FStandaloneShapeWorld& tileShape=page?*page:*impl_->shape;
UVoxelMarchingCubesMesher mesher;
mesher.SetGenerator(world.Generator.get());
mesher.SetGeometryVoxelSizeCm(25.0f);
mesher.SetStandaloneShapeWorld(&tileShape);
mesher.bStandaloneSkipUvAndColors=options.skipUvAndColors;
mesher.bStandaloneSkipUV=options.skipUv;mesher.bStandaloneSkipColors=options.skipColors;
mesher.bStandaloneSkipMaterialGroups=options.skipMaterialGroups;
mesher.bGenerateSkirts=world.bGenerateSkirts;mesher.SkirtCells=static_cast<float>(world.SkirtCells);
mesher.bGenerateSkirts=world.bGenerateSkirts;
mesher.SkirtCells=static_cast<float>(world.SkirtCells)*12.5f/25.0f;
mesher.LODOctaveDrop=world.LODOctaveDrop;
const int32 level=key.lod,extent=CHUNK_SIZE<<level;
const FVoxelTileKey nativeKey=ToNative(key);
@@ -942,10 +944,16 @@ namespace VoxelForgeCore
FStandaloneShapeWorld::ResetThreadClassificationStats();
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats();
bool bNativeSurfaceTile=false;
bool bNativeSurfaceBandSafe=false;
if(bHasWorldAssembly)
{
const auto Classification=tileShape.ClassifyBoxDetailed(origin,step,cells);
bNativeSurfaceTile=Classification.bNativeSurface;
// NativeSurfaceDensityAt consumes only the bounded manifest heightfield and its
// bounded type-2/type-4 features. Unknown tile occupancy does not make that Z support
// unknown; edits and cross-strate tiles are still excluded below.
// NativeSurfaceDensityAt ne consomme que le champ et les volumes bornes du manifeste.
bNativeSurfaceBandSafe=bNativeSurfaceTile;
if(bNativeSurfaceTile)
{
out.diagnostics.tileWasClassified=true;
@@ -983,8 +991,107 @@ namespace VoxelForgeCore
}
}
const auto MeshingStart=std::chrono::steady_clock::now();
int32 MeshBandMinZVoxel=options.bandMinZVoxel;
int32 MeshBandMaxZVoxel=options.bandMaxZVoxel;
bool bNativeHeightfieldBandLimited=false;
if(bHasWorldAssembly&&bNativeSurfaceBandSafe
&&!options.hashDensity&&!options.captureDensitySamples&&world.Manager!=nullptr)
{
// La surface 2D et les bornes de volume du manifeste definissent tout le support Z.
// The 2D surface and manifest volume bounds cover every possible density crossing.
const double TileCenterZ=static_cast<double>(origin.Z)+static_cast<double>(extent)*0.5;
const int32 CenterChunkZ=FMath::FloorToInt(TileCenterZ/static_cast<double>(CHUNK_SIZE));
const int32 StrateIndex=world.Manager->GetStrateIndex(
static_cast<float>(TileCenterZ*VOXEL_SIZE));
const auto PlanIt=std::find_if(impl_->worldAssembly->plan.strates.begin(),
impl_->worldAssembly->plan.strates.end(),[StrateIndex,CenterChunkZ](const auto& Plan)
{return Plan.strateIndex==StrateIndex&&CenterChunkZ<=Plan.topChunkZ
&&CenterChunkZ>=Plan.bottomChunkZ;});
if(PlanIt!=impl_->worldAssembly->plan.strates.end())
{
constexpr std::int64_t SubvoxelScale=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
const std::int64_t TileMinimum[3]={
static_cast<std::int64_t>(origin.X-step)*SubvoxelScale,
static_cast<std::int64_t>(origin.Y-step)*SubvoxelScale,
static_cast<std::int64_t>(origin.Z-step)*SubvoxelScale};
const std::int64_t TileMaximum[3]={
static_cast<std::int64_t>(origin.X+extent+step)*SubvoxelScale,
static_cast<std::int64_t>(origin.Y+extent+step)*SubvoxelScale,
static_cast<std::int64_t>(origin.Z+extent+step)*SubvoxelScale};
bool bOverlapsPlayerEdit=false;
if(tileShape.HasEditTransaction())
{
const FIntVector EditMinimum=tileShape.GetEditBoundsMin();
const FIntVector EditMaximum=tileShape.GetEditBoundsMax();
bOverlapsPlayerEdit=EditMinimum.X<=origin.X+extent+step
&&EditMaximum.X>=origin.X-step
&&EditMinimum.Y<=origin.Y+extent+step
&&EditMaximum.Y>=origin.Y-step
&&EditMinimum.Z<=origin.Z+extent+step
&&EditMaximum.Z>=origin.Z-step;
}
if(!bOverlapsPlayerEdit)
{
const VoxelForgeCore::Heightfield2D Heightfield(
impl_->worldAssembly->recipe,*PlanIt);
double RelevantMinimum=std::numeric_limits<double>::infinity();
double RelevantMaximum=-std::numeric_limits<double>::infinity();
const double TileMinimumZ=static_cast<double>(origin.Z-step);
const double TileMaximumZ=static_cast<double>(origin.Z+extent+step);
for(int32 GridY=0;GridY<=cells+2;++GridY)
for(int32 GridX=0;GridX<=cells+2;++GridX)
{
const std::int64_t XSub=static_cast<std::int64_t>(origin.X-step+GridX*step)
*SubvoxelScale;
const std::int64_t YSub=static_cast<std::int64_t>(origin.Y-step+GridY*step)
*SubvoxelScale;
const auto Column=Heightfield.point(XSub,YSub,0);
const double Ground=static_cast<double>(Column.groundHeightSubvoxels)
/static_cast<double>(SubvoxelScale);
const double Cap=static_cast<double>(Column.capHeightSubvoxels)
/static_cast<double>(SubvoxelScale);
if(Ground>=TileMinimumZ&&Ground<=TileMaximumZ)
{
RelevantMinimum=std::min(RelevantMinimum,Ground);
RelevantMaximum=std::max(RelevantMaximum,Ground);
}
if(Cap>=TileMinimumZ&&Cap<=TileMaximumZ)
{
RelevantMinimum=std::min(RelevantMinimum,Cap);
RelevantMaximum=std::max(RelevantMaximum,Cap);
}
}
for(const auto& Feature:PlanIt->features)
{
if(Feature.type!=2&&Feature.type!=4)continue;
const bool bOverlapsXY=Feature.minimumSubvoxels[0]<=TileMaximum[0]
&&Feature.maximumSubvoxels[0]>=TileMinimum[0]
&&Feature.minimumSubvoxels[1]<=TileMaximum[1]
&&Feature.maximumSubvoxels[1]>=TileMinimum[1];
const bool bOverlapsTileZ=Feature.minimumSubvoxels[2]<=TileMaximum[2]
&&Feature.maximumSubvoxels[2]>=TileMinimum[2];
if(!bOverlapsXY||!bOverlapsTileZ)continue;
const double FeatureMinimum=static_cast<double>(Feature.minimumSubvoxels[2])
/static_cast<double>(SubvoxelScale);
const double FeatureMaximum=static_cast<double>(Feature.maximumSubvoxels[2])
/static_cast<double>(SubvoxelScale);
RelevantMinimum=std::min(RelevantMinimum,FeatureMinimum);
RelevantMaximum=std::max(RelevantMaximum,FeatureMaximum);
}
if(std::isfinite(RelevantMinimum)&&std::isfinite(RelevantMaximum))
{
const int32 NativeBandMinimum=FMath::FloorToInt(RelevantMinimum)-step;
const int32 NativeBandMaximum=FMath::CeilToInt(RelevantMaximum)+step;
MeshBandMinZVoxel=FMath::Max(MeshBandMinZVoxel,NativeBandMinimum);
MeshBandMaxZVoxel=FMath::Min(MeshBandMaxZVoxel,NativeBandMaximum);
bNativeHeightfieldBandLimited=static_cast<std::int64_t>(MeshBandMaxZVoxel)
-MeshBandMinZVoxel<static_cast<std::int64_t>(extent)+2*step;
}
}
}
}
FVoxelMeshData mesh=mesher.GenerateMesh(origin,step,cells,nullptr,
options.bandMinZVoxel,options.bandMaxZVoxel,&sampleCount,nullptr,nullptr,
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr);
const double MeshingMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-MeshingStart).count();
@@ -1027,8 +1134,9 @@ namespace VoxelForgeCore
out.diagnostics.unknownBoxes=Counts.UnknownBoxes;
out.diagnostics.tileClass=TileClass::unknown;
}
out.diagnostics.meshingMilliseconds=MeshingMilliseconds;
}
out.diagnostics.heightfieldBandLimited=bNativeHeightfieldBandLimited;
out.diagnostics.meshingMilliseconds=MeshingMilliseconds;
out.meshPayloadAllocatedBytes=static_cast<std::uint64_t>(mesh.Vertices.GetAllocatedSize())
+static_cast<std::uint64_t>(mesh.Normals.GetAllocatedSize())
+static_cast<std::uint64_t>(mesh.UVs.GetAllocatedSize())
@@ -831,3 +831,50 @@ New source files: none. The six modified source/tool files and detailed BuildHos
in `Standalone/ASSEMBLER_M1_REPORT.md`. The owner's project has not been built or walked here.
— Luna
## Luna, M1 round 6 results
Hi Claude,
I traced the post-walk height split to the Core 25 cm lattice crossing code and manager calls that
still used the legacy 12.5 cm scale. Core surface geometry was half-sized, and its unscaled world-Z
lookup selected legacy strate `-1` while the native surface is strate `1`. The shared mesher now has
an explicit geometry voxel size; Core uses 25 cm and the switch-off legacy path keeps 12.5 cm.
Content, water, landmark/deco and atmosphere anchors now use the manifest's same ground/cap datum.
BuildHost reported terrain/content ground at `-50,385.254 cm` with zero delta, water at
`-49,375.977 cm`, atmosphere bounds `(-63,200,-12,000) cm`, and the corrected strate `1`.
The far horizon was a broad single-quad LOD8 bridge (`72,407.7 cm`, 5.657 coarse cells). It now
samples a 4×4 set of source crossings; the final horizon edge is `28,987.9 cm` (2.265 cells). Near
LOD normals had `0/96` direction mismatches at both LOD0 and LOD1. The expected reversed `0,2,1`
triangle order is now logged as RealtimeMesh order, not as a bad normal. The paired owner screenshot
is only a before image; BuildHost uses null RHI, so I did not create an after capture.
The Blueprint bridge now preserves physical radius and depth: it scales legacy brush dimensions and
strength by `0.5` and adapts legacy diff subtraction to Core's additive density edits. Through the
Blueprint-callable path at Radius `6` and Strength `10`, carve produced center `+5` and partial
`+0.52` at `60 cm`; fill produced `−5` and `−0.52`; both were zero at `80 cm`.
I measured staged Blueprint speeds of `600 cm/s` walk and `6,000 cm/s` sprint. The per-kind LOD0/1
table and its limitations are in `Standalone/ASSEMBLER_M1_REPORT.md`. The dominant relief mesher p95
dropped from `60.840 → 41.981 ms` at LOD0 and `70.297 → 42.649 ms` at LOD1. The nearest-tile service
p95 corresponds to about `2.52/2.56 m` sprint travel within `8/16 m` tile widths. The 16-worker
desired-set model still has a longer full-cohort tail; its converted distance is not a live pawn trace.
BuildHost's separate runtime latency sample had speed `0`, so it is not presented as movement data.
The final staged-manifest actor test passed in `11m04s` at `3.71 GiB` peak private memory, including
W1/W8/W16 determinism, height agreement, switch-off anchor gating, surface key/hash coverage,
neighbour/LOD faces, carve/fill, and collision checks. The final BuildHost UBT/link-safety compile
passed in `22.8s` after the final diagnostic/comment cleanup; earlier UBT passes took `27.7s` and
`40.9s`. Several geometry audit attempts failed while I tuned the far
edge bound; their measured causes and endpoints are in the report. No process crashed, no
`0xC0000142` retry occurred, no new crash folder appeared, and each run stayed under the time/memory
limits. No GPU capture, owner build/editor, `.uasset` edit, commit, push or stash.
Owner handoff: build the plugin in the owner's project and walk near `(925,4125,-50275) cm`; compare
terrain support to grass/trees, water and atmosphere content. Sprint through nearby relief. Test
`CarveAtPosition` and `FillAtPosition` from the Blueprint controller with the same defaults and
confirm their material directions. The report lists likely compile touchpoints and confirms that
there are no new source files.
— Luna
@@ -139,6 +139,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
// Cell size in voxels. No upper clamp: coarse clipmap levels use bigger steps (the EXTENT
// grows). Coarse tiles also use FEWER cells (InCellsPerAxis) for cheaper gen.
Step = FMath::Max(1, Step);
const float GeometryVoxelSize = VoxelMath::IsFinite(GeometryVoxelSizeCm)
? FMath::Max(1.0f, GeometryVoxelSizeCm) : VOXEL_SIZE;
// T2.b — octave bias for THIS tile: drop LODOctaveDrop octaves per Step doubling from
// the generator's per-voxel volumetric noise (sub-cell octaves can't shape a coarse
@@ -150,7 +152,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
TGuardValue<int32> SampleStepGuard(VoxelGenLOD::SampleStep, Step);
// World-cm origin of the tile's min corner (positions are built relative to this).
const FVector ChunkWorldPos = FVector(OriginVoxels) * VOXEL_SIZE;
const FVector ChunkWorldPos = FVector(OriginVoxels) * GeometryVoxelSize;
//=========================================================================
// VERTEX DEDUPLICATION MAP
@@ -316,11 +318,11 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
uint8 SurfClass = 0;
if (Normal.Z < -0.1f)
{
const float Zv = WorldPos.Z / VOXEL_SIZE;
const float Zv = WorldPos.Z / GeometryVoxelSize;
const float StepF = (float)Step;
const FIntVector MemoKey(
FMath::RoundToInt(WorldPos.X / (VOXEL_SIZE * StepF)),
FMath::RoundToInt(WorldPos.Y / (VOXEL_SIZE * StepF)),
FMath::RoundToInt(WorldPos.X / (GeometryVoxelSize * StepF)),
FMath::RoundToInt(WorldPos.Y / (GeometryVoxelSize * StepF)),
FMath::FloorToInt(Zv / (float)CHUNK_SIZE));
FVector2f* Col = SurfColMemo.Find(MemoKey);
if (!Col)
@@ -330,12 +332,23 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
static_cast<float>(MemoKey.X*Step),static_cast<float>(MemoKey.Y*Step),
MemoKey.Z,TerrainZ,CeilSurf);
if(!bSurf)
bSurf=Generator->GetSurfaceHeightAt(
(float)(MemoKey.X * Step), (float)(MemoKey.Y * Step), MemoKey.Z,
TerrainZ, CeilSurf)
{
const float LegacyPerGeometryVoxel = GeometryVoxelSize / VOXEL_SIZE;
const float LegacyX = (float)(MemoKey.X * Step) * LegacyPerGeometryVoxel;
const float LegacyY = (float)(MemoKey.Y * Step) * LegacyPerGeometryVoxel;
const int32 LegacyChunkZ = FMath::FloorToInt(
(WorldPos.Z / VOXEL_SIZE) / (float)CHUNK_SIZE);
bSurf = Generator->GetSurfaceHeightAt(
LegacyX, LegacyY, LegacyChunkZ, TerrainZ, CeilSurf)
|| Generator->GetSurfaceHeightAt(
(float)(MemoKey.X * Step), (float)(MemoKey.Y * Step), MemoKey.Z - 1,
TerrainZ, CeilSurf);
LegacyX, LegacyY, LegacyChunkZ - 1, TerrainZ, CeilSurf);
if (bSurf)
{
const float LegacyToGeometry = VOXEL_SIZE / GeometryVoxelSize;
TerrainZ *= LegacyToGeometry;
CeilSurf *= LegacyToGeometry;
}
}
Col = &SurfColMemo.Add(MemoKey, bSurf ? FVector2f(TerrainZ, CeilSurf)
: FVector2f(FLT_MAX, -FLT_MAX));
}
@@ -1481,7 +1494,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
const int32 GY = cy + CornerOffsets[i].Y;
const int32 GZ = cz + CornerOffsets[i].Z;
Positions[i] = ChunkWorldPos
+ FVector(GX * Step, GY * Step, GZ * Step) * VOXEL_SIZE;
+ FVector(GX * Step, GY * Step, GZ * Step) * GeometryVoxelSize;
Gradients[i] = GradAt(GX, GY, GZ);
}
if (bSampleCell)
@@ -1512,8 +1525,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
{
const FVector Midpoint=(Positions[A]+Positions[B])*0.5f;
const float MidDensity=ShapeWorldContext->DensityAtUnclamped(
Midpoint.X/VOXEL_SIZE,Midpoint.Y/VOXEL_SIZE,
Midpoint.Z/VOXEL_SIZE);
Midpoint.X/GeometryVoxelSize,Midpoint.Y/GeometryVoxelSize,
Midpoint.Z/GeometryVoxelSize);
if(FMath::Abs(MidDensity-IsoLevel)<1.0e-6f)
T=0.5f;
else if((MidDensity<IsoLevel)==(D1<IsoLevel))
@@ -1615,10 +1628,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
TArray<uint8> GroundSkirtFaceMasks,CapSkirtFaceMasks;
if (bGenerateSkirts && (GroundTris.Num() + CapTris.Num()) > 0)
{
const float ExtentCm = (float)(CellsPerAxis * Step) * VOXEL_SIZE;
const float ExtentCm = (float)(CellsPerAxis * Step) * GeometryVoxelSize;
const float MinX = ChunkWorldPos.X, MinY = ChunkWorldPos.Y, MinZ = ChunkWorldPos.Z;
const float MaxX = MinX + ExtentCm, MaxY = MinY + ExtentCm, MaxZ = MinZ + ExtentCm;
const float SkirtDepth = FMath::Max(1.0f, SkirtCells) * (float)Step * VOXEL_SIZE;
const float SkirtDepth = FMath::Max(1.0f, SkirtCells) * (float)Step * GeometryVoxelSize;
auto BoundaryFaceMask = [&](const FVector& A, const FVector& B) -> uint8
{
@@ -1656,12 +1669,12 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
{
if(!ShapeWorldContext)return 0.0f;
if(OriginalIndex<0||static_cast<size_t>(OriginalIndex)>=SurfaceVertexDensitySamples.size())
return ShapeWorldContext->DensityAt(Position.X/VOXEL_SIZE,
Position.Y/VOXEL_SIZE,Position.Z/VOXEL_SIZE);
return ShapeWorldContext->DensityAt(Position.X/GeometryVoxelSize,
Position.Y/GeometryVoxelSize,Position.Z/GeometryVoxelSize);
float& Density=SurfaceVertexDensitySamples[static_cast<size_t>(OriginalIndex)];
if(std::isnan(Density))
Density=ShapeWorldContext->DensityAt(Position.X/VOXEL_SIZE,
Position.Y/VOXEL_SIZE,Position.Z/VOXEL_SIZE);
Density=ShapeWorldContext->DensityAt(Position.X/GeometryVoxelSize,
Position.Y/GeometryVoxelSize,Position.Z/GeometryVoxelSize);
return Density;
};
@@ -1685,8 +1698,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
{
FSkirtVertex Vertex{Position,Normal,Color,0.0f,OriginalIndex};
if(!bOnSurface&&ShapeWorldContext)
Vertex.Density=ShapeWorldContext->DensityAt(Position.X/VOXEL_SIZE,
Position.Y/VOXEL_SIZE,Position.Z/VOXEL_SIZE);
Vertex.Density=ShapeWorldContext->DensityAt(Position.X/GeometryVoxelSize,
Position.Y/GeometryVoxelSize,Position.Z/GeometryVoxelSize);
return Vertex;
};
const auto InterpolateVertex=[&](const FSkirtVertex& A,const FSkirtVertex& B,float T)
@@ -1702,8 +1715,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
static_cast<uint8>(FMath::Clamp(FMath::RoundToInt(FMath::Lerp(static_cast<float>(A.Color.B),static_cast<float>(B.Color.B),T)),0,255)),
static_cast<uint8>(FMath::Clamp(FMath::RoundToInt(FMath::Lerp(static_cast<float>(A.Color.A),static_cast<float>(B.Color.A),T)),0,255)));
Out.Density=ShapeWorldContext
?ShapeWorldContext->DensityAt(Out.Position.X/VOXEL_SIZE,
Out.Position.Y/VOXEL_SIZE,Out.Position.Z/VOXEL_SIZE)
?ShapeWorldContext->DensityAt(Out.Position.X/GeometryVoxelSize,
Out.Position.Y/GeometryVoxelSize,Out.Position.Z/GeometryVoxelSize)
:FMath::Lerp(A.Density,B.Density,T);
Out.OriginalIndex=INDEX_NONE;
return Out;
@@ -1763,8 +1776,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
const FVector Position=P0.Position*static_cast<float>(WA)
+P1.Position*static_cast<float>(WB)
+P2.Position*static_cast<float>(WC);
return ShapeWorldContext->DensityAt(Position.X/VOXEL_SIZE,
Position.Y/VOXEL_SIZE,Position.Z/VOXEL_SIZE);
return ShapeWorldContext->DensityAt(Position.X/GeometryVoxelSize,
Position.Y/GeometryVoxelSize,Position.Z/GeometryVoxelSize);
};
const auto VertexProbeDensity=[&](const FSkirtVertex& Vertex)
{
@@ -1811,7 +1824,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
else if(ShapeWorldContext)
{
const FVector Center=(P0.Position+P1.Position+P2.Position)
/(3.0f*VOXEL_SIZE);
/(3.0f*GeometryVoxelSize);
if(ShapeWorldContext->DensityAt(Center.X,Center.Y,Center.Z)>0.0f)continue;
}
Tris.Add(PolygonIndices[0]);Tris.Add(PolygonIndices[Fan]);Tris.Add(PolygonIndices[Fan+1]);
@@ -94,6 +94,10 @@ public:
void SetGenerator(const UVoxelGenerator* InGenerator) { Generator = InGenerator; }
// Core tile coordinates use 25 cm voxels; legacy generation keeps 12.5 cm.
// Les coordonnees des tuiles Core utilisent des voxels de 25 cm; le legacy reste a 12,5 cm.
void SetGeometryVoxelSizeCm(float InVoxelSizeCm) { GeometryVoxelSizeCm = InVoxelSizeCm; }
// AVoxelWorld installs its bShuttingDown flag here. The commandlet path leaves it null.
// This is intentionally read-only: workers may observe shutdown, but never mutate world state.
void SetShutdownFlag(const std::atomic<bool>* InShutdownFlag) { ShutdownFlag = InShutdownFlag; }
@@ -116,6 +120,7 @@ private:
const std::atomic<bool>* ShutdownFlag = nullptr;
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
const FStandaloneShapeWorld* ShapeWorldContext = nullptr;
float GeometryVoxelSizeCm = VOXEL_SIZE;
public:
+21
View File
@@ -216,6 +216,27 @@ if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] native_biome_field=1 .*sampled_
if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] matched_surface_tile_timing=1 .*same_keys=1 tiles=2553') {
throw "BuildHost Core actor log is missing the matched native/legacy-bridge surface timing. See $FinalLog."
}
foreach ($Marker in @(
'[VoxelForgeCoreWorldTest] geometry_scale_agreement=PASS',
'[VoxelForgeCoreWorldTest] height_agreement=PASS',
'[VoxelForgeCoreWorldTest] switch_off_content_gate=PASS',
'[VoxelForgeCoreWorldTest] blueprint_carve=PASS',
'[VoxelForgeCoreWorldTest] blueprint_fill=PASS',
'[VoxelForgePawnSpeedFixture] source=staged_blueprints walk_cm_s=600.000 sprint_cm_s=6000.000'
)) {
if ($Text -notmatch [regex]::Escape($Marker)) {
throw "BuildHost actor log is missing the round-6 proof '$Marker'. See $FinalLog."
}
}
foreach ($Backend in @('core_native', 'legacy_surface_bridge')) {
foreach ($LOD in @(0, 1)) {
foreach ($Kind in @('flat_column', 'relief', 'mixed', 'unknown')) {
if ($Text -notmatch "\[WorldAssemblerPawnLag\] sample=unhashed_runtime_fastpath backend=$Backend lod=$LOD kind=$Kind ") {
throw "BuildHost actor log is missing the unhashed $Backend LOD$LOD $Kind streaming-lag row. See $FinalLog."
}
}
}
}
$NewCrashes = @(Get-NewCrashFolders)
if ($NewCrashes.Count -gt 0) {
throw "New UECC crash folders appeared during Core actor validation: $($NewCrashes -join '; ')"