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
@@ -1283,6 +1283,49 @@ sampled ground/cap heights plus the Z bounds of intersecting type-2/type-4 manif
|
||||
with overlapping player edits or another strate keep the full mesh range. See round 6 of the M1
|
||||
report for per-kind streaming results and validation limits.
|
||||
|
||||
### 8.22 Core voxel scale, per-tile features and placement measurements — World Assembler M3 Round 5
|
||||
|
||||
The opt-in Core world has one physical voxel-size source on AVoxelWorld: CoreVoxelSizeCm, default
|
||||
25 cm and constrained to the supported 12.5–25 cm range for this integration. GetActiveVoxelSizeCm()
|
||||
is the runtime conversion point for Core coordinates. CoreTileVoxelCount is 32 voxels; tile length
|
||||
is derived as voxel size × 32 × 2^LOD. Core-off remains on the legacy 12.5 cm VOXEL_SIZE path.
|
||||
Owner/Core compatibility conversions use the resolved values and must not assume that a Core voxel
|
||||
is twice a legacy voxel.
|
||||
|
||||
The resolved physical size is rounded to microns and carried with tileVoxelCount in the immutable
|
||||
Core world configuration. Non-default Core size participates in plan and season identity. The
|
||||
default 25 cm value is omitted from the added identity suffix so recorded default SurfaceWorld and
|
||||
M2 recipe/fixture identities keep their prior form. Water remains fixed for the season; player
|
||||
carve/fill remains unrestricted. Collision support, clipmap keys and LOD stay in Core voxel units
|
||||
and derive centimetres at the adapter/mesh boundary.
|
||||
|
||||
Floating Islands builds a deterministic candidate feature list per tile using conservative
|
||||
inclusive bounds. Candidate IDs retain canonical feature order. A worker-scoped context lets density
|
||||
samples evaluate only the tile's candidate islands and connectors. If bounds cannot prove a feature
|
||||
irrelevant, it stays in the candidate list; future caves, shafts, pits and passages should use the
|
||||
same common tile-list builder. The Round 5 audit reports exact full-field/candidate equality and
|
||||
deliberately removes a feature as a negative control. This API does not change feature planning or
|
||||
field identity. Air-side pruning uses a conservative lower bound for the version-2 scaled root
|
||||
ellipsoids and their smooth-min reductions; a geometric AABB distance alone can overstate their
|
||||
positive density and change hashes outside the visible island bounds. BuildHost compared the staged
|
||||
owner plan's candidate field against the full scan at every queried sample and restored the Round 4
|
||||
island aggregate hashes.
|
||||
|
||||
The current configuration is per-world. A later per-strate size needs a physical-space field-query
|
||||
contract at boundaries, transition meshes with deterministic LOD ancestry, edit rasterization that
|
||||
can touch both resolutions without gaps or double application, and a stable, canonically ordered
|
||||
strate-ID-to-size map in plan/season identity. Border and LOD hashes must cover both sides; mixed-size
|
||||
edits and replays must match on every worker count. Until those rules exist, one world size is the
|
||||
supported contract.
|
||||
|
||||
Round 5 measured content evaluation per placement-grid cell and stationary manager update on staged
|
||||
owner settings. SurfaceWorld uses its known heightfield and made no density queries in the Core
|
||||
measurement. Floating Islands still uses the exact generic density march; a top-anchor shortcut
|
||||
measured slower and was reverted. The sampled authored entries produced no placements, so these
|
||||
figures measure anchor-search work and steady update overhead, not spawn/asset-instancing cost.
|
||||
See Standalone/Research/WorldAssemblerM3/Round5/REPORT.md for the test matrix, owner-plan hash proof,
|
||||
and content-placement measurements.
|
||||
|
||||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||
|
||||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||||
|
||||
+31
-7
@@ -117,6 +117,16 @@ pass. Reusing exact detail-lattice hash corners across adjacent mixed bricks red
|
||||
step-3 259³ median from 1.502908 s to 1.169567 s; the separate step-2f 600 ms worst-profile timing
|
||||
bar still does not pass at the finer voxel size.
|
||||
|
||||
The opt-in Core route resolves physical voxel size from `AVoxelWorld::CoreVoxelSizeCm` (default
|
||||
**25 cm**) and keeps **32 voxels per Core tile**. `GetActiveVoxelSizeCm()` is the runtime conversion
|
||||
point for coordinates, mesh vertices, collision, streaming, LOD, content, water, atmosphere, and
|
||||
brushes; `WorldConfiguration::voxelSizeMicrons` plus `tileVoxelCount` carries the resolved values
|
||||
through Core. A nondefault size participates in Core snapshot/season identity, while 25 cm preserves
|
||||
the recorded default hashes. Core-off continues using the legacy 12.5 cm grid. This is currently a
|
||||
per-world setting; per-strate sizes still need physical-coordinate sampling, transition stitching,
|
||||
cross-boundary edit rasterization, per-strate LOD agreement, and a canonically ordered size map in the
|
||||
hashed identity.
|
||||
|
||||
The reachable roughness envelope is `roughness * VOXEL_NOISE_SCALE * 1.5`, or `roughness * 1.875`
|
||||
voxels. The new defaults keep it below the relevant feature radius: **0.625** of the minimum
|
||||
TunnelNetwork tunnel radius, **0.300** of Maze corridor radius, **0.469/0.625** of shaft/connector
|
||||
@@ -824,6 +834,14 @@ second density path.
|
||||
| `VoxelForgePlayerFitWindow.h` | — | Shared measurement-window policy for player-fit diagnostics (test/editor side only). |
|
||||
|
||||
### 3.13 Core runtime integration (rounds 2–5)
|
||||
Scale symbols: AVoxelWorld::CoreVoxelSizeCm is the opt-in per-world Core size setting and
|
||||
AVoxelWorld::GetActiveVoxelSizeCm() is the authoritative runtime conversion. CoreTileVoxelCount in
|
||||
the public Core API is 32 voxels per tile. UVoxelContentManager::MeasurePlacementCostForValidation
|
||||
is a headless diagnostic only; it measures placement-grid cell queries and steady manager update
|
||||
cost without changing normal spawning. FloatingIslandTileListAudit.cpp and
|
||||
BuildFloatingIslandTileListAudit.ps1 under WorldAssemblerM3/Round5 hold the candidate-list
|
||||
benchmark and dropped-feature negative control. See ARCHITECTURE §8.22.
|
||||
|
||||
The public Core API is linked as the `VoxelForgeCore` runtime module, with canonical implementation
|
||||
sources still under `Standalone/`. UBT validates the 18-row manifest and its shims; BuildHost linked
|
||||
the module and passed `CheckCoreSymbols.ps1` against the actual shim objects. See
|
||||
@@ -856,13 +874,19 @@ settings or assets.
|
||||
|
||||
When enabled, `AVoxelWorld` uses Core desired keys and priorities, compiles/generates through Unreal
|
||||
tasks, carries `Epoch` through the existing MPSC result queues, and uploads converted payloads through
|
||||
RealtimeMesh and its collision cook. Core voxel coordinates use 25 cm per voxel; Core tile keys are
|
||||
retained, and Core mesh vertices are already in centimetres. High-level legacy brush dimensions and
|
||||
density strength are scaled from 12.5 cm voxel units to Core units so their physical effect stays the
|
||||
same. Edits use
|
||||
sequenced `applyEditTransaction` calls after in-flight Core work drains, then regenerate Core's
|
||||
affected keys under a new epoch. Existing streaming anchors participate; the headless move proof
|
||||
covers `CollisionOnly` policy.
|
||||
RealtimeMesh and its collision cook. Core voxel coordinates use the size resolved from
|
||||
`CoreVoxelSizeCm` (25 cm by default); Core tile keys remain in 32-voxel units, and mesh vertices are
|
||||
in centimetres. Legacy brush dimensions and density strength are converted from the 12.5 cm owner
|
||||
grid using the resolved Core size, preserving their physical effect. SurfaceWorld owner-chunk mapping
|
||||
uses the owner/Core ratio as well. Edits use sequenced `applyEditTransaction` calls after in-flight
|
||||
Core work drains, then regenerate Core's affected keys under a new epoch. Existing streaming anchors
|
||||
participate; the headless move proof covers `CollisionOnly` policy.
|
||||
|
||||
The reusable `BuildWorldAssemblyFeatureTileListV1` accepts conservative inclusive feature bounds for
|
||||
future caves, shafts, pits, and passages. Floating Islands builds one `FloatingIslandTileFeatureListV1`
|
||||
per tile and the scoped worker context makes `FloatingIslandFieldV1::densityAt` evaluate only that
|
||||
candidate list. Candidate order stays canonical, the field hash remains bit-identical, and a dropped-
|
||||
feature negative control is part of the standalone Round 5 audit.
|
||||
|
||||
`-coreworldtest=1` in the BuildHost editor commandlet validates initial tiles, expected collision
|
||||
bodies/empty tiles, anchor movement, a carve transaction, travel to the B landing and native surface
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
namespace VoxelForgeCore
|
||||
{
|
||||
inline constexpr std::uint32_t CoreTileVoxelCount = 32;
|
||||
|
||||
struct Vec2f { float x = 0.0f, y = 0.0f; };
|
||||
struct Vec3d { double x = 0.0, y = 0.0, z = 0.0; };
|
||||
struct Vec3i
|
||||
@@ -34,11 +36,26 @@ namespace VoxelForgeCore
|
||||
|
||||
struct TileKey
|
||||
{
|
||||
Vec3i coordinate{}; // Units of (32 << lod) voxels.
|
||||
Vec3i coordinate{}; // Units of (CoreTileVoxelCount << lod) voxels.
|
||||
std::int32_t lod = 0;
|
||||
friend bool operator==(const TileKey&, const TileKey&) = default;
|
||||
};
|
||||
|
||||
// Reusable broad-phase input for any bounded world-assembly feature family. Bounds are
|
||||
// conservative and inclusive; an invalid box makes the builder fail open by returning all
|
||||
// feature indices. Les bornes sont inclusives; une borne invalide garde toutes les features.
|
||||
struct WorldAssemblyFeatureBoundsV1
|
||||
{
|
||||
std::uint64_t stableId = 0;
|
||||
std::array<std::int64_t, 3> minimumSubvoxels{};
|
||||
std::array<std::int64_t, 3> maximumSubvoxels{};
|
||||
};
|
||||
|
||||
VFCORE_API std::vector<std::uint32_t> BuildWorldAssemblyFeatureTileListV1(
|
||||
std::span<const WorldAssemblyFeatureBoundsV1> features,
|
||||
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
|
||||
const std::array<std::int64_t, 3>& tileMaximumSubvoxels);
|
||||
|
||||
struct VFCORE_API TileKeyHash
|
||||
{
|
||||
std::size_t operator()(const TileKey& key) const noexcept;
|
||||
@@ -125,6 +142,11 @@ namespace VoxelForgeCore
|
||||
|
||||
struct WorldConfiguration
|
||||
{
|
||||
// Physical resolution is part of the resolved world identity. Tile coordinates remain in
|
||||
// abstract voxel units; meshing, collision and Unreal transforms use this size in cm.
|
||||
// La resolution physique fait partie de l'identite resolue; les tuiles restent en voxels.
|
||||
std::uint32_t voxelSizeMicrons = 250000;
|
||||
std::uint32_t tileVoxelCount = CoreTileVoxelCount;
|
||||
std::int32_t seed = 0;
|
||||
std::int32_t clipRadius = 0;
|
||||
std::int32_t maxLod = 0;
|
||||
@@ -292,8 +314,8 @@ namespace VoxelForgeCore
|
||||
// bounded lobed roots and arched spans without changing the feature-family schema.
|
||||
std::uint32_t geometryVersion = 2;
|
||||
std::uint32_t unitsPerVoxel = static_cast<std::uint32_t>(WorldAssemblySubvoxelsPerVoxel);
|
||||
// Import scale is explicit: legacy owner voxels are 12.5 cm and Core voxels are 25 cm.
|
||||
// L'echelle d'import est explicite : voxel owner de 12,5 cm et voxel Core de 25 cm.
|
||||
// Import scale is explicit in microns; the reduced ratio maps owner-authored lengths to Core voxels.
|
||||
// L'echelle d'import est explicite en microns; le rapport reduit convertit les longueurs owner.
|
||||
std::uint32_t ownerVoxelSizeMicrons = 125000;
|
||||
std::uint32_t coreVoxelSizeMicrons = 250000;
|
||||
std::uint32_t ownerToCoreScaleNumerator = 1;
|
||||
@@ -306,8 +328,8 @@ namespace VoxelForgeCore
|
||||
std::int64_t layoutMaximumZSubvoxels = 0;
|
||||
std::uint32_t primaryIslandCount = 3;
|
||||
std::int64_t islandDensityQ16 = 1ll << 15;
|
||||
// Legacy owner lengths use 12.5 cm voxels; Core uses 25 cm, so halve the owner lengths.
|
||||
// Les longueurs owner utilisent des voxels de 12,5 cm ; Core utilise 25 cm, donc les diviser par deux.
|
||||
// Legacy owner lengths use ownerVoxelSizeMicrons; the scale ratio converts them to Core voxels.
|
||||
// Les longueurs owner utilisent ownerVoxelSizeMicrons; le rapport les convertit en voxels Core.
|
||||
std::int64_t spacingSubvoxels = 56 * WorldAssemblySubvoxelsPerVoxel;
|
||||
std::int64_t minimumRadiusSubvoxels = 12 * WorldAssemblySubvoxelsPerVoxel;
|
||||
std::int64_t maximumRadiusSubvoxels = 24 * WorldAssemblySubvoxelsPerVoxel;
|
||||
@@ -364,6 +386,27 @@ namespace VoxelForgeCore
|
||||
std::uint64_t contentHash = 0;
|
||||
};
|
||||
|
||||
struct FloatingIslandFeatureGroupBoundsV1
|
||||
{
|
||||
std::array<std::int64_t, 3> minimumSubvoxels{};
|
||||
std::array<std::int64_t, 3> maximumSubvoxels{};
|
||||
};
|
||||
|
||||
struct FloatingIslandTileFeatureListV1
|
||||
{
|
||||
// Flat stable-order IDs for island and connector features that touch this tile.
|
||||
// Identifiants ordonnes des features qui touchent ce carreau.
|
||||
std::vector<std::uint32_t> featureIndices;
|
||||
// Cheap conservative group bounds preserve exact positive SDF distances while distant
|
||||
// feature evaluators are skipped. Bornes legeres pour garder les distances SDF exactes.
|
||||
std::vector<FloatingIslandFeatureGroupBoundsV1> groupBounds;
|
||||
// Groups are ordered by a tile-wide conservative distance lower bound. Samples stop at
|
||||
// the first group that cannot beat their current density. Ordre par distance minimale.
|
||||
std::vector<std::uint32_t> groupSearchOrder;
|
||||
std::vector<std::int64_t> groupDistanceLowerBoundsSubvoxels;
|
||||
bool groupSearchComplete = true;
|
||||
};
|
||||
|
||||
struct VFCORE_API FloatingIslandManifestV1
|
||||
{
|
||||
FloatingIslandRecipeV1 recipe;
|
||||
@@ -464,6 +507,12 @@ namespace VoxelForgeCore
|
||||
bool isValid() const noexcept;
|
||||
std::int64_t densitySubvoxels(std::int64_t xSubvoxels,
|
||||
std::int64_t ySubvoxels, std::int64_t zSubvoxels) const noexcept;
|
||||
FloatingIslandTileFeatureListV1 buildFeatureTileList(
|
||||
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
|
||||
const std::array<std::int64_t, 3>& tileMaximumSubvoxels) const;
|
||||
std::int64_t densitySubvoxelsForTile(std::int64_t xSubvoxels,
|
||||
std::int64_t ySubvoxels, std::int64_t zSubvoxels,
|
||||
const FloatingIslandTileFeatureListV1& featureList) const noexcept;
|
||||
FieldInterval interval(std::int64_t minimumXSubvoxels,
|
||||
std::int64_t minimumYSubvoxels, std::int64_t minimumZSubvoxels,
|
||||
std::int64_t maximumXSubvoxels, std::int64_t maximumYSubvoxels,
|
||||
|
||||
@@ -553,7 +553,8 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
|
||||
Radial.Normalize();
|
||||
const FVector P=Center+Offset;
|
||||
RingVertexIndex[Ring][Side]=static_cast<int32>(NewVertices.size());
|
||||
NewVertices.emplace_back(P.X*25.0,P.Y*25.0,P.Z*25.0);
|
||||
const double VoxelSizeCm=Shape.GetCoreVoxelSizeCm();
|
||||
NewVertices.emplace_back(P.X*VoxelSizeCm,P.Y*VoxelSizeCm,P.Z*VoxelSizeCm);
|
||||
NewNormals.emplace_back(-Radial.X,-Radial.Y,-Radial.Z);
|
||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(Rings[Ring].T*Length/256.0),
|
||||
static_cast<float>(Side)/SideCount);
|
||||
@@ -703,7 +704,8 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
|
||||
if(!ProjectToSurface(P,Normal)){bProjectionSucceeded=false;break;}
|
||||
const int32 LocalIndex=static_cast<int32>(PatchVertices.size());
|
||||
Grid[static_cast<size_t>(Y)*(Subdivisions+1)+X]=LocalIndex;
|
||||
PatchVertices.emplace_back(P.X*25.0,P.Y*25.0,P.Z*25.0);
|
||||
const double VoxelSizeCm=Shape.GetCoreVoxelSizeCm();
|
||||
PatchVertices.emplace_back(P.X*VoxelSizeCm,P.Y*VoxelSizeCm,P.Z*VoxelSizeCm);
|
||||
PatchNormals.push_back(Normal);
|
||||
if(bHasUVs)
|
||||
{
|
||||
@@ -1013,7 +1015,9 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
|
||||
const int32 Base=static_cast<int32>(NewVertices.size());
|
||||
for(const FPatchVertex& Vertex:Patch)
|
||||
{
|
||||
NewVertices.emplace_back(Vertex.X*25.0,Vertex.Y*25.0,Vertex.Z*25.0);
|
||||
const double VoxelSizeCm=World.CoreVoxelSizeMicrons/10000.0;
|
||||
NewVertices.emplace_back(Vertex.X*VoxelSizeCm,Vertex.Y*VoxelSizeCm,
|
||||
Vertex.Z*VoxelSizeCm);
|
||||
NewNormals.push_back(Vertex.Normal);
|
||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>((Vertex.X-Origin.X)/256.0),
|
||||
static_cast<float>((Vertex.Y-Origin.Y)/256.0));
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
@@ -258,14 +259,20 @@ namespace floating_island_detail
|
||||
recipe.routeConstraints.capsuleRadiusSubvoxels,
|
||||
recipe.routeConstraints.capsuleHeightSubvoxels,
|
||||
recipe.routeConstraints.maximumGradeQ16};
|
||||
const std::uint32_t ScaleGcd = std::gcd(recipe.ownerVoxelSizeMicrons,
|
||||
recipe.coreVoxelSizeMicrons);
|
||||
const std::uint32_t ExpectedNumerator = ScaleGcd > 0
|
||||
? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0;
|
||||
const std::uint32_t ExpectedDenominator = ScaleGcd > 0
|
||||
? recipe.coreVoxelSizeMicrons / ScaleGcd : 0;
|
||||
if (recipe.schemaVersion != 1 || (recipe.geometryVersion != 1 && recipe.geometryVersion != 2)
|
||||
|| (recipe.geometryVersion == 1
|
||||
&& (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0))
|
||||
|| recipe.unitsPerVoxel != Q
|
||||
|| recipe.ownerVoxelSizeMicrons != 125000
|
||||
|| recipe.coreVoxelSizeMicrons != 250000
|
||||
|| recipe.ownerToCoreScaleNumerator != 1
|
||||
|| recipe.ownerToCoreScaleDenominator != 2
|
||||
|| recipe.coreVoxelSizeMicrons < 125000 || recipe.coreVoxelSizeMicrons > 250000
|
||||
|| recipe.ownerToCoreScaleNumerator != ExpectedNumerator
|
||||
|| recipe.ownerToCoreScaleDenominator != ExpectedDenominator
|
||||
|| recipe.primaryIslandCount < 3 || recipe.primaryIslandCount > 9
|
||||
|| recipe.layoutMaximumZSubvoxels <= recipe.layoutMinimumZSubvoxels
|
||||
|| recipe.layoutMaximumZSubvoxels - recipe.layoutMinimumZSubvoxels > 2048 * Q
|
||||
@@ -742,6 +749,103 @@ namespace floating_island_detail
|
||||
return std::clamp(RoundQ(result), -MaxDensity, MaxDensity);
|
||||
}
|
||||
|
||||
inline std::int64_t FieldDensityForTile(const FloatingIslandRecipeV1& recipe,
|
||||
const FloatingIslandPlanV1& plan, std::int64_t x, std::int64_t y, std::int64_t z,
|
||||
const FloatingIslandTileFeatureListV1& featureList)
|
||||
{
|
||||
if (plan.islands.empty()) return MaxDensity;
|
||||
if (featureList.groupBounds.size() != plan.islands.size())
|
||||
return FieldDensity(recipe, plan, x, y, z);
|
||||
const auto& featureIndices = featureList.featureIndices;
|
||||
const std::size_t islandCount = plan.islands.size();
|
||||
const double blend = static_cast<double>(recipe.blendRadiusSubvoxels);
|
||||
double result = static_cast<double>(MaxDensity);
|
||||
|
||||
const auto evaluateLocalGroup = [&](std::size_t islandIndex)
|
||||
{
|
||||
const auto& island = plan.islands[islandIndex];
|
||||
double group = IslandDensity(recipe, island,
|
||||
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z));
|
||||
for (const std::uint32_t edgeIndex : featureIndices)
|
||||
{
|
||||
if (edgeIndex < islandCount) continue;
|
||||
const std::size_t planIndex = static_cast<std::size_t>(edgeIndex) - islandCount;
|
||||
if (planIndex >= plan.clusterEdges.size()) continue;
|
||||
const auto& edge = plan.clusterEdges[planIndex];
|
||||
if (edge.fromIslandId != island.stableId) continue;
|
||||
bool topSurface = false;
|
||||
double alongFraction = 0.0;
|
||||
double connector = ConnectorDensity(recipe, edge,
|
||||
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z),
|
||||
&topSurface, &alongFraction);
|
||||
if (recipe.geometryVersion >= 2)
|
||||
connector = ApplySurfaceDetail(recipe, edge.stableId, connector,
|
||||
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z),
|
||||
topSurface || alongFraction < 0.12 || alongFraction > 0.88 ? 0.25 : 1.0);
|
||||
group = SmoothMin(group, connector, blend);
|
||||
}
|
||||
return group;
|
||||
};
|
||||
|
||||
const auto evaluateDistantGroup = [&](std::size_t islandIndex)
|
||||
{
|
||||
const auto& island = plan.islands[islandIndex];
|
||||
double group = IslandDensity(recipe, island,
|
||||
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z));
|
||||
for (const auto& edge : plan.clusterEdges)
|
||||
{
|
||||
if (edge.fromIslandId != island.stableId) continue;
|
||||
bool topSurface = false;
|
||||
double alongFraction = 0.0;
|
||||
double connector = ConnectorDensity(recipe, edge,
|
||||
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z),
|
||||
&topSurface, &alongFraction);
|
||||
if (recipe.geometryVersion >= 2)
|
||||
connector = ApplySurfaceDetail(recipe, edge.stableId, connector,
|
||||
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z),
|
||||
topSurface || alongFraction < 0.12 || alongFraction > 0.88 ? 0.25 : 1.0);
|
||||
group = SmoothMin(group, connector, blend);
|
||||
}
|
||||
return group;
|
||||
};
|
||||
|
||||
// Each local group keeps its complete ordered connector chain in the tile list.
|
||||
// Les connecteurs d'un groupe local restent complets et dans leur ordre canonique.
|
||||
for (const std::uint32_t index : featureIndices)
|
||||
{
|
||||
if (index >= islandCount) break;
|
||||
result = std::min(result, evaluateLocalGroup(index));
|
||||
}
|
||||
|
||||
// Air-side density magnitudes are part of the canonical field too. The per-tile list
|
||||
// handles nearby groups; conservative group-box lower bounds prune distant exact SDFs.
|
||||
// Les magnitudes d'air restent canoniques; les bornes de groupes eliminent les SDF lointains.
|
||||
if (result >= 0.0)
|
||||
{
|
||||
if ((featureList.groupSearchOrder.size() != islandCount
|
||||
|| featureList.groupDistanceLowerBoundsSubvoxels.size() != islandCount)
|
||||
&& featureList.groupSearchComplete)
|
||||
return FieldDensity(recipe, plan, x, y, z);
|
||||
const std::size_t searchCount = std::min(featureList.groupSearchOrder.size(),
|
||||
featureList.groupDistanceLowerBoundsSubvoxels.size());
|
||||
for (std::size_t orderIndex = 0; orderIndex < searchCount; ++orderIndex)
|
||||
{
|
||||
const std::size_t islandIndex = featureList.groupSearchOrder[orderIndex];
|
||||
if (islandIndex >= islandCount) return FieldDensity(recipe, plan, x, y, z);
|
||||
if (std::binary_search(featureIndices.begin(), featureIndices.end(),
|
||||
static_cast<std::uint32_t>(islandIndex))) continue;
|
||||
const auto& bounds = featureList.groupBounds[islandIndex];
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
if (bounds.minimumSubvoxels[axis] > bounds.maximumSubvoxels[axis])
|
||||
return FieldDensity(recipe, plan, x, y, z);
|
||||
if (static_cast<double>(featureList.groupDistanceLowerBoundsSubvoxels[orderIndex]) > result)
|
||||
break;
|
||||
result = std::min(result, evaluateDistantGroup(islandIndex));
|
||||
}
|
||||
}
|
||||
return std::clamp(RoundQ(result), -MaxDensity, MaxDensity);
|
||||
}
|
||||
|
||||
inline void SetIslandBounds(const FloatingIslandRecipeV1& recipe,
|
||||
FloatingIslandAnchorV1& island)
|
||||
{
|
||||
@@ -1528,7 +1632,7 @@ bool FloatingIslandFieldV1::isValid() const noexcept
|
||||
if (!recipe_ || !plan_ || recipe_->schemaVersion != 1
|
||||
|| recipe_->unitsPerVoxel != floating_island_detail::Q
|
||||
|| recipe_->primaryIslandCount < 3 || plan_->islands.size() < recipe_->primaryIslandCount
|
||||
|| plan_->islands.size() > recipe_->primaryIslandCount + 2
|
||||
|| plan_->islands.size() > 4096 || plan_->clusterEdges.size() > 4096
|
||||
|| recipe_->minimumRadiusSubvoxels <= 0 || recipe_->maximumRadiusSubvoxels < recipe_->minimumRadiusSubvoxels
|
||||
|| recipe_->blendRadiusSubvoxels < 0 || recipe_->contourWarpSubvoxels < 0) return false;
|
||||
return std::all_of(plan_->islands.begin(), plan_->islands.end(), [](const auto& island)
|
||||
@@ -1547,6 +1651,182 @@ std::int64_t FloatingIslandFieldV1::densitySubvoxels(std::int64_t x,
|
||||
return floating_island_detail::FieldDensity(*recipe_, *plan_, x, y, z);
|
||||
}
|
||||
|
||||
FloatingIslandTileFeatureListV1 FloatingIslandFieldV1::buildFeatureTileList(
|
||||
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
|
||||
const std::array<std::int64_t, 3>& tileMaximumSubvoxels) const
|
||||
{
|
||||
FloatingIslandTileFeatureListV1 result;
|
||||
if (!isValid()) return result;
|
||||
std::vector<WorldAssemblyFeatureBoundsV1> features;
|
||||
features.reserve(plan_->islands.size() + plan_->clusterEdges.size());
|
||||
for (const auto& island : plan_->islands)
|
||||
features.push_back({island.stableId, island.influenceMinimumSubvoxels,
|
||||
island.influenceMaximumSubvoxels});
|
||||
for (const auto& edge : plan_->clusterEdges)
|
||||
features.push_back({edge.stableId, edge.influenceMinimumSubvoxels,
|
||||
edge.influenceMaximumSubvoxels});
|
||||
|
||||
result.featureIndices = BuildWorldAssemblyFeatureTileListV1(features,
|
||||
tileMinimumSubvoxels, tileMaximumSubvoxels);
|
||||
const std::size_t islandCount = plan_->islands.size();
|
||||
result.groupBounds.resize(islandCount);
|
||||
for (std::size_t islandIndex = 0; islandIndex < islandCount; ++islandIndex)
|
||||
{
|
||||
result.groupBounds[islandIndex].minimumSubvoxels = plan_->islands[islandIndex].influenceMinimumSubvoxels;
|
||||
result.groupBounds[islandIndex].maximumSubvoxels = plan_->islands[islandIndex].influenceMaximumSubvoxels;
|
||||
}
|
||||
|
||||
const auto findSourceIsland = [&](const FloatingIslandConnectorV1& edge)
|
||||
{
|
||||
return std::find_if(plan_->islands.begin(), plan_->islands.end(),
|
||||
[&edge](const auto& island) { return island.stableId == edge.fromIslandId; });
|
||||
};
|
||||
for (const auto& edge : plan_->clusterEdges)
|
||||
{
|
||||
const auto source = findSourceIsland(edge);
|
||||
if (source == plan_->islands.end())
|
||||
{ result.groupBounds.clear(); result.featureIndices.clear(); return result; }
|
||||
auto& group = result.groupBounds[static_cast<std::size_t>(source - plan_->islands.begin())];
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
group.minimumSubvoxels[axis] = std::min(group.minimumSubvoxels[axis],
|
||||
edge.influenceMinimumSubvoxels[axis]);
|
||||
group.maximumSubvoxels[axis] = std::max(group.maximumSubvoxels[axis],
|
||||
edge.influenceMaximumSubvoxels[axis]);
|
||||
}
|
||||
}
|
||||
|
||||
result.groupSearchOrder.resize(islandCount);
|
||||
std::iota(result.groupSearchOrder.begin(), result.groupSearchOrder.end(), 0u);
|
||||
const auto tileDistanceLowerBound = [&](
|
||||
const std::array<std::int64_t, 3>& featureMinimum,
|
||||
const std::array<std::int64_t, 3>& featureMaximum)
|
||||
{
|
||||
std::int64_t maximumGap = 0;
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
if (featureMinimum[axis] > tileMaximumSubvoxels[axis])
|
||||
maximumGap = std::max(maximumGap,
|
||||
featureMinimum[axis] - tileMaximumSubvoxels[axis]);
|
||||
else if (featureMaximum[axis] < tileMinimumSubvoxels[axis])
|
||||
maximumGap = std::max(maximumGap,
|
||||
tileMinimumSubvoxels[axis] - featureMaximum[axis]);
|
||||
}
|
||||
return maximumGap;
|
||||
};
|
||||
result.groupDistanceLowerBoundsSubvoxels.resize(islandCount);
|
||||
for (std::size_t islandIndex = 0; islandIndex < islandCount; ++islandIndex)
|
||||
{
|
||||
const auto& island = plan_->islands[islandIndex];
|
||||
const std::int64_t islandGap = tileDistanceLowerBound(
|
||||
island.influenceMinimumSubvoxels, island.influenceMaximumSubvoxels);
|
||||
double islandLowerBound = static_cast<double>(islandGap)
|
||||
- floating_island_detail::IslandBlendMargin(*recipe_) - recipe_->roughnessSubvoxels;
|
||||
if (recipe_->geometryVersion >= 2)
|
||||
{
|
||||
// Root lobes use a scaled ellipsoid distance, so their positive values can be
|
||||
// substantially smaller than distance to the island AABB. Supported root radii
|
||||
// guarantee minAxis/maxAxis >= 0.22; include their smooth-min budget before pruning.
|
||||
// Les racines utilisent une distance d'ellipsoide mise a l'echelle; borner leur pente.
|
||||
const double radius = static_cast<double>(island.radiusSubvoxels);
|
||||
const double rootBlend = std::clamp(radius * 0.12,
|
||||
2.0 * floating_island_detail::Q, 5.0 * floating_island_detail::Q);
|
||||
const double rootSmoothBudget = static_cast<double>(
|
||||
floating_island_detail::RootCount(*recipe_, island)) * rootBlend * 0.25;
|
||||
const double rootLowerBound = 0.22 * static_cast<double>(islandGap)
|
||||
- recipe_->roughnessSubvoxels - rootSmoothBudget;
|
||||
islandLowerBound = std::min(islandLowerBound, rootLowerBound);
|
||||
}
|
||||
|
||||
double groupLowerBound = islandLowerBound;
|
||||
for (const auto& edge : plan_->clusterEdges)
|
||||
{
|
||||
if (edge.fromIslandId != island.stableId) continue;
|
||||
const std::int64_t edgeGap = tileDistanceLowerBound(
|
||||
edge.influenceMinimumSubvoxels, edge.influenceMaximumSubvoxels);
|
||||
const double edgeLowerBound = static_cast<double>(edgeGap) - recipe_->roughnessSubvoxels;
|
||||
groupLowerBound = std::min(groupLowerBound, edgeLowerBound)
|
||||
- static_cast<double>(recipe_->blendRadiusSubvoxels) * 0.25;
|
||||
}
|
||||
// Floor with one subvoxel of slack so quantization cannot make a lower bound optimistic.
|
||||
// Arrondir vers le bas avec une marge pour ne jamais surestimer la borne.
|
||||
result.groupDistanceLowerBoundsSubvoxels[islandIndex] =
|
||||
static_cast<std::int64_t>(std::floor(groupLowerBound)) - 1;
|
||||
}
|
||||
std::sort(result.groupSearchOrder.begin(), result.groupSearchOrder.end(),
|
||||
[&result](std::uint32_t a, std::uint32_t b)
|
||||
{
|
||||
const auto distanceA = result.groupDistanceLowerBoundsSubvoxels[a];
|
||||
const auto distanceB = result.groupDistanceLowerBoundsSubvoxels[b];
|
||||
return distanceA == distanceB ? a < b : distanceA < distanceB;
|
||||
});
|
||||
// Store distance in search order so query samples use one sequential early-out walk.
|
||||
// Ranger les distances dans le meme ordre pour arreter le parcours au premier echec.
|
||||
std::vector<std::int64_t> orderedDistances;
|
||||
orderedDistances.reserve(islandCount);
|
||||
for (const std::uint32_t islandIndex : result.groupSearchOrder)
|
||||
orderedDistances.push_back(result.groupDistanceLowerBoundsSubvoxels[islandIndex]);
|
||||
result.groupDistanceLowerBoundsSubvoxels = std::move(orderedDistances);
|
||||
|
||||
std::vector<std::uint8_t> selectedGroups(islandCount, 0);
|
||||
for (const std::uint32_t featureIndex : result.featureIndices)
|
||||
{
|
||||
if (featureIndex < islandCount) selectedGroups[featureIndex] = 1;
|
||||
else
|
||||
{
|
||||
const std::size_t edgeIndex = static_cast<std::size_t>(featureIndex) - islandCount;
|
||||
if (edgeIndex >= plan_->clusterEdges.size())
|
||||
{ result.groupBounds.clear(); result.featureIndices.clear(); return result; }
|
||||
const auto source = findSourceIsland(plan_->clusterEdges[edgeIndex]);
|
||||
if (source == plan_->islands.end())
|
||||
{ result.groupBounds.clear(); result.featureIndices.clear(); return result; }
|
||||
selectedGroups[static_cast<std::size_t>(source - plan_->islands.begin())] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// A connector is composed into its source island's group. Keep that source and every
|
||||
// outgoing connector in the tile list so the ordered smooth-min chain remains exact.
|
||||
// Un connecteur fait partie de son ile source : garder toute la chaine de fondu exacte.
|
||||
for (std::size_t islandIndex = 0; islandIndex < islandCount; ++islandIndex)
|
||||
{
|
||||
if (!selectedGroups[islandIndex]) continue;
|
||||
auto insertSorted = [&](std::uint32_t value)
|
||||
{
|
||||
const auto insertion = std::lower_bound(result.featureIndices.begin(),
|
||||
result.featureIndices.end(), value);
|
||||
if (insertion == result.featureIndices.end() || *insertion != value)
|
||||
result.featureIndices.insert(insertion, value);
|
||||
};
|
||||
insertSorted(static_cast<std::uint32_t>(islandIndex));
|
||||
for (std::size_t edgeIndex = 0; edgeIndex < plan_->clusterEdges.size(); ++edgeIndex)
|
||||
if (plan_->clusterEdges[edgeIndex].fromIslandId == plan_->islands[islandIndex].stableId)
|
||||
insertSorted(static_cast<std::uint32_t>(islandCount + edgeIndex));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::int64_t FloatingIslandFieldV1::densitySubvoxelsForTile(std::int64_t x,
|
||||
std::int64_t y, std::int64_t z,
|
||||
const FloatingIslandTileFeatureListV1& featureList) const noexcept
|
||||
{
|
||||
if (!isValid() || !floating_island_detail::ValidScalar(x)
|
||||
|| !floating_island_detail::ValidScalar(y) || !floating_island_detail::ValidScalar(z))
|
||||
return floating_island_detail::MaxDensity;
|
||||
std::uint32_t previous = 0;
|
||||
bool first = true;
|
||||
const std::size_t featureCount = plan_->islands.size() + plan_->clusterEdges.size();
|
||||
if (featureList.groupBounds.size() != plan_->islands.size())
|
||||
return floating_island_detail::FieldDensity(*recipe_, *plan_, x, y, z);
|
||||
for (const std::uint32_t index : featureList.featureIndices)
|
||||
{
|
||||
if (index >= featureCount || (!first && index <= previous))
|
||||
return floating_island_detail::FieldDensity(*recipe_, *plan_, x, y, z);
|
||||
first = false;
|
||||
previous = index;
|
||||
}
|
||||
return floating_island_detail::FieldDensityForTile(*recipe_, *plan_, x, y, z, featureList);
|
||||
}
|
||||
|
||||
FieldInterval FloatingIslandFieldV1::interval(std::int64_t minX, std::int64_t minY,
|
||||
std::int64_t minZ, std::int64_t maxX, std::int64_t maxY, std::int64_t maxZ) const noexcept
|
||||
{
|
||||
|
||||
@@ -32,6 +32,58 @@
|
||||
class FStandaloneShapeWorld
|
||||
{
|
||||
public:
|
||||
struct FNativeIslandTileFeatureContext
|
||||
{
|
||||
const VoxelForgeCore::FloatingIslandPlanV1* Plan = nullptr;
|
||||
const VoxelForgeCore::FloatingIslandTileFeatureListV1* FeatureList = nullptr;
|
||||
};
|
||||
class FScopedNativeIslandTileFeatureContext
|
||||
{
|
||||
public:
|
||||
FScopedNativeIslandTileFeatureContext(const VoxelForgeCore::FloatingIslandPlanV1* plan,
|
||||
const VoxelForgeCore::FloatingIslandTileFeatureListV1* featureList)
|
||||
{
|
||||
Context.Plan = plan;
|
||||
Context.FeatureList = featureList;
|
||||
Previous = SetNativeIslandTileFeatureContext(plan && featureList ? &Context : nullptr);
|
||||
}
|
||||
~FScopedNativeIslandTileFeatureContext()
|
||||
{ RestoreNativeIslandTileFeatureContext(Previous); }
|
||||
FScopedNativeIslandTileFeatureContext(const FScopedNativeIslandTileFeatureContext&) = delete;
|
||||
FScopedNativeIslandTileFeatureContext& operator=(const FScopedNativeIslandTileFeatureContext&) = delete;
|
||||
|
||||
private:
|
||||
FNativeIslandTileFeatureContext Context{};
|
||||
const FNativeIslandTileFeatureContext* Previous = nullptr;
|
||||
};
|
||||
static const FNativeIslandTileFeatureContext* SetNativeIslandTileFeatureContext(
|
||||
const FNativeIslandTileFeatureContext* context)
|
||||
{
|
||||
auto& active = ActiveNativeIslandTileFeatureContext();
|
||||
const auto* previous = active;
|
||||
active = context;
|
||||
return previous;
|
||||
}
|
||||
static void RestoreNativeIslandTileFeatureContext(const FNativeIslandTileFeatureContext* context)
|
||||
{ ActiveNativeIslandTileFeatureContext() = context; }
|
||||
static const FNativeIslandTileFeatureContext* GetNativeIslandTileFeatureContext()
|
||||
{ return ActiveNativeIslandTileFeatureContext(); }
|
||||
|
||||
double GetCoreVoxelSizeCm() const
|
||||
{
|
||||
return Owner == nullptr || Owner->CoreVoxelSizeMicrons == 0
|
||||
? static_cast<double>(VOXEL_SIZE)
|
||||
: static_cast<double>(Owner->CoreVoxelSizeMicrons) / 10000.0;
|
||||
}
|
||||
|
||||
double GetCoreVoxelsPerOwnerChunk() const
|
||||
{
|
||||
const double OwnerVoxelSizeCm = Owner == nullptr || Owner->OwnerVoxelSizeMicrons == 0
|
||||
? 0.5 * static_cast<double>(VOXEL_SIZE)
|
||||
: static_cast<double>(Owner->OwnerVoxelSizeMicrons) / 10000.0;
|
||||
return static_cast<double>(CHUNK_SIZE) * OwnerVoxelSizeCm / GetCoreVoxelSizeCm();
|
||||
}
|
||||
|
||||
enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel };
|
||||
struct FBoxClassification
|
||||
{
|
||||
@@ -1088,6 +1140,12 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
static const FNativeIslandTileFeatureContext*& ActiveNativeIslandTileFeatureContext()
|
||||
{
|
||||
static thread_local const FNativeIslandTileFeatureContext* Context = nullptr;
|
||||
return Context;
|
||||
}
|
||||
|
||||
static uint64 PackLatticeKey(int32 X,int32 Y,int32 Z)
|
||||
{
|
||||
constexpr uint64 Mask=0x1FFFFFull;
|
||||
@@ -1249,10 +1307,9 @@ public:
|
||||
bOtherStrateOverlap=true;
|
||||
}
|
||||
|
||||
// Floating islands use the explicit 12.5 cm owner to 25 cm Core mapping:
|
||||
// one legacy 4 m chunk occupies 16 Core voxels.
|
||||
// Les iles flottantes suivent l'echelle explicite proprietaire/Core: 16 voxels Core.
|
||||
constexpr int32 CoreVoxelsPerIslandChunk=CHUNK_SIZE/2;
|
||||
// Island strata preserve owner-authored physical bounds through the resolved scale ratio.
|
||||
// Les strates d'iles conservent les bornes physiques owner via le rapport de resolution.
|
||||
const double CoreVoxelsPerIslandChunk=GetCoreVoxelsPerOwnerChunk();
|
||||
const int32 FirstIslandChunk=FMath::FloorToInt(static_cast<float>(BMin[2])
|
||||
/static_cast<float>(CoreVoxelsPerIslandChunk));
|
||||
const int32 LastIslandChunk=FMath::FloorToInt(static_cast<float>(BMax[2])
|
||||
@@ -1340,11 +1397,8 @@ public:
|
||||
{
|
||||
if(!IslandPlan)return Finish(EVoxelTileClass::Mixed,true);
|
||||
constexpr std::int64_t Scale=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
constexpr std::int64_t CoreVoxelsPerLegacyChunk=CHUNK_SIZE/2;
|
||||
const std::int64_t Bottom=static_cast<std::int64_t>(IslandPlan->bottomChunkZ)
|
||||
*CoreVoxelsPerLegacyChunk*Scale;
|
||||
const std::int64_t Top=(static_cast<std::int64_t>(IslandPlan->topChunkZ)+1)
|
||||
*CoreVoxelsPerLegacyChunk*Scale;
|
||||
const std::int64_t Bottom=IslandPlan->manifest.recipe.layoutMinimumZSubvoxels;
|
||||
const std::int64_t Top=IslandPlan->manifest.recipe.layoutMaximumZSubvoxels;
|
||||
const std::int64_t Seal=IslandPlan->manifest.recipe.boundarySealSubvoxels;
|
||||
const std::int64_t BoxBottom=static_cast<std::int64_t>(BMin[2])*Scale;
|
||||
const std::int64_t BoxTop=static_cast<std::int64_t>(BMax[2])*Scale;
|
||||
@@ -1889,7 +1943,7 @@ private:
|
||||
||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))return false;
|
||||
// Convert Core query Z back to the owner's legacy chunk coordinate.
|
||||
// Convertir Z du Core vers la coordonnee de chunk legacy du proprietaire.
|
||||
constexpr int32 CoreVoxelsPerLegacyChunk=CHUNK_SIZE/2;
|
||||
const double CoreVoxelsPerLegacyChunk=GetCoreVoxelsPerOwnerChunk();
|
||||
const int32 ChunkZ=FMath::FloorToInt(Z/static_cast<float>(CoreVoxelsPerLegacyChunk));
|
||||
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
|
||||
!=ECaveGeneratorType::FloatingIslands)return false;
|
||||
@@ -1907,13 +1961,15 @@ private:
|
||||
const VoxelForgeCore::FloatingIslandFieldV1 Field(
|
||||
Plan->manifest.recipe,Plan->manifest.plan);
|
||||
if(!Field.isValid())return false;
|
||||
std::int64_t Density=Field.densitySubvoxels(ToFixed(X),ToFixed(Y),ToFixed(Z));
|
||||
// A legacy 4 m chunk is 16 Core voxels at the published 12.5 cm to 25 cm scale.
|
||||
// Un chunk legacy de 4 m represente 16 voxels Core avec l'echelle publiee.
|
||||
const std::int64_t Bottom=static_cast<std::int64_t>(Plan->bottomChunkZ)
|
||||
*CoreVoxelsPerLegacyChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t Top=(static_cast<std::int64_t>(Plan->topChunkZ)+1)
|
||||
*CoreVoxelsPerLegacyChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
std::int64_t Density=0;
|
||||
const auto* TileContext=GetNativeIslandTileFeatureContext();
|
||||
if(TileContext&&TileContext->Plan==&Plan->manifest.plan&&TileContext->FeatureList)
|
||||
Density=Field.densitySubvoxelsForTile(ToFixed(X),ToFixed(Y),ToFixed(Z),
|
||||
*TileContext->FeatureList);
|
||||
else
|
||||
Density=Field.densitySubvoxels(ToFixed(X),ToFixed(Y),ToFixed(Z));
|
||||
const std::int64_t Bottom=Plan->manifest.recipe.layoutMinimumZSubvoxels;
|
||||
const std::int64_t Top=Plan->manifest.recipe.layoutMaximumZSubvoxels;
|
||||
const std::int64_t Seal=Plan->manifest.recipe.boundarySealSubvoxels;
|
||||
const std::int64_t Zq=ToFixed(Z);
|
||||
if(Zq>=Bottom&&Zq<Bottom+Seal)Density=std::min(Density,-std::max<std::int64_t>(1,Zq-Bottom));
|
||||
|
||||
@@ -21,6 +21,8 @@ namespace vfcore::detail
|
||||
namespace VoxelForgeCore
|
||||
{
|
||||
using namespace ::vfcore::detail;
|
||||
static_assert(CHUNK_SIZE == static_cast<int32>(CoreTileVoxelCount),
|
||||
"Core tile indexing and VoxelForgeCore::CoreTileVoxelCount must agree");
|
||||
|
||||
struct ResolvedWorldSnapshot::Data
|
||||
{
|
||||
@@ -75,7 +77,9 @@ namespace VoxelForgeCore
|
||||
|
||||
WorldConfiguration MakeConfiguration(const FStandaloneOwnerWorld& owner)
|
||||
{
|
||||
WorldConfiguration out;
|
||||
WorldConfiguration out;
|
||||
out.voxelSizeMicrons=owner.CoreVoxelSizeMicrons;
|
||||
out.tileVoxelCount=CoreTileVoxelCount;
|
||||
out.seed=owner.Seed;
|
||||
out.clipRadius=owner.ClipRadius;
|
||||
out.maxLod=owner.MaxClipLevel;
|
||||
@@ -370,8 +374,9 @@ namespace VoxelForgeCore
|
||||
const int32 GY=static_cast<int32>(Index/static_cast<std::size_t>(Cells+1));
|
||||
const double XSub=static_cast<double>(TileOrigin.X+GX*GridStepVoxels)*Scale;
|
||||
const double YSub=static_cast<double>(TileOrigin.Y+GY*GridStepVoxels)*Scale;
|
||||
AddedVertices.emplace_back(XSub/Scale*25.0,YSub/Scale*25.0,
|
||||
Sample.Cap/Scale*25.0);
|
||||
const double VoxelSizeCm=static_cast<double>(Owner.CoreVoxelSizeMicrons)/10000.0;
|
||||
AddedVertices.emplace_back(XSub/Scale*VoxelSizeCm,YSub/Scale*VoxelSizeCm,
|
||||
Sample.Cap/Scale*VoxelSizeCm);
|
||||
AddedNormals.push_back(Sample.Normal);
|
||||
if(bHasUVs)AddedUVs.emplace_back(static_cast<float>(XSub/Scale),
|
||||
static_cast<float>(YSub/Scale));
|
||||
@@ -426,8 +431,9 @@ namespace VoxelForgeCore
|
||||
for(int32 I=0;I<Count;++I)
|
||||
{
|
||||
const auto& V=PolygonB[I];
|
||||
AddedVertices.emplace_back(V.XSub/Scale*25.0,
|
||||
V.YSub/Scale*25.0,V.ZSub/Scale*25.0);
|
||||
const double VoxelSizeCm=static_cast<double>(Owner.CoreVoxelSizeMicrons)/10000.0;
|
||||
AddedVertices.emplace_back(V.XSub/Scale*VoxelSizeCm,
|
||||
V.YSub/Scale*VoxelSizeCm,V.ZSub/Scale*VoxelSizeCm);
|
||||
AddedNormals.push_back(V.Normal);
|
||||
if(bHasUVs)AddedUVs.emplace_back(static_cast<float>(V.XSub/Scale),
|
||||
static_cast<float>(V.YSub/Scale));
|
||||
@@ -894,12 +900,14 @@ namespace VoxelForgeCore
|
||||
const FStandaloneShapeWorld* tileShapeContext=page?page.get():impl_->shape.get();
|
||||
UVoxelMarchingCubesMesher mesher;
|
||||
mesher.SetGenerator(world.Generator.get());
|
||||
mesher.SetGeometryVoxelSizeCm(25.0f);
|
||||
mesher.SetGeometryVoxelSizeCm(static_cast<float>(world.CoreVoxelSizeMicrons)/10000.0f);
|
||||
mesher.bStandaloneSkipUvAndColors=options.skipUvAndColors;
|
||||
mesher.bStandaloneSkipUV=options.skipUv;mesher.bStandaloneSkipColors=options.skipColors;
|
||||
mesher.bStandaloneSkipMaterialGroups=options.skipMaterialGroups;
|
||||
mesher.bGenerateSkirts=world.bGenerateSkirts;
|
||||
mesher.SkirtCells=static_cast<float>(world.SkirtCells)*12.5f/25.0f;
|
||||
mesher.SkirtCells=static_cast<float>(world.SkirtCells)
|
||||
*static_cast<float>(world.OwnerVoxelSizeMicrons)
|
||||
/static_cast<float>(world.CoreVoxelSizeMicrons);
|
||||
mesher.LODOctaveDrop=world.LODOctaveDrop;
|
||||
const int32 level=key.lod,extent=CHUNK_SIZE<<level;
|
||||
const FVoxelTileKey nativeKey=ToNative(key);
|
||||
@@ -1052,9 +1060,13 @@ namespace VoxelForgeCore
|
||||
// La surface 2D et les bornes de volume du manifeste definissent tout le support Z.
|
||||
// The 2D surface and manifest volume bounds cover every possible density crossing.
|
||||
const double TileCenterZ=static_cast<double>(origin.Z)+static_cast<double>(extent)*0.5;
|
||||
const int32 CenterChunkZ=FMath::FloorToInt(TileCenterZ/static_cast<double>(CHUNK_SIZE));
|
||||
const double CoreVoxelSizeCm=static_cast<double>(impl_->configuration.voxelSizeMicrons)/10000.0;
|
||||
const double OwnerVoxelSizeCm=impl_->owner!=nullptr&&impl_->owner->OwnerVoxelSizeMicrons>0
|
||||
?static_cast<double>(impl_->owner->OwnerVoxelSizeMicrons)/10000.0:0.5*static_cast<double>(VOXEL_SIZE);
|
||||
const double CoreVoxelsPerOwnerChunk=static_cast<double>(CHUNK_SIZE)*OwnerVoxelSizeCm/CoreVoxelSizeCm;
|
||||
const int32 CenterChunkZ=FMath::FloorToInt(TileCenterZ/CoreVoxelsPerOwnerChunk);
|
||||
const int32 StrateIndex=world.Manager->GetStrateIndex(
|
||||
static_cast<float>(TileCenterZ*VOXEL_SIZE));
|
||||
static_cast<float>(TileCenterZ*CoreVoxelSizeCm));
|
||||
const auto PlanIt=std::find_if(impl_->worldAssembly->plan.strates.begin(),
|
||||
impl_->worldAssembly->plan.strates.end(),[StrateIndex,CenterChunkZ](const auto& Plan)
|
||||
{return Plan.strateIndex==StrateIndex&&CenterChunkZ<=Plan.topChunkZ
|
||||
@@ -1142,9 +1154,60 @@ namespace VoxelForgeCore
|
||||
}
|
||||
}
|
||||
}
|
||||
FVoxelMeshData mesh=mesher.GenerateMesh(origin,step,cells,nullptr,
|
||||
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr);
|
||||
const WorldAssemblyFloatingIslandPlanV1* IslandTilePlan=nullptr;
|
||||
FloatingIslandTileFeatureListV1 IslandTileFeatureList;
|
||||
if(bHasWorldAssembly)
|
||||
{
|
||||
for(const auto& entry:impl_->worldAssembly->floatingIslands)
|
||||
{
|
||||
const auto& scale=entry.manifest.recipe;
|
||||
const std::int64_t ownerToCoreNumerator=scale.ownerToCoreScaleNumerator;
|
||||
const std::int64_t ownerToCoreDenominator=scale.ownerToCoreScaleDenominator;
|
||||
if(ownerToCoreNumerator<=0||ownerToCoreDenominator<=0)continue;
|
||||
const std::int64_t coreVoxelsPerOwnerChunk=static_cast<std::int64_t>(CHUNK_SIZE)
|
||||
*ownerToCoreNumerator/ownerToCoreDenominator;
|
||||
if(coreVoxelsPerOwnerChunk<=0)continue;
|
||||
const std::int64_t stratumBottom=static_cast<std::int64_t>(entry.bottomChunkZ)
|
||||
*coreVoxelsPerOwnerChunk;
|
||||
const std::int64_t stratumTop=(static_cast<std::int64_t>(entry.topChunkZ)+1)
|
||||
*coreVoxelsPerOwnerChunk;
|
||||
if(static_cast<std::int64_t>(origin.Z)>stratumTop
|
||||
||static_cast<std::int64_t>(origin.Z)+extent<stratumBottom)continue;
|
||||
IslandTilePlan=&entry;
|
||||
break;
|
||||
}
|
||||
if(IslandTilePlan)
|
||||
{
|
||||
const FloatingIslandFieldV1 field(IslandTilePlan->manifest.recipe,
|
||||
IslandTilePlan->manifest.plan);
|
||||
const std::int64_t scale=WorldAssemblySubvoxelsPerVoxel;
|
||||
// Include the mesher's one-sample gradient halo on each face.
|
||||
// Inclure la marge d'un echantillon du maillage sur chaque face.
|
||||
const std::int64_t sampleHalo=static_cast<std::int64_t>(step)*scale;
|
||||
const std::array<std::int64_t,3> tileMinimum={
|
||||
static_cast<std::int64_t>(origin.X)*scale-sampleHalo,
|
||||
static_cast<std::int64_t>(origin.Y)*scale-sampleHalo,
|
||||
static_cast<std::int64_t>(origin.Z)*scale-sampleHalo};
|
||||
const std::array<std::int64_t,3> tileMaximum={
|
||||
(static_cast<std::int64_t>(origin.X)+extent)*scale+sampleHalo,
|
||||
(static_cast<std::int64_t>(origin.Y)+extent)*scale+sampleHalo,
|
||||
(static_cast<std::int64_t>(origin.Z)+extent)*scale+sampleHalo};
|
||||
IslandTileFeatureList=field.buildFeatureTileList(tileMinimum,tileMaximum);
|
||||
}
|
||||
}
|
||||
FVoxelMeshData mesh;
|
||||
if(IslandTilePlan)
|
||||
{
|
||||
FStandaloneShapeWorld::FScopedNativeIslandTileFeatureContext tileFeatureContext(
|
||||
&IslandTilePlan->manifest.plan,&IslandTileFeatureList);
|
||||
mesh=mesher.GenerateMesh(origin,step,cells,nullptr,
|
||||
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr);
|
||||
}
|
||||
else
|
||||
mesh=mesher.GenerateMesh(origin,step,cells,nullptr,
|
||||
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr);
|
||||
const double MeshingMilliseconds=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-MeshingStart).count();
|
||||
const FStandaloneShapeWorld& farShape=tileShape.HasFarWorld()?tileShape:*impl_->shape;
|
||||
|
||||
@@ -11,9 +11,47 @@
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace VoxelForgeCore
|
||||
{
|
||||
std::vector<std::uint32_t> BuildWorldAssemblyFeatureTileListV1(
|
||||
std::span<const WorldAssemblyFeatureBoundsV1> features,
|
||||
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
|
||||
const std::array<std::int64_t, 3>& tileMaximumSubvoxels)
|
||||
{
|
||||
if (features.size() > std::numeric_limits<std::uint32_t>::max())
|
||||
throw std::length_error("world-assembly feature list exceeds 32-bit indexing");
|
||||
|
||||
const auto allFeatures = [&]()
|
||||
{
|
||||
std::vector<std::uint32_t> result;
|
||||
result.reserve(features.size());
|
||||
for (std::size_t index = 0; index < features.size(); ++index)
|
||||
result.push_back(static_cast<std::uint32_t>(index));
|
||||
return result;
|
||||
};
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
if (tileMinimumSubvoxels[axis] > tileMaximumSubvoxels[axis]) return allFeatures();
|
||||
for (const auto& feature : features)
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
if (feature.minimumSubvoxels[axis] > feature.maximumSubvoxels[axis]) return allFeatures();
|
||||
|
||||
std::vector<std::uint32_t> result;
|
||||
result.reserve(features.size());
|
||||
for (std::size_t index = 0; index < features.size(); ++index)
|
||||
{
|
||||
const auto& feature = features[index];
|
||||
bool intersects = true;
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
intersects = intersects
|
||||
&& feature.minimumSubvoxels[axis] <= tileMaximumSubvoxels[axis]
|
||||
&& feature.maximumSubvoxels[axis] >= tileMinimumSubvoxels[axis];
|
||||
if (intersects) result.push_back(static_cast<std::uint32_t>(index));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace world_assembly_detail
|
||||
{
|
||||
constexpr std::int64_t Q16 = 1ll << 16;
|
||||
@@ -1514,14 +1552,20 @@ bool WorldAssemblyManifest::finalize(std::string& error)
|
||||
if (!islands.manifest.validate(error)) return false;
|
||||
if (islands.manifest.recipe.seed != recipe.seed)
|
||||
{ error = "floating-island seed does not match the world assembly season seed"; return false; }
|
||||
constexpr std::int64_t CoreVoxelsPerLegacyChunk = 16;
|
||||
const std::int64_t bottomQ = static_cast<std::int64_t>(islands.bottomChunkZ)
|
||||
* CoreVoxelsPerLegacyChunk * WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t topQ = (static_cast<std::int64_t>(islands.topChunkZ) + 1)
|
||||
* CoreVoxelsPerLegacyChunk * WorldAssemblySubvoxelsPerVoxel;
|
||||
const auto& Scale = islands.manifest.recipe;
|
||||
if (Scale.ownerToCoreScaleNumerator == 0 || Scale.ownerToCoreScaleDenominator == 0)
|
||||
{ error = "floating-island owner-to-Core scale is invalid"; return false; }
|
||||
const double CoreVoxelsPerOwnerChunk = static_cast<double>(CoreTileVoxelCount)
|
||||
* Scale.ownerToCoreScaleNumerator / Scale.ownerToCoreScaleDenominator;
|
||||
const std::int64_t bottomQ = static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(islands.bottomChunkZ) * CoreVoxelsPerOwnerChunk
|
||||
* WorldAssemblySubvoxelsPerVoxel));
|
||||
const std::int64_t topQ = static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(static_cast<std::int64_t>(islands.topChunkZ) + 1)
|
||||
* CoreVoxelsPerOwnerChunk * WorldAssemblySubvoxelsPerVoxel));
|
||||
if (islands.manifest.recipe.layoutMinimumZSubvoxels < bottomQ
|
||||
|| islands.manifest.recipe.layoutMaximumZSubvoxels > topQ)
|
||||
{ error = "floating-island recipe layout escapes the resolved 12.5 cm legacy strate bounds"; return false; }
|
||||
{ error = "floating-island recipe layout escapes the resolved owner strate bounds"; return false; }
|
||||
islandStrateIds.push_back(islands.strateIndex);
|
||||
}
|
||||
recipe.contentHash = world_assembly_detail::HashRecipe(recipe);
|
||||
|
||||
@@ -1077,3 +1077,64 @@ The staged owner settings did contain the old manifest, and the plan test retain
|
||||
One remaining fixture caveat is recorded in [`WorldAssemblerM3/Round4/REPORT.md`](../../WorldAssemblerM3/Round4/REPORT.md): the staged saved `DA_Settings` points to `DA_Strate4`, while the owner’s read-only Play log resolves the active single SurfaceWorld from `DA_Strate1`. The BuildHost commandlet therefore reconstructed that logged one-slot layout as a transient copy at the real `[0,-63]` bounds. No Season asset was assigned in the staged settings, so the season-seed path was not tested. The report includes this distinction, the full pass/fail ledger, crash check, and editor hand-off. No owner project/editor was touched, no `.uasset` was edited, and no commit, push, or stash was made.
|
||||
|
||||
No new source file was added. The post-change UHT/BuildHost compile and Core link audit passed at four actions; the Core-only Standalone rebuild/audit passed. No `0xC0000142` retry or new UECC folder occurred.
|
||||
|
||||
## World assembler M3 Round 5 / Round S1 — 2026-10-18
|
||||
|
||||
The assembler now builds stable per-tile candidate feature lists for Floating Islands. The generic
|
||||
list builder uses conservative inclusive bounds and can serve caves, shafts, pits and passages too.
|
||||
At the current four-island plan, sampled p95 was 0.919 ms full scan and 0.910 ms with lists; building
|
||||
72 lists took 0.0396 ms total. At 256 islands and 240 connectors, sampled tile p95 fell from
|
||||
112.458 ms to 1.972 ms (about 57x); building 3,280 lists took 95.133 ms total (0.0461 ms p95/list).
|
||||
Both dropped-feature controls were caught.
|
||||
|
||||
The first default island hash comparison caught a real bug: empty-list air tiles skipped a distant
|
||||
root because version-2 root lobes use scaled ellipsoid density, which is not the geometric distance
|
||||
assumed by the initial search bound. The bound now includes the validated root slope and smooth-min
|
||||
reductions. The staged owner plan's every-sample candidate/full comparison had zero mismatches, and
|
||||
the 2,802-tile default W1/W8/W16 aggregate now exactly matches Round 4 for density, mesh, payload and
|
||||
topology.
|
||||
|
||||
After removing the temporary comparison code, the clean production 25 cm island actor passed again
|
||||
in 3m48s at 3.16 GiB peak. Its density/mesh/payload/topology hashes were
|
||||
`4dcfb37585d7d986 / 725a18374c5623de / fb345d8cb319d18b / 76f2846be8efb5f4`, matching Round 4.
|
||||
|
||||
Core scale now comes from AVoxelWorld.CoreVoxelSizeCm, default 25 cm, with 32 voxels per tile.
|
||||
Core-off stays on 12.5 cm legacy scale. Staged-owner BuildHost checks at 20 cm passed for SurfaceWorld
|
||||
and Floating Islands, including W1/W8/W16, exact surface anchors, water/atmosphere, BP carve/fill,
|
||||
collision and travel/streaming. The 20 cm controls detect the old 25 cm tile conversion and stale
|
||||
brush strength. Default SurfaceWorld and Floating Islands hashes match Round 4. The 20 cm Floating
|
||||
Islands run generated 2,850 tiles; island ground matched exactly, the connector span was within
|
||||
0.349 cm, water and atmosphere followed the resolved scale, and capsule contacts and BP edits passed.
|
||||
The default V2 actor run passed in 11m57s with 3.67 GiB peak; fixture repeats, borders and 125
|
||||
doorway phases passed too.
|
||||
|
||||
I kept per-strate sizes deferred because they need a shared physical boundary field, deterministic
|
||||
transition LODs, edit rasterization across both grids and a stable size map in world identity. The
|
||||
placement-cost measurement found SurfaceWorld already uses its planned ground directly. Island ground
|
||||
marching is expensive, but the tested top-anchor shortcut was slower, so no shortcut was kept. The
|
||||
sample had no actual placements; the report calls that out.
|
||||
|
||||
The outside review's strongest implemented suggestion is per-tile feature filtering. Per-strate
|
||||
resolution and macro-only far LOD are promising but need seam, edit, silhouette and no-pop evidence.
|
||||
Heading priority already exists; visibility weighting must preserve collision readiness. Keep bridges
|
||||
as field geometry because player edits stay unrestricted. Separate meshes remain appropriate for
|
||||
self-contained dreamcore set pieces.
|
||||
|
||||
CoreVoxelSizeCm is the new per-world setting. In plain terms, it controls how many centimetres one
|
||||
Core voxel covers; at 20 cm, a 32-voxel tile is 640 cm wide, while the default 25 cm tile is 800 cm.
|
||||
Core-off continues using the legacy 12.5 cm world. Per-strate sizes remain deferred until boundary
|
||||
sampling, transition LODs, cross-boundary edits and size-map hashing have explicit contracts.
|
||||
|
||||
The outside review is evaluated in the report. Per-strate size and macro-only far LOD rank highest
|
||||
among deferred ideas; heading scheduling already exists, and rock bridges stay in the editable field
|
||||
because carve/fill remains unrestricted. Placement measurements found that SurfaceWorld already
|
||||
reads its planned height directly. Island density marching is expensive, but the tested top-anchor
|
||||
shortcut was slower, so I kept no shortcut. Sampled content cells had zero placements.
|
||||
|
||||
The full report, exact run ledger, compile spots, source list, M2 checks and owner setting note are in
|
||||
[WorldAssemblerM3/Round5/REPORT.md](../../WorldAssemblerM3/Round5/REPORT.md). New source files are
|
||||
the standalone tile-list audit and its PowerShell build/run script. UHT/BuildHost compile and Core
|
||||
link-safety passed. No 0xC0000142 retry or new UECC folder occurred. I did not open the owner's
|
||||
editor or build his project, edit any .uasset, create a link, commit, push or stash.
|
||||
|
||||
— Luna
|
||||
@@ -0,0 +1,107 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ManifestPath,
|
||||
[string]$OutputDirectory
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$BuildHostProcess = Get-Process -Id $PID
|
||||
$BuildHostProcess.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal
|
||||
$BuildHostProcess.ProcessorAffinity = [IntPtr]0xFFFF
|
||||
if ([string]::IsNullOrWhiteSpace($ManifestPath))
|
||||
{ $ManifestPath = Join-Path $PSScriptRoot '..\Round3\results\floating_island_manifest.json' }
|
||||
if ([string]::IsNullOrWhiteSpace($OutputDirectory))
|
||||
{ $OutputDirectory = Join-Path $PSScriptRoot 'results' }
|
||||
$PluginRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..\..'))
|
||||
$StandaloneRoot = Join-Path $PluginRoot 'Standalone'
|
||||
$BuildRoot = Join-Path $StandaloneRoot 'Build\VoxelForgeCore'
|
||||
$SourcePath = Join-Path $PSScriptRoot 'FloatingIslandTileListAudit.cpp'
|
||||
$CoreLibrary = Join-Path $BuildRoot 'VoxelForgeCore.lib'
|
||||
$AuditExe = Join-Path $BuildRoot 'FloatingIslandTileListAudit.exe'
|
||||
$ResponsePath = Join-Path $BuildRoot 'floating-island-tile-list-audit.rsp'
|
||||
$VcVars = 'E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat'
|
||||
$ManifestPath = [IO.Path]::GetFullPath($ManifestPath)
|
||||
$OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory)
|
||||
|
||||
foreach ($Required in @($SourcePath, $CoreLibrary, $ManifestPath, $VcVars)) {
|
||||
if (-not (Test-Path -LiteralPath $Required -PathType Leaf)) { throw "Required audit input is missing: $Required" }
|
||||
}
|
||||
[IO.Directory]::CreateDirectory($OutputDirectory) | Out-Null
|
||||
$ResultPath = Join-Path $OutputDirectory 'tile_list_audit.json'
|
||||
$StdoutPath = Join-Path $OutputDirectory 'tile_list_audit.stdout.log'
|
||||
$StderrPath = Join-Path $OutputDirectory 'tile_list_audit.stderr.log'
|
||||
$LedgerPath = Join-Path $OutputDirectory 'tile_list_audit_run_ledger.txt'
|
||||
|
||||
$IncludeArgs = @('/nologo','/std:c++20','/O2','/arch:AVX2','/fp:precise','/EHsc','/D_CRT_SECURE_NO_WARNINGS',
|
||||
('/I"{0}"' -f (Join-Path $StandaloneRoot 'Core\include')))
|
||||
$ResponseLines = $IncludeArgs + @(('/Fe"{0}"' -f $AuditExe),('"{0}"' -f $SourcePath),
|
||||
('"{0}"' -f $CoreLibrary))
|
||||
[IO.File]::WriteAllLines($ResponsePath,$ResponseLines,[Text.UTF8Encoding]::new($false))
|
||||
Push-Location $BuildRoot
|
||||
try {
|
||||
$BuildSucceeded = $false
|
||||
for ($Attempt = 1; $Attempt -le 2 -and -not $BuildSucceeded; ++$Attempt) {
|
||||
$CompileCommand = 'call "{0}" >nul && cl.exe @"{1}"' -f $VcVars,$ResponsePath
|
||||
& $env:ComSpec /d /s /c $CompileCommand
|
||||
if ($LASTEXITCODE -eq -1073741502 -and $Attempt -eq 1) {
|
||||
Write-Warning 'Audit build exited with 0xC0000142; retrying once.'
|
||||
continue
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) { throw "Floating-island tile-list audit build failed with exit code $LASTEXITCODE" }
|
||||
$BuildSucceeded = $true
|
||||
}
|
||||
} finally { Pop-Location }
|
||||
|
||||
$RunRecords = [Collections.Generic.List[string]]::new()
|
||||
$Succeeded = $false
|
||||
for ($Attempt = 1; $Attempt -le 2 -and -not $Succeeded; ++$Attempt) {
|
||||
$Process = Start-Process -FilePath $AuditExe -ArgumentList @('"' + $ManifestPath + '"','"' + $ResultPath + '"') `
|
||||
-WorkingDirectory $BuildRoot -WindowStyle Hidden -RedirectStandardOutput $StdoutPath `
|
||||
-RedirectStandardError $StderrPath -PassThru
|
||||
$Process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal
|
||||
$Process.ProcessorAffinity = [IntPtr]0xFFFF
|
||||
$Clock = [Diagnostics.Stopwatch]::StartNew()
|
||||
[int64]$PeakPrivateBytes = 0
|
||||
$LimitFailure = $null
|
||||
while (-not $Process.WaitForExit(250)) {
|
||||
$Process.Refresh()
|
||||
$PeakPrivateBytes = [Math]::Max($PeakPrivateBytes,$Process.PrivateMemorySize64)
|
||||
if ($PeakPrivateBytes -gt 8GB) {
|
||||
$LimitFailure = '8 GiB private-memory limit exceeded'
|
||||
Stop-Process -Id $Process.Id -Force
|
||||
break
|
||||
}
|
||||
if ($Clock.Elapsed.TotalMinutes -ge 15.0) {
|
||||
$LimitFailure = '15-minute time box exceeded'
|
||||
Stop-Process -Id $Process.Id -Force
|
||||
break
|
||||
}
|
||||
}
|
||||
$Process.WaitForExit()
|
||||
$Clock.Stop()
|
||||
$ExitCode = $Process.ExitCode
|
||||
$StdoutText = if (Test-Path -LiteralPath $StdoutPath -PathType Leaf) { Get-Content -LiteralPath $StdoutPath -Raw } else { '' }
|
||||
$ExplicitPass = $StdoutText -match '(?m)^TILE_LIST_AUDIT_PASS\s*$'
|
||||
$RunPassed = $ExitCode -eq 0 -or ([string]::IsNullOrEmpty([string]$ExitCode) -and $ExplicitPass)
|
||||
$Status = if ($LimitFailure) { $LimitFailure } elseif ($RunPassed) { 'passed' } else { "failed exit=$ExitCode" }
|
||||
$RunRecords.Add("attempt=$Attempt status=$Status elapsed_s=$([Math]::Round($Clock.Elapsed.TotalSeconds,2)) peak_private_MiB=$([Math]::Round($PeakPrivateBytes/1MB,1)) exit=$ExitCode")
|
||||
if ($LimitFailure) { break }
|
||||
if ($RunPassed) { $Succeeded = $true; break }
|
||||
if ($ExitCode -eq -1073741502 -and $Attempt -eq 1) {
|
||||
$RunRecords.Add('retry_reason=0xC0000142 single permitted retry')
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
[IO.File]::WriteAllLines($LedgerPath,$RunRecords,[Text.UTF8Encoding]::new($false))
|
||||
if (-not $Succeeded) {
|
||||
Get-Content -LiteralPath $StdoutPath -ErrorAction SilentlyContinue
|
||||
Get-Content -LiteralPath $StderrPath -ErrorAction SilentlyContinue | Write-Error
|
||||
throw "Audit failed or exceeded a process limit. See $LedgerPath"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $ResultPath -PathType Leaf)) { throw "Audit did not write $ResultPath" }
|
||||
Write-Output "audit_results=$ResultPath"
|
||||
Get-Content -LiteralPath $LedgerPath | Write-Output
|
||||
Get-Content -LiteralPath $StdoutPath | Write-Output
|
||||
Get-Content -LiteralPath $StderrPath | Write-Output
|
||||
Get-Content -LiteralPath $ResultPath
|
||||
@@ -0,0 +1,435 @@
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <numeric>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
using Clock = std::chrono::steady_clock;
|
||||
constexpr std::int64_t Q = WorldAssemblySubvoxelsPerVoxel;
|
||||
constexpr std::int64_t TileQ = 32 * Q;
|
||||
constexpr std::int64_t TileStepQ = 4 * Q;
|
||||
constexpr std::int64_t MesherHaloQ = Q;
|
||||
|
||||
struct Tile
|
||||
{
|
||||
std::array<std::int64_t, 3> minimum{};
|
||||
std::array<std::int64_t, 3> maximum{};
|
||||
FloatingIslandTileFeatureListV1 featureList;
|
||||
double buildMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
struct Measurement
|
||||
{
|
||||
std::uint64_t tilesBuilt = 0;
|
||||
std::uint64_t sampledTiles = 0;
|
||||
std::uint64_t samples = 0;
|
||||
std::uint64_t candidateEvaluations = 0;
|
||||
std::uint64_t fullEvaluations = 0;
|
||||
std::uint64_t exactMismatches = 0;
|
||||
std::uint64_t droppedFeatureDetected = 0;
|
||||
double listBuildTotalMilliseconds = 0.0;
|
||||
double listBuildP95Milliseconds = 0.0;
|
||||
double beforeP95Milliseconds = 0.0;
|
||||
double afterP95Milliseconds = 0.0;
|
||||
double beforeMedianMilliseconds = 0.0;
|
||||
double afterMedianMilliseconds = 0.0;
|
||||
double meanCandidateFeatures = 0.0;
|
||||
double p95CandidateFeatures = 0.0;
|
||||
std::string beforeHash;
|
||||
std::string afterHash;
|
||||
bool negativeControlPassed = false;
|
||||
};
|
||||
|
||||
std::string Hex64(std::uint64_t value)
|
||||
{
|
||||
std::ostringstream output;
|
||||
output << std::hex << std::setfill('0') << std::setw(16) << value;
|
||||
return output.str();
|
||||
}
|
||||
|
||||
std::uint64_t HashValues(const std::vector<std::int64_t>& values)
|
||||
{
|
||||
std::uint64_t hash = 14695981039346656037ull;
|
||||
for (const auto value : values)
|
||||
for (int byte = 0; byte < 8; ++byte)
|
||||
{
|
||||
hash ^= static_cast<std::uint8_t>(static_cast<std::uint64_t>(value) >> (byte * 8));
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
double Percentile95(std::vector<double> values)
|
||||
{
|
||||
if (values.empty()) return 0.0;
|
||||
std::sort(values.begin(), values.end());
|
||||
const std::size_t index = (values.size() * 95 + 99) / 100 - 1;
|
||||
return values[std::min(index, values.size() - 1)];
|
||||
}
|
||||
|
||||
double Median(std::vector<double> values)
|
||||
{
|
||||
if (values.empty()) return 0.0;
|
||||
std::sort(values.begin(), values.end());
|
||||
const std::size_t middle = values.size() / 2;
|
||||
if (values.size() & 1u) return values[middle];
|
||||
return 0.5 * (values[middle - 1] + values[middle]);
|
||||
}
|
||||
|
||||
std::int64_t FloorDiv(std::int64_t value, std::int64_t divisor)
|
||||
{
|
||||
std::int64_t quotient = value / divisor;
|
||||
const std::int64_t remainder = value % divisor;
|
||||
if (remainder != 0 && ((remainder < 0) != (divisor < 0))) --quotient;
|
||||
return quotient;
|
||||
}
|
||||
|
||||
void ShiftBounds(std::array<std::int64_t, 3>& minimum,
|
||||
std::array<std::int64_t, 3>& maximum, const std::array<std::int64_t, 3>& delta)
|
||||
{
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
{ minimum[axis] += delta[axis]; maximum[axis] += delta[axis]; }
|
||||
}
|
||||
|
||||
std::uint64_t SyntheticId(std::uint32_t index, std::uint64_t salt)
|
||||
{
|
||||
std::uint64_t value = (static_cast<std::uint64_t>(index) << 32) ^ salt;
|
||||
value += 0x9e3779b97f4a7c15ull;
|
||||
value = (value ^ (value >> 30)) * 0xbf58476d1ce4e5b9ull;
|
||||
value = (value ^ (value >> 27)) * 0x94d049bb133111ebull;
|
||||
value ^= value >> 31;
|
||||
return value == 0 ? 1 : value;
|
||||
}
|
||||
|
||||
FloatingIslandPlanV1 MakeStressPlan(const FloatingIslandRecipeV1& recipe,
|
||||
const FloatingIslandPlanV1& source, std::size_t islandCount)
|
||||
{
|
||||
if (source.islands.empty()) throw std::runtime_error("source plan has no island template");
|
||||
FloatingIslandPlanV1 result;
|
||||
result.islands.reserve(islandCount);
|
||||
const auto& prototype = source.islands.front();
|
||||
constexpr std::size_t Columns = 16;
|
||||
const std::int64_t spacing = 80 * Q;
|
||||
for (std::size_t index = 0; index < islandCount; ++index)
|
||||
{
|
||||
FloatingIslandAnchorV1 island = prototype;
|
||||
const std::int64_t dx = static_cast<std::int64_t>(index % Columns) * spacing;
|
||||
const std::int64_t dy = static_cast<std::int64_t>(index / Columns) * spacing;
|
||||
const std::int64_t dz = static_cast<std::int64_t>(index % 3) * 4 * Q;
|
||||
const std::array<std::int64_t, 3> delta{dx, dy, dz};
|
||||
island.centerSubvoxels = {prototype.centerSubvoxels[0] + dx,
|
||||
prototype.centerSubvoxels[1] + dy, prototype.centerSubvoxels[2] + dz};
|
||||
island.topZSubvoxels += dz;
|
||||
island.bottomZSubvoxels += dz;
|
||||
ShiftBounds(island.influenceMinimumSubvoxels, island.influenceMaximumSubvoxels, delta);
|
||||
island.ordinal = static_cast<std::uint32_t>(index);
|
||||
island.stableId = SyntheticId(static_cast<std::uint32_t>(index), 0x49534c414e44ull);
|
||||
result.islands.push_back(island);
|
||||
}
|
||||
|
||||
result.clusterEdges.reserve(islandCount - islandCount / Columns);
|
||||
for (std::size_t index = 0; index < islandCount; ++index)
|
||||
{
|
||||
if (index % Columns + 1 >= Columns || index + 1 >= islandCount) continue;
|
||||
const auto& from = result.islands[index];
|
||||
const auto& to = result.islands[index + 1];
|
||||
FloatingIslandConnectorV1 edge;
|
||||
edge.stableId = SyntheticId(static_cast<std::uint32_t>(index), 0x425249444745ull);
|
||||
edge.fromIslandId = from.stableId;
|
||||
edge.toIslandId = to.stableId;
|
||||
edge.startSubvoxels = {from.centerSubvoxels[0] + from.radiusSubvoxels,
|
||||
from.centerSubvoxels[1], from.topZSubvoxels};
|
||||
edge.endSubvoxels = {to.centerSubvoxels[0] - to.radiusSubvoxels,
|
||||
to.centerSubvoxels[1], to.topZSubvoxels};
|
||||
edge.halfWidthSubvoxels = recipe.bridgeHalfWidthSubvoxels;
|
||||
edge.thicknessSubvoxels = recipe.bridgeThicknessSubvoxels;
|
||||
const std::int64_t pad = recipe.blendRadiusSubvoxels + recipe.roughnessSubvoxels;
|
||||
const std::int64_t archDepth = recipe.geometryVersion >= 2
|
||||
? 2 * edge.thicknessSubvoxels : 0;
|
||||
const std::int64_t xyPad = edge.halfWidthSubvoxels + pad;
|
||||
edge.influenceMinimumSubvoxels = {
|
||||
std::min(edge.startSubvoxels[0], edge.endSubvoxels[0]) - xyPad,
|
||||
std::min(edge.startSubvoxels[1], edge.endSubvoxels[1]) - xyPad,
|
||||
std::min(edge.startSubvoxels[2], edge.endSubvoxels[2])
|
||||
- edge.thicknessSubvoxels - pad - archDepth};
|
||||
edge.influenceMaximumSubvoxels = {
|
||||
std::max(edge.startSubvoxels[0], edge.endSubvoxels[0]) + xyPad,
|
||||
std::max(edge.startSubvoxels[1], edge.endSubvoxels[1]) + xyPad,
|
||||
std::max(edge.startSubvoxels[2], edge.endSubvoxels[2]) + pad};
|
||||
result.clusterEdges.push_back(edge);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Tile> BuildMixedTiles(const FloatingIslandFieldV1& field,
|
||||
const std::array<std::int64_t, 3>& tileLimitMinimum,
|
||||
const std::array<std::int64_t, 3>& tileLimitMaximum,
|
||||
Measurement& measurement)
|
||||
{
|
||||
std::vector<Tile> tiles;
|
||||
for (std::int64_t z = tileLimitMinimum[2]; z <= tileLimitMaximum[2]; z += TileQ)
|
||||
for (std::int64_t y = tileLimitMinimum[1]; y <= tileLimitMaximum[1]; y += TileQ)
|
||||
for (std::int64_t x = tileLimitMinimum[0]; x <= tileLimitMaximum[0]; x += TileQ)
|
||||
{
|
||||
Tile tile;
|
||||
tile.minimum = {x, y, z};
|
||||
tile.maximum = {x + TileQ, y + TileQ, z + TileQ};
|
||||
const std::array<std::int64_t, 3> sampleMinimum{
|
||||
x - MesherHaloQ, y - MesherHaloQ, z - MesherHaloQ};
|
||||
const std::array<std::int64_t, 3> sampleMaximum{
|
||||
x + TileQ + MesherHaloQ, y + TileQ + MesherHaloQ, z + TileQ + MesherHaloQ};
|
||||
const auto begin = Clock::now();
|
||||
tile.featureList = field.buildFeatureTileList(sampleMinimum, sampleMaximum);
|
||||
tile.buildMilliseconds = std::chrono::duration<double, std::milli>(Clock::now() - begin).count();
|
||||
measurement.listBuildTotalMilliseconds += tile.buildMilliseconds;
|
||||
++measurement.tilesBuilt;
|
||||
if (!tile.featureList.featureIndices.empty()) tiles.push_back(std::move(tile));
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
|
||||
Measurement MeasureField(const FloatingIslandFieldV1& field,
|
||||
std::array<std::int64_t, 3> limitMinimum,
|
||||
std::array<std::int64_t, 3> limitMaximum)
|
||||
{
|
||||
Measurement measurement;
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
limitMinimum[axis] = FloorDiv(limitMinimum[axis], TileQ) * TileQ;
|
||||
limitMaximum[axis] = FloorDiv(limitMaximum[axis], TileQ) * TileQ;
|
||||
}
|
||||
auto tiles = BuildMixedTiles(field, limitMinimum, limitMaximum, measurement);
|
||||
if (tiles.empty()) throw std::runtime_error("tile bounds produced no candidate tiles");
|
||||
std::vector<double> buildTimes, candidateCounts;
|
||||
buildTimes.reserve(tiles.size()); candidateCounts.reserve(tiles.size());
|
||||
for (const Tile& tile : tiles)
|
||||
{ buildTimes.push_back(tile.buildMilliseconds); candidateCounts.push_back(static_cast<double>(tile.featureList.featureIndices.size())); }
|
||||
measurement.listBuildP95Milliseconds = Percentile95(std::move(buildTimes));
|
||||
measurement.meanCandidateFeatures = std::accumulate(candidateCounts.begin(), candidateCounts.end(), 0.0)
|
||||
/ static_cast<double>(candidateCounts.size());
|
||||
measurement.p95CandidateFeatures = Percentile95(std::move(candidateCounts));
|
||||
|
||||
constexpr std::size_t MaximumSampleTiles = 64;
|
||||
if (tiles.size() > MaximumSampleTiles)
|
||||
{
|
||||
std::vector<Tile> selected;
|
||||
selected.reserve(MaximumSampleTiles);
|
||||
for (std::size_t slot = 0; slot < MaximumSampleTiles; ++slot)
|
||||
{
|
||||
const std::size_t index = slot * (tiles.size() - 1) / (MaximumSampleTiles - 1);
|
||||
selected.push_back(std::move(tiles[index]));
|
||||
}
|
||||
tiles = std::move(selected);
|
||||
}
|
||||
|
||||
std::vector<double> beforeTimes, afterTimes;
|
||||
std::vector<std::int64_t> beforeValues, afterValues;
|
||||
beforeTimes.reserve(tiles.size()); afterTimes.reserve(tiles.size());
|
||||
for (const Tile& tile : tiles)
|
||||
{
|
||||
// Cover the mesher's one-sample LOD0 halo as well as the tile at four-voxel spacing.
|
||||
// Couvrir la marge d'un echantillon du maillage LOD0 et la tuile a pas de quatre voxels.
|
||||
const auto buildAxis = [](std::int64_t minimum, std::int64_t maximum)
|
||||
{
|
||||
std::vector<std::int64_t> values;
|
||||
const std::int64_t start = minimum - MesherHaloQ;
|
||||
const std::int64_t end = maximum + MesherHaloQ;
|
||||
for (std::int64_t value = start; value <= end; value += TileStepQ)
|
||||
values.push_back(value);
|
||||
if (values.empty() || values.back() != end) values.push_back(end);
|
||||
return values;
|
||||
};
|
||||
const auto xs = buildAxis(tile.minimum[0], tile.maximum[0]);
|
||||
const auto ys = buildAxis(tile.minimum[1], tile.maximum[1]);
|
||||
const auto zs = buildAxis(tile.minimum[2], tile.maximum[2]);
|
||||
std::vector<std::array<std::int64_t, 3>> points;
|
||||
points.reserve(xs.size() * ys.size() * zs.size());
|
||||
for (const std::int64_t z : zs)
|
||||
for (const std::int64_t y : ys)
|
||||
for (const std::int64_t x : xs)
|
||||
points.push_back({x, y, z});
|
||||
measurement.samples += points.size();
|
||||
measurement.fullEvaluations += points.size();
|
||||
measurement.candidateEvaluations += points.size() * tile.featureList.featureIndices.size();
|
||||
|
||||
const auto beforeStart = Clock::now();
|
||||
for (const auto& point : points)
|
||||
beforeValues.push_back(field.densitySubvoxels(point[0], point[1], point[2]));
|
||||
beforeTimes.push_back(std::chrono::duration<double, std::milli>(Clock::now() - beforeStart).count());
|
||||
|
||||
const auto afterStart = Clock::now();
|
||||
for (const auto& point : points)
|
||||
afterValues.push_back(field.densitySubvoxelsForTile(
|
||||
point[0], point[1], point[2], tile.featureList));
|
||||
afterTimes.push_back(std::chrono::duration<double, std::milli>(Clock::now() - afterStart).count());
|
||||
++measurement.sampledTiles;
|
||||
}
|
||||
measurement.beforeP95Milliseconds = Percentile95(beforeTimes);
|
||||
measurement.afterP95Milliseconds = Percentile95(afterTimes);
|
||||
measurement.beforeMedianMilliseconds = Median(beforeTimes);
|
||||
measurement.afterMedianMilliseconds = Median(afterTimes);
|
||||
measurement.beforeHash = Hex64(HashValues(beforeValues));
|
||||
measurement.afterHash = Hex64(HashValues(afterValues));
|
||||
const std::size_t common = std::min(beforeValues.size(), afterValues.size());
|
||||
for (std::size_t index = 0; index < common; ++index)
|
||||
if (beforeValues[index] != afterValues[index]) ++measurement.exactMismatches;
|
||||
|
||||
if (field.plan() && !field.plan()->islands.empty())
|
||||
{
|
||||
constexpr std::uint32_t droppedFeature = 0;
|
||||
const auto& island = field.plan()->islands.front();
|
||||
const std::array<std::int64_t, 3> point{island.centerSubvoxels[0], island.centerSubvoxels[1],
|
||||
island.bottomZSubvoxels + (island.topZSubvoxels - island.bottomZSubvoxels) / 2};
|
||||
std::array<std::int64_t, 3> minimum{};
|
||||
std::array<std::int64_t, 3> maximum{};
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
minimum[axis] = FloorDiv(point[axis], TileQ) * TileQ;
|
||||
maximum[axis] = minimum[axis] + TileQ;
|
||||
}
|
||||
auto sampleMinimum = minimum;
|
||||
auto sampleMaximum = maximum;
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
sampleMinimum[axis] -= MesherHaloQ;
|
||||
sampleMaximum[axis] += MesherHaloQ;
|
||||
}
|
||||
FloatingIslandTileFeatureListV1 dropped = field.buildFeatureTileList(
|
||||
sampleMinimum, sampleMaximum);
|
||||
auto candidate = std::lower_bound(dropped.featureIndices.begin(), dropped.featureIndices.end(),
|
||||
droppedFeature);
|
||||
if (candidate != dropped.featureIndices.end() && *candidate == droppedFeature)
|
||||
{
|
||||
dropped.featureIndices.erase(candidate);
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
dropped.groupBounds[droppedFeature].minimumSubvoxels[axis] += 100000000000ll;
|
||||
dropped.groupBounds[droppedFeature].maximumSubvoxels[axis] += 100000000000ll;
|
||||
}
|
||||
const auto order = std::find(dropped.groupSearchOrder.begin(),
|
||||
dropped.groupSearchOrder.end(), droppedFeature);
|
||||
if (order != dropped.groupSearchOrder.end())
|
||||
{
|
||||
const std::size_t orderIndex = static_cast<std::size_t>(order - dropped.groupSearchOrder.begin());
|
||||
dropped.groupSearchOrder.erase(order);
|
||||
dropped.groupDistanceLowerBoundsSubvoxels.erase(
|
||||
dropped.groupDistanceLowerBoundsSubvoxels.begin() + orderIndex);
|
||||
dropped.groupSearchComplete = false;
|
||||
const auto full = field.densitySubvoxels(point[0], point[1], point[2]);
|
||||
const auto bad = field.densitySubvoxelsForTile(point[0], point[1], point[2], dropped);
|
||||
if (full != bad) ++measurement.droppedFeatureDetected;
|
||||
}
|
||||
}
|
||||
}
|
||||
measurement.negativeControlPassed = measurement.droppedFeatureDetected > 0;
|
||||
return measurement;
|
||||
}
|
||||
|
||||
void Print(const char* label, const Measurement& measurement,
|
||||
std::size_t islandCount, std::size_t connectorCount)
|
||||
{
|
||||
std::cout << label << " islands=" << islandCount << " connectors=" << connectorCount
|
||||
<< " list_tiles_built=" << measurement.tilesBuilt
|
||||
<< " sampled_tiles=" << measurement.sampledTiles
|
||||
<< " sample_count=" << measurement.samples
|
||||
<< " mean_candidates=" << measurement.meanCandidateFeatures
|
||||
<< " p95_candidates=" << measurement.p95CandidateFeatures
|
||||
<< " list_build_total_ms=" << measurement.listBuildTotalMilliseconds
|
||||
<< " list_build_p95_ms=" << measurement.listBuildP95Milliseconds
|
||||
<< " full_tile_p95_ms=" << measurement.beforeP95Milliseconds
|
||||
<< " candidate_tile_p95_ms=" << measurement.afterP95Milliseconds
|
||||
<< " full_tile_median_ms=" << measurement.beforeMedianMilliseconds
|
||||
<< " candidate_tile_median_ms=" << measurement.afterMedianMilliseconds
|
||||
<< " exact_mismatches=" << measurement.exactMismatches
|
||||
<< " full_hash=" << measurement.beforeHash
|
||||
<< " candidate_hash=" << measurement.afterHash
|
||||
<< " negative_control_detected=" << measurement.negativeControlPassed << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (argc != 3)
|
||||
{ std::cerr << "usage: FloatingIslandTileListAudit <manifest.json> <output.json>\n"; return 2; }
|
||||
std::ifstream input(argv[1], std::ios::binary);
|
||||
if (!input) throw std::runtime_error("could not open the recorded round-3 island manifest");
|
||||
const std::string json{std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
||||
std::string error;
|
||||
const auto manifest = FloatingIslandManifestV1::FromJson(json, error);
|
||||
if (!manifest || !manifest->validate(error)) throw std::runtime_error("manifest invalid: " + error);
|
||||
|
||||
FloatingIslandFieldV1 field(manifest->recipe, manifest->plan);
|
||||
if (!field.isValid()) throw std::runtime_error("round-3 production field did not validate");
|
||||
const auto bounds = field.influenceBounds();
|
||||
const Measurement today = MeasureField(field, bounds.minimumSubvoxels, bounds.maximumSubvoxels);
|
||||
|
||||
const FloatingIslandPlanV1 stressPlan = MakeStressPlan(manifest->recipe, manifest->plan, 256);
|
||||
FloatingIslandFieldV1 stressField(manifest->recipe, stressPlan);
|
||||
if (!stressField.isValid()) throw std::runtime_error("synthetic stress field did not validate");
|
||||
const auto stressBounds = stressField.influenceBounds();
|
||||
const Measurement stress = MeasureField(stressField,
|
||||
stressBounds.minimumSubvoxels, stressBounds.maximumSubvoxels);
|
||||
|
||||
Print("today", today, manifest->plan.islands.size(), manifest->plan.clusterEdges.size());
|
||||
Print("stress", stress, stressPlan.islands.size(), stressPlan.clusterEdges.size());
|
||||
std::ofstream output(argv[2], std::ios::trunc);
|
||||
if (!output) throw std::runtime_error("could not create audit result JSON");
|
||||
output << std::setprecision(12) << "{\n"
|
||||
<< " \"baseline\":{\"islands\":" << manifest->plan.islands.size()
|
||||
<< ",\"connectors\":" << manifest->plan.clusterEdges.size()
|
||||
<< ",\"tiles_built\":" << today.tilesBuilt << ",\"sampled_tiles\":" << today.sampledTiles
|
||||
<< ",\"samples\":" << today.samples << ",\"mean_candidates\":" << today.meanCandidateFeatures
|
||||
<< ",\"p95_candidates\":" << today.p95CandidateFeatures
|
||||
<< ",\"list_build_total_ms\":" << today.listBuildTotalMilliseconds
|
||||
<< ",\"list_build_p95_ms\":" << today.listBuildP95Milliseconds
|
||||
<< ",\"before_p95_ms\":" << today.beforeP95Milliseconds
|
||||
<< ",\"after_p95_ms\":" << today.afterP95Milliseconds
|
||||
<< ",\"before_median_ms\":" << today.beforeMedianMilliseconds
|
||||
<< ",\"after_median_ms\":" << today.afterMedianMilliseconds
|
||||
<< ",\"exact_mismatches\":" << today.exactMismatches
|
||||
<< ",\"before_hash\":\"" << today.beforeHash << "\",\"after_hash\":\"" << today.afterHash
|
||||
<< "\",\"negative_control\":" << (today.negativeControlPassed ? "true" : "false") << "},\n"
|
||||
<< " \"stress\":{\"islands\":" << stressPlan.islands.size()
|
||||
<< ",\"connectors\":" << stressPlan.clusterEdges.size()
|
||||
<< ",\"tiles_built\":" << stress.tilesBuilt << ",\"sampled_tiles\":" << stress.sampledTiles
|
||||
<< ",\"samples\":" << stress.samples << ",\"mean_candidates\":" << stress.meanCandidateFeatures
|
||||
<< ",\"p95_candidates\":" << stress.p95CandidateFeatures
|
||||
<< ",\"list_build_total_ms\":" << stress.listBuildTotalMilliseconds
|
||||
<< ",\"list_build_p95_ms\":" << stress.listBuildP95Milliseconds
|
||||
<< ",\"before_p95_ms\":" << stress.beforeP95Milliseconds
|
||||
<< ",\"after_p95_ms\":" << stress.afterP95Milliseconds
|
||||
<< ",\"before_median_ms\":" << stress.beforeMedianMilliseconds
|
||||
<< ",\"after_median_ms\":" << stress.afterMedianMilliseconds
|
||||
<< ",\"exact_mismatches\":" << stress.exactMismatches
|
||||
<< ",\"before_hash\":\"" << stress.beforeHash << "\",\"after_hash\":\"" << stress.afterHash
|
||||
<< "\",\"negative_control\":" << (stress.negativeControlPassed ? "true" : "false") << "}\n}\n";
|
||||
if (!today.negativeControlPassed || !stress.negativeControlPassed)
|
||||
throw std::runtime_error("dropped-feature negative control was not detected");
|
||||
if (today.exactMismatches || stress.exactMismatches)
|
||||
throw std::runtime_error("tile feature evaluation changed canonical field values");
|
||||
std::cout << "TILE_LIST_AUDIT_PASS\n";
|
||||
return 0;
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
std::cerr << "FLOATING_ISLAND_TILE_LIST_AUDIT_FAIL: " << exception.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
# World Assembler M3 Round 5 / World Assembler Round S1
|
||||
|
||||
**Date:** 2026-10-18
|
||||
**Scope:** tile-local feature lists first, one runtime Core voxel-size source, staged-owner placement-cost measurements, and evaluation of the supplied outside review.
|
||||
|
||||
## Result
|
||||
|
||||
The generic conservative tile-feature-list API is implemented and used by Floating Islands. Its field values and aggregate probe hashes match the full-scan control at four islands and a 256-island stress count; the deliberately dropped-feature control is detected.
|
||||
|
||||
Core now resolves physical voxel size from AVoxelWorld::CoreVoxelSizeCm. The default is 25 cm, the supported range in this integration is 12.5–25 cm, and CoreTileVoxelCount is 32. Core-off remains on the legacy 12.5 cm path. The size is carried in Core world configuration and non-default sizes affect world identity. This is per-world for now; the boundary, edit, LOD and hashing contract required for per-strate sizes is listed below.
|
||||
|
||||
The staged owner SurfaceWorld and Floating Islands routes passed end-to-end BuildHost checks at 20 cm, including W1/W8/W16, field/height anchors, content/water/atmosphere, BP carve/fill, collision and movement/streaming checks. Default 25 cm SurfaceWorld and Floating Islands aggregate hashes now match Round 4 exactly. A root-aware air-side search bound was needed to keep the tile-list field bit-identical outside island geometry; the staged 2,802-tile actor plan compared candidate and full-scan density at every sampled point with zero mismatches.
|
||||
|
||||
## 1. One Core voxel-size source
|
||||
|
||||
### Contract and implementation
|
||||
|
||||
- AVoxelWorld::CoreVoxelSizeCm is the one world-level Core voxel-size setting. GetActiveVoxelSizeCm() resolves it at runtime. CoreTileVoxelCount is 32 voxels; physical tile width at LOD L is derived from the active size, 32 and 2^L.
|
||||
- The size is represented in microns in VoxelForgeCore::WorldConfiguration alongside tileVoxelCount. Plan serialization and the season/content identity include the resolved non-default size. At the default 25 cm, the new identity suffix is omitted to preserve the old default identity.
|
||||
- Core-off reads the legacy VOXEL_SIZE path and does not use the Core setting.
|
||||
- Active Core physical lengths and coordinate conversions now derive from the getter for field/mesher geometry, transforms, height probes, content and tree placement, water, atmosphere, brushes and edit strength, collision bounds, streaming, clipmap and LOD. The legacy-grid manager bridge remains explicit and derives its ratio from the active size. Core tile coordinates stay in voxel units.
|
||||
- The SurfaceWorld Core-to-owner chunk-coordinate mapping is computed from active size; it no longer assumes a fixed 25-to-12.5 ratio.
|
||||
- The setting is a reflected AVoxelWorld field. Plain-language owner note: it changes how many centimetres one Core voxel covers; at the default of 25 cm the world keeps its old Core scale, and a value of 20 cm makes the same 32-voxel tile 640 cm wide instead of 800 cm. It does not change the legacy world when Core is off.
|
||||
|
||||
The staged 20 cm identity check reported SHA-1 4A62DB4F07FD958075856FA5A2F1B508937F757C; the alternate 25 cm identity was CCF5E42EC8405CDAB4065952AFFAA0DC932D3594. The default 25 cm check reported the inverse pair and distinct=1. This proves that peers using different active Core sizes do not share the same season/content identity.
|
||||
|
||||
### BuildHost proof
|
||||
|
||||
The staged-owner 20 cm SurfaceWorld and Floating Islands runs both completed in BuildHost. The SurfaceWorld run took 12m11s (731.65 seconds) and peaked at about 3.95 GiB. It reported W1/W8/W16-identical hashes:
|
||||
|
||||
| SurfaceWorld hash | 20 cm |
|
||||
|---|---|
|
||||
| Density | 0x0cd2a1a4152ee4aa |
|
||||
| Mesh | 0x540f8671bb53e7c8 |
|
||||
| Payload | 0x250aed7c9f875659 |
|
||||
| Topology | 0xed49c64851e0ce73 |
|
||||
|
||||
The exact native height and content ground were both -30,126.797 cm (0 cm delta); water was -29,260.781 cm; atmosphere bounds were (-40,320, 640) cm. Content anchors, water and atmosphere all followed the same planned scale. The BP carve/fill fixture passed with a 75 cm brush radius, Core strength 6.25, exact center density changes of +6.25/-6.25, a partial value at 60 cm and zero change at 80 cm. Collision bodies, anchor motion and W1/W8/W16 passed. The Core-off content gate reported legacy 12.5 cm.
|
||||
|
||||
The 20 cm Floating Islands actor route also passed W1/W8/W16, native island/span content anchors, water and atmosphere, Blueprint edits, capsule contacts on the island and connector, and Core-off legacy behavior. Its assembly hash was 6bc8e57a763a6ca5; recipe hash f12c1b9c291ff311; plan hash fbf909dc4bddcf18. Island top agreement was exact; connector span delta was 0.349 cm. Water was -29,260.801 cm and atmosphere bounds were (-40,320, 640) cm. Density/mesh/payload/topology matrix: 0x9dbd7dc8f3fe872f / 0xa120324982cd49b2 / 0xf836bd7f7288a9e3 / 0xbb6402d5aa697991.
|
||||
|
||||
The 20 cm checks also include two falsifiable scale controls:
|
||||
|
||||
1. Reintroducing a fixed 25 cm tile conversion predicts 800 cm for a 32-voxel tile; the active 20 cm result is 640 cm. The expected 160 cm span error is detected by the geometry audit.
|
||||
2. Reintroducing the default-size brush strength at 20 cm uses 5 instead of the required 6.25 Core density units. The BP carve/fill center-value assertion detects the stale magnitude and sign/direction checks still distinguish carve from fill.
|
||||
|
||||
At the default 25 cm, the fresh SurfaceWorld W1/W8/W16 hashes exactly match Round 4: 0x0cd2a1a4152ee4aa / 0x17775c835711e5a3 / 0xa27224958fc27d7b / 0x3821fea90b134ce4. Height delta was 0 cm, water was -36,575.977 cm, atmosphere bounds were (-50,400, 800) cm, and the BP edit, collision and streaming gates passed.
|
||||
|
||||
The default Floating Islands assembly, recipe and plan hashes remain 9c372e7bfb5b8310 / 5e0370d9f95f1c15 / dbbda4932378047a. After correcting the distant-group bound, the default W1/W8/W16 hashes are exactly the Round 4 aggregate: density 0x4dcfb37585d7d986, mesh 0x725a18374c5623de, payload 0xfb345d8cb319d18b, topology 0x76f2846be8efb5f4. The actor run used the same 2,802-key clipmap, and the temporary diagnostic compared candidate and full-scan density for every field call with no mismatches. Its height/content, water/atmosphere, capsule contact, Blueprint edit and Core-off checks passed.
|
||||
|
||||
At 20 cm, the corrected Floating Islands actor generated 2,850 tiles and passed W1/W8/W16: density/mesh/payload/topology 0xb4282f8a486c6e92 / 0x83a186d80937d98a / 0x92fe9b3bcfc25e44 / 0xaee5ad83fca9da82. Island ground matched terrain exactly; connector-span residual was 0.349 cm. Water was -29,260.801 cm, atmosphere bounds (-40,320, 640) cm, both capsule contacts passed, the 75 cm BP carve/fill test passed, and Core-off remained 12.5 cm legacy.
|
||||
|
||||
The default M2 V1/V2 analytic fixture-only validation passed exact repeats for five fixtures, same-LOD border identity, V2 marching-cubes identity and all 125 doorway phases. The full default V2 actor run also passed; the run ledger records its timing, hashes and log.
|
||||
|
||||
### Per-strate size decision
|
||||
|
||||
Per-strate resolution is deferred. It is not a safe scalar substitution: one physical query can cross a boundary where sample spacing changes. Before enabling it, define:
|
||||
|
||||
1. Canonical physical-space field queries and ownership rules at a strate boundary.
|
||||
2. Deterministic transition meshes and common LOD ancestry so both sides agree on shared borders.
|
||||
3. Edit rasterization/replay that reaches both grids with no holes or double application, including edits crossing the boundary.
|
||||
4. A stable sorted strate-ID-to-size map in plan/season identity, plus worker and border hashes that include both sizes.
|
||||
5. Streaming and collision policies for a tile spanning or supporting two resolutions.
|
||||
|
||||
## 2. Per-tile feature lists
|
||||
|
||||
BuildWorldAssemblyFeatureTileListV1 is the reusable API. It consumes conservative inclusive feature bounds and returns stable ordered feature indices for a tile. FloatingIslandFieldV1 builds the list once for each tile and the worker-scoped evaluation context sends densityAt through that candidate list. Bounds include the features' full influence; a feature that cannot be proven irrelevant stays in the list. This supports future caves, shafts, pits and passages without making each field family invent its own skip rule.
|
||||
|
||||
The Round 5 audit compares full scan and candidate-list output at exactly the same sample locations. The hash and mismatch assertions make a false skip observable.
|
||||
|
||||
| Plan | Features | List build | Full scan p95 / median per sampled tile | Candidate p95 / median | Candidates p95 | Equality |
|
||||
|---|---:|---:|---:|---:|---:|---|
|
||||
| Current | 4 islands + 3 connectors | 72 lists, 0.0396 ms total; 0.0009 ms p95/list | 0.9189 / 0.8644 ms | 0.9097 / 0.5010 ms | 6 | 60,000 samples; 0 mismatches; hashes 1ae5da3039bdcbdd / 1ae5da3039bdcbdd |
|
||||
| Stress | 256 islands + 240 connectors | 3,280 lists, 95.133 ms total; 0.0461 ms p95/list | 112.458 / 111.660 ms | 1.972 / 1.326 ms | 4 | 64,000 samples; 0 mismatches; hashes 7b92fa4e8d6acf49 / 7b92fa4e8d6acf49 |
|
||||
|
||||
The stress plan reduced sampled tile p95 from 112.46 ms to 1.97 ms (about 57×). List construction is a one-time per-plan/per-tile preparation cost in the audit; the stress build took 95.133 ms total, with the 95th percentile list taking 0.0461 ms. The negative control drops an island and its summary bound; the field-hash assertion fails and reports negative_control_detected=1. Both audit plans have exact_mismatches=0. The staged owner plan adds a broader check: the 2,802-tile default actor run compared candidate and full scan at each queried point and reproduced the Round 4 aggregate.
|
||||
|
||||
## 3. Content and tree placement measurement
|
||||
|
||||
The BuildHost commandlet timed the staged owner settings at 64 placement-grid cells and eight columns per cell. There are six authored decoration entries. The sampled cells produced zero actual placements, so the numbers below isolate ground-search cost and steady manager update overhead. They do not include spawn, component creation, instance upload or a populated placement cell. The per-frame number is the stationary manager Update measurement; it is not a rendered frame capture.
|
||||
|
||||
| Route | Voxel / cell width | Density queries per cell | Planned-surface queries per cell | Cell p95 | Stationary update p95 | Placements |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| SurfaceWorld Core 25 cm | 25 / 800 cm | 0 | 160 | 0.1945 ms | 0.0006 ms | 0 |
|
||||
| SurfaceWorld Core 20 cm | 20 / 640 cm | 0 | 160 | 0.2127 ms | 0.0005 ms | 0 |
|
||||
| SurfaceWorld legacy (20 cm comparison run) | 12.5 / 400 cm | 64 | 160 | 0.3326 ms | 0.0004 ms | 0 |
|
||||
| SurfaceWorld legacy (25 cm comparison run) | 12.5 / 400 cm | 64 | 160 | 0.3215 ms | 0.0004 ms | 0 |
|
||||
| Floating Islands Core 25 cm | 25 / 800 cm | 7,763.38 | 0 | 7.2511 ms | 0.0005 ms | 0 |
|
||||
| Floating Islands Core 20 cm | 20 / 640 cm | 7,528.38 | 0 | 7.2045 ms | 0.0007 ms | 0 |
|
||||
| Floating Islands legacy | 12.5 / 400 cm | 193.25 | 0 | 0.2928 ms | 0.0004 ms | 0 |
|
||||
|
||||
SurfaceWorld already reads its planned Heightfield2D directly; this eliminated density-column marching in the Core measurement while keeping the actor's content/terrain height agreement exact. Keep that path.
|
||||
|
||||
Floating Islands' generic density march is expensive on this sampled route. I tested using the existing island GetTopAnchor path in place of the march. It preserved the tested anchor to a maximum residual of 0.027344 Core density units but increased p95 from 7.2045 ms to 7.9887 ms and density queries from 7,528.38 to 7,569.69 per cell. The candidate change was reverted. The immediate recommendation is to build an exact, cached island-top query over the tile-local feature list, measure populated cells and total active frame cost, then adopt it only if it lowers cost while preserving exact anchor hashes and no-floating-tree checks. No production shortcut was retained.
|
||||
|
||||
## 4. Outside review evaluation
|
||||
|
||||
The supplied share is generic advice; it correctly separates planning, density evaluation and mesh generation and warns that a small feature cannot survive a coarse sampling grid merely because it has an SDF.
|
||||
|
||||
### What VoxelForge already has
|
||||
|
||||
- A startup director/adapter that builds a deterministic plan from resolved owner settings and authored strate layout; the plan and season identity are hashed.
|
||||
- Analytic Core field families, bounded features, conservative interval proofs and a stable construction plan for SurfaceWorld and Floating Islands.
|
||||
- A clipmap for Core streaming, worker-count-stable generation, an edit diff layer, collision-support radius/policy and Blueprint carve/fill through the canonical field.
|
||||
- Native SurfaceWorld heightfield sampling and a planned route with capsule support for Floating Islands.
|
||||
- M2 MC, hybrid and DC fixture routes, including repeated geometry and border checks.
|
||||
- Heading-aware near scheduling already promotes the current support tile and next tile along pawn heading. The supplied advice's general topology-first principle is already in the design.
|
||||
|
||||
The edit diff layer is an in-memory overlay in this route; this report does not claim save/load persistence or network transport. The current far LOD is not a complete macro-only representation that is proven to agree across every LOD.
|
||||
|
||||
### What the advice gets wrong for this game
|
||||
|
||||
The claim that bridges should generally be separate meshes ignores the owner's unrestricted player carve/fill rule. A terrain-connected bridge must remain in the editable canonical field, or any mesh needs a field and collision proxy with an explicit edit interaction contract. Keep rock connectors as field features. Independent meshes are suitable for self-contained dreamcore/megastructure set pieces and ordinary decorations where terrain edits need not cut through the object.
|
||||
|
||||
The advice also treats adaptive resolution as a mostly local sampling optimization. In this game it changes shared boundary samples, LOD transitions, collision support and how a player edit crosses resolutions. It is a useful direction but not a quick per-feature toggle. Trees can be separate instanced content because they do not need to modify the terrain field, but their roots/anchors still need exact planned ground placement.
|
||||
|
||||
### Ideas ranked by expected payoff
|
||||
|
||||
| Rank | Idea | Expected payoff | Evidence required |
|
||||
|---:|---|---|---|
|
||||
| 1 | Conservative per-tile feature lists | High and already implemented. The 256-island stress case cut sampled tile p95 from 112.46 ms to 1.97 ms with identical sampled values and a dropped-feature control. | Keep exhaustive/randomized bound probes, full-versus-candidate hashes, negative controls and list-build costs as each feature family is added. |
|
||||
| 2 | Per-strate resolution | High potential for broad mountains with fine caves, but high seam/edit risk. Deferred until the boundary contract is designed. | Boundary and transition LOD hashes on every worker count; no visible/collision cracks; edits across both resolutions replay identically; stable size-map identity. |
|
||||
| 3 | Far LOD from macro shape with cross-LOD agreement | Medium-high. It can reduce far detail and improve horizon shape. Current cap and repair work is not a proof of this full scheme. | Compare silhouette, ground profile and water datum across all LOD transitions; test camera approach and retreat for pops; verify edit regions retain the authoritative field. |
|
||||
| 4 | Visibility and heading-aware scheduling | Medium. Heading prefix exists; add visibility weighting only if it improves time-to-visible geometry without starving collision/support tiles or causing turn-around stalls. | Same pawn path at fixed speeds with visible tile-ready latency, collision-ready latency, rapid 180-degree turns and occluded-but-near tests. |
|
||||
| 5 | Separate structure meshes for selected set pieces | Conditional and lower payoff for this editable terrain. Use them for non-terrain-connected authored dreamcore pieces; keep bridges and traversable rock in the field. | Carve/fill through and around a structure, collision parity, seam behavior, and deterministic placement/streaming. |
|
||||
|
||||
## Run ledger, limits and failures
|
||||
|
||||
- BuildHost UBT/UHT and Core link-safety passed after the production edits. The Core link audit passed with 18 shim objects. Final production build passed on attempt 1.
|
||||
- The 20 cm SurfaceWorld actor run passed in 12m11s with about 3.95 GiB peak. The default 25 cm SurfaceWorld actor run passed in 11m53s. The corrected 20 cm Floating Islands actor run passed in about 4m11s with 3.18 GiB peak; it produced 2,850 tiles and passed W1/W8/W16, island/span anchors, water/atmosphere, capsule contacts, BP carve/fill and Core-off.
|
||||
- The corrected default 25 cm Floating Islands actor run passed in about 5m45s with 3.18 GiB peak. It compared each candidate density sample with the full scan and logged no mismatches. Its 2,802-tile W1/W8/W16 aggregate exactly matched Round 4 across density, mesh, payload and topology.
|
||||
- After removing the temporary comparison code, the clean production 25 cm Floating Islands run passed again in 3m48s with 3.16 GiB peak. It reproduced the same Round 4 aggregate and passed the live height/content anchor, water/atmosphere, capsule, BP edit and Core-off checks.
|
||||
- The hybrid fixture-only run passed at default 25 cm: V1/V2 five analytic fixtures, exact repeats, same-LOD borders, V2 MC identity and 125 doorway phases.
|
||||
- The full default V2 actor check passed: 11m57s (717.31 seconds), 3,944,955,904 peak commandlet private bytes (3.67 GiB). Its W1/W8/W16 SurfaceWorld hashes matched the 25 cm Round 4 hashes; height/content delta was 0 cm, BP carve and collision passed, and the V2 five-fixture, same-LOD border and 125-phase doorway checks passed. Log: Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/CoreWorldValidation-owner_surface.log.
|
||||
- Per-tile audit iterations: the first wrapper invocation used a PowerShell variable scope that lost the script-root path; it was corrected. A naive AABB candidate filter mismatched 493/10,565 samples and was replaced with a conservative sorted lower-bound query. A compile probe had a lambda mixed-return C3487/C2672 error and was corrected. The stress negative control initially retained the central island and its summary bounds, so it did not fail; the control was corrected to remove both. The wrapper also lost Process.ExitCode before an explicit pass-marker fallback was added. The final audit wrapper printed a missing `vswhere.exe` notice, but compiled and ran successfully on its first measured attempt.
|
||||
- The first post-halo default island candidate run passed the actor harness but failed the historical-hash invariant: density/mesh/payload/topology were 0x2f18fe65d88140f9 / 0x29fa0e8876a5a839 / 0xb1098a1ab6afa4bd / 0xec859a6373a47022. A full-scan control for the same 2,802 tiles reproduced Round 4 exactly. A runtime comparison then found candidate/full density mismatches on empty-list air tiles (for example fixed subvoxels `(-18944,-16896,-180736)`: 25,801 versus 18,135). The old skip bound assumed distance-like root magnitudes; v2 root lobes use a scaled ellipsoid distance. The bound now includes the validated root slope and smooth-min budget. The 4/256 audit and owner-plan full-scan comparison then passed bit-identically, restoring the Round 4 aggregate.
|
||||
- A first runtime diagnostic used `UE_LOG` in the Core shim, where logging is compiled out; the build warned C4390 and the run produced no probe markers. I replaced it with temporary stdout diagnostics, captured the mismatch above, then removed the diagnostic code. The final production build and corrected 20 cm actor run passed afterward.
|
||||
- The direct island top-anchor shortcut was a measured performance regression and was reverted after the full 20 cm island actor checks passed.
|
||||
- No run returned 0xC0000142; no retry for that status occurred. No process exceeded 8 GiB or its 15-minute step time box. No viewer was launched.
|
||||
- Crash audit: BuildHost has only the pre-existing UECC-Windows-F1A3EFD5469F5DD62E68EBBACAC84376_0000 folder dated 2026-09-17. No new UECC-* folder appeared after these runs.
|
||||
- The full SurfaceWorld and island actor tests used the staged copies of the owner's five assets and the transient owner-height layout described in Round 4. No owner asset or project file was edited. The owner's project/editor was not opened, closed or built. No .uasset, junction, symlink, commit, push or stash was created.
|
||||
|
||||
## Files and likely compile spots
|
||||
|
||||
New source files:
|
||||
|
||||
- Standalone/Research/WorldAssemblerM3/Round5/FloatingIslandTileListAudit.cpp
|
||||
- Standalone/Research/WorldAssemblerM3/Round5/BuildFloatingIslandTileListAudit.ps1 (audit build/run script)
|
||||
|
||||
New report: Standalone/Research/WorldAssemblerM3/Round5/REPORT.md. Dialogue entry: Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md.
|
||||
|
||||
Modified production/build sources:
|
||||
|
||||
- Source/VoxelForge/Public/VoxelWorld.h and Private/VoxelWorld.cpp
|
||||
- Source/VoxelForge/Public/VoxelContentManager.h and Private/VoxelContentManager.cpp
|
||||
- Source/VoxelForge/Public/VoxelForgeCoreAssetAdapter.h and Private/VoxelForgeCoreAdapter.cpp
|
||||
- Source/VoxelForge/Private/VoxelAtmosphereManager.cpp
|
||||
- Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp
|
||||
- Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
||||
- Standalone/Core/src/FarShellsInternal.h, FloatingIslandsInternal.h, ShapeWorldInternal.h, VoxelForgeCore.cpp and WorldAssemblerInternal.h
|
||||
- Standalone/Source/OwnerWorldLoader.h
|
||||
- Tools/BuildVoxelForgeCoreHost.ps1 and Tools/TestVoxelForgeCoreWorldHost.ps1
|
||||
- CODEMAP.md and ARCHITECTURE.md
|
||||
|
||||
Likely compile touchpoints are UHT/reflection for VoxelWorld.h and VoxelContentManager.h; runtime compilation of VoxelWorld.cpp, VoxelContentManager.cpp, VoxelForgeCoreAdapter.cpp and VoxelAtmosphereManager.cpp; Core API and implementation shims; and the editor commandlet. The completed BuildHost UHT/build and Core link-safety audit cover these touchpoints.
|
||||
|
||||
## Completion ledger
|
||||
|
||||
Implementation and requested proof runs are complete. The standalone tile-list audit passes at current and stress counts with zero mismatches and a detected dropped-feature control. The 20 cm BuildHost SurfaceWorld and Floating Islands cross-system checks pass; Core size changes world identity; default 25 cm SurfaceWorld, Floating Islands and M2 compatibility hashes match their recorded baselines. Content placement measurement found no measured spawn cost because the sampled cells produced zero placements; the direct SurfaceWorld heightfield path is retained and the slower island shortcut was reverted. Outside-review evaluation and the per-strate boundary/edit/LOD/hash plan are recorded above.
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"baseline":{"islands":4,"connectors":3,"tiles_built":72,"sampled_tiles":60,"samples":60000,"mean_candidates":2.63333333333,"p95_candidates":6,"list_build_total_ms":0.0396,"list_build_p95_ms":0.0009,"before_p95_ms":0.9189,"after_p95_ms":0.9097,"before_median_ms":0.8644,"after_median_ms":0.501,"exact_mismatches":0,"before_hash":"1ae5da3039bdcbdd","after_hash":"1ae5da3039bdcbdd","negative_control":true},
|
||||
"stress":{"islands":256,"connectors":240,"tiles_built":3280,"sampled_tiles":64,"samples":64000,"mean_candidates":3.13170731707,"p95_candidates":4,"list_build_total_ms":95.1326,"list_build_p95_ms":0.0461,"before_p95_ms":112.458,"after_p95_ms":1.972,"before_median_ms":111.66035,"after_median_ms":1.32645,"exact_mismatches":0,"before_hash":"7b92fa4e8d6acf49","after_hash":"7b92fa4e8d6acf49","negative_control":true}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
attempt=1 status=passed elapsed_s=7.54 peak_private_MiB=64.7 exit=
|
||||
@@ -9,6 +9,9 @@
|
||||
#include "VoxelStrateManager.h"
|
||||
#include "VoxelTerrainOpDefinition.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
struct FStandaloneParitySample
|
||||
{
|
||||
FVector PositionVoxels;
|
||||
@@ -38,6 +41,8 @@ struct FStandaloneOwnerWorld
|
||||
bool bGenerateSkirts = true;
|
||||
int32 SkirtCells = 2;
|
||||
int32 LODOctaveDrop = 0;
|
||||
std::uint32_t OwnerVoxelSizeMicrons = 125000;
|
||||
std::uint32_t CoreVoxelSizeMicrons = 250000;
|
||||
};
|
||||
|
||||
namespace StandaloneOwnerLoader
|
||||
@@ -294,22 +299,41 @@ namespace StandaloneOwnerLoader
|
||||
|
||||
inline bool Resolve(const VFJson::Value& Root, FStandaloneOwnerWorld& Out, std::string& Error)
|
||||
{
|
||||
if (Root.Get("voxel_size_cm").Number() != VOXEL_SIZE)
|
||||
{
|
||||
Error = "export voxel_size_cm does not match the standalone VOXEL_SIZE";
|
||||
return false;
|
||||
}
|
||||
const VFJson::Value& OwnerVoxelSize = Root.Get("owner_voxel_size_cm");
|
||||
const VFJson::Value& CoreVoxelSize = Root.Get("core_voxel_size_cm");
|
||||
const VFJson::Value& CoreVoxelSizeMicrons = Root.Get("core_voxel_size_microns");
|
||||
const VFJson::Value& OwnerVoxelSize = Root.Get("owner_voxel_size_cm");
|
||||
const VFJson::Value& OwnerVoxelSizeMicrons = Root.Get("owner_voxel_size_microns");
|
||||
const VFJson::Value& OwnerToCoreScale = Root.Get("owner_to_core_length_scale");
|
||||
if (!std::holds_alternative<std::nullptr_t>(OwnerVoxelSize.Data)
|
||||
&& (!OwnerVoxelSize.IsNumber() || OwnerVoxelSize.Number() != 12.5
|
||||
|| !CoreVoxelSize.IsNumber() || CoreVoxelSize.Number() != 25.0
|
||||
|| !OwnerToCoreScale.IsNumber() || OwnerToCoreScale.Number() != 0.5))
|
||||
const double CoreCm = CoreVoxelSize.Number(Root.Get("voxel_size_cm").Number(VOXEL_SIZE));
|
||||
const double OwnerCm = OwnerVoxelSize.Number(VOXEL_SIZE * 0.5);
|
||||
const double CoreMicrons = CoreVoxelSizeMicrons.Number(CoreCm * 10000.0);
|
||||
const double OwnerMicrons = OwnerVoxelSizeMicrons.Number(OwnerCm * 10000.0);
|
||||
if (!std::isfinite(CoreCm) || !std::isfinite(OwnerCm)
|
||||
|| CoreCm < VOXEL_SIZE * 0.5 || CoreCm > VOXEL_SIZE || OwnerCm <= 0.0
|
||||
|| std::abs(CoreMicrons - CoreCm * 10000.0) > 0.01
|
||||
|| std::abs(OwnerMicrons - OwnerCm * 10000.0) > 0.01
|
||||
|| !Root.Get("voxel_size_cm").IsNumber()
|
||||
|| std::abs(Root.Get("voxel_size_cm").Number() - CoreCm) > 0.0001)
|
||||
{
|
||||
Error = "owner-to-Core import scale must be 12.5 cm to 25 cm at a 0.5 length ratio";
|
||||
Error = "exported Core voxel size is invalid or inconsistent: core_cm="
|
||||
+ std::to_string(CoreCm) + " owner_cm=" + std::to_string(OwnerCm)
|
||||
+ " root_voxel_cm=" + std::to_string(Root.Get("voxel_size_cm").Number())
|
||||
+ " core_microns=" + std::to_string(CoreMicrons)
|
||||
+ " owner_microns=" + std::to_string(OwnerMicrons);
|
||||
return false;
|
||||
}
|
||||
const double ExpectedRatio = OwnerCm / CoreCm;
|
||||
const bool bHasImportScale = OwnerVoxelSize.IsNumber()
|
||||
|| CoreVoxelSize.IsNumber() || OwnerToCoreScale.IsNumber();
|
||||
if (bHasImportScale && (!OwnerVoxelSize.IsNumber() || !CoreVoxelSize.IsNumber()
|
||||
|| !OwnerToCoreScale.IsNumber()
|
||||
|| std::abs(OwnerToCoreScale.Number() - ExpectedRatio) > 1.0e-9))
|
||||
{
|
||||
Error = "owner-to-Core import scale does not match the resolved voxel sizes";
|
||||
return false;
|
||||
}
|
||||
Out.OwnerVoxelSizeMicrons = static_cast<std::uint32_t>(std::llround(OwnerMicrons));
|
||||
Out.CoreVoxelSizeMicrons = static_cast<std::uint32_t>(std::llround(CoreMicrons));
|
||||
Out.Seed = static_cast<int32>(Root.Get("seed").Integer());
|
||||
Out.ClipRadius = static_cast<int32>(Root.Get("clip_radius").Integer(6));
|
||||
Out.MaxClipLevel = static_cast<int32>(Root.Get("max_clip_level").Integer(8));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateRange(1, 4)][int]$MaxParallelActions = 4,
|
||||
[ValidateRange(1, 16)][int]$MaxParallelActions = 16,
|
||||
[ValidateRange(60, 900)][int]$TimeoutSeconds = 900
|
||||
)
|
||||
|
||||
@@ -166,7 +166,7 @@ $buildSucceeded = $false
|
||||
for ($attempt = 1; $attempt -le 2 -and -not $buildSucceeded; ++$attempt) {
|
||||
$attemptStdout = if ($attempt -eq 1) { $BuildStdout } else { Join-Path $LogRoot 'UBT-CoreIntegration.retry.stdout.log' }
|
||||
$attemptStderr = if ($attempt -eq 1) { $BuildStderr } else { Join-Path $LogRoot 'UBT-CoreIntegration.retry.stderr.log' }
|
||||
Write-Host "BuildHost UBT attempt $attempt of 2; max parallel actions=$MaxParallelActions; BelowNormal priority."
|
||||
Write-Host "BuildHost UBT attempt $attempt of 2; max parallel actions=$MaxParallelActions; Normal priority."
|
||||
Wait-ForLaunchMemory
|
||||
$foreignEditors = @(Get-Process -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.ProcessName -like 'UnrealEditor*' })
|
||||
@@ -177,7 +177,7 @@ for ($attempt = 1; $attempt -le 2 -and -not $buildSucceeded; ++$attempt) {
|
||||
$process = Start-Process -FilePath $dotnet -ArgumentList $argumentList -WorkingDirectory $PluginRoot `
|
||||
-RedirectStandardOutput $attemptStdout -RedirectStandardError $attemptStderr `
|
||||
-WindowStyle Hidden -PassThru
|
||||
$process.PriorityClass = [Diagnostics.ProcessPriorityClass]::BelowNormal
|
||||
$process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal
|
||||
$started = [datetime]::UtcNow
|
||||
while (-not $process.HasExited) {
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateRange(60, 900)][int]$TimeoutSeconds = 900,
|
||||
[ValidateRange(12.5, 25.0)][double]$CoreVoxelSizeCm = 25.0,
|
||||
[string]$WorldAssemblyManifestPath = '',
|
||||
[ValidateSet('MarchingCubesV1', 'WorldAssemblerHybridV1', 'WorldAssemblerHybridV2')][string]$CoreMesherRecipe = 'MarchingCubesV1',
|
||||
[switch]$EnableHybridFixturePlacement,
|
||||
@@ -34,11 +35,16 @@ $ZenData = Join-Path $OutputRoot 'ZenData'
|
||||
$EditorCmd = 'E:\Program Files\Epic Games\UE_5.7\Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
|
||||
$MemoryLimitBytes = 8GB
|
||||
$CrashRoots = @((Join-Path $HostRoot 'Saved\Crashes'), (Join-Path $StagePlugin 'Saved\Crashes'))
|
||||
$RunName = if ($FloatingIslandsOnly) { 'floating_islands' }
|
||||
elseif ($CoreFallbackTest -or $MismatchedManifestFallback) { 'mismatched_fallback' }
|
||||
elseif ($CoreOffTest) { 'core_off' }
|
||||
elseif ($CorePlanOnly) { if ($CoreTestSeed -ne [int]::MinValue) { "plan_seed_$CoreTestSeed" } else { 'plan_only' } }
|
||||
else { 'owner_surface' }
|
||||
$CoreSizeSuffix = ''
|
||||
if ([Math]::Abs($CoreVoxelSizeCm - 25.0) -gt 0.000001) {
|
||||
$CoreSizeText = $CoreVoxelSizeCm.ToString('0.####', [Globalization.CultureInfo]::InvariantCulture)
|
||||
$CoreSizeSuffix = "_voxel_$CoreSizeText"
|
||||
}
|
||||
$RunName = if ($FloatingIslandsOnly) { "floating_islands$CoreSizeSuffix" }
|
||||
elseif ($CoreFallbackTest -or $MismatchedManifestFallback) { "mismatched_fallback$CoreSizeSuffix" }
|
||||
elseif ($CoreOffTest) { "core_off$CoreSizeSuffix" }
|
||||
elseif ($CorePlanOnly) { if ($CoreTestSeed -ne [int]::MinValue) { "plan_seed_$CoreTestSeed$CoreSizeSuffix" } else { "plan_only$CoreSizeSuffix" } }
|
||||
else { "owner_surface$CoreSizeSuffix" }
|
||||
|
||||
if ($MismatchedManifestFallback) { $CoreFallbackTest = $true }
|
||||
$SelectedModeFlags = @(
|
||||
@@ -223,6 +229,7 @@ $BaseArguments = @(
|
||||
('-LocalDataCachePath="{0}"' -f $DerivedDataCache),
|
||||
'-run=VoxelForgeExplore', '-seed=0', '-archetype=TunnelNetwork', '-slot=0',
|
||||
'-modes=export', '-gameconfig=1', '-coreworldtest=1',
|
||||
('-corevoxelsizecm={0}' -f $CoreVoxelSizeCm.ToString('0.####', [Globalization.CultureInfo]::InvariantCulture)),
|
||||
("-coreislandtest=$FloatingIslandsOnlyFlag"),
|
||||
("-corefallbacktest=$CoreFallbackTestFlag"),
|
||||
("-coreofftest=$CoreOffTestFlag"),
|
||||
@@ -273,11 +280,11 @@ try {
|
||||
$ids = ($foreignEditors | ForEach-Object { "$($_.ProcessName):$($_.Id)" }) -join ', '
|
||||
throw "UnrealEditor is already running; leaving it untouched: $ids"
|
||||
}
|
||||
Write-Host "BuildHost Core actor commandlet attempt $Attempt of 2; BelowNormal priority; run=$RunName."
|
||||
Write-Host "BuildHost Core actor commandlet attempt $Attempt of 2; Normal priority; run=$RunName."
|
||||
$Process = Start-Process -FilePath $EditorCmd -ArgumentList $Arguments -WorkingDirectory $HostRoot `
|
||||
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath `
|
||||
-WindowStyle Hidden -PassThru
|
||||
$Process.PriorityClass = [Diagnostics.ProcessPriorityClass]::BelowNormal
|
||||
$Process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal
|
||||
$StartedUtc = [datetime]::UtcNow
|
||||
$PeakPrivateBytes = [int64]0
|
||||
while (-not $Process.HasExited) {
|
||||
|
||||
Reference in new issue
Block a user