wip(assembler): round S1 -- one Core voxel-size source; per-tile feature lists (islands)
AVoxelWorld::CoreVoxelSizeCm (default 25, 32 voxels/tile) flows through plan, field, mesher, content, water, atmosphere, edits and collision. Staged-owner BuildHost passes at 20 cm (SurfaceWorld + islands, W1/W8/W16, anchors, BP edits, collision) with a fixed-25cm negative control; default-size hashes match round 4; Core off stays legacy 12.5 cm. Reusable BuildWorldAssemblyFeatureTileListV1; wired into FloatingIslands only so far. 256-island stress: tile p95 112.5 -> 2.0 ms, 0 mismatches, dropped-feature control fails. Content placement measured (no populated cells): island ground search 7.25 ms/cell vs legacy 0.29 ms -- open. Outside-review evaluation in Round5/REPORT.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
111cf3d262
commit
d6e69420e8
25 files changed
+2136
-250
No files matched your search
@@ -52,6 +52,8 @@ void UVoxelAtmosphereManager::UpdateForPlayer(const FVector& PlayerWorldPos)
|
||||
const FTransform Xf = O->GetActorTransform();
|
||||
const FVector LocalPos = Xf.InverseTransformPosition(PlayerWorldPos);
|
||||
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(O);
|
||||
const float VoxelSizeCm = VoxelWorld ? VoxelWorld->GetActiveVoxelSizeCm() : VOXEL_SIZE;
|
||||
const float ActiveToOwnerVoxelScale = VoxelSizeCm / VOXEL_SIZE;
|
||||
const float ManagerLocalZ = VoxelWorld
|
||||
? VoxelWorld->ToStrateManagerLocalZ(LocalPos.Z) : LocalPos.Z;
|
||||
|
||||
@@ -65,8 +67,11 @@ void UVoxelAtmosphereManager::UpdateForPlayer(const FVector& PlayerWorldPos)
|
||||
if (Generator && Def)
|
||||
{
|
||||
const int32 ChunkZ = FMath::FloorToInt((ManagerLocalZ / VOXEL_SIZE) / CHUNK_SIZE);
|
||||
Biome = Generator->GetDominantBiomeAt(LocalPos.X / VOXEL_SIZE,
|
||||
LocalPos.Y / VOXEL_SIZE, ChunkZ);
|
||||
// The biome generator still consumes owner voxels; pass through the active Core metric first.
|
||||
// Le generateur de biome consomme encore les voxels owner; convertir depuis la mesure Core active.
|
||||
const float OwnerVoxelX = (LocalPos.X / VoxelSizeCm) * ActiveToOwnerVoxelScale;
|
||||
const float OwnerVoxelY = (LocalPos.Y / VoxelSizeCm) * ActiveToOwnerVoxelScale;
|
||||
Biome = Generator->GetDominantBiomeAt(OwnerVoxelX, OwnerVoxelY, ChunkZ);
|
||||
}
|
||||
|
||||
// React on strate change (full apply) OR biome change within a strate (fog/sky only —
|
||||
|
||||
@@ -21,10 +21,12 @@
|
||||
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes
|
||||
#include <VoxelForgeCore/VoxelForgeCore.h>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
#include <string>
|
||||
|
||||
struct FCoreSurfaceSnapshot
|
||||
{
|
||||
double CoreVoxelSizeCm = 2.0 * VOXEL_SIZE;
|
||||
VoxelForgeCore::WorldAssemblyRecipeV1 Recipe;
|
||||
TMap<int32, VoxelForgeCore::WorldAssemblyStratumPlanV1> Plans;
|
||||
|
||||
@@ -33,21 +35,18 @@ struct FCoreSurfaceSnapshot
|
||||
return Plans.Find(StrateIndex);
|
||||
}
|
||||
|
||||
bool GetHeights(int32 StrateIndex, float LegacyVoxelX, float LegacyVoxelY,
|
||||
float& OutGroundLegacyVoxels, float& OutCapLegacyVoxels) const
|
||||
bool GetHeights(int32 StrateIndex, float CoreVoxelX, float CoreVoxelY,
|
||||
float& OutGroundCoreVoxels, float& OutCapCoreVoxels) const
|
||||
{
|
||||
const auto* Plan = FindPlan(StrateIndex);
|
||||
if (!Plan || !VoxelMath::IsFinite(LegacyVoxelX) || !VoxelMath::IsFinite(LegacyVoxelY))
|
||||
if (!Plan || !VoxelMath::IsFinite(CoreVoxelX) || !VoxelMath::IsFinite(CoreVoxelY))
|
||||
{
|
||||
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)
|
||||
constexpr double SubvoxelsPerCoreVoxel = VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const double MaxInput = static_cast<double>(MAX_int64) / SubvoxelsPerCoreVoxel;
|
||||
if (FMath::Abs(static_cast<double>(CoreVoxelX)) > MaxInput
|
||||
|| FMath::Abs(static_cast<double>(CoreVoxelY)) > MaxInput)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -58,9 +57,9 @@ struct FCoreSurfaceSnapshot
|
||||
return false;
|
||||
}
|
||||
const int64 XSubvoxels = static_cast<int64>(std::llround(
|
||||
static_cast<double>(LegacyVoxelX) * LegacyVoxelToSubvoxels));
|
||||
static_cast<double>(CoreVoxelX) * SubvoxelsPerCoreVoxel));
|
||||
const int64 YSubvoxels = static_cast<int64>(std::llround(
|
||||
static_cast<double>(LegacyVoxelY) * LegacyVoxelToSubvoxels));
|
||||
static_cast<double>(CoreVoxelY) * SubvoxelsPerCoreVoxel));
|
||||
const auto Sample = Heightfield.point(
|
||||
XSubvoxels, YSubvoxels, 0);
|
||||
int64 SurfaceHeightSubvoxels = Sample.groundHeightSubvoxels;
|
||||
@@ -103,9 +102,9 @@ struct FCoreSurfaceSnapshot
|
||||
PreviousDensity = Density;
|
||||
}
|
||||
}
|
||||
OutGroundLegacyVoxels = static_cast<float>(SurfaceHeightSubvoxels / SubvoxelsPerLegacyVoxel);
|
||||
OutCapLegacyVoxels = static_cast<float>(Sample.capHeightSubvoxels / SubvoxelsPerLegacyVoxel);
|
||||
return VoxelMath::IsFinite(OutGroundLegacyVoxels) && VoxelMath::IsFinite(OutCapLegacyVoxels);
|
||||
OutGroundCoreVoxels = static_cast<float>(SurfaceHeightSubvoxels / SubvoxelsPerCoreVoxel);
|
||||
OutCapCoreVoxels = static_cast<float>(Sample.capHeightSubvoxels / SubvoxelsPerCoreVoxel);
|
||||
return VoxelMath::IsFinite(OutGroundCoreVoxels) && VoxelMath::IsFinite(OutCapCoreVoxels);
|
||||
}
|
||||
|
||||
bool GetWater(int32 StrateIndex, bool& OutHasPlan, bool& OutEnabled, float& OutLocalCm) const
|
||||
@@ -122,7 +121,8 @@ struct FCoreSurfaceSnapshot
|
||||
OutEnabled = Plan->waterEnabled;
|
||||
if (OutEnabled)
|
||||
{
|
||||
OutLocalCm = static_cast<float>(Plan->waterSurfaceSubvoxels) * (25.0f / 256.0f);
|
||||
OutLocalCm = static_cast<float>(Plan->waterSurfaceSubvoxels
|
||||
* CoreVoxelSizeCm / VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -137,6 +137,7 @@ struct FCoreFloatingIslandFieldSnapshot
|
||||
|
||||
struct FCoreFloatingIslandSnapshot
|
||||
{
|
||||
double CoreVoxelSizeCm = 2.0 * VOXEL_SIZE;
|
||||
TMap<int32, TSharedPtr<const FCoreFloatingIslandFieldSnapshot, ESPMode::ThreadSafe>> Fields;
|
||||
|
||||
const FCoreFloatingIslandFieldSnapshot* FindField(int32 StrateIndex) const
|
||||
@@ -145,32 +146,30 @@ struct FCoreFloatingIslandSnapshot
|
||||
return Found != nullptr && Found->IsValid() ? Found->Get() : nullptr;
|
||||
}
|
||||
|
||||
bool GetDensity(int32 StrateIndex, float LegacyVoxelX, float LegacyVoxelY,
|
||||
float LegacyVoxelZ, float& OutDensity) const
|
||||
bool GetDensity(int32 StrateIndex, float CoreVoxelX, float CoreVoxelY,
|
||||
float CoreVoxelZ, float& OutDensity) const
|
||||
{
|
||||
OutDensity = 1.0f;
|
||||
const FCoreFloatingIslandFieldSnapshot* Entry = FindField(StrateIndex);
|
||||
if (Entry == nullptr || !Entry->Field.IsValid()
|
||||
|| !FMath::IsFinite(LegacyVoxelX) || !FMath::IsFinite(LegacyVoxelY)
|
||||
|| !FMath::IsFinite(LegacyVoxelZ))
|
||||
|| !FMath::IsFinite(CoreVoxelX) || !FMath::IsFinite(CoreVoxelY)
|
||||
|| !FMath::IsFinite(CoreVoxelZ))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// A legacy voxel is 128 subvoxels at 12.5 cm; Core field distances are converted back to legacy units.
|
||||
// Un voxel historique vaut 128 sous-voxels a 12,5 cm ; la distance Core revient en unites historiques.
|
||||
constexpr double LegacyVoxelSubvoxels = 128.0;
|
||||
constexpr double MaximumInput = static_cast<double>(MAX_int64) / LegacyVoxelSubvoxels;
|
||||
if (FMath::Abs(static_cast<double>(LegacyVoxelX)) > MaximumInput
|
||||
|| FMath::Abs(static_cast<double>(LegacyVoxelY)) > MaximumInput
|
||||
|| FMath::Abs(static_cast<double>(LegacyVoxelZ)) > MaximumInput)
|
||||
constexpr double SubvoxelsPerCoreVoxel = VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const double MaximumInput = static_cast<double>(MAX_int64) / SubvoxelsPerCoreVoxel;
|
||||
if (FMath::Abs(static_cast<double>(CoreVoxelX)) > MaximumInput
|
||||
|| FMath::Abs(static_cast<double>(CoreVoxelY)) > MaximumInput
|
||||
|| FMath::Abs(static_cast<double>(CoreVoxelZ)) > MaximumInput)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const int64 Density = Entry->Field->densitySubvoxels(
|
||||
static_cast<int64>(std::llround(static_cast<double>(LegacyVoxelX) * LegacyVoxelSubvoxels)),
|
||||
static_cast<int64>(std::llround(static_cast<double>(LegacyVoxelY) * LegacyVoxelSubvoxels)),
|
||||
static_cast<int64>(std::llround(static_cast<double>(LegacyVoxelZ) * LegacyVoxelSubvoxels)));
|
||||
OutDensity = static_cast<float>(static_cast<double>(Density) / LegacyVoxelSubvoxels);
|
||||
static_cast<int64>(std::llround(static_cast<double>(CoreVoxelX) * SubvoxelsPerCoreVoxel)),
|
||||
static_cast<int64>(std::llround(static_cast<double>(CoreVoxelY) * SubvoxelsPerCoreVoxel)),
|
||||
static_cast<int64>(std::llround(static_cast<double>(CoreVoxelZ) * SubvoxelsPerCoreVoxel)));
|
||||
OutDensity = static_cast<float>(static_cast<double>(Density) / SubvoxelsPerCoreVoxel);
|
||||
return FMath::IsFinite(OutDensity);
|
||||
}
|
||||
|
||||
@@ -185,8 +184,8 @@ struct FCoreFloatingIslandSnapshot
|
||||
{
|
||||
return false;
|
||||
}
|
||||
constexpr double SubvoxelsPerCoreVoxel = 256.0;
|
||||
constexpr double CentimetersPerCoreVoxel = 25.0;
|
||||
constexpr double SubvoxelsPerCoreVoxel = VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const double CentimetersPerCoreVoxel = CoreVoxelSizeCm;
|
||||
const double XSubvoxels = static_cast<double>(LocalXYCm.X)
|
||||
* SubvoxelsPerCoreVoxel / CentimetersPerCoreVoxel;
|
||||
const double YSubvoxels = static_cast<double>(LocalXYCm.Y)
|
||||
@@ -301,6 +300,8 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
|
||||
{
|
||||
TSharedPtr<FCoreSurfaceSnapshot, ESPMode::ThreadSafe> Snapshot =
|
||||
MakeShared<FCoreSurfaceSnapshot, ESPMode::ThreadSafe>();
|
||||
Snapshot->CoreVoxelSizeCm = VoxelWorld != nullptr
|
||||
? VoxelWorld->GetActiveVoxelSizeCm() : 2.0 * VOXEL_SIZE;
|
||||
Snapshot->Recipe = Manifest->recipe;
|
||||
for (const auto& Plan : Manifest->plan.strates)
|
||||
{
|
||||
@@ -312,6 +313,8 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
|
||||
{
|
||||
TSharedPtr<FCoreFloatingIslandSnapshot, ESPMode::ThreadSafe> IslandSnapshot =
|
||||
MakeShared<FCoreFloatingIslandSnapshot, ESPMode::ThreadSafe>();
|
||||
IslandSnapshot->CoreVoxelSizeCm = VoxelWorld != nullptr
|
||||
? VoxelWorld->GetActiveVoxelSizeCm() : 2.0 * VOXEL_SIZE;
|
||||
for (const auto& IslandPlan : Manifest->floatingIslands)
|
||||
{
|
||||
TSharedPtr<FCoreFloatingIslandFieldSnapshot, ESPMode::ThreadSafe> Entry =
|
||||
@@ -363,15 +366,16 @@ bool UVoxelContentManager::QueryNativeSurfaceAnchors(
|
||||
return false;
|
||||
}
|
||||
|
||||
float GroundLegacyVoxels = 0.0f;
|
||||
float CapLegacyVoxels = 0.0f;
|
||||
const float VoxelSizeCm = VoxelWorld->GetActiveVoxelSizeCm();
|
||||
float GroundCoreVoxels = 0.0f;
|
||||
float CapCoreVoxels = 0.0f;
|
||||
if (!NativeSurfaceSnapshot->GetHeights(StrateIndex,
|
||||
LocalXYCm.X / VOXEL_SIZE, LocalXYCm.Y / VOXEL_SIZE,
|
||||
GroundLegacyVoxels, CapLegacyVoxels))
|
||||
LocalXYCm.X / VoxelSizeCm, LocalXYCm.Y / VoxelSizeCm,
|
||||
GroundCoreVoxels, CapCoreVoxels))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OutGroundLocalZCm = GroundLegacyVoxels * VOXEL_SIZE;
|
||||
OutGroundLocalZCm = GroundCoreVoxels * VoxelSizeCm;
|
||||
bool bHasPlan = false;
|
||||
if (!NativeSurfaceSnapshot->GetWater(StrateIndex, bHasPlan,
|
||||
bOutHasWater, OutWaterLocalZCm)
|
||||
@@ -553,8 +557,8 @@ void UVoxelContentManager::UpdateDecorations(const FVector& PlayerWorldPos)
|
||||
if (!OwnerActor) return;
|
||||
|
||||
const FTransform OwnerXf = OwnerActor->GetActorTransform();
|
||||
// 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.
|
||||
// All strate/water/density queries are in actor-LOCAL space; Core uses the actor's active voxel step.
|
||||
// Les requetes de strate/eau/densite sont locales a l'acteur; Core utilise son pas de voxel actif.
|
||||
const FVector LocalPlayer = OwnerXf.InverseTransformPosition(PlayerWorldPos);
|
||||
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(OwnerActor);
|
||||
const float ManagerLocalZ = VoxelWorld
|
||||
@@ -573,7 +577,8 @@ void UVoxelContentManager::UpdateDecorations(const FVector& PlayerWorldPos)
|
||||
return;
|
||||
}
|
||||
|
||||
const float CellWorld = (float)DECO_CELL_VOXELS * VOXEL_SIZE;
|
||||
const float VoxelSizeCm = VoxelWorld ? VoxelWorld->GetActiveVoxelSizeCm() : VOXEL_SIZE;
|
||||
const float CellWorld = static_cast<float>(DECO_CELL_VOXELS) * VoxelSizeCm;
|
||||
const FIntPoint PlayerCell(
|
||||
FMath::FloorToInt(LocalPlayer.X / CellWorld),
|
||||
FMath::FloorToInt(LocalPlayer.Y / CellWorld));
|
||||
@@ -581,8 +586,10 @@ void UVoxelContentManager::UpdateDecorations(const FVector& PlayerWorldPos)
|
||||
// Shared per-update strate context (a strate is a horizontal slab → same for every cell).
|
||||
CurrentCtx = FDecoContext();
|
||||
CurrentCtx.StrateIndex = StrateIndex;
|
||||
CurrentCtx.TopVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(TopZ) : TopZ) / VOXEL_SIZE;
|
||||
CurrentCtx.BottomVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(BotZ) : BotZ) / VOXEL_SIZE;
|
||||
CurrentCtx.VoxelSizeCm = VoxelSizeCm;
|
||||
CurrentCtx.ActiveToOwnerVoxelScale = VoxelSizeCm / VOXEL_SIZE;
|
||||
CurrentCtx.TopVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(TopZ) : TopZ) / VoxelSizeCm;
|
||||
CurrentCtx.BottomVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(BotZ) : BotZ) / VoxelSizeCm;
|
||||
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);
|
||||
@@ -706,6 +713,208 @@ void UVoxelContentManager::UpdateDecorations(const FVector& PlayerWorldPos)
|
||||
ProcessDecoResults(PlayerCell);
|
||||
}
|
||||
|
||||
bool UVoxelContentManager::MeasurePlacementCostForValidation(
|
||||
const FVector& PlayerWorldPos, int32 SampleCellCount, int32 FrameSampleCount,
|
||||
FDecorationPlacementCostAudit& OutAudit, FString& OutError)
|
||||
{
|
||||
OutAudit = FDecorationPlacementCostAudit();
|
||||
OutError.Reset();
|
||||
if (!Settings || !Generator || !StrateManager || !Owner.IsValid()
|
||||
|| SampleCellCount < 1 || SampleCellCount > 256
|
||||
|| FrameSampleCount < 1 || FrameSampleCount > 256)
|
||||
{
|
||||
OutError = TEXT("Decoration cost audit requires an initialized manager and bounded sample counts.");
|
||||
return false;
|
||||
}
|
||||
if (!WaitForDecorationTasks(FPlatformTime::Seconds() + 10.0))
|
||||
{
|
||||
OutError = TEXT("Decoration cost audit could not drain prior worker tasks.");
|
||||
return false;
|
||||
}
|
||||
|
||||
struct FRestoreDecorationConcurrency
|
||||
{
|
||||
UVoxelSettings* Settings = nullptr;
|
||||
int32 Original = 0;
|
||||
~FRestoreDecorationConcurrency()
|
||||
{
|
||||
if (Settings != nullptr) Settings->MaxConcurrentDecorationTasks = Original;
|
||||
}
|
||||
} Restore{Settings, Settings->MaxConcurrentDecorationTasks};
|
||||
|
||||
// Resolve the current route and palette once, while preventing test work from reaching HISMs.
|
||||
Settings->MaxConcurrentDecorationTasks = 0;
|
||||
UpdateDecorations(PlayerWorldPos);
|
||||
if (CurrentCtx.Def == nullptr)
|
||||
{
|
||||
OutError = TEXT("Decoration cost audit position is outside the resolved strate layout.");
|
||||
return false;
|
||||
}
|
||||
OutAudit.bNativeSurface = CurrentCtx.bNativeSurface;
|
||||
OutAudit.bNativeFloatingIslands = CurrentCtx.bNativeFloatingIslands;
|
||||
OutAudit.NearEntries = NearGrid.Entries.Num();
|
||||
OutAudit.FarEntries = FarGrid.Entries.Num();
|
||||
|
||||
auto MakeAuditEntries = [&](const FDecoGrid& Grid, TArray<FStrateDecoration>& Entries,
|
||||
TArray<int32>& EntryBiome) -> bool
|
||||
{
|
||||
if (!Grid.Entries.IsEmpty())
|
||||
{
|
||||
Entries = Grid.Entries;
|
||||
EntryBiome = Grid.EntryBiome;
|
||||
return false;
|
||||
}
|
||||
|
||||
OutAudit.bSyntheticWorkload = true;
|
||||
const int32 BiomeCount = CurrentCtx.BiomeCtx.IsValid()
|
||||
? CurrentCtx.BiomeCtx.Biomes.Num() : 0;
|
||||
const int32 EntryCount = FMath::Max(1, BiomeCount);
|
||||
Entries.Reserve(EntryCount);
|
||||
EntryBiome.Reserve(EntryCount);
|
||||
for (int32 Index = 0; Index < EntryCount; ++Index)
|
||||
{
|
||||
FStrateDecoration Entry;
|
||||
Entry.Profile.ActorClass = AActor::StaticClass();
|
||||
Entry.Profile.SurfacePlacement = ESurfaceType::Floor;
|
||||
Entry.SpawnDensity = 1.0f;
|
||||
Entry.MaxPerChunk = 4;
|
||||
Entries.Add(MoveTemp(Entry));
|
||||
EntryBiome.Add(BiomeCount > 0 ? Index : -1);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
TArray<FStrateDecoration> NearEntriesAudit, FarEntriesAudit;
|
||||
TArray<int32> NearEntryBiomeAudit, FarEntryBiomeAudit;
|
||||
const bool bNearSynthetic = MakeAuditEntries(NearGrid, NearEntriesAudit, NearEntryBiomeAudit);
|
||||
const bool bFarSynthetic = MakeAuditEntries(FarGrid, FarEntriesAudit, FarEntryBiomeAudit);
|
||||
|
||||
AActor* OwnerActor = Owner.Get();
|
||||
const FTransform OwnerXf = OwnerActor->GetActorTransform();
|
||||
const FVector LocalPlayer = OwnerXf.InverseTransformPosition(PlayerWorldPos);
|
||||
const float CellWorldCm = static_cast<float>(DECO_CELL_VOXELS) * CurrentCtx.VoxelSizeCm;
|
||||
const FIntPoint PlayerCell(FMath::FloorToInt(LocalPlayer.X / CellWorldCm),
|
||||
FMath::FloorToInt(LocalPlayer.Y / CellWorldCm));
|
||||
const int32 GridSide = FMath::CeilToInt(FMath::Sqrt(static_cast<float>(SampleCellCount)));
|
||||
const int32 Step = FMath::Max(1, Settings->DecorationMarchStepVoxels);
|
||||
const int32 MaxCrossings = FMath::Max(1, Settings->DecorationMaxCrossingsPerColumn);
|
||||
const float ColumnDepth = static_cast<float>(FMath::Max(8, Settings->DecorationColumnDepthVoxels));
|
||||
TArray<double> CellMilliseconds;
|
||||
CellMilliseconds.Reserve(SampleCellCount);
|
||||
int64 TotalColumns = 0;
|
||||
int64 TotalDensityQueries = 0;
|
||||
int64 TotalSurfaceQueries = 0;
|
||||
|
||||
for (int32 Sample = 0; Sample < SampleCellCount; ++Sample)
|
||||
{
|
||||
const int32 X = Sample % GridSide - GridSide / 2;
|
||||
const int32 Y = Sample / GridSide - GridSide / 2;
|
||||
const FIntPoint Cell(PlayerCell.X + X, PlayerCell.Y + Y);
|
||||
const double CellStart = FPlatformTime::Seconds();
|
||||
for (const auto& GridAudit : {
|
||||
std::make_pair(&NearGrid, &NearEntriesAudit),
|
||||
std::make_pair(&FarGrid, &FarEntriesAudit)})
|
||||
{
|
||||
const FDecoGrid& Grid = *GridAudit.first;
|
||||
const TArray<FStrateDecoration>& Entries = *GridAudit.second;
|
||||
const TArray<int32>& EntryBiome = Grid.Tier == EDecoStreamTier::Near
|
||||
? NearEntryBiomeAudit : FarEntryBiomeAudit;
|
||||
if (Entries.IsEmpty()) continue;
|
||||
TArray<FDecoSpawn> Spawns;
|
||||
FPlacementBuildCounters Counters;
|
||||
BuildCellSpawns(Generator, OwnerXf, Cell, CurrentCtx, Entries, EntryBiome,
|
||||
static_cast<uint32>(Seed), Grid.Spacing, static_cast<float>(Step),
|
||||
MaxCrossings, ColumnDepth, Spawns, &Counters);
|
||||
TotalColumns += Counters.Columns;
|
||||
TotalDensityQueries += Counters.DensityQueries;
|
||||
TotalSurfaceQueries += Counters.SurfaceQueries;
|
||||
OutAudit.Placements += Spawns.Num();
|
||||
|
||||
const bool bSyntheticGrid = Grid.Tier == EDecoStreamTier::Near
|
||||
? bNearSynthetic : bFarSynthetic;
|
||||
if (bSyntheticGrid)
|
||||
{
|
||||
for (const FDecoSpawn& Spawn : Spawns)
|
||||
{
|
||||
const FVector LocalSpawn = OwnerXf.InverseTransformPosition(Spawn.Xf.GetLocation());
|
||||
if (CurrentCtx.bNativeSurface && CurrentCtx.NativeSurface.IsValid())
|
||||
{
|
||||
float GroundZ = 0.0f;
|
||||
bool bHasWater = false;
|
||||
float WaterZ = -FLT_MAX;
|
||||
if (QueryNativeSurfaceAnchors(CurrentCtx.StrateIndex,
|
||||
FVector2D(LocalSpawn.X, LocalSpawn.Y), GroundZ, bHasWater, WaterZ))
|
||||
{
|
||||
OutAudit.MaximumAnchorErrorCm = FMath::Max(
|
||||
OutAudit.MaximumAnchorErrorCm,
|
||||
FMath::Abs(static_cast<double>(LocalSpawn.Z - GroundZ)));
|
||||
++OutAudit.AnchorChecks;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float Density = 1.0f;
|
||||
const bool bSampled = CurrentCtx.bNativeFloatingIslands
|
||||
&& CurrentCtx.NativeIslands.IsValid()
|
||||
? CurrentCtx.NativeIslands->GetDensity(CurrentCtx.StrateIndex,
|
||||
LocalSpawn.X / CurrentCtx.VoxelSizeCm,
|
||||
LocalSpawn.Y / CurrentCtx.VoxelSizeCm,
|
||||
LocalSpawn.Z / CurrentCtx.VoxelSizeCm, Density)
|
||||
: (Density = Generator->GetDensityAt(
|
||||
LocalSpawn.X / CurrentCtx.VoxelSizeCm * CurrentCtx.ActiveToOwnerVoxelScale,
|
||||
LocalSpawn.Y / CurrentCtx.VoxelSizeCm * CurrentCtx.ActiveToOwnerVoxelScale,
|
||||
LocalSpawn.Z / CurrentCtx.VoxelSizeCm * CurrentCtx.ActiveToOwnerVoxelScale), true);
|
||||
if (bSampled)
|
||||
{
|
||||
OutAudit.MaximumDensityResidual = FMath::Max(
|
||||
OutAudit.MaximumDensityResidual,
|
||||
FMath::Abs(static_cast<double>(Density)));
|
||||
++OutAudit.AnchorChecks;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CellMilliseconds.Add((FPlatformTime::Seconds() - CellStart) * 1000.0);
|
||||
}
|
||||
|
||||
auto MedianAndP95 = [](TArray<double> Samples, double& OutMedian, double& OutP95)
|
||||
{
|
||||
Samples.Sort();
|
||||
if (Samples.IsEmpty())
|
||||
{
|
||||
OutMedian = 0.0;
|
||||
OutP95 = 0.0;
|
||||
return;
|
||||
}
|
||||
OutMedian = Samples[Samples.Num() / 2];
|
||||
const int32 P95Index = FMath::Clamp(FMath::CeilToInt(Samples.Num() * 0.95f) - 1,
|
||||
0, Samples.Num() - 1);
|
||||
OutP95 = Samples[P95Index];
|
||||
};
|
||||
OutAudit.SampledCells = SampleCellCount;
|
||||
OutAudit.ColumnsPerCell = static_cast<int32>(TotalColumns / SampleCellCount);
|
||||
OutAudit.DensityQueriesPerCell = static_cast<double>(TotalDensityQueries) / SampleCellCount;
|
||||
OutAudit.SurfaceQueriesPerCell = static_cast<double>(TotalSurfaceQueries) / SampleCellCount;
|
||||
MedianAndP95(MoveTemp(CellMilliseconds), OutAudit.CellMedianMilliseconds,
|
||||
OutAudit.CellP95Milliseconds);
|
||||
|
||||
// The game-thread update cost is sampled with task dispatch disabled; worker-cell cost is reported
|
||||
// separately above, so the frame figure is not polluted by waiting for asynchronous work.
|
||||
UpdateDecorations(PlayerWorldPos);
|
||||
TArray<double> UpdateMilliseconds;
|
||||
UpdateMilliseconds.Reserve(FrameSampleCount);
|
||||
for (int32 Frame = 0; Frame < FrameSampleCount; ++Frame)
|
||||
{
|
||||
const double UpdateStart = FPlatformTime::Seconds();
|
||||
UpdateDecorations(PlayerWorldPos);
|
||||
UpdateMilliseconds.Add((FPlatformTime::Seconds() - UpdateStart) * 1000.0);
|
||||
}
|
||||
MedianAndP95(MoveTemp(UpdateMilliseconds), OutAudit.UpdateMedianMilliseconds,
|
||||
OutAudit.UpdateP95Milliseconds);
|
||||
return true;
|
||||
}
|
||||
|
||||
void UVoxelContentManager::RebuildDesiredCells(FDecoGrid& G, const FIntPoint& PlayerCell)
|
||||
{
|
||||
// REGION-granular streaming, per grid. Decoration cells are grouped into RxR regions; a region is the
|
||||
@@ -882,8 +1091,10 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
const TArray<FStrateDecoration>& Entries,
|
||||
const TArray<int32>& EntryBiome, uint32 InSeed,
|
||||
int32 Spacing, float Step, int32 MaxCrossings, float ColumnDepth,
|
||||
TArray<FDecoSpawn>& OutSpawns)
|
||||
TArray<FDecoSpawn>& OutSpawns,
|
||||
FPlacementBuildCounters* OutCounters)
|
||||
{
|
||||
if (OutCounters != nullptr) *OutCounters = FPlacementBuildCounters();
|
||||
if (!Gen || Entries.Num() == 0) return;
|
||||
|
||||
const int32 PerAxis = FMath::Max(1, CHUNK_SIZE / Spacing);
|
||||
@@ -912,9 +1123,9 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
// cheap. The cache is local to this worker task (determinism-safe — pure function of XY/seed/Ctx).
|
||||
FChunkBiomeCache BiomeCache;
|
||||
const bool bHasBiomes = Ctx.BiomeCtx.IsValid();
|
||||
|
||||
auto D = [&](float VX, float VY, float VZ)
|
||||
{
|
||||
if (OutCounters != nullptr) ++OutCounters->DensityQueries;
|
||||
if (Ctx.bNativeFloatingIslands && Ctx.NativeIslands.IsValid())
|
||||
{
|
||||
float NativeDensity = 1.0f;
|
||||
@@ -922,16 +1133,27 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
return NativeDensity;
|
||||
return 1.0f;
|
||||
}
|
||||
return Gen->GetDensityAt(VX, VY, VZ);
|
||||
return Gen->GetDensityAt(VX * Ctx.ActiveToOwnerVoxelScale,
|
||||
VY * Ctx.ActiveToOwnerVoxelScale, VZ * Ctx.ActiveToOwnerVoxelScale);
|
||||
};
|
||||
auto GetSurface = [&](float VX, float VY, float& OutGround, float& OutCap)
|
||||
{
|
||||
if (OutCounters != nullptr) ++OutCounters->SurfaceQueries;
|
||||
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);
|
||||
float OwnerGround = 0.0f;
|
||||
float OwnerCap = 0.0f;
|
||||
if (!Gen->GetSurfaceHeightAt(VX * Ctx.ActiveToOwnerVoxelScale,
|
||||
VY * Ctx.ActiveToOwnerVoxelScale, Ctx.RepChunkZ, OwnerGround, OwnerCap))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OutGround = OwnerGround / Ctx.ActiveToOwnerVoxelScale;
|
||||
OutCap = OwnerCap / Ctx.ActiveToOwnerVoxelScale;
|
||||
return true;
|
||||
};
|
||||
|
||||
// Build a companion satellite's transform from its profile + per-instance hash at a snapped surface point
|
||||
@@ -939,7 +1161,7 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
auto MakeCompanionXf = [&](const FPlacementProfile& P, uint32 IHash,
|
||||
float sVX, float sVY, float sZ, const FVector& sN) -> FTransform
|
||||
{
|
||||
const FVector Local(sVX * VOXEL_SIZE, sVY * VOXEL_SIZE, sZ * VOXEL_SIZE);
|
||||
const FVector Local(sVX * Ctx.VoxelSizeCm, sVY * Ctx.VoxelSizeCm, sZ * Ctx.VoxelSizeCm);
|
||||
const FVector Pos = OwnerXf.TransformPosition(Local) + sN * P.SurfaceOffset + P.LocationOffset;
|
||||
FQuat Q = P.bAlignToSurface ? FRotationMatrix::MakeFromZ(sN).ToQuat() : FQuat::Identity;
|
||||
Q = Q * P.RotationOffset.Quaternion();
|
||||
@@ -965,7 +1187,7 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
const bool bCeiling = NormalWorld.Z < -0.5f;
|
||||
const bool bWall = !bFloor && !bCeiling;
|
||||
|
||||
const FVector LocalPos(VX * VOXEL_SIZE, VY * VOXEL_SIZE, ZC * VOXEL_SIZE);
|
||||
const FVector LocalPos(VX * Ctx.VoxelSizeCm, VY * Ctx.VoxelSizeCm, ZC * Ctx.VoxelSizeCm);
|
||||
const FVector PosWorld = OwnerXf.TransformPosition(LocalPos);
|
||||
const bool bBelowWater = Ctx.bHasWater && (LocalPos.Z < Ctx.WaterLocalZ);
|
||||
|
||||
@@ -1014,7 +1236,9 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
// F7 aware placement: relational conditions (relief/moisture/biome-border). Opt-in per entry —
|
||||
// skipped entirely when the list is empty. Worker-safe pure query; uses the cell's biome context.
|
||||
if (Deco.Profile.Conditions.Num() > 0 &&
|
||||
!Gen->EvaluateTerrainConditions(Deco.Profile.Conditions, VX, VY, Ctx.BiomeCtx)) continue;
|
||||
!Gen->EvaluateTerrainConditions(Deco.Profile.Conditions,
|
||||
VX * Ctx.ActiveToOwnerVoxelScale, VY * Ctx.ActiveToOwnerVoxelScale,
|
||||
Ctx.BiomeCtx)) continue;
|
||||
|
||||
const FVector SpawnPos = PosWorld + NormalWorld * Deco.Profile.SurfaceOffset + Deco.Profile.LocationOffset;
|
||||
// Rotation: optional surface-align → fixed offset → per-axis hash random (same model as landmarks).
|
||||
@@ -1074,7 +1298,9 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
|
||||
// Optional per-satellite gating (relief/moisture/biome-border at ITS own XY).
|
||||
if (Comp.Profile.Conditions.Num() > 0 &&
|
||||
!Gen->EvaluateTerrainConditions(Comp.Profile.Conditions, SatVX, SatVY, Ctx.BiomeCtx)) continue;
|
||||
!Gen->EvaluateTerrainConditions(Comp.Profile.Conditions,
|
||||
SatVX * Ctx.ActiveToOwnerVoxelScale,
|
||||
SatVY * Ctx.ActiveToOwnerVoxelScale, Ctx.BiomeCtx)) continue;
|
||||
|
||||
// Surface: SNAP to the real ground/ceiling at the satellite XY (kills floaters); or inherit.
|
||||
float SatZ; FVector SatN;
|
||||
@@ -1121,7 +1347,9 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
const float SubVY = SatVY + SinSAng * SRadVox;
|
||||
|
||||
if (Sub.Profile.Conditions.Num() > 0 &&
|
||||
!Gen->EvaluateTerrainConditions(Sub.Profile.Conditions, SubVX, SubVY, Ctx.BiomeCtx)) continue;
|
||||
!Gen->EvaluateTerrainConditions(Sub.Profile.Conditions,
|
||||
SubVX * Ctx.ActiveToOwnerVoxelScale,
|
||||
SubVY * Ctx.ActiveToOwnerVoxelScale, Ctx.BiomeCtx)) continue;
|
||||
|
||||
FDecoSpawn& SO = OutSpawns.AddDefaulted_GetRef();
|
||||
SO.EntryIdx = EntryIdx;
|
||||
@@ -1141,9 +1369,9 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
for (int32 gy = 0; gy < PerAxis; ++gy)
|
||||
for (int32 gx = 0; gx < PerAxis; ++gx)
|
||||
{
|
||||
// INTEGER voxel jitter: columns land on integer XY so the generator's surface-column cache
|
||||
// (T1.a, §8.10) applies — FRACTIONAL XY recomputes the noise-heavy heightfield+biome on EVERY
|
||||
// sample. A 12.5 cm grid offset is imperceptible; random yaw/scale still breaks up the regularity.
|
||||
if (OutCounters != nullptr) ++OutCounters->Columns;
|
||||
// INTEGER active-voxel jitter: native Core and legacy routes both stay on their field's column grid.
|
||||
// Gigue entière en voxels actifs : les routes Core et legacy restent sur la grille de colonnes du champ.
|
||||
const uint32 HJ = DecoHash(Cell.X, Cell.Y, gx, gy, -1, 0, InSeed, 0x10C0u);
|
||||
const int32 JX = FMath::Min(Spacing - 1, (int32)(VoxelHash::ToFloat01(HJ) * (float)Spacing));
|
||||
const int32 JY = FMath::Min(Spacing - 1, (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(HJ ^ 0x68BC21EBu)) * (float)Spacing));
|
||||
@@ -1155,7 +1383,9 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
int32 ColBiome = -1;
|
||||
if (bHasBiomes)
|
||||
{
|
||||
const FBiomeSample BS = Gen->ResolveBiomeSampleAt(VX, VY, Ctx.RepChunkZ, Ctx.BiomeCtx, BiomeCache);
|
||||
const FBiomeSample BS = Gen->ResolveBiomeSampleAt(
|
||||
VX * Ctx.ActiveToOwnerVoxelScale, VY * Ctx.ActiveToOwnerVoxelScale,
|
||||
Ctx.RepChunkZ, Ctx.BiomeCtx, BiomeCache);
|
||||
ColBiome = BS.DominantIndex; // index into Ctx.BiomeCtx.Biomes == the entry tag
|
||||
|
||||
// SOFTEN THE BORDER: a hard dominant pick still switches deco sets on a crisp line. In the
|
||||
@@ -1563,7 +1793,8 @@ bool UVoxelContentManager::FindLandmarkColumn(const UVoxelGenerator* Gen, const
|
||||
}
|
||||
return 1.0f;
|
||||
}
|
||||
return Gen->GetDensityAt(SampleX, SampleY, SampleZ);
|
||||
return Gen->GetDensityAt(SampleX * Ctx.ActiveToOwnerVoxelScale,
|
||||
SampleY * Ctx.ActiveToOwnerVoxelScale, SampleZ * Ctx.ActiveToOwnerVoxelScale);
|
||||
};
|
||||
|
||||
if (Ctx.bSurfaceWorld)
|
||||
@@ -1576,8 +1807,17 @@ bool UVoxelContentManager::FindLandmarkColumn(const UVoxelGenerator* Gen, const
|
||||
return Ctx.NativeSurface->GetHeights(
|
||||
Ctx.StrateIndex, SampleX, SampleY, OutGround, OutCap);
|
||||
}
|
||||
return Gen->GetSurfaceHeightAt(
|
||||
SampleX, SampleY, Ctx.RepChunkZ, OutGround, OutCap);
|
||||
float OwnerGround = 0.0f;
|
||||
float OwnerCap = 0.0f;
|
||||
if (!Gen->GetSurfaceHeightAt(SampleX * Ctx.ActiveToOwnerVoxelScale,
|
||||
SampleY * Ctx.ActiveToOwnerVoxelScale, Ctx.RepChunkZ,
|
||||
OwnerGround, OwnerCap))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OutGround = OwnerGround / Ctx.ActiveToOwnerVoxelScale;
|
||||
OutCap = OwnerCap / Ctx.ActiveToOwnerVoxelScale;
|
||||
return true;
|
||||
};
|
||||
if (!GetSurface(VX, VY, hC, cC)) return false;
|
||||
|
||||
@@ -1665,13 +1905,14 @@ bool UVoxelContentManager::SpawnFromProfile(const FPlacementProfile& P, uint32 H
|
||||
FTransform& OutXf, FLandmarkInstance& Out)
|
||||
{
|
||||
if (!Generator) return false;
|
||||
const float VX = LocalX / VOXEL_SIZE;
|
||||
const float VY = LocalY / VOXEL_SIZE;
|
||||
const float VX = LocalX / Ctx.VoxelSizeCm;
|
||||
const float VY = LocalY / Ctx.VoxelSizeCm;
|
||||
|
||||
// Biome filter (resolved at the candidate XY, same field the density/deco paths use).
|
||||
if (P.RequiredBiome)
|
||||
{
|
||||
const UVoxelBiomeDefinition* Bio = Generator->GetDominantBiomeAt(VX, VY, Ctx.RepChunkZ);
|
||||
const UVoxelBiomeDefinition* Bio = Generator->GetDominantBiomeAt(
|
||||
VX * Ctx.ActiveToOwnerVoxelScale, VY * Ctx.ActiveToOwnerVoxelScale, Ctx.RepChunkZ);
|
||||
if (Bio != P.RequiredBiome) return false; // leaves Out empty → evaluated, nothing placed
|
||||
}
|
||||
|
||||
@@ -1685,7 +1926,7 @@ bool UVoxelContentManager::SpawnFromProfile(const FPlacementProfile& P, uint32 H
|
||||
if (P.MinSlopeAngle > 0.01f &&
|
||||
FMath::Abs(N.Z) > VoxelMath::DetCos(FMath::DegreesToRadians(P.MinSlopeAngle))) return false;
|
||||
|
||||
const FVector LocalPos(LocalX, LocalY, ZC * VOXEL_SIZE);
|
||||
const FVector LocalPos(LocalX, LocalY, ZC * Ctx.VoxelSizeCm);
|
||||
if (P.bRequireWaterRelative && Ctx.bHasWater)
|
||||
{
|
||||
const bool bBelowWater = (LocalPos.Z < Ctx.WaterLocalZ);
|
||||
@@ -1695,7 +1936,9 @@ bool UVoxelContentManager::SpawnFromProfile(const FPlacementProfile& P, uint32 H
|
||||
// F7 aware placement: relational conditions (relief/moisture/biome-border), evaluated at the candidate
|
||||
// XY. Opt-in — empty list is free. "A monument only on high mesas / near a biome edge" lives here.
|
||||
if (P.Conditions.Num() > 0 &&
|
||||
!Generator->EvaluateTerrainConditions(P.Conditions, VX, VY, Ctx.BiomeCtx)) return false;
|
||||
!Generator->EvaluateTerrainConditions(P.Conditions,
|
||||
VX * Ctx.ActiveToOwnerVoxelScale, VY * Ctx.ActiveToOwnerVoxelScale,
|
||||
Ctx.BiomeCtx)) return false;
|
||||
|
||||
// Rotation: optional surface-align → fixed offset → per-axis hash random.
|
||||
FQuat Q = P.bAlignToSurface ? FRotationMatrix::MakeFromZ(N).ToQuat() : FQuat::Identity;
|
||||
@@ -1752,7 +1995,7 @@ void UVoxelContentManager::SpawnLandmarkInstance(const FStrateLandmark& L, uint3
|
||||
return; // Out stays empty → evaluated, nothing placed
|
||||
|
||||
// Mini-sun light orb (landmark-only): record world-space data for the terrain material's raymarched
|
||||
// shadows. Distances convert voxels→cm (×VOXEL_SIZE); the emitter radius scales with the instance too.
|
||||
// shadows. Distances convert active voxels→cm; the emitter radius scales with the instance too.
|
||||
if (L.bIsLightOrb)
|
||||
{
|
||||
const float Scale = Xf.GetScale3D().X;
|
||||
@@ -1760,9 +2003,11 @@ void UVoxelContentManager::SpawnLandmarkInstance(const FStrateLandmark& L, uint3
|
||||
Out.Orb.WorldPos = Xf.GetLocation();
|
||||
Out.Orb.Color = L.OrbColor;
|
||||
Out.Orb.Intensity = L.OrbIntensity;
|
||||
Out.Orb.RadiusWorld = L.OrbRadiusVoxels * VOXEL_SIZE * Scale;
|
||||
Out.Orb.FalloffWorld = L.OrbFalloffVoxels * VOXEL_SIZE;
|
||||
Out.Orb.MaxShadowDistWorld = L.OrbMaxShadowDistanceVoxels * VOXEL_SIZE;
|
||||
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
|
||||
const float VoxelSizeCm = VoxelWorld ? VoxelWorld->GetActiveVoxelSizeCm() : VOXEL_SIZE;
|
||||
Out.Orb.RadiusWorld = L.OrbRadiusVoxels * VoxelSizeCm * Scale;
|
||||
Out.Orb.FalloffWorld = L.OrbFalloffVoxels * VoxelSizeCm;
|
||||
Out.Orb.MaxShadowDistWorld = L.OrbMaxShadowDistanceVoxels * VoxelSizeCm;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1849,13 +2094,16 @@ void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
|
||||
LastLandmarkStrate = StrateIndex;
|
||||
}
|
||||
|
||||
const float ChunkWorld = (float)CHUNK_SIZE * VOXEL_SIZE; // one chunk footprint in cm
|
||||
const float VoxelSizeCm = VoxelWorld ? VoxelWorld->GetActiveVoxelSizeCm() : VOXEL_SIZE;
|
||||
const float ChunkWorld = static_cast<float>(CHUNK_SIZE) * VoxelSizeCm;
|
||||
|
||||
// Shared strate context (a strate is a horizontal slab → same everywhere this update).
|
||||
FDecoContext Ctx;
|
||||
Ctx.StrateIndex = StrateIndex;
|
||||
Ctx.TopVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(TopZ) : TopZ) / VOXEL_SIZE;
|
||||
Ctx.BottomVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(BotZ) : BotZ) / VOXEL_SIZE;
|
||||
Ctx.VoxelSizeCm = VoxelSizeCm;
|
||||
Ctx.ActiveToOwnerVoxelScale = VoxelSizeCm / VOXEL_SIZE;
|
||||
Ctx.TopVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(TopZ) : TopZ) / VoxelSizeCm;
|
||||
Ctx.BottomVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(BotZ) : BotZ) / VoxelSizeCm;
|
||||
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);
|
||||
@@ -1971,8 +2219,8 @@ void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
|
||||
|
||||
// Passage endpoints are GLOBAL VOXEL coords (same space as the SDF path) → local cm.
|
||||
const FVector MouthVox = bDescent ? Pg.UpperPoint : Pg.LowerPoint;
|
||||
const float LocalX = MouthVox.X * VOXEL_SIZE;
|
||||
const float LocalY = MouthVox.Y * VOXEL_SIZE;
|
||||
const float LocalX = (MouthVox.X / Ctx.ActiveToOwnerVoxelScale) * Ctx.VoxelSizeCm;
|
||||
const float LocalY = (MouthVox.Y / Ctx.ActiveToOwnerVoxelScale) * Ctx.VoxelSizeCm;
|
||||
|
||||
const float ddx = LocalX - LocalPlayer.X, ddy = LocalY - LocalPlayer.Y;
|
||||
const float DistSq = ddx * ddx + ddy * ddy;
|
||||
@@ -2066,11 +2314,13 @@ void UVoxelContentManager::UpdateWater(const FVector& PlayerWorldPos)
|
||||
}
|
||||
|
||||
// Player chunk (voxel→chunk) for the strate / water-level lookup.
|
||||
const float ChunkWorld = (float)CHUNK_SIZE * VOXEL_SIZE; // 1 chunk footprint in cm
|
||||
const float ManagerChunkWorld = static_cast<float>(CHUNK_SIZE) * VOXEL_SIZE;
|
||||
const float VoxelSizeCm = VoxelWorld ? VoxelWorld->GetActiveVoxelSizeCm() : VOXEL_SIZE;
|
||||
const float ChunkWorld = static_cast<float>(CHUNK_SIZE) * VoxelSizeCm;
|
||||
const FIntVector PlayerChunk(
|
||||
FMath::FloorToInt(ManagerLocalPos.X / ChunkWorld),
|
||||
FMath::FloorToInt(ManagerLocalPos.Y / ChunkWorld),
|
||||
FMath::FloorToInt(ManagerLocalPos.Z / ChunkWorld));
|
||||
FMath::FloorToInt(ManagerLocalPos.X / ManagerChunkWorld),
|
||||
FMath::FloorToInt(ManagerLocalPos.Y / ManagerChunkWorld),
|
||||
FMath::FloorToInt(ManagerLocalPos.Z / ManagerChunkWorld));
|
||||
|
||||
float WaterLocalZ = -FLT_MAX;
|
||||
const int32 StrateIndex = StrateManager->GetStrateIndex(ManagerLocalPos.Z);
|
||||
@@ -2185,7 +2435,9 @@ void UVoxelContentManager::QueryDecoDebugAt(const FVector& LocalPos, bool& bAppl
|
||||
CellsTotal = R * R;
|
||||
|
||||
// World-LOCAL XY → deco cell → region (same math the scatter uses).
|
||||
const float CellWorld = (float)DECO_CELL_VOXELS * VOXEL_SIZE;
|
||||
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
|
||||
const float VoxelSizeCm = VoxelWorld ? VoxelWorld->GetActiveVoxelSizeCm() : VOXEL_SIZE;
|
||||
const float CellWorld = static_cast<float>(DECO_CELL_VOXELS) * VoxelSizeCm;
|
||||
const FIntPoint Cell(FMath::FloorToInt(LocalPos.X / CellWorld),
|
||||
FMath::FloorToInt(LocalPos.Y / CellWorld));
|
||||
const FIntPoint Region = CellToRegion(Cell, R);
|
||||
|
||||
@@ -15,12 +15,14 @@
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelStrateManager.h"
|
||||
#include "VoxelTerrainOpDefinition.h"
|
||||
#include "VoxelTypes.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -28,9 +30,7 @@
|
||||
namespace
|
||||
{
|
||||
using FCoreJsonWriter = TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>;
|
||||
constexpr double CoreVoxelSizeCm = 25.0;
|
||||
constexpr double OwnerVoxelSizeCm = 12.5;
|
||||
constexpr double OwnerToCoreScale = OwnerVoxelSizeCm / CoreVoxelSizeCm;
|
||||
constexpr double OwnerVoxelSizeCm = VOXEL_SIZE;
|
||||
|
||||
std::int64_t WorldAssemblyFixed(double Voxels)
|
||||
{
|
||||
@@ -440,7 +440,7 @@ bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
|
||||
|
||||
bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
|
||||
const FStrateSlot& Slot, VoxelForgeCore::WorldAssemblyManifest& Manifest,
|
||||
FString& OutError)
|
||||
double CoreVoxelSizeCm, FString& OutError)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
const FIntVector Chunk(0, 0, Slot.TopChunkZ);
|
||||
@@ -452,13 +452,28 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
|
||||
auto& Recipe = IslandPlan.manifest.recipe;
|
||||
Recipe.geometryVersion = 2;
|
||||
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
||||
const std::int64_t BottomCoreVoxels = static_cast<std::int64_t>(Slot.BottomChunkZ) * 16;
|
||||
const std::int64_t TopCoreVoxels = static_cast<std::int64_t>(Slot.TopChunkZ + 1) * 16;
|
||||
Recipe.layoutMinimumZSubvoxels = BottomCoreVoxels * WorldAssemblySubvoxelsPerVoxel;
|
||||
Recipe.layoutMaximumZSubvoxels = TopCoreVoxels * WorldAssemblySubvoxelsPerVoxel;
|
||||
Recipe.layoutCenterTopZSubvoxels = (BottomCoreVoxels + TopCoreVoxels)
|
||||
* WorldAssemblySubvoxelsPerVoxel / 2;
|
||||
Recipe.ownerVoxelSizeMicrons = static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0));
|
||||
Recipe.coreVoxelSizeMicrons = static_cast<std::uint32_t>(std::llround(CoreVoxelSizeCm * 10000.0));
|
||||
const std::uint32_t ScaleGcd = std::gcd(
|
||||
Recipe.ownerVoxelSizeMicrons, Recipe.coreVoxelSizeMicrons);
|
||||
if (ScaleGcd == 0)
|
||||
{
|
||||
OutError = TEXT("Core voxel size cannot be represented in the floating-island scale.");
|
||||
return false;
|
||||
}
|
||||
Recipe.ownerToCoreScaleNumerator = Recipe.ownerVoxelSizeMicrons / ScaleGcd;
|
||||
Recipe.ownerToCoreScaleDenominator = Recipe.coreVoxelSizeMicrons / ScaleGcd;
|
||||
const double CoreVoxelsPerOwnerChunk = static_cast<double>(CHUNK_SIZE)
|
||||
* Recipe.ownerToCoreScaleNumerator / Recipe.ownerToCoreScaleDenominator;
|
||||
Recipe.layoutMinimumZSubvoxels = WorldAssemblyFixed(
|
||||
static_cast<double>(Slot.BottomChunkZ) * CoreVoxelsPerOwnerChunk);
|
||||
Recipe.layoutMaximumZSubvoxels = WorldAssemblyFixed(
|
||||
static_cast<double>(Slot.TopChunkZ + 1) * CoreVoxelsPerOwnerChunk);
|
||||
Recipe.layoutCenterTopZSubvoxels = (Recipe.layoutMinimumZSubvoxels
|
||||
+ Recipe.layoutMaximumZSubvoxels) / 2;
|
||||
Recipe.islandDensityQ16 = WorldAssemblyQ16(FMath::Clamp(Params.IslandDensity, 0.0f, 1.0f));
|
||||
const double OwnerToCoreScale = static_cast<double>(Recipe.ownerToCoreScaleNumerator)
|
||||
/ Recipe.ownerToCoreScaleDenominator;
|
||||
Recipe.spacingSubvoxels = WorldAssemblyFixed(Params.IslandSpacing * OwnerToCoreScale);
|
||||
Recipe.minimumRadiusSubvoxels = WorldAssemblyFixed(Params.IslandMinRadius * OwnerToCoreScale);
|
||||
Recipe.maximumRadiusSubvoxels = WorldAssemblyFixed(Params.IslandMaxRadius * OwnerToCoreScale);
|
||||
@@ -739,6 +754,7 @@ void WriteCoreJsonValue(FCoreJsonWriter& Writer, const TCHAR* Identifier,
|
||||
|
||||
bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
const UVoxelStrateManager* StrateManager,
|
||||
double CoreVoxelSizeCm,
|
||||
const TSharedRef<FJsonObject>& Root,
|
||||
FString& OutError)
|
||||
{
|
||||
@@ -812,7 +828,8 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
}
|
||||
else if (Type == ECaveGeneratorType::FloatingIslands)
|
||||
{
|
||||
if (!BuildFloatingIslandStratumPlan(StrateManager, Slot, Generated, OutError))
|
||||
if (!BuildFloatingIslandStratumPlan(StrateManager, Slot, Generated,
|
||||
CoreVoxelSizeCm, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -847,6 +864,17 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::uint32_t ExpectedCoreVoxelMicrons = static_cast<std::uint32_t>(
|
||||
std::llround(CoreVoxelSizeCm * 10000.0));
|
||||
for (const auto& Island : Manifest->floatingIslands)
|
||||
{
|
||||
if (Island.manifest.recipe.coreVoxelSizeMicrons != ExpectedCoreVoxelMicrons)
|
||||
{
|
||||
OutError = TEXT("floating-island override uses a different Core voxel size; regenerate the plan.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int32 SurfaceSlotCount = 0;
|
||||
int32 FloatingIslandSlotCount = 0;
|
||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||
@@ -947,11 +975,21 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
const UVoxelStrateManager* StrateManager,
|
||||
int32 ClipRadius,
|
||||
int32 MaxClipLevel,
|
||||
float CoreVoxelSizeCm,
|
||||
FString& OutJson,
|
||||
FString& OutError)
|
||||
{
|
||||
OutJson.Reset();
|
||||
OutError.Reset();
|
||||
if (!FMath::IsFinite(CoreVoxelSizeCm) || CoreVoxelSizeCm < VOXEL_SIZE
|
||||
|| CoreVoxelSizeCm > 2.0f * VOXEL_SIZE)
|
||||
{
|
||||
OutError = TEXT("Core voxel size must be between one and two legacy voxel widths.");
|
||||
return false;
|
||||
}
|
||||
const std::uint32_t CoreVoxelSizeMicrons = static_cast<std::uint32_t>(
|
||||
std::llround(static_cast<double>(CoreVoxelSizeCm) * 10000.0));
|
||||
const double ResolvedCoreVoxelSizeCm = static_cast<double>(CoreVoxelSizeMicrons) / 10000.0;
|
||||
if (Settings == nullptr || StrateManager == nullptr || StrateManager->GetLayout().IsEmpty())
|
||||
{
|
||||
OutError = TEXT("Core snapshot export requires initialized settings and a resolved strate layout.");
|
||||
@@ -979,10 +1017,14 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
Root->SetNumberField(TEXT("schema_version"), 1);
|
||||
Root->SetStringField(TEXT("source"), TEXT("VoxelForge Runtime Core snapshot"));
|
||||
Root->SetNumberField(TEXT("voxel_size_cm"), CoreVoxelSizeCm);
|
||||
Root->SetNumberField(TEXT("voxel_size_cm"), ResolvedCoreVoxelSizeCm);
|
||||
Root->SetNumberField(TEXT("owner_voxel_size_cm"), OwnerVoxelSizeCm);
|
||||
Root->SetNumberField(TEXT("core_voxel_size_cm"), CoreVoxelSizeCm);
|
||||
Root->SetNumberField(TEXT("owner_to_core_length_scale"), OwnerVoxelSizeCm / CoreVoxelSizeCm);
|
||||
Root->SetNumberField(TEXT("owner_voxel_size_microns"),
|
||||
static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0)));
|
||||
Root->SetNumberField(TEXT("core_voxel_size_cm"), ResolvedCoreVoxelSizeCm);
|
||||
Root->SetNumberField(TEXT("core_voxel_size_microns"), CoreVoxelSizeMicrons);
|
||||
Root->SetNumberField(TEXT("owner_to_core_length_scale"),
|
||||
OwnerVoxelSizeCm / ResolvedCoreVoxelSizeCm);
|
||||
Root->SetNumberField(TEXT("seed"), Seed);
|
||||
Root->SetBoolField(TEXT("open_surface_entry"), Settings->bOpenSurfaceEntry);
|
||||
Root->SetNumberField(TEXT("origin_spine_radius"), OriginSpineRadius);
|
||||
@@ -1052,7 +1094,7 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
Passages.Add(MakeShared<FJsonValueObject>(CorePassageJson(Passage)));
|
||||
}
|
||||
Root->SetArrayField(TEXT("passages"), MoveTemp(Passages));
|
||||
if (!AttachValidatedWorldAssembly(Settings, StrateManager, Root, OutError))
|
||||
if (!AttachValidatedWorldAssembly(Settings, StrateManager, ResolvedCoreVoxelSizeCm, Root, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7176,7 +7176,10 @@ FVector AVoxelWorld::WorldToLocalCm(FVector WorldPos) const
|
||||
|
||||
float AVoxelWorld::GetActiveVoxelSizeCm() const
|
||||
{
|
||||
return bUseVoxelForgeCore ? 25.0f : VOXEL_SIZE;
|
||||
if (!bUseVoxelForgeCore) return VOXEL_SIZE;
|
||||
const float Requested = FMath::IsFinite(CoreVoxelSizeCm) ? CoreVoxelSizeCm : 2.0f * VOXEL_SIZE;
|
||||
const float Clamped = FMath::Clamp(Requested, VOXEL_SIZE, 2.0f * VOXEL_SIZE);
|
||||
return static_cast<float>(FMath::RoundToInt(Clamped * 10000.0f)) / 10000.0f;
|
||||
}
|
||||
|
||||
float AVoxelWorld::GetLegacyVoxelToActiveVoxelScale() const
|
||||
@@ -7260,10 +7263,12 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
|
||||
const int32 ActiveCoreClipRadius = FMath::Clamp(CoreClipRadius, 1, 64);
|
||||
const int32 ActiveCoreMaxClipLevel = FMath::Clamp(CoreMaxClipLevel, 0, 8);
|
||||
const float ActiveCoreVoxelSizeCm = GetActiveVoxelSizeCm();
|
||||
FString Json;
|
||||
FString ExportError;
|
||||
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
Settings, StrateManager, ActiveCoreClipRadius, ActiveCoreMaxClipLevel, Json, ExportError))
|
||||
Settings, StrateManager, ActiveCoreClipRadius, ActiveCoreMaxClipLevel,
|
||||
ActiveCoreVoxelSizeCm, Json, ExportError))
|
||||
{
|
||||
OutFailureReason = FString::Printf(TEXT("snapshot export failed: %s"), *ExportError);
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] %s"), *OutFailureReason);
|
||||
@@ -7310,14 +7315,14 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
FVector InitialPlayerPosition = FVector::ZeroVector;
|
||||
if (TryGetPlayerPosition(InitialPlayerPosition))
|
||||
{
|
||||
CameraVoxels = WorldToLocalCm(InitialPlayerPosition) / 25.0f;
|
||||
CameraVoxels = WorldToLocalCm(InitialPlayerPosition) / GetActiveVoxelSizeCm();
|
||||
}
|
||||
else if (State->World->configuration().viewerStartFound)
|
||||
{
|
||||
const VoxelForgeCore::Vec3d Start = State->World->configuration().viewerStartVoxels;
|
||||
CameraVoxels = FVector(Start.x, Start.y, Start.z);
|
||||
}
|
||||
CoreFallbackCenterLocalCm = CameraVoxels * 25.0f;
|
||||
CoreFallbackCenterLocalCm = CameraVoxels * GetActiveVoxelSizeCm();
|
||||
|
||||
const std::vector<VoxelForgeCore::TileKey> InitialDesired = State->World->desiredTiles(
|
||||
{CameraVoxels.X, CameraVoxels.Y, CameraVoxels.Z});
|
||||
@@ -7372,7 +7377,8 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
VoxelForgeCoreState = State;
|
||||
ActiveTaskCount.fetch_add(1, std::memory_order_relaxed);
|
||||
UE::Tasks::Launch(TEXT("VoxelForgeCoreCompile"),
|
||||
[this, State, Request, InitialDesired, ActiveCoreClipRadius, ActiveCoreMaxClipLevel]()
|
||||
[this, State, Request, InitialDesired, ActiveCoreClipRadius, ActiveCoreMaxClipLevel,
|
||||
ActiveCoreVoxelSizeCm]()
|
||||
{
|
||||
struct FTaskGuard
|
||||
{
|
||||
@@ -7413,9 +7419,9 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
State->bReady.store(true, std::memory_order_release);
|
||||
State->bRebuildDesired.store(true, std::memory_order_release);
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreRuntime] compiled=1 tiles=%d clip_radius=%d max_level=%d voxel_cm=25"),
|
||||
TEXT("[VoxelForgeCoreRuntime] compiled=1 tiles=%d clip_radius=%d max_level=%d voxel_cm=%.4f"),
|
||||
static_cast<int32>(InitialDesired.size()), ActiveCoreClipRadius,
|
||||
ActiveCoreMaxClipLevel);
|
||||
ActiveCoreMaxClipLevel, ActiveCoreVoxelSizeCm);
|
||||
}, UE::Tasks::ETaskPriority::BackgroundNormal);
|
||||
|
||||
UE_LOG(LogTemp, Display,
|
||||
@@ -9866,7 +9872,7 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector&
|
||||
const TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State = VoxelForgeCoreState;
|
||||
if (!State.IsValid() || !State->bReady.load(std::memory_order_acquire)) return;
|
||||
|
||||
const FVector CameraVoxels = WorldToLocalCm(PlayerPosition) / 25.0f;
|
||||
const FVector CameraVoxels = WorldToLocalCm(PlayerPosition) / GetActiveVoxelSizeCm();
|
||||
std::vector<VoxelForgeCore::TileKey> CoreDesired;
|
||||
{
|
||||
FReadScopeLock Lock(State->WorldLock);
|
||||
@@ -12291,16 +12297,27 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
||||
// the cap is by definition the topmost surface in the tile (mid as a gap fallback).
|
||||
// This was the "far cap renders with the ground material" residue: the min-corner lookup
|
||||
// missed the surface strate entirely on tall far tiles.
|
||||
// Core's 25 cm voxel is two legacy 12.5 cm voxels; map only the lookup into the existing
|
||||
// strate material layout, while keeping tile keys and mesh coordinates in Core units.
|
||||
// Le voxel Core de 25 cm vaut deux anciens voxels de 12,5 cm ; seule la recherche de
|
||||
// matériau est convertie vers le layout existant, les clés et maillages restent en Core.
|
||||
const int32 CoreToStrateChunkScale = bUseVoxelForgeCore
|
||||
? FMath::RoundToInt(GetActiveVoxelSizeCm() / VOXEL_SIZE) : 1;
|
||||
const FIntVector MinChunk = Tile.Coord * ((1 << Tile.Level) * CoreToStrateChunkScale);
|
||||
const int32 TileChunks = (1 << Tile.Level) * CoreToStrateChunkScale;
|
||||
const int32 ZLoC = FMath::Max(MinChunk.Z, Result.BandChunkLo);
|
||||
const int32 ZHiC = FMath::Min(MinChunk.Z + TileChunks - 1, Result.BandChunkHi);
|
||||
// Map the physical tile bounds into the existing owner stride layout. Fractional ratios
|
||||
// are valid (for example, 20 cm Core voxels span 1.6 legacy voxels each).
|
||||
// Convertir les bornes physiques de tuile vers les strates owner, y compris les rapports fractionnaires.
|
||||
const double OwnerChunksPerCoreTile = static_cast<double>(GetActiveVoxelSizeCm())
|
||||
/ VOXEL_SIZE;
|
||||
const double TileExtentCoreVoxels = static_cast<double>(CHUNK_SIZE << Tile.Level);
|
||||
const int32 MinOwnerChunkX = FMath::FloorToInt(
|
||||
Tile.Coord.X * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE);
|
||||
const int32 MinOwnerChunkY = FMath::FloorToInt(
|
||||
Tile.Coord.Y * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE);
|
||||
const int32 MinOwnerChunkZ = FMath::FloorToInt(
|
||||
Tile.Coord.Z * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE);
|
||||
const int32 MaxOwnerChunkX = FMath::CeilToInt(
|
||||
(Tile.Coord.X + 1.0) * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE) - 1;
|
||||
const int32 MaxOwnerChunkY = FMath::CeilToInt(
|
||||
(Tile.Coord.Y + 1.0) * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE) - 1;
|
||||
const int32 MaxOwnerChunkZ = FMath::CeilToInt(
|
||||
(Tile.Coord.Z + 1.0) * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE) - 1;
|
||||
const FIntVector MinChunk(MinOwnerChunkX, MinOwnerChunkY, MinOwnerChunkZ);
|
||||
const int32 ZLoC = FMath::Max(MinOwnerChunkZ, Result.BandChunkLo);
|
||||
const int32 ZHiC = FMath::Min(MaxOwnerChunkZ, Result.BandChunkHi);
|
||||
if (UVoxelStrateDefinition* StrateDef =
|
||||
StrateManager->GetStrateForChunk(FIntVector(MinChunk.X, MinChunk.Y, ZLoC)))
|
||||
{
|
||||
@@ -12631,9 +12648,10 @@ bool AVoxelWorld::GetVoxelSurfaceHeightAt(FVector WorldLocation, float& OutSurfa
|
||||
if (!State.IsValid()) return false;
|
||||
|
||||
const FVector LocalCm = WorldToLocalCm(WorldLocation);
|
||||
const double XVoxel = LocalCm.X / 25.0;
|
||||
const double YVoxel = LocalCm.Y / 25.0;
|
||||
const double ZVoxel = LocalCm.Z / 25.0;
|
||||
const double CoreVoxelSize = GetActiveVoxelSizeCm();
|
||||
const double XVoxel = LocalCm.X / CoreVoxelSize;
|
||||
const double YVoxel = LocalCm.Y / CoreVoxelSize;
|
||||
const double ZVoxel = LocalCm.Z / CoreVoxelSize;
|
||||
FReadScopeLock Lock(State->WorldLock);
|
||||
if (!State->World) return false;
|
||||
|
||||
@@ -12684,12 +12702,12 @@ bool AVoxelWorld::GetVoxelSurfaceHeightAt(FVector WorldLocation, float& OutSurfa
|
||||
if (bFoundGround)
|
||||
{
|
||||
OutSurfaceWorldZ = GetActorTransform().TransformPosition(
|
||||
FVector(LocalCm.X, LocalCm.Y, GroundVoxelZ * 25.0)).Z;
|
||||
FVector(LocalCm.X, LocalCm.Y, GroundVoxelZ * CoreVoxelSize)).Z;
|
||||
}
|
||||
if (bFoundCeiling)
|
||||
{
|
||||
OutCeilingWorldZ = GetActorTransform().TransformPosition(
|
||||
FVector(LocalCm.X, LocalCm.Y, CeilingVoxelZ * 25.0)).Z;
|
||||
FVector(LocalCm.X, LocalCm.Y, CeilingVoxelZ * CoreVoxelSize)).Z;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -13384,7 +13402,8 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
FString SnapshotJson;
|
||||
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(Settings, StrateManager,
|
||||
FMath::Clamp(CoreClipRadius, 1, 64),
|
||||
FMath::Clamp(CoreMaxClipLevel, 0, 8), SnapshotJson, AssemblyError))
|
||||
FMath::Clamp(CoreMaxClipLevel, 0, 8), GetActiveVoxelSizeCm(),
|
||||
SnapshotJson, AssemblyError))
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
@@ -13406,7 +13425,9 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
}
|
||||
}
|
||||
const bool bHybridMesher = CoreMesherRecipe != EVoxelForgeCoreMesherRecipe::MarchingCubesV1;
|
||||
if (AssemblyHash.IsEmpty() && !bHybridMesher)
|
||||
const uint32 CoreVoxelSizeMicrons = static_cast<uint32>(
|
||||
FMath::RoundToInt(GetActiveVoxelSizeCm() * 10000.0f));
|
||||
if (AssemblyHash.IsEmpty() && !bHybridMesher && CoreVoxelSizeMicrons == 250000)
|
||||
{
|
||||
return SeasonHash;
|
||||
}
|
||||
@@ -13431,6 +13452,10 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
Payload += TEXT("|world_assembly=");
|
||||
Payload += AssemblyHash;
|
||||
}
|
||||
if (CoreVoxelSizeMicrons != 250000)
|
||||
{
|
||||
Payload += FString::Printf(TEXT("|core_voxel_size_microns=%u"), CoreVoxelSizeMicrons);
|
||||
}
|
||||
if (CoreMesherRecipe == EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV1)
|
||||
{
|
||||
Payload += TEXT("|core_mesher=WorldAssemblerHybridV1");
|
||||
|
||||
@@ -120,6 +120,32 @@ public:
|
||||
* launch async march tasks (capped), and apply finished results budgeted. Call every Tick. */
|
||||
void UpdateDecorations(const FVector& PlayerWorldPos);
|
||||
|
||||
/** Headless audit of worker-cell and steady UpdateDecorations cost using the active world route.
|
||||
* Audit sans rendu du coût par cellule worker et de UpdateDecorations avec la route active. */
|
||||
struct FDecorationPlacementCostAudit
|
||||
{
|
||||
bool bNativeSurface = false;
|
||||
bool bNativeFloatingIslands = false;
|
||||
bool bSyntheticWorkload = false;
|
||||
int32 NearEntries = 0;
|
||||
int32 FarEntries = 0;
|
||||
int32 SampledCells = 0;
|
||||
int32 ColumnsPerCell = 0;
|
||||
int32 Placements = 0;
|
||||
int32 AnchorChecks = 0;
|
||||
double MaximumAnchorErrorCm = 0.0;
|
||||
double MaximumDensityResidual = 0.0;
|
||||
double DensityQueriesPerCell = 0.0;
|
||||
double SurfaceQueriesPerCell = 0.0;
|
||||
double CellMedianMilliseconds = 0.0;
|
||||
double CellP95Milliseconds = 0.0;
|
||||
double UpdateMedianMilliseconds = 0.0;
|
||||
double UpdateP95Milliseconds = 0.0;
|
||||
};
|
||||
bool MeasurePlacementCostForValidation(const FVector& PlayerWorldPos,
|
||||
int32 SampleCellCount, int32 FrameSampleCount,
|
||||
FDecorationPlacementCostAudit& OutAudit, FString& OutError);
|
||||
|
||||
//--- LANDMARKS (rare large objects on a coarse hash lattice — the "mini-suns") -----------
|
||||
/** Stream rare landmark objects around the player. Unlike decorations, these sit on a COARSE hash
|
||||
* lattice (cell = `FStrateLandmark::SpacingChunks` chunks), so cost scales with the number of
|
||||
@@ -176,6 +202,12 @@ public:
|
||||
bool bInstanced = false;
|
||||
FTransform Xf = FTransform::Identity;
|
||||
};
|
||||
struct FPlacementBuildCounters
|
||||
{
|
||||
int32 Columns = 0;
|
||||
int32 DensityQueries = 0;
|
||||
int32 SurfaceQueries = 0;
|
||||
};
|
||||
/** A finished cell march: a snapshot of the resolved decoration list + the spawn commands. */
|
||||
struct FDecoCellResult
|
||||
{
|
||||
@@ -259,6 +291,8 @@ private:
|
||||
const UVoxelStrateDefinition* Def = nullptr; // game-thread only (biome/decoration resolve)
|
||||
int32 StrateIndex = INDEX_NONE;
|
||||
int32 RepChunkZ = 0; // representative chunk-Z for biome lookups
|
||||
float VoxelSizeCm = VOXEL_SIZE; // active field voxel size: Core when enabled, legacy otherwise
|
||||
float ActiveToOwnerVoxelScale = 1.0f; // map active Core units to the legacy generator's voxel units
|
||||
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)
|
||||
@@ -303,7 +337,8 @@ private:
|
||||
const TArray<FStrateDecoration>& Entries,
|
||||
const TArray<int32>& EntryBiome, uint32 InSeed,
|
||||
int32 Spacing, float Step, int32 MaxCrossings, float ColumnDepth,
|
||||
TArray<FDecoSpawn>& OutSpawns);
|
||||
TArray<FDecoSpawn>& OutSpawns,
|
||||
FPlacementBuildCounters* OutCounters = nullptr);
|
||||
|
||||
// Each step operates on ONE grid (G = NearGrid or FarGrid). LaunchDecoTasks throttles against the
|
||||
// COMBINED in-flight count (OtherInFlight = the other grid's in-flight cells) so the two grids share
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace VoxelForgeCoreAdapter
|
||||
const UVoxelStrateManager* StrateManager,
|
||||
int32 ClipRadius,
|
||||
int32 MaxClipLevel,
|
||||
float CoreVoxelSizeCm,
|
||||
FString& OutJson,
|
||||
FString& OutError);
|
||||
}
|
||||
@@ -284,6 +284,12 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (DisplayName = "Core Mesher Recipe"))
|
||||
EVoxelForgeCoreMesherRecipe CoreMesherRecipe = EVoxelForgeCoreMesherRecipe::MarchingCubesV1;
|
||||
|
||||
/** Physical Core voxel size in centimeters. Tile coordinates remain 32 voxels per tile.
|
||||
* Taille physique d'un voxel Core en centimetres; chaque tuile contient 32 voxels. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core",
|
||||
meta = (DisplayName = "Core Voxel Size (cm)", ClampMin = "12.5", ClampMax = "25.0"))
|
||||
float CoreVoxelSizeCm = 25.0f;
|
||||
|
||||
/** Core-only clipmap radius, in level-0 Core tiles. Does not read Settings->ClipRadius. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (ClampMin = "1", ClampMax = "64"))
|
||||
int32 CoreClipRadius = 3;
|
||||
@@ -440,10 +446,10 @@ public:
|
||||
* Applies a spherical brush that subtracts density.
|
||||
*
|
||||
* @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 - Legacy Runtime density offset; Core scales it to 25 cm units.
|
||||
* Offset de densite Runtime historique; Core le convertit en unite de 25 cm.
|
||||
* @param Radius - Brush radius in legacy Runtime voxels; Core converts it using its configured
|
||||
* voxel size to preserve the physical brush size.
|
||||
* @param Strength - Legacy Runtime density offset; Core scales it to its configured size.
|
||||
* Offset de densite Runtime historique; Core le convertit selon sa resolution.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||
void CarveAtPosition(FVector Position, float Radius = 6.0f, float Strength = 10.0f);
|
||||
@@ -453,19 +459,19 @@ public:
|
||||
* Applies a spherical brush that adds density.
|
||||
*
|
||||
* @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 - Legacy Runtime density offset; Core scales it to 25 cm units.
|
||||
* Offset de densite Runtime historique; Core le convertit en unite de 25 cm.
|
||||
* @param Radius - Brush radius in legacy Runtime voxels; Core converts it using its configured
|
||||
* voxel size to preserve the physical brush size.
|
||||
* @param Strength - Legacy Runtime density offset; Core scales it to its configured size.
|
||||
* Offset de densite Runtime historique; Core le convertit selon sa resolution.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||
void FillAtPosition(FVector Position, float Radius = 6.0f, float Strength = 10.0f);
|
||||
|
||||
/**
|
||||
* 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 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.
|
||||
* box half-size in legacy Runtime voxels. Core converts it using its configured voxel size.
|
||||
* Strength is a legacy-voxel density offset; Core scales it and forces its sign.
|
||||
* La force est un decalage de densite historique; Core la convertit selon sa resolution et impose le signe.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||
void CarveBox(FVector Position, FVector ExtentVoxels, float Strength = 12.0f);
|
||||
@@ -475,8 +481,8 @@ public:
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* use legacy Runtime voxels; Core converts both using its configured voxel size.
|
||||
* Creuse/remplit une capsule entre deux points monde; rayon et decalage historiques suivent la resolution Core.
|
||||
* Great for boring tunnels or laying solid pillars/walls.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||
@@ -487,7 +493,7 @@ public:
|
||||
|
||||
/**
|
||||
* Apply a fully-specified modification (any shape). Centers/endpoints and brush dimensions
|
||||
* are expected in active voxel coordinates here (12.5 cm legacy, 25 cm Core); high-level
|
||||
* are expected in active voxel coordinates here (legacy or configured Core size); high-level
|
||||
* helpers convert legacy brush sizes to preserve physical dimensions.
|
||||
* Returns nothing; re-meshes affected chunks.
|
||||
*/
|
||||
@@ -593,7 +599,7 @@ 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.
|
||||
// Convert actor-local physical Z to/from UVoxelStrateManager's legacy coordinate space.
|
||||
// Convertit Z local physique vers/depuis l'espace historique de UVoxelStrateManager.
|
||||
float ToStrateManagerLocalZ(float LocalZCm) const;
|
||||
float FromStrateManagerLocalZ(float ManagerLocalZCm) const;
|
||||
|
||||
@@ -337,12 +337,13 @@ namespace
|
||||
|
||||
const int32 LOD = static_cast<int32>(Mesh.key.lod);
|
||||
const VoxelForgeCore::WorldConfiguration& Configuration = World.configuration();
|
||||
const double CoreVoxelSizeCm = static_cast<double>(Configuration.voxelSizeMicrons) / 10000.0;
|
||||
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;
|
||||
const double CellWorldCm = CoreVoxelSizeCm * 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;
|
||||
@@ -488,13 +489,13 @@ namespace
|
||||
// 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};
|
||||
Vertex.position.x / CoreVoxelSizeCm + NX * NormalOffsetVoxels,
|
||||
Vertex.position.y / CoreVoxelSizeCm + NY * NormalOffsetVoxels,
|
||||
Vertex.position.z / CoreVoxelSizeCm + 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};
|
||||
Vertex.position.x / CoreVoxelSizeCm - NX * NormalOffsetVoxels,
|
||||
Vertex.position.y / CoreVoxelSizeCm - NY * NormalOffsetVoxels,
|
||||
Vertex.position.z / CoreVoxelSizeCm - NZ * NormalOffsetVoxels};
|
||||
const float PositiveDensity = World.densityAt(Positive);
|
||||
const float NegativeDensity = World.densityAt(Negative);
|
||||
if (PositiveDensity + 0.01f < NegativeDensity)
|
||||
@@ -520,7 +521,7 @@ namespace
|
||||
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.ExpectedTileSpanCm = static_cast<double>(ExtentVoxels) * CoreVoxelSizeCm;
|
||||
OutAudit.CellWorldCm = CellWorldCm;
|
||||
OutAudit.SpanXcm = MaxX - MinX;
|
||||
OutAudit.SpanYcm = MaxY - MinY;
|
||||
@@ -798,6 +799,7 @@ struct FExploreArguments
|
||||
FString SettingsReference;
|
||||
FString CoreWorldAssemblyManifestPath;
|
||||
int32 CoreHybridMesherVersion = 0;
|
||||
float CoreVoxelSizeCm = 25.0f;
|
||||
bool bCoreHybridMesher = false;
|
||||
bool bCoreHybridMesherV2 = false;
|
||||
bool bCoreHybridFixturePlacement = false;
|
||||
@@ -921,6 +923,14 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
int32 CoreWorldTest = 0;
|
||||
FParse::Value(*Params, TEXT("coreworldtest="), CoreWorldTest);
|
||||
OutArguments.bCoreWorldTest = CoreWorldTest != 0;
|
||||
FParse::Value(*Params, TEXT("corevoxelsizecm="), OutArguments.CoreVoxelSizeCm);
|
||||
if (!FMath::IsFinite(OutArguments.CoreVoxelSizeCm)
|
||||
|| OutArguments.CoreVoxelSizeCm < VOXEL_SIZE
|
||||
|| OutArguments.CoreVoxelSizeCm > 2.0f * VOXEL_SIZE)
|
||||
{
|
||||
OutError = TEXT("corevoxelsizecm must be between one and two legacy voxel widths.");
|
||||
return false;
|
||||
}
|
||||
int32 CoreFloatingIslandTest = 0;
|
||||
FParse::Value(*Params, TEXT("coreislandtest="), CoreFloatingIslandTest);
|
||||
OutArguments.bCoreFloatingIslandTest = CoreFloatingIslandTest != 0;
|
||||
@@ -14909,9 +14919,9 @@ bool RunCorePlanOnlyTest(const FExploreArguments& Arguments, FString& OutError)
|
||||
FString RuntimeJson;
|
||||
FString RepeatJson;
|
||||
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
PlanSettings, PlanManager.Get(), 3, 8, RuntimeJson, OutError)
|
||||
PlanSettings, PlanManager.Get(), 3, 8, Arguments.CoreVoxelSizeCm, RuntimeJson, OutError)
|
||||
|| !VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
PlanSettings, PlanManager.Get(), 3, 8, RepeatJson, OutError))
|
||||
PlanSettings, PlanManager.Get(), 3, 8, Arguments.CoreVoxelSizeCm, RepeatJson, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -15137,6 +15147,9 @@ bool RunCoreOffActorTest(const FExploreArguments& Arguments, FString& OutError)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RunCoreContentPlacementCostAudit(AVoxelWorld* Actor, const FVector& CoreLocalProbeCm,
|
||||
const TCHAR* Label, FString& OutError);
|
||||
|
||||
bool RunCoreFloatingIslandActorIntegrationTest(
|
||||
const FExploreArguments& Arguments, FString& OutError)
|
||||
{
|
||||
@@ -15237,7 +15250,7 @@ bool RunCoreFloatingIslandActorIntegrationTest(
|
||||
|
||||
FString RuntimeJson;
|
||||
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
TestSettings.Get(), Manager.Get(), 3, 8, RuntimeJson, OutError))
|
||||
TestSettings.Get(), Manager.Get(), 3, 8, Arguments.CoreVoxelSizeCm, RuntimeJson, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -15287,18 +15300,19 @@ bool RunCoreFloatingIslandActorIntegrationTest(
|
||||
const VoxelForgeCore::WorldAssemblyRoutePointV1& StartRoute =
|
||||
NativeIslandPlan.manifest.plan.route.front();
|
||||
constexpr double SubvoxelsPerCoreVoxel = 256.0;
|
||||
constexpr double CoreVoxelSizeCm = 25.0;
|
||||
const double CoreVoxelSizeCm = Arguments.CoreVoxelSizeCm;
|
||||
const FVector CameraVoxels(
|
||||
static_cast<double>(StartRoute.positionSubvoxels[0]) / SubvoxelsPerCoreVoxel,
|
||||
static_cast<double>(StartRoute.positionSubvoxels[1]) / SubvoxelsPerCoreVoxel,
|
||||
static_cast<double>(StartRoute.positionSubvoxels[2]) / SubvoxelsPerCoreVoxel);
|
||||
const FVector CameraLocalCm = CameraVoxels * CoreVoxelSizeCm;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreIslandTest] plan_generated=PASS hash=%016llx recipe_hash=%016llx plan_hash=%016llx seed=%d strate=%d owner_bounds=[%d,%d] scale_owner_cm=12.5 scale_core_cm=25 islands=%d connectors=%d route_segments=%llu route_clearance_subvoxels=%lld water_fixed=%d"),
|
||||
TEXT("[VoxelForgeCoreIslandTest] plan_generated=PASS hash=%016llx recipe_hash=%016llx plan_hash=%016llx seed=%d strate=%d owner_bounds=[%d,%d] scale_owner_cm=%.3f scale_core_cm=%.3f islands=%d connectors=%d route_segments=%llu route_clearance_subvoxels=%lld water_fixed=%d"),
|
||||
static_cast<unsigned long long>(NativeAssembly.contentHash),
|
||||
static_cast<unsigned long long>(NativeIslandPlan.manifest.recipe.contentHash),
|
||||
static_cast<unsigned long long>(NativeIslandPlan.manifest.plan.contentHash),
|
||||
EffectiveSeed, IslandStrateIndex, IslandSlot->TopChunkZ, IslandSlot->BottomChunkZ,
|
||||
static_cast<double>(VOXEL_SIZE), CoreVoxelSizeCm,
|
||||
static_cast<int32>(NativeIslandPlan.manifest.plan.islands.size()),
|
||||
static_cast<int32>(NativeIslandPlan.manifest.plan.clusterEdges.size()),
|
||||
static_cast<unsigned long long>(RouteAudit.segmentsTested),
|
||||
@@ -15740,6 +15754,7 @@ bool RunCoreFloatingIslandActorIntegrationTest(
|
||||
Actor->Settings = TestSettings.Get();
|
||||
Actor->bManageAtmosphere = true;
|
||||
Actor->bUseVoxelForgeCore = true;
|
||||
Actor->CoreVoxelSizeCm = Arguments.CoreVoxelSizeCm;
|
||||
Actor->CoreMesherRecipe = EVoxelForgeCoreMesherRecipe::MarchingCubesV1;
|
||||
Actor->CoreClipRadius = 3;
|
||||
Actor->CoreMaxClipLevel = 8;
|
||||
@@ -15763,7 +15778,8 @@ bool RunCoreFloatingIslandActorIntegrationTest(
|
||||
FString ActorRuntimeJson;
|
||||
FString ActorRuntimeError;
|
||||
const bool bActorRuntimeSerialized = VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
Actor->Settings, Actor->StrateManager, 3, 8, ActorRuntimeJson, ActorRuntimeError);
|
||||
Actor->Settings, Actor->StrateManager, 3, 8, Actor->CoreVoxelSizeCm,
|
||||
ActorRuntimeJson, ActorRuntimeError);
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreIslandTest] world_initialized=1 core=1 actor_initialized=%d player=(%.1f,%.1f,%.1f) possessed=(%.1f,%.1f,%.1f) snapshot_parity=%d expected_bytes=%d actor_bytes=%d"),
|
||||
Actor->IsActorInitialized() ? 1 : 0,
|
||||
@@ -16020,23 +16036,23 @@ bool RunCoreFloatingIslandActorIntegrationTest(
|
||||
Actor->CollisionReadyTiles.Num(), Actor->CollisionNotRequiredTiles.Num(),
|
||||
Lod0Tiles, Lod1Tiles, TransitionFaces);
|
||||
|
||||
const auto LocalPointFromSubvoxels = [](const std::int64_t Position[3])
|
||||
const auto LocalPointFromSubvoxels = [CoreVoxelSizeCm](const std::int64_t Position[3])
|
||||
{
|
||||
return FVector(
|
||||
static_cast<double>(Position[0]) * 25.0 / 256.0,
|
||||
static_cast<double>(Position[1]) * 25.0 / 256.0,
|
||||
static_cast<double>(Position[2]) * 25.0 / 256.0);
|
||||
static_cast<double>(Position[0]) * CoreVoxelSizeCm / 256.0,
|
||||
static_cast<double>(Position[1]) * CoreVoxelSizeCm / 256.0,
|
||||
static_cast<double>(Position[2]) * CoreVoxelSizeCm / 256.0);
|
||||
};
|
||||
const FVector IslandTopXY = LocalPointFromSubvoxels(
|
||||
NativeIslandPlan.manifest.plan.islands.front().centerSubvoxels.data());
|
||||
const auto& Connector = NativeIslandPlan.manifest.plan.clusterEdges.front();
|
||||
const FVector SpanXY(
|
||||
(static_cast<double>(Connector.startSubvoxels[0] + Connector.endSubvoxels[0]) * 0.5)
|
||||
* 25.0 / 256.0,
|
||||
* CoreVoxelSizeCm / 256.0,
|
||||
(static_cast<double>(Connector.startSubvoxels[1] + Connector.endSubvoxels[1]) * 0.5)
|
||||
* 25.0 / 256.0,
|
||||
* CoreVoxelSizeCm / 256.0,
|
||||
(static_cast<double>(Connector.startSubvoxels[2] + Connector.endSubvoxels[2]) * 0.5)
|
||||
* 25.0 / 256.0);
|
||||
* CoreVoxelSizeCm / 256.0);
|
||||
float IslandContentGroundCm = 0.0f;
|
||||
float SpanContentGroundCm = 0.0f;
|
||||
if (Actor->ContentManager == nullptr
|
||||
@@ -16147,7 +16163,7 @@ bool RunCoreFloatingIslandActorIntegrationTest(
|
||||
return false;
|
||||
}
|
||||
const FVector LocalHit = Actor->GetActorTransform().InverseTransformPosition(Hit.ImpactPoint);
|
||||
constexpr double CoreTileExtentCm = 32.0 * 25.0;
|
||||
const double CoreTileExtentCm = static_cast<double>(CHUNK_SIZE) * Arguments.CoreVoxelSizeCm;
|
||||
const FVoxelTileKey Tile(FIntVector(
|
||||
FMath::FloorToInt(LocalHit.X / CoreTileExtentCm),
|
||||
FMath::FloorToInt(LocalHit.Y / CoreTileExtentCm),
|
||||
@@ -16202,7 +16218,8 @@ bool RunCoreFloatingIslandActorIntegrationTest(
|
||||
return false;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreIslandTest] blueprint_edit=PASS carve_sign=air fill_sign=solid radius_legacy_voxels=6 radius_cm=75 strength_legacy=10 density_before=%.4f carved=%.4f filled=%.4f outside_90cm_delta=%.6f"),
|
||||
TEXT("[VoxelForgeCoreIslandTest] blueprint_edit=PASS carve_sign=air fill_sign=solid radius_legacy_voxels=6 radius_cm=%.3f strength_legacy=10 density_before=%.4f carved=%.4f filled=%.4f outside_90cm_delta=%.6f"),
|
||||
6.0 * static_cast<double>(VOXEL_SIZE),
|
||||
DensityBefore, CarvedDensity, FilledDensity,
|
||||
FMath::Abs(OutsideAfterFill - OutsideBefore));
|
||||
|
||||
@@ -16231,10 +16248,89 @@ bool RunCoreFloatingIslandActorIntegrationTest(
|
||||
*CoreSeasonHash, *LegacySeasonHash, VOXEL_SIZE,
|
||||
LegacyDensityBeforeSwitch, LegacyDensityAfterSwitch);
|
||||
|
||||
if (!RunCoreContentPlacementCostAudit(Actor,
|
||||
FVector(IslandTopXY.X, IslandTopXY.Y, IslandContentGroundCm + 100.0f),
|
||||
TEXT("FloatingIslands"), OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCoreIslandTest] pass complete=1"));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RunCoreContentPlacementCostAudit(AVoxelWorld* Actor, const FVector& CoreLocalProbeCm,
|
||||
const TCHAR* Label, FString& OutError)
|
||||
{
|
||||
if (Actor == nullptr || Actor->ContentManager == nullptr)
|
||||
{
|
||||
OutError = TEXT("Content placement cost audit has no owner actor or content manager.");
|
||||
return false;
|
||||
}
|
||||
constexpr int32 SampleCells = 64;
|
||||
constexpr int32 SampleFrames = 32;
|
||||
const float CoreVoxelSizeCm = Actor->CoreVoxelSizeCm;
|
||||
const FTransform OwnerXf = Actor->GetActorTransform();
|
||||
|
||||
UVoxelContentManager::FDecorationPlacementCostAudit CoreAudit;
|
||||
Actor->bUseVoxelForgeCore = true;
|
||||
const FVector CorePlayerWorld = OwnerXf.TransformPosition(CoreLocalProbeCm);
|
||||
if (!Actor->ContentManager->MeasurePlacementCostForValidation(
|
||||
CorePlayerWorld, SampleCells, SampleFrames, CoreAudit, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UVoxelContentManager::FDecorationPlacementCostAudit LegacyAudit;
|
||||
Actor->bUseVoxelForgeCore = false;
|
||||
const float CoreToOwnerScale = VOXEL_SIZE / CoreVoxelSizeCm;
|
||||
const FVector LegacyPlayerWorld = OwnerXf.TransformPosition(CoreLocalProbeCm * CoreToOwnerScale);
|
||||
if (!Actor->ContentManager->MeasurePlacementCostForValidation(
|
||||
LegacyPlayerWorld, SampleCells, SampleFrames, LegacyAudit, OutError))
|
||||
{
|
||||
Actor->bUseVoxelForgeCore = true;
|
||||
return false;
|
||||
}
|
||||
Actor->bUseVoxelForgeCore = true;
|
||||
|
||||
if (CoreAudit.bSyntheticWorkload && CoreAudit.AnchorChecks > 0
|
||||
&& (CoreAudit.MaximumAnchorErrorCm > 0.5 || CoreAudit.MaximumDensityResidual > 0.25))
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Core synthetic decoration anchors drifted: height_error_cm=%.6f density_residual=%.6f."),
|
||||
CoreAudit.MaximumAnchorErrorCm, CoreAudit.MaximumDensityResidual);
|
||||
return false;
|
||||
}
|
||||
if (LegacyAudit.bSyntheticWorkload && LegacyAudit.AnchorChecks > 0
|
||||
&& LegacyAudit.MaximumDensityResidual > 0.25)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Legacy synthetic decoration anchors drifted: density_residual=%.6f."),
|
||||
LegacyAudit.MaximumDensityResidual);
|
||||
return false;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeContentCost] label=%s route=core voxel_cm=%.3f tile_cell_cm=%.3f authored_entries=%d synthetic=%d cells=%d columns_per_cell=%d density_queries_per_cell=%.2f surface_queries_per_cell=%.2f cell_median_ms=%.5f cell_p95_ms=%.5f update_median_ms=%.5f update_p95_ms=%.5f placements=%d anchor_checks=%d max_anchor_error_cm=%.6f max_density_residual=%.6f"),
|
||||
Label, CoreVoxelSizeCm, CHUNK_SIZE * CoreVoxelSizeCm,
|
||||
CoreAudit.NearEntries + CoreAudit.FarEntries, CoreAudit.bSyntheticWorkload ? 1 : 0,
|
||||
CoreAudit.SampledCells, CoreAudit.ColumnsPerCell, CoreAudit.DensityQueriesPerCell,
|
||||
CoreAudit.SurfaceQueriesPerCell, CoreAudit.CellMedianMilliseconds,
|
||||
CoreAudit.CellP95Milliseconds, CoreAudit.UpdateMedianMilliseconds,
|
||||
CoreAudit.UpdateP95Milliseconds, CoreAudit.Placements, CoreAudit.AnchorChecks,
|
||||
CoreAudit.MaximumAnchorErrorCm, CoreAudit.MaximumDensityResidual);
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeContentCost] label=%s route=legacy voxel_cm=%.3f tile_cell_cm=%.3f authored_entries=%d synthetic=%d cells=%d columns_per_cell=%d density_queries_per_cell=%.2f surface_queries_per_cell=%.2f cell_median_ms=%.5f cell_p95_ms=%.5f update_median_ms=%.5f update_p95_ms=%.5f placements=%d anchor_checks=%d max_anchor_error_cm=%.6f max_density_residual=%.6f"),
|
||||
Label, VOXEL_SIZE, CHUNK_SIZE * VOXEL_SIZE,
|
||||
LegacyAudit.NearEntries + LegacyAudit.FarEntries, LegacyAudit.bSyntheticWorkload ? 1 : 0,
|
||||
LegacyAudit.SampledCells, LegacyAudit.ColumnsPerCell, LegacyAudit.DensityQueriesPerCell,
|
||||
LegacyAudit.SurfaceQueriesPerCell, LegacyAudit.CellMedianMilliseconds,
|
||||
LegacyAudit.CellP95Milliseconds, LegacyAudit.UpdateMedianMilliseconds,
|
||||
LegacyAudit.UpdateP95Milliseconds, LegacyAudit.Placements, LegacyAudit.AnchorChecks,
|
||||
LegacyAudit.MaximumAnchorErrorCm, LegacyAudit.MaximumDensityResidual);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FString& OutError)
|
||||
{
|
||||
if (Arguments.bCoreFloatingIslandTest)
|
||||
@@ -16349,7 +16445,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
}
|
||||
FString Json;
|
||||
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
AuthoredSettings, Manager.Get(), 3, 8, Json, OutError))
|
||||
AuthoredSettings, Manager.Get(), 3, 8, Arguments.CoreVoxelSizeCm, Json, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -16373,7 +16469,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
FVector CarveLocalPositionCm = FVector::ZeroVector;
|
||||
FVoxelTileKey CarveTargetTile;
|
||||
constexpr float CarveBrushRadiusLegacyVoxels = 6.0f;
|
||||
constexpr double CoreVoxelSizeCm = 25.0;
|
||||
const double CoreVoxelSizeCm = Arguments.CoreVoxelSizeCm;
|
||||
const double CarveBrushRadiusCoreVoxels =
|
||||
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
|
||||
const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4);
|
||||
@@ -16857,7 +16953,18 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
Brush.halfExtent = {5.0, 5.0, 5.0};
|
||||
Brush.radius = CarveBrushRadiusCoreVoxels;
|
||||
Brush.falloff = 2.0f;
|
||||
Brush.strength = 5.0f;
|
||||
const double ExpectedCoreBrushStrength = 10.0 * VOXEL_SIZE / CoreVoxelSizeCm;
|
||||
Brush.strength = static_cast<float>(ExpectedCoreBrushStrength);
|
||||
if (!FMath::IsNearlyEqual(CoreVoxelSizeCm, 25.0, 0.000001)
|
||||
&& FMath::IsNearlyEqual(ExpectedCoreBrushStrength, 5.0, 0.000001))
|
||||
{
|
||||
OutError = TEXT("Non-default Core voxel size reused the fixed 25 cm carve strength.");
|
||||
return false;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreWorldTest] carve_scale_control=PASS legacy_fixed_strength=5.000 dynamic_strength=%.3f stale_default_control_detected=%d"),
|
||||
ExpectedCoreBrushStrength,
|
||||
FMath::IsNearlyEqual(ExpectedCoreBrushStrength, 5.0, 0.000001) ? 0 : 1);
|
||||
Transaction.brushes.push_back(Brush);
|
||||
const std::optional<VoxelForgeCore::EditResult> Edit =
|
||||
ReferenceWorld.applyEditTransaction(Transaction, Desired, CoreError);
|
||||
@@ -17112,7 +17219,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
BestSpanGeometryAuditL0 = Candidate;
|
||||
}
|
||||
}
|
||||
if (SpanRatioX >= 0.75 && SpanRatioY >= 0.75
|
||||
if (SpanRatioX >= 0.95 && SpanRatioY >= 0.95
|
||||
&& SpanRatioX <= 1.05 && SpanRatioY <= 1.05)
|
||||
{
|
||||
if (LOD == 0 && Distance < GeometryAuditL0Distance)
|
||||
@@ -17188,6 +17295,21 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
HorizonAuditLOD, GeometryAuditHorizon.bValid ? 1 : 0);
|
||||
return false;
|
||||
}
|
||||
if (!FMath::IsNearlyEqual(Arguments.CoreVoxelSizeCm, 25.0f, 0.0001f))
|
||||
{
|
||||
const double StaleDefault25CmSpan = static_cast<double>(CHUNK_SIZE) * 25.0;
|
||||
const bool bFixed25CmControlWouldFail =
|
||||
!FMath::IsNearlyEqual(GeometryAuditL0.SpanXcm, StaleDefault25CmSpan, 0.1)
|
||||
|| !FMath::IsNearlyEqual(GeometryAuditL0.SpanYcm, StaleDefault25CmSpan, 0.1);
|
||||
if (!bFixed25CmControlWouldFail)
|
||||
{
|
||||
OutError = TEXT("The injected fixed-25-cm geometry negative control was not detected.");
|
||||
return false;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreWorldTest] geometry_scale_negative_control=DETECTED injected_fixed_voxel_cm=25 expected_tile_span_cm=%.1f actual_span_cm=(%.1f,%.1f)"),
|
||||
StaleDefault25CmSpan, GeometryAuditL0.SpanXcm, GeometryAuditL0.SpanYcm);
|
||||
}
|
||||
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 "
|
||||
@@ -17531,6 +17653,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
Actor->Settings = AuthoredSettings;
|
||||
Actor->bManageAtmosphere = false;
|
||||
Actor->bUseVoxelForgeCore = true;
|
||||
Actor->CoreVoxelSizeCm = Arguments.CoreVoxelSizeCm;
|
||||
Actor->CoreMesherRecipe = Arguments.CoreHybridMesherVersion == 2
|
||||
? EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV2
|
||||
: (Arguments.CoreHybridMesherVersion == 1
|
||||
@@ -17555,6 +17678,23 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
: TEXT("MarchingCubesV1")),
|
||||
TestWorld->HasBegunPlay() ? 1 : 0, Actor->IsActorInitialized() ? 1 : 0,
|
||||
Actor->HasActorBegunPlay() ? 1 : 0);
|
||||
const float ActiveCoreVoxelSizeCm = Actor->CoreVoxelSizeCm;
|
||||
const FString ActiveSeasonIdentity = Actor->GetCurrentSeasonContentHash();
|
||||
const float AlternateCoreVoxelSizeCm = FMath::IsNearlyEqual(ActiveCoreVoxelSizeCm, 25.0f, 0.0001f)
|
||||
? 20.0f : 25.0f;
|
||||
Actor->CoreVoxelSizeCm = AlternateCoreVoxelSizeCm;
|
||||
const FString AlternateSeasonIdentity = Actor->GetCurrentSeasonContentHash();
|
||||
Actor->CoreVoxelSizeCm = ActiveCoreVoxelSizeCm;
|
||||
if (ActiveSeasonIdentity.IsEmpty() || AlternateSeasonIdentity.IsEmpty()
|
||||
|| ActiveSeasonIdentity == AlternateSeasonIdentity)
|
||||
{
|
||||
OutError = TEXT("Core voxel size did not distinguish the hashed season/world identity.");
|
||||
return false;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreWorldTest] voxel_size_identity=PASS active_cm=%.3f active_hash=%s alternate_cm=%.3f alternate_hash=%s distinct=1"),
|
||||
ActiveCoreVoxelSizeCm, *ActiveSeasonIdentity, AlternateCoreVoxelSizeCm,
|
||||
*AlternateSeasonIdentity);
|
||||
if (Arguments.bCoreHybridMesher
|
||||
&& !RunHybridPluginMesherFixtures(
|
||||
Actor->Generator, Arguments.bCoreHybridMesherV2, OutError))
|
||||
@@ -17762,7 +17902,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
const float ContentGroundWorldZ = Actor->GetActorTransform().TransformPosition(
|
||||
FVector(SurfaceLocalPosition.X, SurfaceLocalPosition.Y, ContentGroundLocalZ)).Z;
|
||||
const float GroundAgreementCm = FMath::Abs(TerrainGroundWorldZ - ContentGroundWorldZ);
|
||||
if (GroundAgreementCm > 25.0f
|
||||
if (GroundAgreementCm > 1.0f
|
||||
|| ContentGroundLocalZ < AtmosphereBottomLocalZ
|
||||
|| ContentGroundLocalZ > AtmosphereTopLocalZ
|
||||
|| (bContentHasWater && (ContentWaterLocalZ < AtmosphereBottomLocalZ
|
||||
@@ -17815,7 +17955,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
}
|
||||
Anchor->SetRootComponent(AnchorRoot);
|
||||
AnchorRoot->RegisterComponent();
|
||||
constexpr double CoreTileExtentCm = 32.0 * 25.0;
|
||||
const double CoreTileExtentCm = static_cast<double>(CHUNK_SIZE) * Arguments.CoreVoxelSizeCm;
|
||||
const FVector OldAnchorPosition(CoreTileExtentCm * 20.0, 0.0, 0.0);
|
||||
const FVector NewAnchorPosition(CoreTileExtentCm * 21.0, 0.0, 0.0);
|
||||
Anchor->SetActorLocation(OldAnchorPosition);
|
||||
@@ -17877,13 +18017,14 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
const float CarveCenterDelta = CarveDensityAfter - CarveDensityBefore;
|
||||
const float CarveInsideDelta = InsideDensityAfter - InsideDensityBefore;
|
||||
const float CarveOutsideDelta = OutsideDensityAfter - OutsideDensityBefore;
|
||||
if (!FMath::IsNearlyEqual(CarveCenterDelta, 5.0f, 0.01f)
|
||||
const float ExpectedBrushCenterDelta = static_cast<float>(10.0 * VOXEL_SIZE / CoreVoxelSizeCm);
|
||||
if (!FMath::IsNearlyEqual(CarveCenterDelta, ExpectedBrushCenterDelta, 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);
|
||||
TEXT("Blueprint carve sign/size failed: center=%.4f expected=+%.4f inside60cm=%.4f outside80cm=%.4f; expected partial +, zero."),
|
||||
CarveCenterDelta, ExpectedBrushCenterDelta, CarveInsideDelta, CarveOutsideDelta);
|
||||
return false;
|
||||
}
|
||||
if (Actor->LastVoxelForgeCoreEditSequence != 1
|
||||
@@ -17921,8 +18062,9 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
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);
|
||||
TEXT("[VoxelForgeCoreWorldTest] blueprint_carve=PASS radius_legacy=6 radius_cm=%.3f strength_legacy=10 center_delta=%.4f expected_center_delta=%.4f inside_60cm_delta=%.4f outside_80cm_delta=%.4f"),
|
||||
6.0 * static_cast<double>(VOXEL_SIZE), CarveCenterDelta, ExpectedBrushCenterDelta,
|
||||
CarveInsideDelta, CarveOutsideDelta);
|
||||
|
||||
Actor->UnregisterStreamingAnchor(Anchor);
|
||||
Anchor->Destroy();
|
||||
@@ -18498,18 +18640,25 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
const float FillCenterDelta = FillDensityAfter - FillDensityBefore;
|
||||
const float FillInsideDelta = FillInsideDensityAfter - FillInsideDensityBefore;
|
||||
const float FillOutsideDelta = FillOutsideDensityAfter - FillOutsideDensityBefore;
|
||||
if (!FMath::IsNearlyEqual(FillCenterDelta, -5.0f, 0.01f)
|
||||
const float ExpectedFillCenterDelta = -ExpectedBrushCenterDelta;
|
||||
if (!FMath::IsNearlyEqual(FillCenterDelta, ExpectedFillCenterDelta, 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);
|
||||
TEXT("Blueprint fill sign/size failed: center=%.4f expected=%.4f inside60cm=%.4f outside80cm=%.4f; expected partial -, zero."),
|
||||
FillCenterDelta, ExpectedFillCenterDelta, 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);
|
||||
TEXT("[VoxelForgeCoreWorldTest] blueprint_fill=PASS radius_legacy=6 radius_cm=%.3f strength_legacy=10 center_delta=%.4f expected_center_delta=%.4f inside_60cm_delta=%.4f outside_80cm_delta=%.4f"),
|
||||
6.0 * static_cast<double>(VOXEL_SIZE), FillCenterDelta, ExpectedFillCenterDelta,
|
||||
FillInsideDelta, FillOutsideDelta);
|
||||
if (!RunCoreContentPlacementCostAudit(Actor, SurfaceLocalPosition,
|
||||
TEXT("SurfaceWorld"), OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user