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