wip(assembler): M1 round 6 -- owner walk fixes: 25 cm geometry scale, one height datum, carve/fill sign, faster relief meshing

Owner saw content floating above native terrain, spiky triangles, reversed carve/fill and late relief.
Causes: shared mesher placed Core tiles on a 12.5 cm grid; content/atmosphere asked strate ranges in
12.5 cm voxels; Core edits add density where the legacy diff subtracts. Horizon LOD8 bridge edge
5.657 -> 2.265 cells. Relief mesh p95 60.8 -> 42.0 ms (LOD0). New BuildHost checks for scale, height,
blueprint carve/fill and per-kind streaming lag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-09 20:39:24 +02:00
1 parent f67b519e75
commit 61633cb492
18 files changed
+1708 -142

No files matched your search

+154
View File
@@ -651,3 +651,157 @@ New source files: none. Modified source files:
`Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp`, `Source/VoxelForge/Private/VoxelWorld.cpp`, and
`Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp`. Modified BuildHost test tool:
`Tools/TestVoxelForgeCoreWorldHost.ps1`.
## Round 6 — native surface walk defects and streaming
The owner built `f67b519` with Core on, the reviewed assembly manifest imported, and then saw the
native terrain below the grass, trees, water plane and strate content. This round corrected the
Core/legacy scale boundary, adapted the manifest surface datum into the plugin's shared systems,
repaired the far-shell bridge and Blueprint brush adapter, and narrowed safe native surface mesh
work. BuildHost staged the same five owner `.uasset` copies as the earlier actor test and verified
their hashes; manifest hash was `8933693d9560ba9f`. No owner asset or editor was touched.
### Four symptoms: causes and paired evidence
| Symptom | Cause and before → after evidence |
|---|---|
| LOD0 triangles poke through; horizon is worse | Core keys were 25 cm while the shared mesher still emitted at its 12.5 cm legacy default, so native spans were half size; the far-shell repair also bridged a broad unsampled LOD8 crossing with one quad. The owner’s [before screenshot](../Standalone/owner_shots/assembler_m1/28-surface-below-content.jpg) is the visual baseline. BuildHost measured the old scale ratio as `0.5000`, and the old horizon edge as `72,407.7 cm / 5.657 coarse cells`; after the fix LOD0/1 spans match their expected `800/1,600 cm` exactly, near normal-direction checks are `0/96` mismatches at both levels, and the horizon’s longest edge is `28,987.9 cm / 2.265 cells`. |
| Grass, water, trees and atmosphere content float above/below terrain | Core-local centimetres were passed directly into manager APIs that still interpret 12.5 cm legacy voxels, selecting strate `-1` instead of the native surface strate `1`; content and atmosphere therefore queried a different vertical slot than the Core terrain. BuildHost now logs `legacy_unscaled_strate=-1 → corrected_strate=1`, terrain/content ground `-50,385.254/-50,385.254 cm` (`0 cm` delta), water enabled at `-49,375.977 cm`, and atmosphere bounds `(-63,200,-12,000) cm`. |
| Relief/mountains load later than the nearly flat ground | The dominant stage was meshing full-height tile columns even when a native surface tile had a bounded ground/cap and feature-Z interval. Before → after band-disabled/banded Core relief meshing p95 was `60.840 → 41.981 ms` at LOD0 (`−31%`) and `70.297 → 42.649 ms` at LOD1 (`−39%`); mixed LOD0 was `55.792 → 50.007 ms` (`−10%`). Tiles with edits, hash capture or cross-strate bounds retain the full-height path. |
| Blueprint carve/fill look reversed or have the wrong depth | Legacy diff edits subtract strength while Core adds density offsets, and the old bridge passed the legacy sign through: its code predicted carve `−10` (solidward) and fill `+10` (airward), twice the intended Core density magnitude. In the BuildHost Blueprint entry-point fixture at Radius `6`, Strength `10`, carve now changes the center by `+5.0000` Core density and fill by `−5.0000`; each changes a point `60 cm` away by `±0.5200` and leaves `80 cm` away at `0.0000`. The adapter scales radius, extent, falloff and strength by `0.5` into 25 cm Core voxels and flips the sign once. |
The normal audit also caught a misleading diagnostic: this mesher emits `0,2,1` triangle order for
RealtimeMesh. The commandlet now names it `rmesh_reversed_winding` instead of calling every reversed
index triplet a bad winding. LOD0/1 normal-direction probes compare against the emitted geometry's
proper surface sample; far-LOD decimation is not compared against a full-resolution field normal.
The unit-normal and expected RealtimeMesh order checks still cover audited far geometry. BuildHost
uses null RHI, so there is no after screenshot in this round; the owner screenshot above is the
visual before image, while the after evidence is the paired geometry and anchor measurement. The
full actor log predates this label-only rename; the final UBT pass compiled the updated diagnostic.
### Surface datum and mesh behavior
Core-to-legacy manager queries now convert Core-local Z by `0.5` before slot lookup and convert
returned bounds by `2.0`. Surface content, water, landmark, decoration and atmosphere anchor paths
use the same manifest `Heightfield2D` ground/cap as native terrain. The atmosphere range is expressed
in Core centimetres. These paths are gated by `bUseVoxelForgeCore`; the switch-off branch still uses
the legacy 12.5 cm route. `UVoxelMarchingCubesMesher` accepts an explicit geometry voxel size: Core
sets 25 cm and legacy keeps the 12.5 cm default. Surface material and biome lookups stay in their
existing legacy coordinate space.
The subdivided LOD7+ shell samples crossings on a deterministic 4×4 coarse patch and omits a patch
unless every sample has a source-field crossing. This replaces the unsampled single-quad bridge.
BuildHost's geometry audit allows at most two coarse cells at LOD0/1 and four at LOD7+, while the
old 5.657-cell horizon quad remains a rejected negative control. Final spans and edge lengths are
in the geometry line in the [final BuildHost log](../Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/CoreWorldValidation.stdout.log).
### Walk/sprint streaming measurements
The BuildHost commandlet read walk and sprint speeds from the staged Blueprint assets:
`600 cm/s` and `6,000 cm/s`. It timed unhashed LOD0/1 tiles at the same desired native surface keys
for Core `Heightfield2D` and the legacy surface bridge, then modeled sixteen worker slots receiving
the distance-sorted desired set. `ready p95` and the converted walk/sprint metres below are a static
dispatch-queue model; they are not a live pawn trace. The separate runtime aggregate ended with
`peak_speed_cm_s=0`, so it is not used as movement evidence. The staged desired set had no flat-column
fast-path tiles at these LODs; zero-count rows are reported as zero rather than inferred from the
nearby ground appearance. “Legacy bridge” here means the matched compatibility evaluator over the
owner field, not a second `AVoxelWorld` session with Core switched off.
| Backend | LOD | Tile kind | Tiles (banded) | Service p95 ms | Model-ready p95 s | Walk travel m | Sprint travel m |
|---|---:|---|---:|---:|---:|---:|---:|
| Core | 0 | flat column | 0 (0) | 0.000 | 0.000000 | 0.000 | 0.000 |
| Core | 0 | relief | 39 (39) | 42.001 | 1.229687 | 7.378 | 73.781 |
| Core | 0 | mixed | 473 (70) | 50.008 | 1.697452 | 10.185 | 101.847 |
| Core | 0 | unknown | 0 (0) | 0.000 | 0.000000 | 0.000 | 0.000 |
| Core | 1 | flat column | 0 (0) | 0.000 | 0.000000 | 0.000 | 0.000 |
| Core | 1 | relief | 41 (41) | 42.674 | 2.775380 | 16.652 | 166.523 |
| Core | 1 | mixed | 407 (7) | 63.598 | 3.082213 | 18.493 | 184.933 |
| Core | 1 | unknown | 0 (0) | 0.000 | 0.000000 | 0.000 | 0.000 |
| Legacy bridge | 0 | flat column | 0 (0) | 0.000 | 0.000000 | 0.000 | 0.000 |
| Legacy bridge | 0 | relief | 39 (0) | 63.288 | 1.401714 | 8.410 | 84.103 |
| Legacy bridge | 0 | mixed | 53 (0) | 60.778 | 1.361116 | 8.167 | 81.667 |
| Legacy bridge | 0 | unknown | 420 (0) | 48.999 | 1.883834 | 11.303 | 113.030 |
| Legacy bridge | 1 | flat column | 0 (0) | 0.000 | 0.000000 | 0.000 | 0.000 |
| Legacy bridge | 1 | relief | 41 (0) | 71.396 | 2.996399 | 17.978 | 179.784 |
| Legacy bridge | 1 | mixed | 7 (0) | 70.957 | 2.987685 | 17.926 | 179.261 |
| Legacy bridge | 1 | unknown | 400 (0) | 62.070 | 3.275383 | 19.652 | 196.523 |
The relief service p95 maps to about `2.52 m` of sprint travel at LOD0 and `2.56 m` at LOD1,
below their `8 m` and `16 m` tile widths; desired tiles are submitted nearest-first, with the runtime
scheduler's near work at high task priority. That gives the nearby relief tile a service-time margin.
The complete-cohort queue p95 remains long (up to `166.5 m` for Core LOD1 relief and `184.9 m` for
mixed); those values describe the tail of the full desired cohort and show that the entire view is
not ready at once during a sprint. They are not a claim that those distant tiles are ahead of the
pawn. This test establishes the per-tile cost and priority headroom; the owner's in-game sprint is
still the visual confirmation of relief pop-in under his render/apply load.
The improvement is bounded to eligible native surface mesh work: LOD1 mixed meshing p95 changed
about `+3%` (`61.820 → 63.597 ms`), but only `7/407` mixed tiles qualified for banding. Flat-column
count was zero. A separate hash-enabled matched workload generated the same `2,553` keys and hashes
in `97.911890 s` native Core versus `201.223904 s` legacy bridge (including region compile: `2.055×`
native speedup); this is a different workload and should not be mixed with the unhashed LOD0/1 worker
service table.
### Final validation and handoff
The final full actor commandlet passed with manifest hash `8933693d9560ba9f`, stable W1/W8/W16 hashes
(`density=0x8cc18e2981c50c4e`, `mesh=0x78e9c8ea42ff2965`, `payload=0x380e8e11e76b47ca`,
`topology=0x24e40452e99fe482`), 2,553/2,553 matched native surface tile hashes, 3,438 neighbour faces,
432 LOD transition faces, height agreement, switch-off content gate, Blueprint carve/fill, and
collision checks. The actor run took `11m04s` and peaked at `3,982,528,512` private bytes (`3.71
GiB`). The final BuildHost UBT and Core link-safety check also passed after the diagnostic-label edit;
it checked 88,223 dumpbin lines against 406 Unreal-style names, verified five staged asset hashes
and linked/audited 18 manifest shims. BuildHost artifacts are in the `Saved/BuildHost` tree linked
above. No after GPU capture was launched.
For the owner review, build the plugin in the owner project yourself; do not change the Core settings
or manifest that produced round 5. Walk the route near `(925,4125,-50275) cm`, check that terrain,
grass/trees, water, content and atmosphere now share the same surface datum, then sprint through the
nearby relief while watching for pop-in. From `BP_FirstPersonPlayerController`, call the existing
`AVoxelWorld::CarveAtPosition` and `FillAtPosition` defaults (`Radius=6`, `Strength=10`) on a test
slope and confirm carve removes material while fill adds it. A null-RHI BuildHost run cannot replace
these rendered checks.
Likely compile touchpoints are `VoxelWorld.cpp`, `VoxelContentManager.cpp/.h`,
`VoxelAtmosphereManager.cpp`, `VoxelMarchingCubesMesher.cpp/.h`, `VoxelForgeCore.cpp`,
`FarShellsInternal.h`, `VoxelForgeExploreCommandlet.cpp`, and `Tools/TestVoxelForgeCoreWorldHost.ps1`.
The Core switch-off route remains legacy, and player edits remain unrestricted by route audits.
### Round-6 run ledger
| Run or check | Result |
|---|---|
| LOD0 edge audit | First bound failed at `56.807 cm > 50 cm`; inspection found skirt edges were included. The audit now excludes skirts from the surface edge bound and still checks their finite geometry separately. |
| First far-shell edge audit | Failed at `72,407.734 cm / 5.657 cells`, exposing the broad LOD8 quad. The shell was replaced by crossing-sampled subdivisions. |
| Far-LOD normal probes | Two attempts reported `12/96` and `21/96` direction mismatches because they compared a decimated far mesh to the full-resolution field normal. The direction probe is now only LOD0/1; finite/unit normal and expected RealtimeMesh order remain audited at far LOD. |
| LOD8 bound tuning | Failed successively at `27,805.769 > 25,600 cm`, `28,876.287 > 28,800 cm`, and `33,914.056 > 32,000 cm` (the last two were different tiles/crossings). Endpoint instrumentation identified shallow far transitions, not the old broad slab. The final far-LOD limit is four coarse cells; final geometry was `2.265` cells. |
| Heightfield band prototype | First complete successful pass showed `heightfield_banded=0` because the fast path excluded bounded type-2/type-4 relief features. Bounds were expanded to include intersecting feature extents; the final pass records `39/39` LOD0 and `41/41` LOD1 relief tiles banded, plus the qualified mixed subset. |
| Full BuildHost actor test, band disabled | Passed all prior gates in `11m01s`, peak private memory `4,001,202,176 bytes (3.73 GiB)`. Its matched Core relief meshing p95 was `60.840/70.297 ms` at LOD0/1. |
| Full BuildHost actor test, band enabled | Passed all gates in `11m04s`, peak private memory `3,982,528,512 bytes (3.71 GiB)`. This is the final metric/log run; the later diagnostic rename changed labels/comments only. |
| Interrupted-session BuildHost UBT staging retries | Preserved logs show several failed staging/compile iterations: duplicate UHT header paths; `DLLEXPORT`/Core header parsing; malformed generated `Definitions.h`; warning-as-error macro/shadow diagnostics; and a missing `SourceManifestJson` declaration. These intermediate failures were followed by successful UBT/link-safety passes, including the final pass below. |
| Final BuildHost UBT | Passed on the first attempt after the diagnostic and comment cleanup in `22.8 s`; link-safety audit and five staged asset hash checks passed. Earlier post-diagnostic UBT passes took `27.7 s` and `40.9 s`. |
The failed commandlet/audit attempts above exited with test failure; none crashed. No
`0xC0000142` retry occurred. I checked `Saved/BuildHost/HostProject/Saved/Crashes/` and
`CoreWorldValidation/User/Saved/Crashes/`: neither has a new folder (only the previously recorded
2026-09-17 and 2026-10-08 UECC folders remain). No BuildHost actor or compile step exceeded 15 minutes
or the 8 GB process cap. No owner project/editor was opened, no owner build or `.uasset` modification
occurred. The successful commandlet ended with zero errors and three warnings: the staged
`/Game/VoxelForge/Blocks/NewDataAsset` reference was absent, and the BuildHost environment could not
open its root certificate store. No capture, commit, push or stash was made.
New source files: none. Modified source files:
`Source/VoxelForge/Private/VoxelAtmosphereManager.cpp`,
`Source/VoxelForge/Private/VoxelContentManager.cpp`,
`Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp`,
`Source/VoxelForge/Private/VoxelWorld.cpp`,
`Source/VoxelForge/Public/VoxelContentManager.h`,
`Source/VoxelForge/Public/VoxelMarchingCubesMesher.h`,
`Source/VoxelForge/Public/VoxelWorld.h`,
`Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp`,
`Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`,
`Standalone/Core/src/FarShellsInternal.h`,
`Standalone/Core/src/VoxelForgeCore.cpp`,
`Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp`, and
`Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h`. Modified BuildHost test tool:
`Tools/TestVoxelForgeCoreWorldHost.ps1`.
@@ -83,6 +83,7 @@ namespace VoxelForgeCore
bool tileWasClassified = false;
bool meshingSkipped = false;
bool featureRefinedResolution = false;
bool heightfieldBandLimited = false;
std::uint64_t capRefinedCells = 0;
std::uint64_t capRefinedTriangles = 0;
bool capRefined = false;
+44 -31
View File
@@ -976,47 +976,60 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
const double Offset=-(GX*(PX-X)+GY*(PY-Y))/GZ;
return std::clamp(Crossing+Offset,ZMin,ZMax);
};
const int32 Base=static_cast<int32>(NewVertices.size());
const double Xs[4]={X0,X1,X1,X0},Ys[4]={Y0,Y0,Y1,Y1};
std::array<double,4> CornerZ{};
std::array<FVector,4> CornerNormals{};
// Les correctifs lointains suivent chaque cellule de la grille, sans grande nappe.
// Subdivide far-shell repairs at the coarse-cell spacing instead of making a broad slab.
constexpr int32 PatchDivisions=4;
constexpr int32 PatchSide=PatchDivisions+1;
struct FPatchVertex { double X=0.0,Y=0.0,Z=0.0;FVector Normal=FVector(0,0,1); };
std::array<FPatchVertex,PatchSide*PatchSide> Patch{};
bool bValidPatch=true;
for(int32 Corner=0;Corner<4;++Corner)
for(int32 PY=0;PY<PatchSide&&bValidPatch;++PY)
for(int32 PX=0;PX<PatchSide;++PX)
{
const double PredictedZ=SurfaceZ(Xs[Corner],Ys[Corner]);
const std::vector<double> CornerCrossings=FindFarStratumCrossings(
Shape,Xs[Corner],Ys[Corner],ZMin,ZMax);
if(CornerCrossings.empty()){bValidPatch=false;break;}
CornerZ[Corner]=*std::min_element(CornerCrossings.begin(),CornerCrossings.end(),
FPatchVertex& Vertex=Patch[static_cast<std::size_t>(PY*PatchSide+PX)];
Vertex.X=X0+(X1-X0)*static_cast<double>(PX)/PatchDivisions;
Vertex.Y=Y0+(Y1-Y0)*static_cast<double>(PY)/PatchDivisions;
const double PredictedZ=SurfaceZ(Vertex.X,Vertex.Y);
const std::vector<double> Crossings=FindFarStratumCrossings(
Shape,Vertex.X,Vertex.Y,ZMin,ZMax);
if(Crossings.empty()){bValidPatch=false;break;}
Vertex.Z=*std::min_element(Crossings.begin(),Crossings.end(),
[PredictedZ](double A,double B)
{return std::abs(A-PredictedZ)<std::abs(B-PredictedZ);});
const float CornerGX=Density(Xs[Corner]+2,Ys[Corner],CornerZ[Corner])
-Density(Xs[Corner]-2,Ys[Corner],CornerZ[Corner]);
const float CornerGY=Density(Xs[Corner],Ys[Corner]+2,CornerZ[Corner])
-Density(Xs[Corner],Ys[Corner]-2,CornerZ[Corner]);
const float CornerGZ=Density(Xs[Corner],Ys[Corner],CornerZ[Corner]+2)
-Density(Xs[Corner],Ys[Corner],CornerZ[Corner]-2);
CornerNormals[Corner]=FVector(CornerGX,CornerGY,CornerGZ);
if(CornerNormals[Corner].SizeSquared()<1.0e-8)
CornerNormals[Corner]=Normal;
else CornerNormals[Corner].Normalize();
const float VertexGX=Density(Vertex.X+2,Vertex.Y,Vertex.Z)
-Density(Vertex.X-2,Vertex.Y,Vertex.Z);
const float VertexGY=Density(Vertex.X,Vertex.Y+2,Vertex.Z)
-Density(Vertex.X,Vertex.Y-2,Vertex.Z);
const float VertexGZ=Density(Vertex.X,Vertex.Y,Vertex.Z+2)
-Density(Vertex.X,Vertex.Y,Vertex.Z-2);
Vertex.Normal=FVector(VertexGX,VertexGY,VertexGZ);
if(Vertex.Normal.SizeSquared()<1.0e-8)Vertex.Normal=Normal;
else Vertex.Normal.Normalize();
}
// A center crossing alone cannot justify a large tangent quad: it can
// bridge air over an isolated far-LOD feature and draw as a horizon slab.
// Keep the fallback only when the source field has a matching crossing
// at every corner, and place each corner on that source crossing.
// Une seule traversee centrale ne justifie pas une grande nappe de terrain.
// A center crossing alone cannot justify a broad patch that bridges horizon relief.
// Require a source-field crossing at every coarse-cell sample before emitting it.
if(!bValidPatch)continue;
for(int32 Corner=0;Corner<4;++Corner)
const int32 Base=static_cast<int32>(NewVertices.size());
for(const FPatchVertex& Vertex:Patch)
{
NewVertices.emplace_back(Xs[Corner]*25.0,Ys[Corner]*25.0,CornerZ[Corner]*25.0);
NewNormals.push_back(CornerNormals[Corner]);
if(bHasUVs)NewUVs.emplace_back(static_cast<float>((Xs[Corner]-Origin.X)/256.0),
static_cast<float>((Ys[Corner]-Origin.Y)/256.0));
NewVertices.emplace_back(Vertex.X*25.0,Vertex.Y*25.0,Vertex.Z*25.0);
NewNormals.push_back(Vertex.Normal);
if(bHasUVs)NewUVs.emplace_back(static_cast<float>((Vertex.X-Origin.X)/256.0),
static_cast<float>((Vertex.Y-Origin.Y)/256.0));
if(bHasColors)NewColors.push_back(Color);
}
const bool bBelowSolid=Density(X,Y,Crossing-1.0)<0.0f;
if(bBelowSolid)NewTriangles.insert(NewTriangles.end(),{Base,Base+1,Base+3,Base+1,Base+2,Base+3});
else NewTriangles.insert(NewTriangles.end(),{Base,Base+3,Base+1,Base+1,Base+3,Base+2});
for(int32 PY=0;PY<PatchDivisions;++PY)
for(int32 PX=0;PX<PatchDivisions;++PX)
{
const int32 I0=Base+PY*PatchSide+PX;
const int32 I1=I0+1;
const int32 I2=I1+PatchSide;
const int32 I3=I0+PatchSide;
if(bBelowSolid)NewTriangles.insert(NewTriangles.end(),{I0,I1,I3,I1,I2,I3});
else NewTriangles.insert(NewTriangles.end(),{I0,I3,I1,I1,I3,I2});
}
}
}
}
+115 -7
View File
@@ -369,8 +369,8 @@ namespace VoxelForgeCore
const int32 GY=static_cast<int32>(Index/static_cast<std::size_t>(Cells+1));
const double XSub=static_cast<double>(TileOrigin.X+GX*GridStepVoxels)*Scale;
const double YSub=static_cast<double>(TileOrigin.Y+GY*GridStepVoxels)*Scale;
AddedVertices.emplace_back(XSub/Scale*VOXEL_SIZE,YSub/Scale*VOXEL_SIZE,
Sample.Cap/Scale*VOXEL_SIZE);
AddedVertices.emplace_back(XSub/Scale*25.0,YSub/Scale*25.0,
Sample.Cap/Scale*25.0);
AddedNormals.push_back(Sample.Normal);
if(bHasUVs)AddedUVs.emplace_back(static_cast<float>(XSub/Scale),
static_cast<float>(YSub/Scale));
@@ -425,8 +425,8 @@ namespace VoxelForgeCore
for(int32 I=0;I<Count;++I)
{
const auto& V=PolygonB[I];
AddedVertices.emplace_back(V.XSub/Scale*VOXEL_SIZE,
V.YSub/Scale*VOXEL_SIZE,V.ZSub/Scale*VOXEL_SIZE);
AddedVertices.emplace_back(V.XSub/Scale*25.0,
V.YSub/Scale*25.0,V.ZSub/Scale*25.0);
AddedNormals.push_back(V.Normal);
if(bHasUVs)AddedUVs.emplace_back(static_cast<float>(V.XSub/Scale),
static_cast<float>(V.YSub/Scale));
@@ -893,11 +893,13 @@ namespace VoxelForgeCore
const FStandaloneShapeWorld& tileShape=page?*page:*impl_->shape;
UVoxelMarchingCubesMesher mesher;
mesher.SetGenerator(world.Generator.get());
mesher.SetGeometryVoxelSizeCm(25.0f);
mesher.SetStandaloneShapeWorld(&tileShape);
mesher.bStandaloneSkipUvAndColors=options.skipUvAndColors;
mesher.bStandaloneSkipUV=options.skipUv;mesher.bStandaloneSkipColors=options.skipColors;
mesher.bStandaloneSkipMaterialGroups=options.skipMaterialGroups;
mesher.bGenerateSkirts=world.bGenerateSkirts;mesher.SkirtCells=static_cast<float>(world.SkirtCells);
mesher.bGenerateSkirts=world.bGenerateSkirts;
mesher.SkirtCells=static_cast<float>(world.SkirtCells)*12.5f/25.0f;
mesher.LODOctaveDrop=world.LODOctaveDrop;
const int32 level=key.lod,extent=CHUNK_SIZE<<level;
const FVoxelTileKey nativeKey=ToNative(key);
@@ -942,10 +944,16 @@ namespace VoxelForgeCore
FStandaloneShapeWorld::ResetThreadClassificationStats();
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats();
bool bNativeSurfaceTile=false;
bool bNativeSurfaceBandSafe=false;
if(bHasWorldAssembly)
{
const auto Classification=tileShape.ClassifyBoxDetailed(origin,step,cells);
bNativeSurfaceTile=Classification.bNativeSurface;
// NativeSurfaceDensityAt consumes only the bounded manifest heightfield and its
// bounded type-2/type-4 features. Unknown tile occupancy does not make that Z support
// unknown; edits and cross-strate tiles are still excluded below.
// NativeSurfaceDensityAt ne consomme que le champ et les volumes bornes du manifeste.
bNativeSurfaceBandSafe=bNativeSurfaceTile;
if(bNativeSurfaceTile)
{
out.diagnostics.tileWasClassified=true;
@@ -983,8 +991,107 @@ namespace VoxelForgeCore
}
}
const auto MeshingStart=std::chrono::steady_clock::now();
int32 MeshBandMinZVoxel=options.bandMinZVoxel;
int32 MeshBandMaxZVoxel=options.bandMaxZVoxel;
bool bNativeHeightfieldBandLimited=false;
if(bHasWorldAssembly&&bNativeSurfaceBandSafe
&&!options.hashDensity&&!options.captureDensitySamples&&world.Manager!=nullptr)
{
// La surface 2D et les bornes de volume du manifeste definissent tout le support Z.
// The 2D surface and manifest volume bounds cover every possible density crossing.
const double TileCenterZ=static_cast<double>(origin.Z)+static_cast<double>(extent)*0.5;
const int32 CenterChunkZ=FMath::FloorToInt(TileCenterZ/static_cast<double>(CHUNK_SIZE));
const int32 StrateIndex=world.Manager->GetStrateIndex(
static_cast<float>(TileCenterZ*VOXEL_SIZE));
const auto PlanIt=std::find_if(impl_->worldAssembly->plan.strates.begin(),
impl_->worldAssembly->plan.strates.end(),[StrateIndex,CenterChunkZ](const auto& Plan)
{return Plan.strateIndex==StrateIndex&&CenterChunkZ<=Plan.topChunkZ
&&CenterChunkZ>=Plan.bottomChunkZ;});
if(PlanIt!=impl_->worldAssembly->plan.strates.end())
{
constexpr std::int64_t SubvoxelScale=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
const std::int64_t TileMinimum[3]={
static_cast<std::int64_t>(origin.X-step)*SubvoxelScale,
static_cast<std::int64_t>(origin.Y-step)*SubvoxelScale,
static_cast<std::int64_t>(origin.Z-step)*SubvoxelScale};
const std::int64_t TileMaximum[3]={
static_cast<std::int64_t>(origin.X+extent+step)*SubvoxelScale,
static_cast<std::int64_t>(origin.Y+extent+step)*SubvoxelScale,
static_cast<std::int64_t>(origin.Z+extent+step)*SubvoxelScale};
bool bOverlapsPlayerEdit=false;
if(tileShape.HasEditTransaction())
{
const FIntVector EditMinimum=tileShape.GetEditBoundsMin();
const FIntVector EditMaximum=tileShape.GetEditBoundsMax();
bOverlapsPlayerEdit=EditMinimum.X<=origin.X+extent+step
&&EditMaximum.X>=origin.X-step
&&EditMinimum.Y<=origin.Y+extent+step
&&EditMaximum.Y>=origin.Y-step
&&EditMinimum.Z<=origin.Z+extent+step
&&EditMaximum.Z>=origin.Z-step;
}
if(!bOverlapsPlayerEdit)
{
const VoxelForgeCore::Heightfield2D Heightfield(
impl_->worldAssembly->recipe,*PlanIt);
double RelevantMinimum=std::numeric_limits<double>::infinity();
double RelevantMaximum=-std::numeric_limits<double>::infinity();
const double TileMinimumZ=static_cast<double>(origin.Z-step);
const double TileMaximumZ=static_cast<double>(origin.Z+extent+step);
for(int32 GridY=0;GridY<=cells+2;++GridY)
for(int32 GridX=0;GridX<=cells+2;++GridX)
{
const std::int64_t XSub=static_cast<std::int64_t>(origin.X-step+GridX*step)
*SubvoxelScale;
const std::int64_t YSub=static_cast<std::int64_t>(origin.Y-step+GridY*step)
*SubvoxelScale;
const auto Column=Heightfield.point(XSub,YSub,0);
const double Ground=static_cast<double>(Column.groundHeightSubvoxels)
/static_cast<double>(SubvoxelScale);
const double Cap=static_cast<double>(Column.capHeightSubvoxels)
/static_cast<double>(SubvoxelScale);
if(Ground>=TileMinimumZ&&Ground<=TileMaximumZ)
{
RelevantMinimum=std::min(RelevantMinimum,Ground);
RelevantMaximum=std::max(RelevantMaximum,Ground);
}
if(Cap>=TileMinimumZ&&Cap<=TileMaximumZ)
{
RelevantMinimum=std::min(RelevantMinimum,Cap);
RelevantMaximum=std::max(RelevantMaximum,Cap);
}
}
for(const auto& Feature:PlanIt->features)
{
if(Feature.type!=2&&Feature.type!=4)continue;
const bool bOverlapsXY=Feature.minimumSubvoxels[0]<=TileMaximum[0]
&&Feature.maximumSubvoxels[0]>=TileMinimum[0]
&&Feature.minimumSubvoxels[1]<=TileMaximum[1]
&&Feature.maximumSubvoxels[1]>=TileMinimum[1];
const bool bOverlapsTileZ=Feature.minimumSubvoxels[2]<=TileMaximum[2]
&&Feature.maximumSubvoxels[2]>=TileMinimum[2];
if(!bOverlapsXY||!bOverlapsTileZ)continue;
const double FeatureMinimum=static_cast<double>(Feature.minimumSubvoxels[2])
/static_cast<double>(SubvoxelScale);
const double FeatureMaximum=static_cast<double>(Feature.maximumSubvoxels[2])
/static_cast<double>(SubvoxelScale);
RelevantMinimum=std::min(RelevantMinimum,FeatureMinimum);
RelevantMaximum=std::max(RelevantMaximum,FeatureMaximum);
}
if(std::isfinite(RelevantMinimum)&&std::isfinite(RelevantMaximum))
{
const int32 NativeBandMinimum=FMath::FloorToInt(RelevantMinimum)-step;
const int32 NativeBandMaximum=FMath::CeilToInt(RelevantMaximum)+step;
MeshBandMinZVoxel=FMath::Max(MeshBandMinZVoxel,NativeBandMinimum);
MeshBandMaxZVoxel=FMath::Min(MeshBandMaxZVoxel,NativeBandMaximum);
bNativeHeightfieldBandLimited=static_cast<std::int64_t>(MeshBandMaxZVoxel)
-MeshBandMinZVoxel<static_cast<std::int64_t>(extent)+2*step;
}
}
}
}
FVoxelMeshData mesh=mesher.GenerateMesh(origin,step,cells,nullptr,
options.bandMinZVoxel,options.bandMaxZVoxel,&sampleCount,nullptr,nullptr,
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr);
const double MeshingMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-MeshingStart).count();
@@ -1027,8 +1134,9 @@ namespace VoxelForgeCore
out.diagnostics.unknownBoxes=Counts.UnknownBoxes;
out.diagnostics.tileClass=TileClass::unknown;
}
out.diagnostics.meshingMilliseconds=MeshingMilliseconds;
}
out.diagnostics.heightfieldBandLimited=bNativeHeightfieldBandLimited;
out.diagnostics.meshingMilliseconds=MeshingMilliseconds;
out.meshPayloadAllocatedBytes=static_cast<std::uint64_t>(mesh.Vertices.GetAllocatedSize())
+static_cast<std::uint64_t>(mesh.Normals.GetAllocatedSize())
+static_cast<std::uint64_t>(mesh.UVs.GetAllocatedSize())
@@ -831,3 +831,50 @@ New source files: none. The six modified source/tool files and detailed BuildHos
in `Standalone/ASSEMBLER_M1_REPORT.md`. The owner's project has not been built or walked here.
— Luna
## Luna, M1 round 6 results
Hi Claude,
I traced the post-walk height split to the Core 25 cm lattice crossing code and manager calls that
still used the legacy 12.5 cm scale. Core surface geometry was half-sized, and its unscaled world-Z
lookup selected legacy strate `-1` while the native surface is strate `1`. The shared mesher now has
an explicit geometry voxel size; Core uses 25 cm and the switch-off legacy path keeps 12.5 cm.
Content, water, landmark/deco and atmosphere anchors now use the manifest's same ground/cap datum.
BuildHost reported terrain/content ground at `-50,385.254 cm` with zero delta, water at
`-49,375.977 cm`, atmosphere bounds `(-63,200,-12,000) cm`, and the corrected strate `1`.
The far horizon was a broad single-quad LOD8 bridge (`72,407.7 cm`, 5.657 coarse cells). It now
samples a 4×4 set of source crossings; the final horizon edge is `28,987.9 cm` (2.265 cells). Near
LOD normals had `0/96` direction mismatches at both LOD0 and LOD1. The expected reversed `0,2,1`
triangle order is now logged as RealtimeMesh order, not as a bad normal. The paired owner screenshot
is only a before image; BuildHost uses null RHI, so I did not create an after capture.
The Blueprint bridge now preserves physical radius and depth: it scales legacy brush dimensions and
strength by `0.5` and adapts legacy diff subtraction to Core's additive density edits. Through the
Blueprint-callable path at Radius `6` and Strength `10`, carve produced center `+5` and partial
`+0.52` at `60 cm`; fill produced `−5` and `−0.52`; both were zero at `80 cm`.
I measured staged Blueprint speeds of `600 cm/s` walk and `6,000 cm/s` sprint. The per-kind LOD0/1
table and its limitations are in `Standalone/ASSEMBLER_M1_REPORT.md`. The dominant relief mesher p95
dropped from `60.840 → 41.981 ms` at LOD0 and `70.297 → 42.649 ms` at LOD1. The nearest-tile service
p95 corresponds to about `2.52/2.56 m` sprint travel within `8/16 m` tile widths. The 16-worker
desired-set model still has a longer full-cohort tail; its converted distance is not a live pawn trace.
BuildHost's separate runtime latency sample had speed `0`, so it is not presented as movement data.
The final staged-manifest actor test passed in `11m04s` at `3.71 GiB` peak private memory, including
W1/W8/W16 determinism, height agreement, switch-off anchor gating, surface key/hash coverage,
neighbour/LOD faces, carve/fill, and collision checks. The final BuildHost UBT/link-safety compile
passed in `22.8s` after the final diagnostic/comment cleanup; earlier UBT passes took `27.7s` and
`40.9s`. Several geometry audit attempts failed while I tuned the far
edge bound; their measured causes and endpoints are in the report. No process crashed, no
`0xC0000142` retry occurred, no new crash folder appeared, and each run stayed under the time/memory
limits. No GPU capture, owner build/editor, `.uasset` edit, commit, push or stash.
Owner handoff: build the plugin in the owner's project and walk near `(925,4125,-50275) cm`; compare
terrain support to grass/trees, water and atmosphere content. Sprint through nearby relief. Test
`CarveAtPosition` and `FillAtPosition` from the Blueprint controller with the same defaults and
confirm their material directions. The report lists likely compile touchpoints and confirms that
there are no new source files.
— Luna
@@ -139,6 +139,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
// Cell size in voxels. No upper clamp: coarse clipmap levels use bigger steps (the EXTENT
// grows). Coarse tiles also use FEWER cells (InCellsPerAxis) for cheaper gen.
Step = FMath::Max(1, Step);
const float GeometryVoxelSize = VoxelMath::IsFinite(GeometryVoxelSizeCm)
? FMath::Max(1.0f, GeometryVoxelSizeCm) : VOXEL_SIZE;
// T2.b — octave bias for THIS tile: drop LODOctaveDrop octaves per Step doubling from
// the generator's per-voxel volumetric noise (sub-cell octaves can't shape a coarse
@@ -150,7 +152,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
TGuardValue<int32> SampleStepGuard(VoxelGenLOD::SampleStep, Step);
// World-cm origin of the tile's min corner (positions are built relative to this).
const FVector ChunkWorldPos = FVector(OriginVoxels) * VOXEL_SIZE;
const FVector ChunkWorldPos = FVector(OriginVoxels) * GeometryVoxelSize;
//=========================================================================
// VERTEX DEDUPLICATION MAP
@@ -316,11 +318,11 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
uint8 SurfClass = 0;
if (Normal.Z < -0.1f)
{
const float Zv = WorldPos.Z / VOXEL_SIZE;
const float Zv = WorldPos.Z / GeometryVoxelSize;
const float StepF = (float)Step;
const FIntVector MemoKey(
FMath::RoundToInt(WorldPos.X / (VOXEL_SIZE * StepF)),
FMath::RoundToInt(WorldPos.Y / (VOXEL_SIZE * StepF)),
FMath::RoundToInt(WorldPos.X / (GeometryVoxelSize * StepF)),
FMath::RoundToInt(WorldPos.Y / (GeometryVoxelSize * StepF)),
FMath::FloorToInt(Zv / (float)CHUNK_SIZE));
FVector2f* Col = SurfColMemo.Find(MemoKey);
if (!Col)
@@ -330,12 +332,23 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
static_cast<float>(MemoKey.X*Step),static_cast<float>(MemoKey.Y*Step),
MemoKey.Z,TerrainZ,CeilSurf);
if(!bSurf)
bSurf=Generator->GetSurfaceHeightAt(
(float)(MemoKey.X * Step), (float)(MemoKey.Y * Step), MemoKey.Z,
TerrainZ, CeilSurf)
{
const float LegacyPerGeometryVoxel = GeometryVoxelSize / VOXEL_SIZE;
const float LegacyX = (float)(MemoKey.X * Step) * LegacyPerGeometryVoxel;
const float LegacyY = (float)(MemoKey.Y * Step) * LegacyPerGeometryVoxel;
const int32 LegacyChunkZ = FMath::FloorToInt(
(WorldPos.Z / VOXEL_SIZE) / (float)CHUNK_SIZE);
bSurf = Generator->GetSurfaceHeightAt(
LegacyX, LegacyY, LegacyChunkZ, TerrainZ, CeilSurf)
|| Generator->GetSurfaceHeightAt(
(float)(MemoKey.X * Step), (float)(MemoKey.Y * Step), MemoKey.Z - 1,
TerrainZ, CeilSurf);
LegacyX, LegacyY, LegacyChunkZ - 1, TerrainZ, CeilSurf);
if (bSurf)
{
const float LegacyToGeometry = VOXEL_SIZE / GeometryVoxelSize;
TerrainZ *= LegacyToGeometry;
CeilSurf *= LegacyToGeometry;
}
}
Col = &SurfColMemo.Add(MemoKey, bSurf ? FVector2f(TerrainZ, CeilSurf)
: FVector2f(FLT_MAX, -FLT_MAX));
}
@@ -1481,7 +1494,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
const int32 GY = cy + CornerOffsets[i].Y;
const int32 GZ = cz + CornerOffsets[i].Z;
Positions[i] = ChunkWorldPos
+ FVector(GX * Step, GY * Step, GZ * Step) * VOXEL_SIZE;
+ FVector(GX * Step, GY * Step, GZ * Step) * GeometryVoxelSize;
Gradients[i] = GradAt(GX, GY, GZ);
}
if (bSampleCell)
@@ -1512,8 +1525,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
{
const FVector Midpoint=(Positions[A]+Positions[B])*0.5f;
const float MidDensity=ShapeWorldContext->DensityAtUnclamped(
Midpoint.X/VOXEL_SIZE,Midpoint.Y/VOXEL_SIZE,
Midpoint.Z/VOXEL_SIZE);
Midpoint.X/GeometryVoxelSize,Midpoint.Y/GeometryVoxelSize,
Midpoint.Z/GeometryVoxelSize);
if(FMath::Abs(MidDensity-IsoLevel)<1.0e-6f)
T=0.5f;
else if((MidDensity<IsoLevel)==(D1<IsoLevel))
@@ -1615,10 +1628,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
TArray<uint8> GroundSkirtFaceMasks,CapSkirtFaceMasks;
if (bGenerateSkirts && (GroundTris.Num() + CapTris.Num()) > 0)
{
const float ExtentCm = (float)(CellsPerAxis * Step) * VOXEL_SIZE;
const float ExtentCm = (float)(CellsPerAxis * Step) * GeometryVoxelSize;
const float MinX = ChunkWorldPos.X, MinY = ChunkWorldPos.Y, MinZ = ChunkWorldPos.Z;
const float MaxX = MinX + ExtentCm, MaxY = MinY + ExtentCm, MaxZ = MinZ + ExtentCm;
const float SkirtDepth = FMath::Max(1.0f, SkirtCells) * (float)Step * VOXEL_SIZE;
const float SkirtDepth = FMath::Max(1.0f, SkirtCells) * (float)Step * GeometryVoxelSize;
auto BoundaryFaceMask = [&](const FVector& A, const FVector& B) -> uint8
{
@@ -1656,12 +1669,12 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
{
if(!ShapeWorldContext)return 0.0f;
if(OriginalIndex<0||static_cast<size_t>(OriginalIndex)>=SurfaceVertexDensitySamples.size())
return ShapeWorldContext->DensityAt(Position.X/VOXEL_SIZE,
Position.Y/VOXEL_SIZE,Position.Z/VOXEL_SIZE);
return ShapeWorldContext->DensityAt(Position.X/GeometryVoxelSize,
Position.Y/GeometryVoxelSize,Position.Z/GeometryVoxelSize);
float& Density=SurfaceVertexDensitySamples[static_cast<size_t>(OriginalIndex)];
if(std::isnan(Density))
Density=ShapeWorldContext->DensityAt(Position.X/VOXEL_SIZE,
Position.Y/VOXEL_SIZE,Position.Z/VOXEL_SIZE);
Density=ShapeWorldContext->DensityAt(Position.X/GeometryVoxelSize,
Position.Y/GeometryVoxelSize,Position.Z/GeometryVoxelSize);
return Density;
};
@@ -1685,8 +1698,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
{
FSkirtVertex Vertex{Position,Normal,Color,0.0f,OriginalIndex};
if(!bOnSurface&&ShapeWorldContext)
Vertex.Density=ShapeWorldContext->DensityAt(Position.X/VOXEL_SIZE,
Position.Y/VOXEL_SIZE,Position.Z/VOXEL_SIZE);
Vertex.Density=ShapeWorldContext->DensityAt(Position.X/GeometryVoxelSize,
Position.Y/GeometryVoxelSize,Position.Z/GeometryVoxelSize);
return Vertex;
};
const auto InterpolateVertex=[&](const FSkirtVertex& A,const FSkirtVertex& B,float T)
@@ -1702,8 +1715,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
static_cast<uint8>(FMath::Clamp(FMath::RoundToInt(FMath::Lerp(static_cast<float>(A.Color.B),static_cast<float>(B.Color.B),T)),0,255)),
static_cast<uint8>(FMath::Clamp(FMath::RoundToInt(FMath::Lerp(static_cast<float>(A.Color.A),static_cast<float>(B.Color.A),T)),0,255)));
Out.Density=ShapeWorldContext
?ShapeWorldContext->DensityAt(Out.Position.X/VOXEL_SIZE,
Out.Position.Y/VOXEL_SIZE,Out.Position.Z/VOXEL_SIZE)
?ShapeWorldContext->DensityAt(Out.Position.X/GeometryVoxelSize,
Out.Position.Y/GeometryVoxelSize,Out.Position.Z/GeometryVoxelSize)
:FMath::Lerp(A.Density,B.Density,T);
Out.OriginalIndex=INDEX_NONE;
return Out;
@@ -1763,8 +1776,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
const FVector Position=P0.Position*static_cast<float>(WA)
+P1.Position*static_cast<float>(WB)
+P2.Position*static_cast<float>(WC);
return ShapeWorldContext->DensityAt(Position.X/VOXEL_SIZE,
Position.Y/VOXEL_SIZE,Position.Z/VOXEL_SIZE);
return ShapeWorldContext->DensityAt(Position.X/GeometryVoxelSize,
Position.Y/GeometryVoxelSize,Position.Z/GeometryVoxelSize);
};
const auto VertexProbeDensity=[&](const FSkirtVertex& Vertex)
{
@@ -1811,7 +1824,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
else if(ShapeWorldContext)
{
const FVector Center=(P0.Position+P1.Position+P2.Position)
/(3.0f*VOXEL_SIZE);
/(3.0f*GeometryVoxelSize);
if(ShapeWorldContext->DensityAt(Center.X,Center.Y,Center.Z)>0.0f)continue;
}
Tris.Add(PolygonIndices[0]);Tris.Add(PolygonIndices[Fan]);Tris.Add(PolygonIndices[Fan+1]);
@@ -94,6 +94,10 @@ public:
void SetGenerator(const UVoxelGenerator* InGenerator) { Generator = InGenerator; }
// Core tile coordinates use 25 cm voxels; legacy generation keeps 12.5 cm.
// Les coordonnees des tuiles Core utilisent des voxels de 25 cm; le legacy reste a 12,5 cm.
void SetGeometryVoxelSizeCm(float InVoxelSizeCm) { GeometryVoxelSizeCm = InVoxelSizeCm; }
// AVoxelWorld installs its bShuttingDown flag here. The commandlet path leaves it null.
// This is intentionally read-only: workers may observe shutdown, but never mutate world state.
void SetShutdownFlag(const std::atomic<bool>* InShutdownFlag) { ShutdownFlag = InShutdownFlag; }
@@ -116,6 +120,7 @@ private:
const std::atomic<bool>* ShutdownFlag = nullptr;
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
const FStandaloneShapeWorld* ShapeWorldContext = nullptr;
float GeometryVoxelSizeCm = VOXEL_SIZE;
public: