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

@@ -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;
}