wip(assembler): round L18 -- FloatingIslands false floors removed; shared strate ceiling
FI geometry v3 drops the all-XY lower boundary seal that created false floors (3.5/8 m); the ceiling comes from the shared VF_ApplyUpperBoundarySeal and AppendStratumBoundaryCeilings path for every generator, at each strate's own configured upper boundary (owner: every strate has a ceiling). Far-LOD ceiling continuity: 0 gaps at LOD4-8. SurfaceWorld bit-identical to L14 (14 configs + 24 m crossing); FI deterministic (28-row matrix); route parity and proofs pass. Unreal: FI geometryVersion 2->3 and the shared upper-seal helper extracted (UBT compile needed on the owner PC). Open: small ceiling holes; lower-stratum LOD ring steps seen through the FI void. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
No files matched your search
@@ -73,7 +73,7 @@ is the only density path; a degenerate (zero-height) strate has none and is open
|
|||||||
| Maze | `FMazeGenerationParams` | `BuildMazeStack` | origin-directed spanning-tree corridors on a 3D lattice, with capped loop edges and a thread-local per-cell capsule cache |
|
| Maze | `FMazeGenerationParams` | `BuildMazeStack` | origin-directed spanning-tree corridors on a 3D lattice, with capped loop edges and a thread-local per-cell capsule cache |
|
||||||
| SurfaceWorld | `FSurfaceGenerationParams` | `BuildSurfaceStack` | heightfield terrain: domain-warped continents+ridged mtns+detail, a low-freq **relief map** (`M`) that scales mountains/elevation for plains↔highland variety, **F20 heightfield terrain ops** (`Surface|Ops` — all default off ⇒ byte-identical): **Cliff** (slope-gated steepening — where the analytic structural slope > `CliffSlopeThreshold`, push the height away from the local mean by `CliffSharpness` ⇒ gentle slopes become sheer walls/canyon faces that hug real steep ground, gentle areas untouched; 4 structural resamples only when enabled), **Terrace** (relief-gated plateau quantize + `TerraceHardness` soft-round↔crisp-mesa), **LayerLines** (sedimentary sine shelves, slope-expressed) — pure per-column height remaps in the single oracle `ComputeSurfaceTerrainZ` (`SampleSurfaceStructuralZ` = pre-op raw height, re-sampled at an XY offset for Cliff's slope), biome-selected + border-blended for free via each biome's `SurfaceParams` + the height output-lerp; plus **phase-2 Overhang** (the first VOLUMETRIC op — real jutting shelves like a cliff lip): in `FOverhangShelfMod`, for AIR voxels in the window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height and unioned in. Low in the window the shift is ~0 (borrows nearby low rock ⇒ stays air over the void); high up it reaches the far cliff (solid) ⇒ a shelf attached to the cliff, tapering out over the void with air underneath. Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill dir `(DirX,DirY)` are resolved once in `ComputeSurfaceColumn` — the gradient sampled at the REACH scale (`OverhangReach`) so a point out over the void can "see" the cliff to know which way is uphill — and cached on `FSurfaceColumn`. It's genuine 3D (per-voxel structural re-eval), so it's gated hard to steep overhang columns; the union only ADDS rock (never removes), capped at `TerrainZ+OverhangHeight`, so `ClassifyTile` forces Mixed only in `(TerrainZ, TerrainZ+OverhangMargin]` (upward-only; margin = max `OverhangHeight`) — a shelf never holes a trivially-skipped tile, and only the thin cliff-edge band of air tiles is woken (NOT far-field like phase-3 spikes/holes). Overhang undersides classify as ground rock by the F17 rule (down-facing but below `TerrainZ`). Known v1 limit: applies at all LODs (Step-agnostic) — may alias far; gate to fine tiles later. Beaches at water line, high sky-cap ceiling. The cap is shapeable terrain in its own right (`ComputeSurfaceCeiling`, `Surface|Sky` params: `CeilingUndulation` broad inverted hills/valleys, `CeilingRidgeStrength` hanging ridgelines, `CeilingRoughness`+freq fine bumps, `CeilingWarp*`) — defaults (strengths 0, freq 0.04) = old flat-ish cap. (`Surface|Macro` params = the cheap precursor to biomes; `ReliefStrength=0` ⇒ old uniform terrain.) **Sky-cap vs ground is a PER-TRIANGLE surface class (F17), not a per-tile verdict**: the mesher classifies each unique vertex on the **worker** — only down-facing verts (`N.Z<-0.1`) pay a memoized `GetSurfaceHeightAt` column query; nearer `CeilSurf` ⇒ sky-cap, nearer `TerrainZ` ⇒ terrain overhang stays ground (a future SurfaceWorld cave roof — down-facing but below `TerrainZ` — also lands ground by the same rule; non-surface strates always ground). Triangles take the majority class of their 3 verts and are packed as **two contiguous index runs** (ground then cap, `FVoxelMeshData::NumCeilingTriangles`; skirts inherit their source triangle's run) → RMC **polygroups 0/1** → one **section per non-empty group** (`ApplyMeshToTile`, material slot = group index): slot 0 = strate `OverrideMaterial`/default (min-corner chunk), slot 1 = `CeilingMaterial` resolved at the tile's **TOP chunk** (mid as gap fallback, then min; fallback: ground material) — a coarse tile is 2^level chunks tall, so its min corner can sit in a lower strate/gap while the cap belongs to the strate above (this was the "far cap = ground material" residue), so the shadowless overhead rock is tinted separately instead of reading flat/bright. Shadow is **per section** via `FRealtimeMeshSectionConfig::bCastsShadow` (NOT the component `SetCastShadow` — RMC's proxy ignores the component flag; this is also why level≥2 far tiles only stopped casting once the section flag was wired): ground casts at level≤1, the cap section never casts, so the rock ceiling never shadows the terrain below it. History: v1 was a game-thread centre height-oracle (misclassified coarse far tiles → terrain material on the cap underside); v2 a whole-tile worker normal VOTE — which painted **mixed coarse tiles** (one far tile spanning terrain AND cap) entirely with the winner's material, and put sky material under terrain overhangs. The per-triangle class fixes both and is the identity channel caves/F8 will reuse. |
|
| SurfaceWorld | `FSurfaceGenerationParams` | `BuildSurfaceStack` | heightfield terrain: domain-warped continents+ridged mtns+detail, a low-freq **relief map** (`M`) that scales mountains/elevation for plains↔highland variety, **F20 heightfield terrain ops** (`Surface|Ops` — all default off ⇒ byte-identical): **Cliff** (slope-gated steepening — where the analytic structural slope > `CliffSlopeThreshold`, push the height away from the local mean by `CliffSharpness` ⇒ gentle slopes become sheer walls/canyon faces that hug real steep ground, gentle areas untouched; 4 structural resamples only when enabled), **Terrace** (relief-gated plateau quantize + `TerraceHardness` soft-round↔crisp-mesa), **LayerLines** (sedimentary sine shelves, slope-expressed) — pure per-column height remaps in the single oracle `ComputeSurfaceTerrainZ` (`SampleSurfaceStructuralZ` = pre-op raw height, re-sampled at an XY offset for Cliff's slope), biome-selected + border-blended for free via each biome's `SurfaceParams` + the height output-lerp; plus **phase-2 Overhang** (the first VOLUMETRIC op — real jutting shelves like a cliff lip): in `FOverhangShelfMod`, for AIR voxels in the window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height and unioned in. Low in the window the shift is ~0 (borrows nearby low rock ⇒ stays air over the void); high up it reaches the far cliff (solid) ⇒ a shelf attached to the cliff, tapering out over the void with air underneath. Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill dir `(DirX,DirY)` are resolved once in `ComputeSurfaceColumn` — the gradient sampled at the REACH scale (`OverhangReach`) so a point out over the void can "see" the cliff to know which way is uphill — and cached on `FSurfaceColumn`. It's genuine 3D (per-voxel structural re-eval), so it's gated hard to steep overhang columns; the union only ADDS rock (never removes), capped at `TerrainZ+OverhangHeight`, so `ClassifyTile` forces Mixed only in `(TerrainZ, TerrainZ+OverhangMargin]` (upward-only; margin = max `OverhangHeight`) — a shelf never holes a trivially-skipped tile, and only the thin cliff-edge band of air tiles is woken (NOT far-field like phase-3 spikes/holes). Overhang undersides classify as ground rock by the F17 rule (down-facing but below `TerrainZ`). Known v1 limit: applies at all LODs (Step-agnostic) — may alias far; gate to fine tiles later. Beaches at water line, high sky-cap ceiling. The cap is shapeable terrain in its own right (`ComputeSurfaceCeiling`, `Surface|Sky` params: `CeilingUndulation` broad inverted hills/valleys, `CeilingRidgeStrength` hanging ridgelines, `CeilingRoughness`+freq fine bumps, `CeilingWarp*`) — defaults (strengths 0, freq 0.04) = old flat-ish cap. (`Surface|Macro` params = the cheap precursor to biomes; `ReliefStrength=0` ⇒ old uniform terrain.) **Sky-cap vs ground is a PER-TRIANGLE surface class (F17), not a per-tile verdict**: the mesher classifies each unique vertex on the **worker** — only down-facing verts (`N.Z<-0.1`) pay a memoized `GetSurfaceHeightAt` column query; nearer `CeilSurf` ⇒ sky-cap, nearer `TerrainZ` ⇒ terrain overhang stays ground (a future SurfaceWorld cave roof — down-facing but below `TerrainZ` — also lands ground by the same rule; non-surface strates always ground). Triangles take the majority class of their 3 verts and are packed as **two contiguous index runs** (ground then cap, `FVoxelMeshData::NumCeilingTriangles`; skirts inherit their source triangle's run) → RMC **polygroups 0/1** → one **section per non-empty group** (`ApplyMeshToTile`, material slot = group index): slot 0 = strate `OverrideMaterial`/default (min-corner chunk), slot 1 = `CeilingMaterial` resolved at the tile's **TOP chunk** (mid as gap fallback, then min; fallback: ground material) — a coarse tile is 2^level chunks tall, so its min corner can sit in a lower strate/gap while the cap belongs to the strate above (this was the "far cap = ground material" residue), so the shadowless overhead rock is tinted separately instead of reading flat/bright. Shadow is **per section** via `FRealtimeMeshSectionConfig::bCastsShadow` (NOT the component `SetCastShadow` — RMC's proxy ignores the component flag; this is also why level≥2 far tiles only stopped casting once the section flag was wired): ground casts at level≤1, the cap section never casts, so the rock ceiling never shadows the terrain below it. History: v1 was a game-thread centre height-oracle (misclassified coarse far tiles → terrain material on the cap underside); v2 a whole-tile worker normal VOTE — which painted **mixed coarse tiles** (one far tile spanning terrain AND cap) entirely with the winner's material, and put sky material under terrain overhangs. The per-triangle class fixes both and is the identity channel caves/F8 will reuse. |
|
||||||
| VerticalShafts | `FVerticalShaftParams` | `BuildVerticalShaftStack` | full-height shafts + horizontal connectors + partial ledges |
|
| VerticalShafts | `FVerticalShaftParams` | `BuildVerticalShaftStack` | full-height shafts + horizontal connectors + partial ledges |
|
||||||
| FloatingIslands | `FFloatingIslandParams` | `BuildFloatingIslandStack` | asymmetric islands: flat land top + underside tapering to a point, lobed (domain-warped) outline, in an open void |
|
| FloatingIslands | `FFloatingIslandParams` | `BuildFloatingIslandStack` | asymmetric islands: flat land top + underside tapering to a point, lobed (domain-warped) outline, beneath the shared ceiling at its configured strate upper boundary |
|
||||||
| Underwater | `FStrateGenerationParams` + water | (reuses `BuildTunnelNetworkStack`) | tunnel rock + high water table |
|
| Underwater | `FStrateGenerationParams` + water | (reuses `BuildTunnelNetworkStack`) | tunnel rock + high water table |
|
||||||
|
|
||||||
All stacks share the convention: internal **positive=solid**, and `AppendStructuralPost` ends every
|
All stacks share the convention: internal **positive=solid**, and `AppendStructuralPost` ends every
|
||||||
@@ -1431,8 +1431,9 @@ density roots, including a 100 cm datum-offset negative control. See
|
|||||||
evaluators keep their recorded cap behavior. The clip joins the exact near density root at the owning
|
evaluators keep their recorded cap behavior. The clip joins the exact near density root at the owning
|
||||||
boundary without changing LOD0–3 density hashes. Future families with planar roofs must declare their
|
boundary without changing LOD0–3 density hashes. Future families with planar roofs must declare their
|
||||||
cap height and bounds through the family plan/provider contract; the shared tile generator consumes
|
cap height and bounds through the family plan/provider contract; the shared tile generator consumes
|
||||||
that declaration. FloatingIslands declares no planar roof, so open sky above a surface island is the
|
that declaration. FloatingIslands uses the same resolved strate-boundary data flow: its upper seal
|
||||||
expected view.
|
follows its plan's chunk range and the shared boundary-seal primitive, while SurfaceWorld keeps its
|
||||||
|
existing heightfield cap.
|
||||||
|
|
||||||
For each requested far SurfaceWorld XY column at LOD4–8, desired-tile selection adds cap-height
|
For each requested far SurfaceWorld XY column at LOD4–8, desired-tile selection adds cap-height
|
||||||
bands only when they do not overlap an existing or already accepted 3D clipmap tile. The original
|
bands only when they do not overlap an existing or already accepted 3D clipmap tile. The original
|
||||||
@@ -1471,8 +1472,9 @@ neighborhood have installed collision (or are explicitly collision-not-required)
|
|||||||
now** — the active view has ground, horizon and the family-appropriate ceiling, with no exposed
|
now** — the active view has ground, horizon and the family-appropriate ceiling, with no exposed
|
||||||
coverage hole or unexpected sky; (3) **refined soon** — each target LOD resolves within its stated
|
coverage hole or unexpected sky; (3) **refined soon** — each target LOD resolves within its stated
|
||||||
time or travel distance after entering range. While moving, report the largest frame/time gap in
|
time or travel distance after entering range. While moving, report the largest frame/time gap in
|
||||||
enclosure and maximum distance that unresolved work trails the pawn. FloatingIslands has open sky by
|
enclosure and maximum distance that unresolved work trails the pawn. FloatingIslands is enclosed by a
|
||||||
design, so its enclosure check excludes a planar roof.
|
planar roof at the upper boundary of its configured strate range; a taller strate places that roof
|
||||||
|
farther above its islands.
|
||||||
|
|
||||||
The shared `AVoxelWorld::UpdateChunksAroundPosition` scheduler serves both the Core snapshot path and
|
The shared `AVoxelWorld::UpdateChunksAroundPosition` scheduler serves both the Core snapshot path and
|
||||||
Core-off legacy path. It admits the collision-critical 3×3 ring first. The measured S5 order remains
|
Core-off legacy path. It admits the collision-critical 3×3 ring first. The measured S5 order remains
|
||||||
@@ -1840,6 +1842,20 @@ fill and check page tiles against the evaluator they actually sample. This is th
|
|||||||
the Unreal adapter and its asynchronous MPSC/Epoch flow are unchanged. See
|
the Unreal adapter and its asynchronous MPSC/Epoch flow are unchanged. See
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md).
|
[`Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md).
|
||||||
|
|
||||||
|
### 8.37 Shared strate boundaries and FloatingIslands lower seal — World Assembler L18
|
||||||
|
|
||||||
|
`FStandaloneShapeWorld::GetResolvedStratumBounds` supplies one physical lower/upper-bound descriptor
|
||||||
|
to layer ranges, far-field sample protection, and far-shell continuity for every strate. SurfaceWorld
|
||||||
|
retains its exact `topChunkZ * 32` Core-voxel mapping and native heightfield cap. Native FloatingIslands
|
||||||
|
takes its resolved bounds from its plan, whose chunk range is converted through the serialized
|
||||||
|
owner/Core voxel ratio. Its version-3 field removes only the lower all-XY seal and calls the same
|
||||||
|
`VF_ApplyUpperBoundarySeal` primitive used by the shared two-sided `VF_ApplyBoundarySeal` for its upper
|
||||||
|
boundary. The ceiling therefore sits at that strate's own upper boundary, with the common
|
||||||
|
`boundarySealSubvoxels` thickness; there is no FloatingIslands-specific height. The recipe range is
|
||||||
|
un-inset to match the Unreal adapter. Versions 1 and 2 retain their serialized two-sided seals.
|
||||||
|
SurfaceWorld's field and output hashes remain unchanged. See
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md).
|
||||||
|
|
||||||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||||
|
|
||||||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||||||
|
|||||||
@@ -208,14 +208,16 @@ all eight archetypes; there is no other path. See [Docs/archive/OPSTACK-PLAN.md]
|
|||||||
| `FVoxelBoxHypotheses` + `FVoxelBoxSdfInterval` + `VF_ForceHypotheses` / `VF_FoldEffect` / `VF_FoldOp` | The fold that turns a stack into an `EVoxelTileClass`, carrying the SDF interval between independent writers and consumers. Unknown is biased toward `Mixed`; the mapping and soundness rule are written out in the header. |
|
| `FVoxelBoxHypotheses` + `FVoxelBoxSdfInterval` + `VF_ForceHypotheses` / `VF_FoldEffect` / `VF_FoldOp` | The fold that turns a stack into an `EVoxelTileClass`, carrying the SDF interval between independent writers and consumers. Unknown is biased toward `Mixed`; the mapping and soundness rule are written out in the header. |
|
||||||
|
|
||||||
### 3.2c Structural primitives — `Public/VoxelDensityPrimitives.h`
|
### 3.2c Structural primitives — `Public/VoxelDensityPrimitives.h`
|
||||||
`VF_ApplyOriginSpine` · `VF_ApplyBoundarySeal` · `VF_ApplyPassageCarving` · `VF_ApplyXYEdgeSeal` —
|
`VF_ApplyOriginSpine` · `VF_ApplyBoundarySeal` · `VF_ApplyUpperBoundarySeal` · `VF_ApplyPassageCarving` · `VF_ApplyXYEdgeSeal` —
|
||||||
the four world invariants every archetype appends, with landing/floor geometry joined by the
|
the four world invariants every archetype appends, with landing/floor geometry joined by the
|
||||||
manager passage post, **moved here 2026-07-27** so the generator and
|
manager passage post, **moved here 2026-07-27** so the generator and
|
||||||
the operator stack share ONE copy. The XY helper is a radial smooth ramp in actor-space and is a
|
the operator stack share ONE copy. The XY helper is a radial smooth ramp in actor-space and is a
|
||||||
true no-op when `WorldRadiusVoxels == 0`; `VF_XYEdgeSealForcedMarginOverBox` is the shared sound
|
true no-op when `WorldRadiusVoxels == 0`; `VF_XYEdgeSealForcedMarginOverBox` is the shared sound
|
||||||
proof for `ClassifyBox` and T1.d's global shell skip. `VoxelGenerator.cpp` keeps same-named
|
proof for `ClassifyBox` and T1.d's global shell skip. `VoxelGenerator.cpp` keeps same-named
|
||||||
`static FORCEINLINE` forwarders for `ApplyBoundarySeal` and `ApplyOriginSpine`; the passage and
|
`static FORCEINLINE` forwarders for `ApplyBoundarySeal` and `ApplyOriginSpine`; the passage and
|
||||||
edge helpers are called directly. **Convention: INTERNAL (positive = solid); `VF_ApplyXYEdgeSealMC` is the output wrapper.**
|
edge helpers are called directly. `VF_ApplyUpperBoundarySealValue` contains the common upper-band
|
||||||
|
math used by both one-sided and two-sided seals. **Convention: INTERNAL (positive = solid);
|
||||||
|
`VF_ApplyXYEdgeSealMC` is the output wrapper.**
|
||||||
|
|
||||||
### 3.2d Operator stack — `Public/VoxelDensityOpStack.h` + `Private/VoxelDensityOpStack.cpp`
|
### 3.2d Operator stack — `Public/VoxelDensityOpStack.h` + `Private/VoxelDensityOpStack.cpp`
|
||||||
⚠️ **This IS the game's generation path.** `GetDensityAt` builds the stack in its per-chunk refetch
|
⚠️ **This IS the game's generation path.** `GetDensityAt` builds the stack in its per-chunk refetch
|
||||||
@@ -1137,6 +1139,17 @@ Implementation and evidence: [`Standalone/Research/WorldAssemblerM3/Linux9/REPOR
|
|||||||
The base manifest field is authoritative for tile generation and exact queries. See ARCHITECTURE
|
The base manifest field is authoritative for tile generation and exact queries. See ARCHITECTURE
|
||||||
§8.36 and [`Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md).
|
§8.36 and [`Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md).
|
||||||
|
|
||||||
|
### 3.23 Shared strate boundaries and FloatingIslands seal — World Assembler M3 Linux18
|
||||||
|
|
||||||
|
| Symbol | Canonical file | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `FStandaloneShapeWorld::FResolvedStratumBounds`, `MakeResolvedStratumBounds`, `GetResolvedStratumBounds` | `Standalone/Core/src/ShapeWorldInternal.h` | Resolves each strate's physical lower/upper range and seal width for layer ranges, far-field sample protection, and far-shell continuity. Native FloatingIslands uses its plan's owner/Core ratio; SurfaceWorld retains its `32` Core voxels per slot chunk. |
|
||||||
|
| `VF_ApplyUpperBoundarySeal` / `VF_ApplyUpperBoundarySealValue` | `Standalone/Source/VoxelForge/Public/VoxelDensityPrimitives.h` and `Source/VoxelForge/Public/VoxelDensityPrimitives.h` | Shared one-sided top seal used by version-3 native FloatingIslands; the two-sided seal reuses the same upper-band math. |
|
||||||
|
|
||||||
|
Geometry version 3 removes the FI lower all-XY seal while retaining its ceiling through the shared
|
||||||
|
upper-bound path. Versions 1 and 2 retain their serialized two-sided behavior. See ARCHITECTURE §8.37
|
||||||
|
and [`Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md).
|
||||||
|
|
||||||
## 4. The density pipeline (most-edited hot path)
|
## 4. The density pipeline (most-edited hot path)
|
||||||
|
|
||||||
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
|
|||||||
IslandPlan.topChunkZ = Slot.TopChunkZ;
|
IslandPlan.topChunkZ = Slot.TopChunkZ;
|
||||||
IslandPlan.bottomChunkZ = Slot.BottomChunkZ;
|
IslandPlan.bottomChunkZ = Slot.BottomChunkZ;
|
||||||
auto& Recipe = IslandPlan.manifest.recipe;
|
auto& Recipe = IslandPlan.manifest.recipe;
|
||||||
Recipe.geometryVersion = 2;
|
Recipe.geometryVersion = 3;
|
||||||
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
||||||
Recipe.ownerVoxelSizeMicrons = static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0));
|
Recipe.ownerVoxelSizeMicrons = static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0));
|
||||||
Recipe.coreVoxelSizeMicrons = static_cast<std::uint32_t>(std::llround(CoreVoxelSizeCm * 10000.0));
|
Recipe.coreVoxelSizeMicrons = static_cast<std::uint32_t>(std::llround(CoreVoxelSizeCm * 10000.0));
|
||||||
|
|||||||
@@ -41,21 +41,27 @@
|
|||||||
// SealFactor > 0 et BaseDensity > 0, le résultat est solide GARANTI quelle qu'ait été l'entrée.
|
// SealFactor > 0 et BaseDensity > 0, le résultat est solide GARANTI quelle qu'ait été l'entrée.
|
||||||
// C'est exactement ce qu'encode `IVoxelDensityOp::ClassifyBox` (voir VoxelDensityOp.h), et la
|
// C'est exactement ce qu'encode `IVoxelDensityOp::ClassifyBox` (voir VoxelDensityOp.h), et la
|
||||||
// raison pour laquelle cette méthode existe.
|
// raison pour laquelle cette méthode existe.
|
||||||
FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
|
FORCEINLINE void VF_ApplyUpperBoundarySealValue(float& Density, float WorldZ,
|
||||||
float StrateTopZ, float StrateBottomZ,
|
float StrateTopZ, float Thickness, float BaseDensity)
|
||||||
float Thickness, float BaseDensity)
|
|
||||||
{
|
{
|
||||||
if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return;
|
const float DistTop = StrateTopZ - WorldZ;
|
||||||
|
|
||||||
const float DistTop = StrateTopZ - WorldZ; // + si on est sous le plafond
|
|
||||||
const float DistBot = WorldZ - StrateBottomZ; // + si on est au-dessus du sol
|
|
||||||
|
|
||||||
if (DistTop >= 0.0f && DistTop < Thickness)
|
if (DistTop >= 0.0f && DistTop < Thickness)
|
||||||
{
|
{
|
||||||
float SealFactor = 1.0f - (DistTop / Thickness);
|
float SealFactor = 1.0f - (DistTop / Thickness);
|
||||||
SealFactor = SmoothStep01(SealFactor);
|
SealFactor = SmoothStep01(SealFactor);
|
||||||
Density = FMath::Max(Density, SealFactor * BaseDensity);
|
Density = FMath::Max(Density, SealFactor * BaseDensity);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
|
||||||
|
float StrateTopZ, float StrateBottomZ,
|
||||||
|
float Thickness, float BaseDensity)
|
||||||
|
{
|
||||||
|
if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return;
|
||||||
|
|
||||||
|
const float DistBot = WorldZ - StrateBottomZ; // + si on est au-dessus du sol
|
||||||
|
|
||||||
|
VF_ApplyUpperBoundarySealValue(Density, WorldZ, StrateTopZ, Thickness, BaseDensity);
|
||||||
if (DistBot >= 0.0f && DistBot < Thickness)
|
if (DistBot >= 0.0f && DistBot < Thickness)
|
||||||
{
|
{
|
||||||
float SealFactor = 1.0f - (DistBot / Thickness);
|
float SealFactor = 1.0f - (DistBot / Thickness);
|
||||||
@@ -64,6 +70,16 @@ FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The same structural seal, limited to a strate's upper boundary. Native finite fields use this
|
||||||
|
// when their lower boundary intentionally has no seal; keeping the math here makes that ceiling
|
||||||
|
// use the same shared density primitive as every two-sided strate seal.
|
||||||
|
FORCEINLINE void VF_ApplyUpperBoundarySeal(float& Density, float WorldZ,
|
||||||
|
float StrateTopZ, float Thickness, float BaseDensity)
|
||||||
|
{
|
||||||
|
if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return;
|
||||||
|
VF_ApplyUpperBoundarySealValue(Density, WorldZ, StrateTopZ, Thickness, BaseDensity);
|
||||||
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// CARVE DE PASSAGE / PASSAGE CARVING
|
// CARVE DE PASSAGE / PASSAGE CARVING
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
|
|||||||
@@ -153,8 +153,13 @@ radius range, top flattening, taper, bounded contour warp, roughness, blend, con
|
|||||||
route constraints. `PlanFloatingIslandCluster` emits stable island and connector IDs, a connected
|
route constraints. `PlanFloatingIslandCluster` emits stable island and connector IDs, a connected
|
||||||
cluster graph, conservative influence AABBs, and a centerline route. At least the primary chain is
|
cluster graph, conservative influence AABBs, and a centerline route. At least the primary chain is
|
||||||
always planned; `islandDensityQ16` controls up to two optional side sites. Connectors carry a sharp-tag
|
always planned; `islandDensityQ16` controls up to two optional side sites. Connectors carry a sharp-tag
|
||||||
ready bit for a later hybrid-mesher adapter. The schema accepts geometry version 1 and 2. Manifests
|
ready bit for a later hybrid-mesher adapter. The schema accepts geometry versions 1 through 3.
|
||||||
without a geometry version retain version-1 evaluation and canonical hashes.
|
Version 3 keeps the version-2 island form, removes the all-XY lower seal, and obtains its ceiling from
|
||||||
|
the shared upper-strate boundary seal. The resolved upper bound comes from the strate's own chunk
|
||||||
|
range; `boundarySealSubvoxels` sets the shared seal thickness and remains a planning margin for island
|
||||||
|
features. It does not define a FloatingIslands-specific ceiling height. Versions 1 and 2 retain their
|
||||||
|
recorded two-sided boundary seals. Manifests without a geometry version retain version-1 evaluation
|
||||||
|
and canonical hashes.
|
||||||
|
|
||||||
`FloatingIslandFieldV1::densitySubvoxels` evaluates the canonical island and connector field.
|
`FloatingIslandFieldV1::densitySubvoxels` evaluates the canonical island and connector field.
|
||||||
`interval` returns unknown for boxes overlapping any influence AABB and a certified all-air range
|
`interval` returns unknown for boxes overlapping any influence AABB and a certified all-air range
|
||||||
|
|||||||
@@ -392,7 +392,9 @@ namespace VoxelForgeCore
|
|||||||
std::uint32_t schemaVersion = 1;
|
std::uint32_t schemaVersion = 1;
|
||||||
// Profile 1 preserves the original shallow pads. Profile 2 adds a thick crust,
|
// Profile 1 preserves the original shallow pads. Profile 2 adds a thick crust,
|
||||||
// bounded lobed roots and arched spans without changing the feature-family schema.
|
// bounded lobed roots and arched spans without changing the feature-family schema.
|
||||||
std::uint32_t geometryVersion = 2;
|
// Version 3 keeps the version-2 island shapes, removes the all-XY lower boundary seal,
|
||||||
|
// and routes its upper seal through the shared strate-boundary primitive.
|
||||||
|
std::uint32_t geometryVersion = 3;
|
||||||
std::uint32_t unitsPerVoxel = static_cast<std::uint32_t>(WorldAssemblySubvoxelsPerVoxel);
|
std::uint32_t unitsPerVoxel = static_cast<std::uint32_t>(WorldAssemblySubvoxelsPerVoxel);
|
||||||
// Import scale is explicit in microns; the reduced ratio maps owner-authored lengths to Core voxels.
|
// Import scale is explicit in microns; the reduced ratio maps owner-authored lengths to Core voxels.
|
||||||
// L'echelle d'import est explicite en microns; le rapport reduit convertit les longueurs owner.
|
// L'echelle d'import est explicite en microns; le rapport reduit convertit les longueurs owner.
|
||||||
|
|||||||
@@ -864,6 +864,46 @@ struct FFarMeshColumnIndex
|
|||||||
return Hits;
|
return Hits;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool HasDownwardHit(double X,double Y,double Z,double Tolerance) const
|
||||||
|
{
|
||||||
|
const int32 CX=std::clamp(static_cast<int32>(std::floor((X-Origin.X)/Step)),0,Cells-1);
|
||||||
|
const int32 CY=std::clamp(static_cast<int32>(std::floor((Y-Origin.Y)/Step)),0,Cells-1);
|
||||||
|
const auto& Candidates=ByColumn[static_cast<size_t>(CY)*Cells+CX];
|
||||||
|
for(const int32 Triangle:Candidates)
|
||||||
|
{
|
||||||
|
const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1],
|
||||||
|
IC=Mesh.Triangles[Triangle*3+2];
|
||||||
|
if(IA<0||IB<0||IC<0||IA>=Mesh.Normals.Num()||IB>=Mesh.Normals.Num()
|
||||||
|
||IC>=Mesh.Normals.Num())continue;
|
||||||
|
if(Mesh.Normals[IA].Z+Mesh.Normals[IB].Z+Mesh.Normals[IC].Z>=-1.5)continue;
|
||||||
|
const double AX=PositionAxis(IA,0),AY=PositionAxis(IA,1),AZ=PositionAxis(IA,2);
|
||||||
|
const double BX=PositionAxis(IB,0),BY=PositionAxis(IB,1),BZ=PositionAxis(IB,2);
|
||||||
|
const double CXv=PositionAxis(IC,0),CYv=PositionAxis(IC,1),CZ=PositionAxis(IC,2);
|
||||||
|
const double Den=(BX-AX)*(CYv-AY)-(BY-AY)*(CXv-AX);
|
||||||
|
if(std::abs(Den)<1.0e-10)continue;
|
||||||
|
const double U=((X-AX)*(CYv-AY)-(Y-AY)*(CXv-AX))/Den;
|
||||||
|
const double V=((BX-AX)*(Y-AY)-(BY-AY)*(X-AX))/Den;
|
||||||
|
const double W=1.0-U-V;
|
||||||
|
if(U<-1.0e-7||V<-1.0e-7||W<-1.0e-7)continue;
|
||||||
|
const double Hit=AZ+U*(BZ-AZ)+V*(CZ-AZ);
|
||||||
|
if(std::abs(Hit-Z)<=Tolerance)return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasDownwardCellCoverage(double X0,double Y0,double X1,double Y1,
|
||||||
|
double Z,double Tolerance) const
|
||||||
|
{
|
||||||
|
constexpr double Samples[3]={0.125,0.5,0.875};
|
||||||
|
for(const double V:Samples)for(const double U:Samples)
|
||||||
|
{
|
||||||
|
const double X=X0+(X1-X0)*U;
|
||||||
|
const double Y=Y0+(Y1-Y0)*V;
|
||||||
|
if(!HasDownwardHit(X,Y,Z,Tolerance))return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
double PositionAxis(int32 Index,int32 Axis) const
|
double PositionAxis(int32 Index,int32 Axis) const
|
||||||
{
|
{
|
||||||
@@ -974,11 +1014,11 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
|
|||||||
std::vector<FBoundary> Boundaries;
|
std::vector<FBoundary> Boundaries;
|
||||||
for(const FStrateSlot& Slot:Layout)
|
for(const FStrateSlot& Slot:Layout)
|
||||||
{
|
{
|
||||||
const FStrateGenerationParams Params=World.Manager->GetGenerationParams(
|
FStandaloneShapeWorld::FResolvedStratumBounds Resolved;
|
||||||
FIntVector(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2));
|
if(!Shape.GetResolvedStratumBounds(Slot,Resolved))continue;
|
||||||
const double HalfWidth=std::max(8.0,static_cast<double>(Params.BoundarySealThickness)+8.0);
|
const double HalfWidth=std::max(8.0,Resolved.SealThickness+8.0);
|
||||||
Boundaries.push_back({static_cast<double>(Slot.BottomChunkZ)*CHUNK_SIZE,HalfWidth});
|
Boundaries.push_back({Resolved.BottomZ,HalfWidth});
|
||||||
Boundaries.push_back({static_cast<double>(Slot.TopChunkZ+1)*CHUNK_SIZE,HalfWidth});
|
Boundaries.push_back({Resolved.TopZ,HalfWidth});
|
||||||
}
|
}
|
||||||
std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B){return A.Z<B.Z;});
|
std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B){return A.Z<B.Z;});
|
||||||
Boundaries.erase(std::unique(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B)
|
Boundaries.erase(std::unique(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B)
|
||||||
@@ -1132,3 +1172,129 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
|
|||||||
Mesh.TriangleSkirtFaceMasks=std::move(CombinedFaceMasks);
|
Mesh.TriangleSkirtFaceMasks=std::move(CombinedFaceMasks);
|
||||||
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
||||||
|
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
||||||
|
{
|
||||||
|
if(Key.Level<0||Step<=0||!World.Manager)return;
|
||||||
|
const FIntVector Origin=Key.OriginVoxels();
|
||||||
|
const int32 Extent=Key.ExtentVoxels();
|
||||||
|
const int32 GridStep=Step;
|
||||||
|
if(Extent<=0||GridStep<=0||Extent%GridStep!=0)return;
|
||||||
|
const int32 GridSide=Extent/GridStep+1;
|
||||||
|
struct FCandidate
|
||||||
|
{
|
||||||
|
FStandaloneShapeWorld::FResolvedStratumBounds Bounds;
|
||||||
|
std::vector<uint8> Crossing;
|
||||||
|
};
|
||||||
|
std::vector<FCandidate> Candidates;
|
||||||
|
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
||||||
|
{
|
||||||
|
FCandidate Candidate;
|
||||||
|
if(!Shape.GetResolvedStratumBounds(Slot,Candidate.Bounds)
|
||||||
|
||Candidate.Bounds.SealThickness<=0.0
|
||||||
|
||Candidate.Bounds.TopZ<Origin.Z-1.0e-6
|
||||||
|
||Candidate.Bounds.TopZ>Origin.Z+Extent+1.0e-6)continue;
|
||||||
|
const double Epsilon=std::min(0.5,Candidate.Bounds.SealThickness*0.25);
|
||||||
|
if(Epsilon<=0.0)continue;
|
||||||
|
Candidate.Crossing.resize(static_cast<std::size_t>(GridSide)*GridSide,0);
|
||||||
|
bool AnyCrossing=false;
|
||||||
|
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
||||||
|
{
|
||||||
|
const double WorldX=Origin.X+X*GridStep;
|
||||||
|
const double WorldY=Origin.Y+Y*GridStep;
|
||||||
|
const float Below=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
||||||
|
static_cast<float>(Candidate.Bounds.TopZ-Epsilon));
|
||||||
|
const float Above=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
||||||
|
static_cast<float>(Candidate.Bounds.TopZ+Epsilon));
|
||||||
|
const bool bCrosses=std::isfinite(Below)&&std::isfinite(Above)&&Below<0.0f&&Above>0.0f;
|
||||||
|
Candidate.Crossing[static_cast<std::size_t>(Y*GridSide+X)]=bCrosses?1:0;
|
||||||
|
AnyCrossing=AnyCrossing||bCrosses;
|
||||||
|
}
|
||||||
|
if(AnyCrossing)Candidates.push_back(std::move(Candidate));
|
||||||
|
}
|
||||||
|
if(Candidates.empty())return;
|
||||||
|
|
||||||
|
const int32 Cells=std::max(1,World.CoarseTileCells);
|
||||||
|
FFarMeshColumnIndex MeshIndex(Mesh,Origin,Extent,Cells,Step);
|
||||||
|
const int32 OldVertexCount=Mesh.Vertices.Num();
|
||||||
|
const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount);
|
||||||
|
const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount;
|
||||||
|
const bool bHasColors=Mesh.Colors.Num()==OldVertexCount;
|
||||||
|
const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255);
|
||||||
|
const double VoxelSizeCm=static_cast<double>(World.CoreVoxelSizeMicrons)/10000.0;
|
||||||
|
std::vector<FVector> NewVertices,NewNormals;
|
||||||
|
std::vector<FVector2f> NewUVs;
|
||||||
|
std::vector<FColor> NewColors;
|
||||||
|
std::vector<int32> NewTriangles;
|
||||||
|
for(const FCandidate& Candidate:Candidates)
|
||||||
|
{
|
||||||
|
std::vector<int32> VertexMap(static_cast<std::size_t>(GridSide)*GridSide,INDEX_NONE);
|
||||||
|
const double CeilingZ=Candidate.Bounds.TopZ;
|
||||||
|
const auto AddVertex=[&](int32 X,int32 Y)
|
||||||
|
{
|
||||||
|
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
|
int32& Mapped=VertexMap[GridIndex];
|
||||||
|
if(Mapped!=INDEX_NONE)return Mapped;
|
||||||
|
Mapped=static_cast<int32>(NewVertices.size());
|
||||||
|
const double WorldX=Origin.X+X*GridStep,WorldY=Origin.Y+Y*GridStep;
|
||||||
|
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,CeilingZ*VoxelSizeCm);
|
||||||
|
NewNormals.emplace_back(0.0,0.0,-1.0);
|
||||||
|
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
||||||
|
if(bHasColors)NewColors.push_back(Color);
|
||||||
|
return Mapped;
|
||||||
|
};
|
||||||
|
for(int32 Y=0;Y+1<GridSide;++Y)for(int32 X=0;X+1<GridSide;++X)
|
||||||
|
{
|
||||||
|
const std::size_t I00=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
|
const std::size_t I10=I00+1,I01=I00+GridSide,I11=I01+1;
|
||||||
|
if(!Candidate.Crossing[I00]||!Candidate.Crossing[I10]
|
||||||
|
||!Candidate.Crossing[I01]||!Candidate.Crossing[I11])continue;
|
||||||
|
const double CellX0=Origin.X+X*GridStep,CellY0=Origin.Y+Y*GridStep;
|
||||||
|
const double CellX1=CellX0+GridStep,CellY1=CellY0+GridStep;
|
||||||
|
// Suppress a coincident plane only when it covers the full cell. A hit at just the
|
||||||
|
// center of a curved shell triangle is not enough to leave the rest of this cap cell open.
|
||||||
|
if(MeshIndex.HasDownwardCellCoverage(CellX0,CellY0,CellX1,CellY1,CeilingZ,0.001))continue;
|
||||||
|
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
||||||
|
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
||||||
|
NewTriangles.insert(NewTriangles.end(),{A,C,B,A,D,C});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(NewTriangles.empty())return;
|
||||||
|
|
||||||
|
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
||||||
|
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
||||||
|
{
|
||||||
|
using TArrayType=std::remove_reference_t<decltype(Existing)>;
|
||||||
|
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
|
||||||
|
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
|
||||||
|
for(const auto& Value:Added)Combined.Add(Value);
|
||||||
|
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
|
||||||
|
Existing=std::move(Combined);
|
||||||
|
};
|
||||||
|
InsertVertices(Mesh.Vertices,NewVertices);InsertVertices(Mesh.Normals,NewNormals);
|
||||||
|
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
|
||||||
|
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
|
||||||
|
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
|
||||||
|
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
|
||||||
|
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(
|
||||||
|
Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
|
||||||
|
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
||||||
|
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
|
||||||
|
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
||||||
|
const int32 OldTriangleCount=Mesh.Triangles.Num()/3;
|
||||||
|
TArray<uint8> ExistingFaceMasks=Mesh.TriangleSkirtFaceMasks;
|
||||||
|
ExistingFaceMasks.SetNum(OldTriangleCount);
|
||||||
|
TArray<uint8> CombinedFaceMasks;CombinedFaceMasks.Reserve(
|
||||||
|
OldTriangleCount+static_cast<int32>(NewTriangles.size()/3));
|
||||||
|
for(int32 Triangle=0;Triangle<CeilingStart/3;++Triangle)
|
||||||
|
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
|
||||||
|
for(std::size_t Triangle=0;Triangle<NewTriangles.size()/3;++Triangle)
|
||||||
|
CombinedFaceMasks.Add(0);
|
||||||
|
for(int32 Triangle=CeilingStart/3;Triangle<OldTriangleCount;++Triangle)
|
||||||
|
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
|
||||||
|
Mesh.Triangles=std::move(CombinedTriangles);
|
||||||
|
Mesh.TriangleSkirtFaceMasks=std::move(CombinedFaceMasks);
|
||||||
|
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||||
|
Mesh.NumCeilingTriangles+=static_cast<int32>(NewTriangles.size()/3);
|
||||||
|
}
|
||||||
@@ -266,7 +266,7 @@ namespace floating_island_detail
|
|||||||
? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0;
|
? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0;
|
||||||
const std::uint32_t ExpectedDenominator = ScaleGcd > 0
|
const std::uint32_t ExpectedDenominator = ScaleGcd > 0
|
||||||
? recipe.coreVoxelSizeMicrons / ScaleGcd : 0;
|
? recipe.coreVoxelSizeMicrons / ScaleGcd : 0;
|
||||||
if (recipe.schemaVersion != 1 || (recipe.geometryVersion != 1 && recipe.geometryVersion != 2)
|
if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 3
|
||||||
|| (recipe.geometryVersion == 1
|
|| (recipe.geometryVersion == 1
|
||||||
&& (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0))
|
&& (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0))
|
||||||
|| recipe.unitsPerVoxel != Q
|
|| recipe.unitsPerVoxel != Q
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "VoxelCaveMorphology.h"
|
#include "VoxelCaveMorphology.h"
|
||||||
#include "VoxelNoise.h"
|
#include "VoxelNoise.h"
|
||||||
#include "VoxelStrateManager.h"
|
#include "VoxelStrateManager.h"
|
||||||
|
#include "VoxelDensityPrimitives.h"
|
||||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||||
#include "VoxelForgeFieldShared.h"
|
#include "VoxelForgeFieldShared.h"
|
||||||
|
|
||||||
@@ -32,6 +33,13 @@
|
|||||||
class FStandaloneShapeWorld
|
class FStandaloneShapeWorld
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
struct FResolvedStratumBounds
|
||||||
|
{
|
||||||
|
double BottomZ=0.0;
|
||||||
|
double TopZ=0.0;
|
||||||
|
double SealThickness=0.0;
|
||||||
|
};
|
||||||
|
|
||||||
double GetCoreVoxelSizeCm() const
|
double GetCoreVoxelSizeCm() const
|
||||||
{
|
{
|
||||||
return Owner == nullptr || Owner->CoreVoxelSizeMicrons == 0
|
return Owner == nullptr || Owner->CoreVoxelSizeMicrons == 0
|
||||||
@@ -47,6 +55,41 @@ public:
|
|||||||
return static_cast<double>(CHUNK_SIZE) * OwnerVoxelSizeCm / GetCoreVoxelSizeCm();
|
return static_cast<double>(CHUNK_SIZE) * OwnerVoxelSizeCm / GetCoreVoxelSizeCm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool MakeResolvedStratumBounds(double BottomZ,double TopZ,double SealThickness,
|
||||||
|
FResolvedStratumBounds& Out)
|
||||||
|
{
|
||||||
|
if(!std::isfinite(BottomZ)||!std::isfinite(TopZ)||!std::isfinite(SealThickness)
|
||||||
|
||TopZ<=BottomZ||SealThickness<0.0)return false;
|
||||||
|
Out={BottomZ,TopZ,SealThickness};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GetResolvedStratumBounds(const FStrateSlot& Slot,FResolvedStratumBounds& Out) const
|
||||||
|
{
|
||||||
|
if(!Owner||!Owner->Manager)return false;
|
||||||
|
const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2;
|
||||||
|
if(bUseNativeManifestFields&&WorldAssembly
|
||||||
|
&&Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,MidChunkZ))
|
||||||
|
==ECaveGeneratorType::FloatingIslands)
|
||||||
|
{
|
||||||
|
const auto* Plan=FindWorldAssemblyFloatingIslandStratum(Slot.StrateIndex,MidChunkZ);
|
||||||
|
if(Plan&&Plan->manifest.recipe.geometryVersion>=3)
|
||||||
|
{
|
||||||
|
constexpr double Scale=static_cast<double>(
|
||||||
|
VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||||
|
return MakeResolvedStratumBounds(
|
||||||
|
static_cast<double>(Plan->manifest.recipe.layoutMinimumZSubvoxels)/Scale,
|
||||||
|
static_cast<double>(Plan->manifest.recipe.layoutMaximumZSubvoxels)/Scale,
|
||||||
|
static_cast<double>(Plan->manifest.recipe.boundarySealSubvoxels)/Scale,Out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const FStrateGenerationParams Params=Owner->Manager->GetGenerationParams(
|
||||||
|
FIntVector(0,0,MidChunkZ));
|
||||||
|
return MakeResolvedStratumBounds(static_cast<double>(Slot.BottomChunkZ)*CHUNK_SIZE,
|
||||||
|
static_cast<double>(Slot.TopChunkZ+1)*CHUNK_SIZE,
|
||||||
|
static_cast<double>(Params.BoundarySealThickness),Out);
|
||||||
|
}
|
||||||
|
|
||||||
enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel };
|
enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel };
|
||||||
struct FBoxClassification
|
struct FBoxClassification
|
||||||
{
|
{
|
||||||
@@ -714,12 +757,12 @@ public:
|
|||||||
const double TileZ0=Origin.Z,TileZ1=Origin.Z+static_cast<double>(Cells)*Step;
|
const double TileZ0=Origin.Z,TileZ1=Origin.Z+static_cast<double>(Cells)*Step;
|
||||||
for(const FStrateSlot& Slot:Owner->Manager->GetLayout())
|
for(const FStrateSlot& Slot:Owner->Manager->GetLayout())
|
||||||
{
|
{
|
||||||
const FStrateGenerationParams Params=Owner->Manager->GetGenerationParams(
|
FResolvedStratumBounds Bounds;
|
||||||
FIntVector(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2));
|
if(!GetResolvedStratumBounds(Slot,Bounds))continue;
|
||||||
const float HalfWidth=static_cast<float>(std::max(8.0,
|
const float HalfWidth=static_cast<float>(std::max(8.0,
|
||||||
static_cast<double>(Params.BoundarySealThickness)+8.0));
|
Bounds.SealThickness+8.0));
|
||||||
const float Boundaries[2]={static_cast<float>(Slot.BottomChunkZ)*CHUNK_SIZE,
|
const float Boundaries[2]={static_cast<float>(Bounds.BottomZ),
|
||||||
static_cast<float>(Slot.TopChunkZ+1)*CHUNK_SIZE};
|
static_cast<float>(Bounds.TopZ)};
|
||||||
for(const float Z:Boundaries)
|
for(const float Z:Boundaries)
|
||||||
if(Z+HalfWidth>=TileZ0&&Z-HalfWidth<=TileZ1)
|
if(Z+HalfWidth>=TileZ0&&Z-HalfWidth<=TileZ1)
|
||||||
Out.stratumBoundaries.push_back({Z,HalfWidth});
|
Out.stratumBoundaries.push_back({Z,HalfWidth});
|
||||||
@@ -1549,13 +1592,18 @@ public:
|
|||||||
const std::int64_t Seal=IslandPlan->manifest.recipe.boundarySealSubvoxels;
|
const std::int64_t Seal=IslandPlan->manifest.recipe.boundarySealSubvoxels;
|
||||||
const std::int64_t BoxBottom=static_cast<std::int64_t>(BMin[2])*Scale;
|
const std::int64_t BoxBottom=static_cast<std::int64_t>(BMin[2])*Scale;
|
||||||
const std::int64_t BoxTop=static_cast<std::int64_t>(BMax[2])*Scale;
|
const std::int64_t BoxTop=static_cast<std::int64_t>(BMax[2])*Scale;
|
||||||
const bool bBottomSealOverlap=BoxBottom<Bottom+Seal&&BoxTop>=Bottom;
|
const bool bSharedTopSealEnabled=IslandPlan->manifest.recipe.geometryVersion<3
|
||||||
const bool bTopSealOverlap=BoxBottom<=Top&&BoxTop>Top-Seal;
|
||!VoxelDensityAblation::IsBoundarySealOff();
|
||||||
|
const bool bTopSealOverlap=bSharedTopSealEnabled
|
||||||
|
&&BoxBottom<=Top&&BoxTop>Top-Seal;
|
||||||
|
const bool bBottomSealOverlap=IslandPlan->manifest.recipe.geometryVersion<3
|
||||||
|
&&BoxBottom<Bottom+Seal&&BoxTop>=Bottom;
|
||||||
if(bBottomSealOverlap)
|
if(bBottomSealOverlap)
|
||||||
return Finish(BoxBottom>=Bottom&&BoxTop<=Bottom+Seal
|
return Finish(BoxBottom>=Bottom&&BoxTop<=Bottom+Seal
|
||||||
?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed);
|
?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed);
|
||||||
if(bTopSealOverlap)
|
if(bTopSealOverlap)
|
||||||
return Finish(BoxBottom>=Top-Seal&&BoxTop<=Top
|
return Finish((IslandPlan->manifest.recipe.geometryVersion<3
|
||||||
|
?BoxBottom>=Top-Seal:BoxBottom>Top-Seal)&&BoxTop<=Top
|
||||||
?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed);
|
?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed);
|
||||||
const VoxelForgeCore::FloatingIslandFieldV1 Block(
|
const VoxelForgeCore::FloatingIslandFieldV1 Block(
|
||||||
IslandPlan->manifest.recipe,IslandPlan->manifest.plan);
|
IslandPlan->manifest.recipe,IslandPlan->manifest.plan);
|
||||||
@@ -2130,9 +2178,28 @@ private:
|
|||||||
const std::int64_t Top=Plan->manifest.recipe.layoutMaximumZSubvoxels;
|
const std::int64_t Top=Plan->manifest.recipe.layoutMaximumZSubvoxels;
|
||||||
const std::int64_t Seal=Plan->manifest.recipe.boundarySealSubvoxels;
|
const std::int64_t Seal=Plan->manifest.recipe.boundarySealSubvoxels;
|
||||||
const std::int64_t Zq=ToFixed(Z);
|
const std::int64_t Zq=ToFixed(Z);
|
||||||
if(Zq>=Bottom&&Zq<Bottom+Seal)Density=std::min(Density,-std::max<std::int64_t>(1,Zq-Bottom));
|
if(Plan->manifest.recipe.geometryVersion<3)
|
||||||
if(Zq<=Top&&Zq>Top-Seal)Density=std::min(Density,-std::max<std::int64_t>(1,Top-Zq));
|
{
|
||||||
|
if(Zq>=Bottom&&Zq<Bottom+Seal)
|
||||||
|
Density=std::min(Density,-std::max<std::int64_t>(1,Zq-Bottom));
|
||||||
|
if(Zq<=Top&&Zq>Top-Seal)
|
||||||
|
Density=std::min(Density,-std::max<std::int64_t>(1,Top-Zq));
|
||||||
OutDensity=static_cast<float>(static_cast<double>(Density)/Scale);
|
OutDensity=static_cast<float>(static_cast<double>(Density)/Scale);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OutDensity=static_cast<float>(static_cast<double>(Density)/Scale);
|
||||||
|
FResolvedStratumBounds Bounds;
|
||||||
|
if(!MakeResolvedStratumBounds(static_cast<double>(Bottom)/Scale,
|
||||||
|
static_cast<double>(Top)/Scale,static_cast<double>(Seal)/Scale,Bounds))
|
||||||
|
return false;
|
||||||
|
const FFloatingIslandParams Params=Owner->Manager->GetFloatingIslandParamsForChunk(
|
||||||
|
FIntVector(0,0,ChunkZ));
|
||||||
|
float InternalDensity=-OutDensity;
|
||||||
|
VF_ApplyUpperBoundarySeal(InternalDensity,Z,static_cast<float>(Bounds.TopZ),
|
||||||
|
static_cast<float>(Bounds.SealThickness),Params.BaseDensity);
|
||||||
|
OutDensity=-InternalDensity;
|
||||||
|
}
|
||||||
return std::isfinite(OutDensity);
|
return std::isfinite(OutDensity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2351,19 +2418,23 @@ private:
|
|||||||
int32 Top=MIN_int32,Bottom=MAX_int32;
|
int32 Top=MIN_int32,Bottom=MAX_int32;
|
||||||
for(const FStrateSlot& Slot:Layout)
|
for(const FStrateSlot& Slot:Layout)
|
||||||
{
|
{
|
||||||
Top=std::max(Top,Slot.TopChunkZ);Bottom=std::min(Bottom,Slot.BottomChunkZ);
|
FResolvedStratumBounds Bounds;
|
||||||
|
if(!GetResolvedStratumBounds(Slot,Bounds))continue;
|
||||||
|
const int32 ResolvedBottom=FMath::FloorToInt(static_cast<float>(Bounds.BottomZ));
|
||||||
|
const int32 ResolvedTop=FMath::CeilToInt(static_cast<float>(Bounds.TopZ));
|
||||||
|
Top=std::max(Top,ResolvedTop);Bottom=std::min(Bottom,ResolvedBottom);
|
||||||
if(!Slot.Definition)continue;
|
if(!Slot.Definition)continue;
|
||||||
const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2;
|
const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2;
|
||||||
FLayerField Field;
|
FLayerField Field;
|
||||||
Field.Lo=Slot.BottomChunkZ*CHUNK_SIZE;
|
Field.Lo=ResolvedBottom;
|
||||||
Field.Hi=(Slot.TopChunkZ+1)*CHUNK_SIZE;
|
Field.Hi=ResolvedTop;
|
||||||
Field.StrateIndex=Slot.StrateIndex;
|
Field.StrateIndex=Slot.StrateIndex;
|
||||||
Field.Params=Owner->Manager->GetGenerationParams(FIntVector(0,0,MidChunkZ));
|
Field.Params=Owner->Manager->GetGenerationParams(FIntVector(0,0,MidChunkZ));
|
||||||
LayerFields.push_back(std::move(Field));
|
LayerFields.push_back(std::move(Field));
|
||||||
}
|
}
|
||||||
if(Top>=Bottom)
|
if(Top>=Bottom)
|
||||||
{
|
{
|
||||||
LayerRanges.push_back({Bottom*CHUNK_SIZE,(Top+1)*CHUNK_SIZE});
|
LayerRanges.push_back({Bottom,Top});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2204,6 +2204,8 @@ namespace VoxelForgeCore
|
|||||||
out.diagnostics.horizonGroundTrianglesRemoved=GroundStats.RemovedTriangles;
|
out.diagnostics.horizonGroundTrianglesRemoved=GroundStats.RemovedTriangles;
|
||||||
out.diagnostics.horizonCapTriangles=CapStats.Triangles;
|
out.diagnostics.horizonCapTriangles=CapStats.Triangles;
|
||||||
}
|
}
|
||||||
|
if(impl_->worldAssembly&&!bOverlapsPlayerEdit)
|
||||||
|
AppendStratumBoundaryCeilings(world,farShape,nativeKey,step,mesh);
|
||||||
|
|
||||||
out.densitySampleCount=static_cast<std::uint64_t>(std::max<int64>(0,sampleCount));
|
out.densitySampleCount=static_cast<std::uint64_t>(std::max<int64>(0,sampleCount));
|
||||||
if(bHasWorldAssembly)
|
if(bHasWorldAssembly)
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# L18 build resources
|
||||||
|
|
||||||
|
Limits: CPUs 0–11, `-j12`, normal priority, 6 GiB address-space cap. Governor stayed `powersave`.
|
||||||
|
These are the final builds after the cell-coverage and owner ceiling corrections.
|
||||||
|
|
||||||
|
- Clang 19, `-ffp-contract=off`: exit 0, 31.689 s, peak process RSS 436,088 KiB, peak VMS 586,240 KiB.
|
||||||
|
- GCC 14, default FP contraction: exit 0, 48.648 s, peak process RSS 903,168 KiB, peak VMS 1,014,716 KiB.
|
||||||
|
- Largest measured runtime process was the final Clang gate run at 897,276 KiB RSS. The determinism matrix peaked at 603,172 KiB and the crossing matrix at 548,664 KiB.
|
||||||
|
- An earlier GCC build was interrupted by owner steering before producing a binary; the final GCC build above completed successfully.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render the matched L18 review views as small readable JPEGs."""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
CAPTURE = ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-linux18-clang"
|
||||||
|
EXPORTS = ROOT / "Standalone/Research/WorldAssemblerM3/Linux18/exports"
|
||||||
|
OUT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux18/captures"
|
||||||
|
LOGS = ROOT / "Saved/Linux"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["LP_NUM_THREADS"] = "12"
|
||||||
|
env["XDG_CACHE_HOME"] = str(ROOT / "Saved/Linux/mesa-cache")
|
||||||
|
|
||||||
|
views = [
|
||||||
|
("floating-islands-flat-square-after", "floating-islands-flat-square.vfmesh",
|
||||||
|
(-11.612444, -118.221237, -19.8452), 0, 0, True),
|
||||||
|
("floating-islands-flat-square-no-edges", "floating-islands-flat-square.vfmesh",
|
||||||
|
(-11.612444, -118.221237, -19.8452), 0, 0, False),
|
||||||
|
("floating-islands-island-view-after", "floating-islands-island-view.vfmesh",
|
||||||
|
(-22, -19, -22.8), 0, -4, True),
|
||||||
|
("floating-islands-ceiling-up", "floating-islands-island-view.vfmesh",
|
||||||
|
(-22, -19, -22.8), 0, 80, True),
|
||||||
|
("surface-world-l1-spawn-after", "surface-world-l1-spawn.vfmesh",
|
||||||
|
(0, -35, -487.94), 0, -22, False),
|
||||||
|
("surface-world-horizon-after", "surface-world-l1-spawn.vfmesh",
|
||||||
|
(0, -35, -487.94), 0, 0, False),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, scene_name, camera, yaw, pitch, tile_edges in views:
|
||||||
|
scene = EXPORTS / scene_name
|
||||||
|
png = LOGS / f"l18-{name}.png"
|
||||||
|
jpg = OUT / f"{name}.jpg"
|
||||||
|
render_log = LOGS / f"l18-{name}-render.log"
|
||||||
|
convert_log = LOGS / f"l18-{name}-jpeg.log"
|
||||||
|
command = [str(CAPTURE), str(scene), str(png), "--size=1600x900",
|
||||||
|
"--shade=readable", f"--tile-edges={'on' if tile_edges else 'off'}",
|
||||||
|
"--tile-edge-width-m=0.15", "--camera-m="+",".join(map(str, camera)),
|
||||||
|
f"--yaw-deg={yaw}", f"--pitch-deg={pitch}"]
|
||||||
|
with render_log.open("w", encoding="utf-8") as stream:
|
||||||
|
subprocess.run(command, cwd=ROOT, env=env, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, check=True)
|
||||||
|
ffmpeg = ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-y",
|
||||||
|
"-i", str(png), "-frames:v", "1", "-q:v", "4", str(jpg)]
|
||||||
|
with convert_log.open("w", encoding="utf-8") as stream:
|
||||||
|
subprocess.run(ffmpeg, cwd=ROOT, env=env, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, check=True)
|
||||||
|
if jpg.stat().st_size > 300_000:
|
||||||
|
raise SystemExit(f"{jpg.name} exceeds the 300 KB review limit")
|
||||||
|
print(f"[Capture] {jpg.relative_to(ROOT)} bytes={jpg.stat().st_size} status=PASS")
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Audit the emitted FloatingIslands roof faces and same-LOD tile-edge continuity."""
|
||||||
|
import array
|
||||||
|
import collections
|
||||||
|
import math
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def read_exact(stream, size):
|
||||||
|
data = stream.read(size)
|
||||||
|
if len(data) != size:
|
||||||
|
raise EOFError(f"wanted {size} bytes, got {len(data)}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def height_at(triangles, x, y, preferred_z):
|
||||||
|
best = None
|
||||||
|
for (x0, y0, z0), (x1, y1, z1), (x2, y2, z2) in triangles:
|
||||||
|
den = (y1-y2)*(x0-x2)+(x2-x1)*(y0-y2)
|
||||||
|
if abs(den) < 1.0e-10:
|
||||||
|
continue
|
||||||
|
a = ((y1-y2)*(x-x2)+(x2-x1)*(y-y2))/den
|
||||||
|
b = ((y2-y0)*(x-x2)+(x0-x2)*(y-y2))/den
|
||||||
|
c = 1.0-a-b
|
||||||
|
if min(a,b,c) >= -1.0e-5:
|
||||||
|
z = a*z0+b*z1+c*z2
|
||||||
|
if best is None or abs(z-preferred_z) < abs(best-preferred_z):
|
||||||
|
best = z
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
def quantile(values, fraction):
|
||||||
|
if not values:
|
||||||
|
return math.nan
|
||||||
|
ordered = sorted(values)
|
||||||
|
return ordered[math.floor(fraction*(len(ordered)-1))]
|
||||||
|
|
||||||
|
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
raise SystemExit("usage: CeilingContinuity.py scene.vfmesh expected-cap-plane-metres")
|
||||||
|
scene_path = sys.argv[1]
|
||||||
|
root_z = float(sys.argv[2])
|
||||||
|
tiles = {}
|
||||||
|
voxel_size_m = None
|
||||||
|
with open(scene_path, "rb") as stream:
|
||||||
|
if read_exact(stream, 8) != b"VFMSH001":
|
||||||
|
raise SystemExit("invalid VFMSH001 scene")
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", read_exact(stream, 12))
|
||||||
|
camera = struct.unpack("<3d", read_exact(stream, 24))
|
||||||
|
yaw, pitch = struct.unpack("<2d", read_exact(stream, 16))
|
||||||
|
voxel_size_m = voxel_um/1_000_000.0
|
||||||
|
for _ in range(tile_count):
|
||||||
|
key = struct.unpack("<4i", read_exact(stream, 16))
|
||||||
|
surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", read_exact(stream, 20))
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(read_exact(stream, vertex_count*12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(read_exact(stream, vertex_count*12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(read_exact(stream, index_count*4))
|
||||||
|
masks = read_exact(stream, mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
extent = 32*(1 << key[3])*voxel_size_m
|
||||||
|
z0, z1 = key[2]*extent, (key[2]+1)*extent
|
||||||
|
ceiling = []
|
||||||
|
for triangle in range(index_count//3):
|
||||||
|
if masks and masks[triangle] != 0:
|
||||||
|
continue
|
||||||
|
a,b,c = indices[triangle*3:triangle*3+3]
|
||||||
|
if max(a,b,c) >= surface_vertices:
|
||||||
|
continue
|
||||||
|
nz = (normals[a*3+2]+normals[b*3+2]+normals[c*3+2])/3.0
|
||||||
|
if nz > -0.65:
|
||||||
|
continue
|
||||||
|
points = tuple((positions[v*3],positions[v*3+1],positions[v*3+2])
|
||||||
|
for v in (a,b,c))
|
||||||
|
center_z = sum(point[2] for point in points)/3.0
|
||||||
|
if abs(center_z-root_z) <= 1.0:
|
||||||
|
ceiling.append(points)
|
||||||
|
tiles[key] = {
|
||||||
|
"extent": extent, "x0": key[0]*extent, "y0": key[1]*extent,
|
||||||
|
"x1": (key[0]+1)*extent, "y1": (key[1]+1)*extent,
|
||||||
|
"z0": z0, "z1": z1, "ceiling": ceiling,
|
||||||
|
}
|
||||||
|
|
||||||
|
by_lod = collections.defaultdict(dict)
|
||||||
|
for key, tile in tiles.items():
|
||||||
|
if tile["z0"] <= root_z+1.0 and tile["z1"] >= root_z-1.0:
|
||||||
|
by_lod[key[3]][key] = tile
|
||||||
|
|
||||||
|
print(f"scene={scene_path} version={version} voxel_um={voxel_um} tiles={tile_count} "
|
||||||
|
f"camera_m={camera} yaw={yaw} pitch={pitch} expected_cap_m={root_z:.9f}")
|
||||||
|
print("lod zband_tiles cap_tiles missing_tiles cap_triangles face_z_m_p05/median/p95 "
|
||||||
|
"tile_samples hit_samples max_abs_root_delta_cm shared_edges shared_hits edge_gaps "
|
||||||
|
"edge_delta_cm_median/p95/max")
|
||||||
|
all_pass = True
|
||||||
|
for lod in range(9):
|
||||||
|
target = by_lod[lod]
|
||||||
|
cap_tiles = {key: tile for key,tile in target.items() if tile["ceiling"]}
|
||||||
|
face_z = [point[2] for tile in cap_tiles.values() for tri in tile["ceiling"]
|
||||||
|
for point in tri]
|
||||||
|
deltas = []
|
||||||
|
sample_count = 0
|
||||||
|
hit_count = 0
|
||||||
|
for key,tile in target.items():
|
||||||
|
if not tile["ceiling"]:
|
||||||
|
continue
|
||||||
|
for iy in range(16):
|
||||||
|
for ix in range(16):
|
||||||
|
x = tile["x0"]+(ix+0.5)*tile["extent"]/16
|
||||||
|
y = tile["y0"]+(iy+0.5)*tile["extent"]/16
|
||||||
|
sample_count += 1
|
||||||
|
height = height_at(tile["ceiling"],x,y,root_z)
|
||||||
|
if height is not None:
|
||||||
|
hit_count += 1
|
||||||
|
deltas.append(abs(height-root_z)*100.0)
|
||||||
|
|
||||||
|
edge_deltas = []
|
||||||
|
edge_count = 0
|
||||||
|
edge_gaps = 0
|
||||||
|
for key,a in cap_tiles.items():
|
||||||
|
x,y,z,level = key
|
||||||
|
for neighbor_key,axis in (((x+1,y,z,level),"x"),((x,y+1,z,level),"y")):
|
||||||
|
b = cap_tiles.get(neighbor_key)
|
||||||
|
if b is None:
|
||||||
|
continue
|
||||||
|
edge_count += 1
|
||||||
|
epsilon = min(0.02,a["extent"]*1.0e-4)
|
||||||
|
matched = 0
|
||||||
|
for index in range(33):
|
||||||
|
t = (index+0.5)/33
|
||||||
|
if axis == "x":
|
||||||
|
fixed = a["x1"]
|
||||||
|
varying = a["y0"]+(a["y1"]-a["y0"])*t
|
||||||
|
za = height_at(a["ceiling"],fixed-epsilon,varying,root_z)
|
||||||
|
zb = height_at(b["ceiling"],fixed+epsilon,varying,root_z)
|
||||||
|
else:
|
||||||
|
fixed = a["y1"]
|
||||||
|
varying = a["x0"]+(a["x1"]-a["x0"])*t
|
||||||
|
za = height_at(a["ceiling"],varying,fixed-epsilon,root_z)
|
||||||
|
zb = height_at(b["ceiling"],varying,fixed+epsilon,root_z)
|
||||||
|
if za is not None and zb is not None:
|
||||||
|
edge_deltas.append(abs(za-zb)*100.0)
|
||||||
|
matched += 1
|
||||||
|
if matched == 0:
|
||||||
|
edge_gaps += 1
|
||||||
|
missing = len(target)-len(cap_tiles)
|
||||||
|
max_root = max(deltas) if deltas else math.nan
|
||||||
|
print(f"{lod} {len(target)} {len(cap_tiles)} {missing} "
|
||||||
|
f"{sum(len(tile['ceiling']) for tile in cap_tiles.values())} "
|
||||||
|
f"{quantile(face_z,.05):.6f}/{quantile(face_z,.5):.6f}/{quantile(face_z,.95):.6f} "
|
||||||
|
f"{sample_count} {hit_count} {max_root:.3f} {edge_count} {len(edge_deltas)} "
|
||||||
|
f"{edge_gaps} {quantile(edge_deltas,.5):.3f}/{quantile(edge_deltas,.95):.3f}/"
|
||||||
|
f"{max(edge_deltas) if edge_deltas else math.nan:.3f}")
|
||||||
|
if lod >= 4 and target and (missing or hit_count != sample_count or edge_gaps):
|
||||||
|
all_pass = False
|
||||||
|
print(f"[CeilingContinuity] result={'PASS' if all_pass else 'FAIL'} "
|
||||||
|
"criteria=LOD4-8 requested z-band tiles emit an upper-boundary ceiling; 16x16 ray samples hit; "
|
||||||
|
"shared same-LOD tile edges have paired hits")
|
||||||
|
raise SystemExit(0 if all_pass else 1)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compare all L18 crossing configurations to the L14 24 m crossing hashes."""
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
RUNS = ROOT / "Saved/Linux/l18-crossing"
|
||||||
|
PATTERN = re.compile(
|
||||||
|
r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
failed = []
|
||||||
|
logs = sorted(RUNS.glob("*-w*-r*.log"))
|
||||||
|
for path in logs:
|
||||||
|
parts = path.stem.split("-")
|
||||||
|
compiler = parts[0]
|
||||||
|
reference_compiler = "clang" if compiler == "clang" else "gcc"
|
||||||
|
reference_path = ROOT / f"Saved/Linux/l14-final-crossing-24m-{reference_compiler}-w12.log"
|
||||||
|
expected = PATTERN.findall(reference_path.read_text(encoding="utf-8"))
|
||||||
|
actual = PATTERN.findall(path.read_text(encoding="utf-8", errors="replace"))
|
||||||
|
if actual != expected:
|
||||||
|
failed.append(path.name)
|
||||||
|
print(f"[CrossingReferenceMismatch] {path.name} expected_rows={len(expected)} "
|
||||||
|
f"actual_rows={len(actual)}")
|
||||||
|
print(f"[CrossingSurfaceHashGate] reference=L14-final-crossing-24m-W12 rows={len(logs)} "
|
||||||
|
f"result={'FAIL' if failed else 'PASS'}")
|
||||||
|
raise SystemExit(1 if failed or len(logs) != 14 else 0)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compare every L18 SurfaceWorld matrix hash row with the approved L14 reference."""
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
RUNS = ROOT / "Saved/Linux/l18-matrix"
|
||||||
|
REFERENCE = ROOT / "Saved/Linux/l14-final-hash-sw-clang-off-w12-r5.log"
|
||||||
|
PATTERN = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
expected = PATTERN.findall(REFERENCE.read_text(encoding="utf-8"))
|
||||||
|
if not expected or len(expected) != 12:
|
||||||
|
raise SystemExit(f"invalid L14 reference hash rows: {len(expected)}")
|
||||||
|
logs = sorted(RUNS.glob("surface-world-*.log"))
|
||||||
|
if len(logs) != 14:
|
||||||
|
raise SystemExit(f"expected 14 SurfaceWorld matrix logs, found {len(logs)}")
|
||||||
|
failed = []
|
||||||
|
for path in logs:
|
||||||
|
actual = PATTERN.findall(path.read_text(encoding="utf-8", errors="replace"))
|
||||||
|
if actual != expected:
|
||||||
|
failed.append(path.name)
|
||||||
|
print(f"[SurfaceHashMismatch] {path.name} expected={len(expected)} actual={len(actual)}")
|
||||||
|
print(f"[SurfaceHashGate] reference={REFERENCE.name} rows={len(logs)} expected_rows=14 "
|
||||||
|
f"result={'FAIL' if failed else 'PASS'}")
|
||||||
|
raise SystemExit(1 if failed else 0)
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
#include "Json.h"
|
||||||
|
#include "OwnerWorldLoader.h"
|
||||||
|
#include "StandaloneCoreScheduler.h"
|
||||||
|
#include "WorldAssemblyPrototype.h"
|
||||||
|
#include "StandaloneSnapshotAdapter.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr double CoreVoxelsPerMeter=4.0;
|
||||||
|
constexpr std::int64_t SubvoxelsPerCoreVoxel=
|
||||||
|
VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||||
|
|
||||||
|
void PrintDensity(VoxelForgeCore::World& World,const char* Label,double X,double Y,double Z)
|
||||||
|
{
|
||||||
|
const VoxelForgeCore::Vec3d Point{X*CoreVoxelsPerMeter,Y*CoreVoxelsPerMeter,
|
||||||
|
Z*CoreVoxelsPerMeter};
|
||||||
|
const double Density=World.densityAt(Point);
|
||||||
|
const double Raw=World.unclampedDensityAt(Point);
|
||||||
|
std::printf("[DensitySample] label=%s xyz_m=%.6f,%.6f,%.6f density_core_vox=%.9f "
|
||||||
|
"density_cm=%.4f unclamped_core_vox=%.9f sign=%s\n",Label,X,Y,Z,Density,
|
||||||
|
Density*25.0,Raw,Density<0.0?"solid":"air");
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrintCrossing(VoxelForgeCore::World& World,const char* Label,double X,double Y,
|
||||||
|
double BottomM,double TopM)
|
||||||
|
{
|
||||||
|
const auto DensityAt=[&](double Z)
|
||||||
|
{
|
||||||
|
return World.densityAt({X*CoreVoxelsPerMeter,Y*CoreVoxelsPerMeter,
|
||||||
|
Z*CoreVoxelsPerMeter});
|
||||||
|
};
|
||||||
|
double Lo=BottomM,Hi=TopM;
|
||||||
|
double LoDensity=DensityAt(Lo),HiDensity=DensityAt(Hi);
|
||||||
|
if((LoDensity<0.0)==(HiDensity<0.0))
|
||||||
|
{
|
||||||
|
std::printf("[DensityCrossing] label=%s bracket_m=%.6f..%.6f signs=%s/%s result=none\n",
|
||||||
|
Label,Lo,Hi,LoDensity<0.0?"solid":"air",HiDensity<0.0?"solid":"air");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for(int Iteration=0;Iteration<64;++Iteration)
|
||||||
|
{
|
||||||
|
const double Mid=(Lo+Hi)*0.5;
|
||||||
|
const double MidDensity=DensityAt(Mid);
|
||||||
|
if((MidDensity<0.0)==(LoDensity<0.0))
|
||||||
|
{Lo=Mid;LoDensity=MidDensity;}
|
||||||
|
else
|
||||||
|
{Hi=Mid;HiDensity=MidDensity;}
|
||||||
|
}
|
||||||
|
std::printf("[DensityCrossing] label=%s z_m=%.9f bracket_m=%.6f..%.6f "
|
||||||
|
"density_core_vox=%.9f/%.9f\n",Label,(Lo+Hi)*0.5,BottomM,TopM,
|
||||||
|
LoDensity,HiDensity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc,char** argv)
|
||||||
|
{
|
||||||
|
if(argc!=2)
|
||||||
|
{
|
||||||
|
std::fprintf(stderr,"usage: %s owner-world.json\n",argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
VFJson::Value Root;
|
||||||
|
std::string Error;
|
||||||
|
{
|
||||||
|
std::ifstream Input(argv[1],std::ios::binary);
|
||||||
|
if(!Input){std::fprintf(stderr,"owner JSON open failed\n");return 2;}
|
||||||
|
std::string Text{std::istreambuf_iterator<char>(Input),std::istreambuf_iterator<char>{}};
|
||||||
|
if(!VFJson::Parser(std::move(Text)).Parse(Root,Error))
|
||||||
|
{std::fprintf(stderr,"owner JSON parse failed: %s\n",Error.c_str());return 2;}
|
||||||
|
}
|
||||||
|
if(!StandaloneWorldAssemblyPrototype::SelectTopStrateAsFloatingIslands(Root,Error))
|
||||||
|
{std::fprintf(stderr,"FI selection failed: %s\n",Error.c_str());return 2;}
|
||||||
|
auto Owner=std::make_shared<FStandaloneOwnerWorld>();
|
||||||
|
if(!StandaloneOwnerLoader::Resolve(Root,*Owner,Error))
|
||||||
|
{std::fprintf(stderr,"owner resolve failed: %s\n",Error.c_str());return 2;}
|
||||||
|
VoxelForgeCore::WorldAssemblyManifest Assembly;
|
||||||
|
if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(
|
||||||
|
*Owner,Root,Error,{},&Assembly)
|
||||||
|
||!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
||||||
|
*Owner,Root,Assembly,Error))
|
||||||
|
{std::fprintf(stderr,"plan construction failed: %s\n",Error.c_str());return 2;}
|
||||||
|
auto Snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(Root,Owner->Seed,3,8,
|
||||||
|
false,false,Error);
|
||||||
|
if(!Snapshot)
|
||||||
|
{std::fprintf(stderr,"snapshot resolve failed: %s\n",Error.c_str());return 2;}
|
||||||
|
const auto* ResolvedAssembly=Snapshot.worldAssembly();
|
||||||
|
if(!ResolvedAssembly||ResolvedAssembly->floatingIslands.empty())
|
||||||
|
{std::fprintf(stderr,"resolved FloatingIslands plan missing\n");return 2;}
|
||||||
|
|
||||||
|
VoxelForgeCore::World World(std::move(Snapshot));
|
||||||
|
VoxelForgeCore::ShapeBuildRequest Request;
|
||||||
|
Request.minVoxel={-96,-512,-288};
|
||||||
|
Request.maxVoxel={96,96,64};
|
||||||
|
Request.clipMinVoxel=Request.minVoxel;
|
||||||
|
Request.clipMaxVoxel=Request.maxVoxel;
|
||||||
|
Request.workerLimit=12;
|
||||||
|
Request.includeFarWorld=false;
|
||||||
|
FStandaloneCoreScheduler Scheduler;
|
||||||
|
if(!World.compile(Request,Scheduler,Error))
|
||||||
|
{std::fprintf(stderr,"shape compile failed: %s\n",Error.c_str());return 2;}
|
||||||
|
|
||||||
|
const auto& Plan=ResolvedAssembly->floatingIslands.front();
|
||||||
|
const auto& Recipe=Plan.manifest.recipe;
|
||||||
|
std::printf("[FloatingIslandBounds] geometry_version=%u owner_voxel_um=%u core_voxel_um=%u "
|
||||||
|
"owner_to_core=%u/%u chunks=%d..%d layout_z_m=%.6f..%.6f ceiling_start_m=%.6f "
|
||||||
|
"seal_m=%.6f bottom_cap=%s\n",Recipe.geometryVersion,Recipe.ownerVoxelSizeMicrons,
|
||||||
|
Recipe.coreVoxelSizeMicrons,Recipe.ownerToCoreScaleNumerator,
|
||||||
|
Recipe.ownerToCoreScaleDenominator,Plan.topChunkZ,Plan.bottomChunkZ,
|
||||||
|
static_cast<double>(Recipe.layoutMinimumZSubvoxels)/SubvoxelsPerCoreVoxel/4.0,
|
||||||
|
static_cast<double>(Recipe.layoutMaximumZSubvoxels)/SubvoxelsPerCoreVoxel/4.0,
|
||||||
|
static_cast<double>(Recipe.layoutMaximumZSubvoxels-Recipe.boundarySealSubvoxels)
|
||||||
|
/SubvoxelsPerCoreVoxel/4.0,
|
||||||
|
static_cast<double>(Recipe.boundarySealSubvoxels)/SubvoxelsPerCoreVoxel/4.0,
|
||||||
|
Recipe.geometryVersion<3?"legacy":"removed");
|
||||||
|
std::printf("[DensityProbe] flat_square_xy_m=-11.612444,-118.221237 "
|
||||||
|
"compiled=%d core_voxel_size_cm=25\n",World.isCompiled()?1:0);
|
||||||
|
constexpr double X=-11.612443923950195;
|
||||||
|
constexpr double Y=-118.22123718261719;
|
||||||
|
for(double Z:{2.75,2.99,3.00,3.01,3.25,3.49,3.50,3.51,3.75,3.99,4.00,4.01,
|
||||||
|
4.05,4.125,4.25,4.50,5.00,
|
||||||
|
7.75,8.00,8.25,-60.25,-60.00,-59.75,-59.50,-59.25})
|
||||||
|
PrintDensity(World,"flat-square",X,Y,Z);
|
||||||
|
PrintCrossing(World,"shared-upper-ceiling",X,Y,3.50,3.75);
|
||||||
|
PrintCrossing(World,"upper-boundary-cap",X,Y,4.00,4.25);
|
||||||
|
PrintCrossing(World,"old-eight-metre-wrapper",X,Y,7.75,8.25);
|
||||||
|
PrintCrossing(World,"removed-lower-boundary-seal",X,Y,-60.25,-59.75);
|
||||||
|
for(const auto& Probe:std::array<std::array<double,2>,3>{{
|
||||||
|
{{-66.3,-38.7}},{{-62.3,-54.6}},{{-53.34,-22.84}}}})
|
||||||
|
{
|
||||||
|
std::printf("[DensityProbeXY] xy_m=%.3f,%.3f\n",Probe[0],Probe[1]);
|
||||||
|
for(double Z:{3.49,3.50,3.51,3.75,3.875,3.99,4.00,4.01,4.05,4.125})
|
||||||
|
PrintDensity(World,"boundary-ray",Probe[0],Probe[1],Z);
|
||||||
|
PrintCrossing(World,"boundary-ray",Probe[0],Probe[1],3.49,4.125);
|
||||||
|
}
|
||||||
|
for(const auto& Probe:std::array<std::array<double,2>,4>{{
|
||||||
|
{{-67.0,-39.0}},{{-66.0,-39.0}},{{-67.0,-38.0}},{{-66.0,-38.0}}}})
|
||||||
|
{
|
||||||
|
PrintDensity(World,"boundary-grid-node",Probe[0],Probe[1],3.875);
|
||||||
|
PrintDensity(World,"boundary-grid-node",Probe[0],Probe[1],4.125);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Measure peak per-process RSS for a build process tree under the L17 limits."""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def descendants(root):
|
||||||
|
pending = [root]
|
||||||
|
seen = set()
|
||||||
|
while pending:
|
||||||
|
pid = pending.pop()
|
||||||
|
if pid in seen:
|
||||||
|
continue
|
||||||
|
seen.add(pid)
|
||||||
|
try:
|
||||||
|
children = Path(f"/proc/{pid}/task/{pid}/children").read_text().split()
|
||||||
|
pending.extend(int(child) for child in children)
|
||||||
|
except (FileNotFoundError, PermissionError, ProcessLookupError):
|
||||||
|
pass
|
||||||
|
return seen
|
||||||
|
|
||||||
|
|
||||||
|
def status(pid):
|
||||||
|
try:
|
||||||
|
fields = {}
|
||||||
|
for line in Path(f"/proc/{pid}/status").read_text().splitlines():
|
||||||
|
if line.startswith(("Name:", "VmRSS:", "VmSize:")):
|
||||||
|
key, *value = line.split()
|
||||||
|
fields[key[:-1]] = value[0] if value else ""
|
||||||
|
return (int(fields.get("VmRSS", "0")), int(fields.get("VmSize", "0")),
|
||||||
|
fields.get("Name", ""))
|
||||||
|
except (FileNotFoundError, PermissionError, ProcessLookupError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
raise SystemExit("usage: MeasureBuild.py command [arguments ...]")
|
||||||
|
if not os.sched_getaffinity(0).issubset(set(range(12))):
|
||||||
|
raise SystemExit("run under taskset -c 0-11")
|
||||||
|
|
||||||
|
started = time.monotonic()
|
||||||
|
process = subprocess.Popen(sys.argv[1:], start_new_session=True)
|
||||||
|
peak_rss = (0, 0, "")
|
||||||
|
peak_vms = (0, 0, "")
|
||||||
|
while process.poll() is None:
|
||||||
|
for pid in descendants(process.pid):
|
||||||
|
measured = status(pid)
|
||||||
|
if measured is None:
|
||||||
|
continue
|
||||||
|
rss, vms, name = measured
|
||||||
|
if rss > peak_rss[0]:
|
||||||
|
peak_rss = (rss, pid, name)
|
||||||
|
if vms > peak_vms[0]:
|
||||||
|
peak_vms = (vms, pid, name)
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
exit_code = process.wait()
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
print(f"[BuildTreeResources] exit_code={exit_code} wall_s={elapsed:.3f} "
|
||||||
|
f"peak_process_rss_kib={peak_rss[0]} peak_rss_pid={peak_rss[1]} "
|
||||||
|
f"peak_rss_name={peak_rss[2]} peak_process_vms_kib={peak_vms[0]} "
|
||||||
|
f"peak_vms_pid={peak_vms[1]} peak_vms_name={peak_vms[2]} "
|
||||||
|
"monitor_interval_s=0.1", flush=True)
|
||||||
|
raise SystemExit(exit_code)
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Summarize exported tile geometry and upward-facing ground for L17."""
|
||||||
|
import array
|
||||||
|
import collections
|
||||||
|
import math
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def read_exact(stream, size):
|
||||||
|
data = stream.read(size)
|
||||||
|
if len(data) != size:
|
||||||
|
raise EOFError(f"wanted {size} bytes, got {len(data)}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
path = sys.argv[1]
|
||||||
|
with open(path, "rb") as stream:
|
||||||
|
if read_exact(stream, 8) != b"VFMSH001":
|
||||||
|
raise SystemExit("bad VFMSH001 header")
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", read_exact(stream, 12))
|
||||||
|
camera = struct.unpack("<3d", read_exact(stream, 24))
|
||||||
|
yaw, pitch = struct.unpack("<2d", read_exact(stream, 16))
|
||||||
|
per_lod = collections.defaultdict(lambda: {"tiles": 0, "vertices": 0,
|
||||||
|
"triangles": 0, "surface_triangles": 0, "up_triangles": 0,
|
||||||
|
"up_centroids": [], "tile_spans": [], "empty": 0})
|
||||||
|
tile_rows = []
|
||||||
|
for _ in range(tile_count):
|
||||||
|
key = struct.unpack("<4i", read_exact(stream, 16))
|
||||||
|
surf_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", read_exact(stream, 20))
|
||||||
|
p = array.array("f")
|
||||||
|
p.frombytes(read_exact(stream, vertex_count * 12))
|
||||||
|
n = array.array("f")
|
||||||
|
n.frombytes(read_exact(stream, vertex_count * 12))
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
p.byteswap(); n.byteswap()
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(read_exact(stream, index_count * 4))
|
||||||
|
masks = read_exact(stream, mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
indices.byteswap()
|
||||||
|
lod = key[3]
|
||||||
|
row = per_lod[lod]
|
||||||
|
row["tiles"] += 1
|
||||||
|
row["vertices"] += vertex_count
|
||||||
|
row["triangles"] += index_count // 3
|
||||||
|
row["surface_triangles"] += index_count // 3 - skirt_count
|
||||||
|
up = []
|
||||||
|
for tri in range(index_count // 3):
|
||||||
|
if masks and masks[tri] != 0:
|
||||||
|
continue
|
||||||
|
a, b, c = indices[tri * 3:tri * 3 + 3]
|
||||||
|
if max(a, b, c) >= surf_vertices:
|
||||||
|
continue
|
||||||
|
if (n[a * 3 + 2] + n[b * 3 + 2] + n[c * 3 + 2]) / 3.0 < 0.65:
|
||||||
|
continue
|
||||||
|
up.append((p[a * 3 + 2] + p[b * 3 + 2] + p[c * 3 + 2]) / 3.0)
|
||||||
|
row["up_triangles"] += len(up)
|
||||||
|
row["up_centroids"].extend(up)
|
||||||
|
if up:
|
||||||
|
up.sort()
|
||||||
|
span = up[math.floor(0.95 * (len(up) - 1))] - up[math.floor(0.05 * (len(up) - 1))]
|
||||||
|
row["tile_spans"].append(span)
|
||||||
|
tile_rows.append((key, vertex_count, index_count // 3, skirt_count,
|
||||||
|
len(up), up[0], up[len(up) // 2], up[-1], span))
|
||||||
|
else:
|
||||||
|
row["empty"] += 1
|
||||||
|
tile_rows.append((key, vertex_count, index_count // 3, skirt_count,
|
||||||
|
0, math.nan, math.nan, math.nan, math.nan))
|
||||||
|
|
||||||
|
print(f"version={version} voxel_um={voxel_um} tiles={tile_count} camera_m={camera} yaw={yaw} pitch={pitch}")
|
||||||
|
print("lod tiles mesh_tiles vertices triangles skirt_triangles upward_surface_triangles "
|
||||||
|
"upward_centroid_z_m_p05/median/p95 tile_up_z_span_m_p50/p95/max")
|
||||||
|
for lod in sorted(per_lod):
|
||||||
|
r = per_lod[lod]
|
||||||
|
z = sorted(r["up_centroids"])
|
||||||
|
s = sorted(r["tile_spans"])
|
||||||
|
def q(values, fraction):
|
||||||
|
return values[math.floor(fraction * (len(values) - 1))] if values else math.nan
|
||||||
|
print(f"{lod} {r['tiles']} {r['tiles']-r['empty']} {r['vertices']} {r['triangles']} "
|
||||||
|
f"{sum(row[3] for row in tile_rows if row[0][3] == lod)} {r['up_triangles']} "
|
||||||
|
f"{q(z,.05):.4f}/{q(z,.5):.4f}/{q(z,.95):.4f} "
|
||||||
|
f"{q(s,.5):.4f}/{q(s,.95):.4f}/{max(s) if s else math.nan:.4f}")
|
||||||
|
|
||||||
|
print("near_camera_up_tiles (sorted by XY tile-centre distance):")
|
||||||
|
near = []
|
||||||
|
for row in tile_rows:
|
||||||
|
key = row[0]
|
||||||
|
lod = key[3]
|
||||||
|
extent = 32 * (1 << lod) * voxel_um / 1_000_000.0
|
||||||
|
cx = (key[0] + 0.5) * extent
|
||||||
|
cy = (key[1] + 0.5) * extent
|
||||||
|
near.append((math.hypot(cx-camera[0], cy-camera[1]), row))
|
||||||
|
for distance, row in sorted(near)[:30]:
|
||||||
|
key, verts, tris, skirts, ups, lo, med, hi, span = row
|
||||||
|
if ups:
|
||||||
|
print(f"key={key} xy_distance_m={distance:.2f} vertices={verts} triangles={tris} "
|
||||||
|
f"skirt_triangles={skirts} upward_triangles={ups} z_m={lo:.3f}/{med:.3f}/{hi:.3f} "
|
||||||
|
f"tile_span_p05_p95_m={span:.3f}")
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Trace review-image pixels through the exported clipmap mesh."""
|
||||||
|
import array
|
||||||
|
import math
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def read_exact(f, count):
|
||||||
|
data = f.read(count)
|
||||||
|
if len(data) != count:
|
||||||
|
raise EOFError((len(data), count))
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def floor_div(n, d):
|
||||||
|
q, r = divmod(n, d)
|
||||||
|
return q - (r < 0)
|
||||||
|
|
||||||
|
|
||||||
|
def visible_mask(key, desired):
|
||||||
|
x, y, z, lod = key
|
||||||
|
extent = 32 << lod
|
||||||
|
origin = (x * extent, y * extent, z * extent)
|
||||||
|
mask = 0
|
||||||
|
for axis in range(3):
|
||||||
|
for side in range(2):
|
||||||
|
probe = [origin[i] + extent // 2 for i in range(3)]
|
||||||
|
probe[axis] = origin[axis] + (extent if side else -1)
|
||||||
|
neighbor = None
|
||||||
|
for level in range(9):
|
||||||
|
neighbor_extent = 32 << level
|
||||||
|
coord = tuple(floor_div(probe[i], neighbor_extent) for i in range(3)) + (level,)
|
||||||
|
if coord in desired:
|
||||||
|
neighbor = level
|
||||||
|
break
|
||||||
|
if neighbor is None or neighbor != lod:
|
||||||
|
mask |= 1 << (axis * 2 + side)
|
||||||
|
return mask
|
||||||
|
|
||||||
|
|
||||||
|
def ray_box(origin, direction, low, high):
|
||||||
|
lo, hi = 0.1, 20000.0
|
||||||
|
for axis in range(3):
|
||||||
|
if abs(direction[axis]) < 1e-12:
|
||||||
|
if origin[axis] < low[axis] or origin[axis] > high[axis]:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
a = (low[axis] - origin[axis]) / direction[axis]
|
||||||
|
b = (high[axis] - origin[axis]) / direction[axis]
|
||||||
|
if a > b:
|
||||||
|
a, b = b, a
|
||||||
|
lo, hi = max(lo, a), min(hi, b)
|
||||||
|
if lo > hi:
|
||||||
|
return None
|
||||||
|
return lo, hi
|
||||||
|
|
||||||
|
|
||||||
|
def ray_tri(origin, direction, a, b, c):
|
||||||
|
e1 = tuple(b[i] - a[i] for i in range(3))
|
||||||
|
e2 = tuple(c[i] - a[i] for i in range(3))
|
||||||
|
p = (direction[1]*e2[2]-direction[2]*e2[1],
|
||||||
|
direction[2]*e2[0]-direction[0]*e2[2],
|
||||||
|
direction[0]*e2[1]-direction[1]*e2[0])
|
||||||
|
det = sum(e1[i]*p[i] for i in range(3))
|
||||||
|
if abs(det) < 1e-10:
|
||||||
|
return None
|
||||||
|
inv = 1.0/det
|
||||||
|
tvec = tuple(origin[i]-a[i] for i in range(3))
|
||||||
|
u = sum(tvec[i]*p[i] for i in range(3))*inv
|
||||||
|
if u < 0 or u > 1:
|
||||||
|
return None
|
||||||
|
q = (tvec[1]*e1[2]-tvec[2]*e1[1],
|
||||||
|
tvec[2]*e1[0]-tvec[0]*e1[2],
|
||||||
|
tvec[0]*e1[1]-tvec[1]*e1[0])
|
||||||
|
v = sum(direction[i]*q[i] for i in range(3))*inv
|
||||||
|
if v < 0 or u+v > 1:
|
||||||
|
return None
|
||||||
|
t = sum(e2[i]*q[i] for i in range(3))*inv
|
||||||
|
return t if t >= 0.1 else None
|
||||||
|
|
||||||
|
|
||||||
|
if len(sys.argv) < 8 or len(sys.argv) % 2 != 1:
|
||||||
|
raise SystemExit("usage: ProbeCaptureRays.py scene.vfmesh camX camY camZ yaw pitch pixelX pixelY [pixelX pixelY ...]")
|
||||||
|
scene_path = sys.argv[1]
|
||||||
|
cam = tuple(map(float, sys.argv[2:5]))
|
||||||
|
yaw, pitch = map(math.radians, map(float, sys.argv[5:7]))
|
||||||
|
pixels = [(int(sys.argv[i]), int(sys.argv[i+1])) for i in range(7, len(sys.argv), 2)]
|
||||||
|
width, height, fov = 1600, 900, math.radians(90)
|
||||||
|
tan_h = math.tan(fov/2)
|
||||||
|
tan_v = tan_h / (width/height)
|
||||||
|
forward = (math.sin(yaw)*math.cos(pitch), math.cos(yaw)*math.cos(pitch), math.sin(pitch))
|
||||||
|
right = (math.cos(yaw), -math.sin(yaw), 0.0)
|
||||||
|
up = (right[1]*forward[2]-right[2]*forward[1], right[2]*forward[0]-right[0]*forward[2], right[0]*forward[1]-right[1]*forward[0])
|
||||||
|
|
||||||
|
with open(scene_path, "rb") as f:
|
||||||
|
if read_exact(f, 8) != b"VFMSH001":
|
||||||
|
raise SystemExit("bad scene")
|
||||||
|
version, voxel_um, count = struct.unpack("<3I", read_exact(f, 12))
|
||||||
|
scene_cam = struct.unpack("<3d", read_exact(f, 24))
|
||||||
|
scene_yaw, scene_pitch = struct.unpack("<2d", read_exact(f, 16))
|
||||||
|
voxel_m = voxel_um / 1_000_000.0
|
||||||
|
tiles = []
|
||||||
|
desired = set()
|
||||||
|
for _ in range(count):
|
||||||
|
key = struct.unpack("<4i", read_exact(f, 16))
|
||||||
|
surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack("<5I", read_exact(f, 20))
|
||||||
|
positions = array.array("f"); positions.frombytes(read_exact(f, vertex_count*12))
|
||||||
|
normals = array.array("f"); normals.frombytes(read_exact(f, vertex_count*12))
|
||||||
|
indices = array.array("I"); indices.frombytes(read_exact(f, index_count*4))
|
||||||
|
masks = read_exact(f, mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap(); normals.byteswap(); indices.byteswap()
|
||||||
|
desired.add(key)
|
||||||
|
tiles.append((key, surface_vertices, skirt_count, positions, normals, indices, masks))
|
||||||
|
|
||||||
|
print(f"scene={scene_path} tiles={count} camera={cam} yaw={math.degrees(yaw):.3f} pitch={math.degrees(pitch):.3f}")
|
||||||
|
for px, py in pixels:
|
||||||
|
nx = 2*px/width - 1
|
||||||
|
ny = 1 - 2*py/height
|
||||||
|
direction0 = tuple(forward[i] + nx*tan_h*right[i] + ny*tan_v*up[i] for i in range(3))
|
||||||
|
length = math.sqrt(sum(v*v for v in direction0))
|
||||||
|
direction = tuple(v/length for v in direction0)
|
||||||
|
hits = []
|
||||||
|
candidate_tiles = 0
|
||||||
|
for key, surface_vertices, skirt_count, positions, normals, indices, masks in tiles:
|
||||||
|
x,y,z,lod = key
|
||||||
|
extent = 32*(1<<lod)*voxel_m
|
||||||
|
low = (x*extent, y*extent, z*extent)
|
||||||
|
high = tuple(v+extent for v in low)
|
||||||
|
if ray_box(cam, direction, low, high) is None:
|
||||||
|
continue
|
||||||
|
candidate_tiles += 1
|
||||||
|
show_mask = visible_mask(key, desired)
|
||||||
|
for tri in range(len(indices)//3):
|
||||||
|
m = masks[tri] if masks else 0
|
||||||
|
if m and (m & show_mask) == 0:
|
||||||
|
continue
|
||||||
|
ia,ib,ic = indices[tri*3:tri*3+3]
|
||||||
|
a,b,c = [],[],[]
|
||||||
|
for idx, out in ((ia,a),(ib,b),(ic,c)):
|
||||||
|
base=idx*3
|
||||||
|
out.extend((positions[base],positions[base+1],positions[base+2]))
|
||||||
|
t=ray_tri(cam,direction,a,b,c)
|
||||||
|
if t is None: continue
|
||||||
|
p=tuple(cam[i]+t*direction[i] for i in range(3))
|
||||||
|
nz=(normals[ia*3+2]+normals[ib*3+2]+normals[ic*3+2])/3
|
||||||
|
hits.append((t,key,tri,m,p,nz,ia>=surface_vertices or ib>=surface_vertices or ic>=surface_vertices))
|
||||||
|
hits.sort(key=lambda h:h[0])
|
||||||
|
print(f"pixel=({px},{py}) ray={direction} candidate_tiles={candidate_tiles} hits={len(hits)}")
|
||||||
|
for t,key,tri,m,p,nz,skirt in hits[:12]:
|
||||||
|
print(f" t={t:.3f}m key={key} tri={tri} mask={m} skirt={skirt} z={p[2]:.3f} normal_z={nz:.3f} xyz=({p[0]:.3f},{p[1]:.3f},{p[2]:.3f})")
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# Linux18 — FloatingIslands stratum boundaries
|
||||||
|
|
||||||
|
**Result:** FloatingIslands geometry v3 removes its false lower all-XY seal, keeps the ceiling from the shared upper-boundary seal, and projects missing ceiling cells for every generator. SurfaceWorld hashes match the approved L14 baseline at `8704129` in every checked configuration.
|
||||||
|
|
||||||
|
## Scale in the Unreal route
|
||||||
|
|
||||||
|
`AVoxelWorld::CoreVoxelSizeCm` defaults to 25 cm, and `GetActiveVoxelSizeCm()` resolves the runtime value ([VoxelWorld.h:300](../../../../Source/VoxelForge/Public/VoxelWorld.h), [VoxelWorld.cpp:7495](../../../../Source/VoxelForge/Private/VoxelWorld.cpp)). The adapter writes the resolved size to the snapshot ([VoxelForgeCoreAdapter.cpp:1019](../../../../Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp)); `AVoxelWorld` imports it with `ResolvedWorldSnapshot::FromJson` ([VoxelWorld.cpp:7597](../../../../Source/VoxelForge/Private/VoxelWorld.cpp), [VoxelForgeCore.cpp:916](../../../../Standalone/Core/src/VoxelForgeCore.cpp)). S1 records this single active size source and the 32-Core-voxel tile rule ([Round5 REPORT.md](../Round5/REPORT.md#1-one-core-voxel-size-source)).
|
||||||
|
|
||||||
|
At the current 25 cm setting, **SurfaceWorld strata use 8 m per slot chunk**: the Unreal surface plan multiplies the slot range by 32 Core voxels ([VoxelForgeCoreAdapter.cpp:280](../../../../Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp); `CHUNK_SIZE=32` in [VoxelTypes.h:211](../../../../Source/VoxelForge/Public/VoxelTypes.h)). **FloatingIslands uses 4 m per owner-authored chunk**: the adapter’s owner size is `VOXEL_SIZE` ([VoxelForgeCoreAdapter.cpp:33](../../../../Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp); 12.5 cm in [VoxelTypes.h:213](../../../../Source/VoxelForge/Public/VoxelTypes.h)), and its plan multiplies 32 owner voxels by the serialized owner/Core ratio, currently 12.5/25 = 1/2, or 16 Core voxels ([VoxelForgeCoreAdapter.cpp:441](../../../../Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp)). For chunks 0…−15 that gives z bounds −60…+4 m. This is the two explicit coordinate conventions in the game route, not a global change from 8 m to 4 m.
|
||||||
|
|
||||||
|
Standalone uses the same owner/Core ratio and chunk formula ([OwnerWorldLoader.h:302](../../../../Standalone/Source/OwnerWorldLoader.h), [WorldAssemblyPrototype.h:488](../../../../Standalone/Source/WorldAssemblyPrototype.h)); no current Unreal/standalone scale disagreement was found. L18 also removed the standalone-only two-Core-voxel bounds inset so recipe bounds match the Unreal plan. The owner decides whether either authored scale changes.
|
||||||
|
|
||||||
|
## Fix and ceiling
|
||||||
|
|
||||||
|
Geometry version 3 preserves the island shapes and removes only the lower all-XY seal. The upper seal calls the shared `VF_ApplyUpperBoundarySeal` density primitive; the shared two-sided seal uses the same upper-band math ([VoxelDensityPrimitives.h:44](../../../../Source/VoxelForge/Public/VoxelDensityPrimitives.h)). Resolved bounds come from each strate’s own range. The shared `AppendStratumBoundaryCeilings` mesh path runs for every generator and LOD, adding a plane at the resolved upper boundary where the mesh lacks coverage. It suppresses an existing plane only when nine probes show full cell coverage ([ShapeWorldInternal.h:36](../../../../Standalone/Core/src/ShapeWorldInternal.h), [FarShellsInternal.h:894](../../../../Standalone/Core/src/FarShellsInternal.h), [VoxelForgeCore.cpp:2207](../../../../Standalone/Core/src/VoxelForgeCore.cpp)). The underside normal points into the room, so the existing material lighting renders the interior face. This adds no FloatingIslands-specific height.
|
||||||
|
|
||||||
|
For the captured FloatingIslands strate, the upper bound is **z=+4 m** and its configured 0.5 m seal gives an interior-facing ceiling surface beginning around **z=+3.5 m**. The density probe crosses into the seal at 3.5 m and exits at z=4.019 m; z=7.75, 8.0, and 8.25 m remain air with no crossing. The lower all-XY seal is removed; remaining geometry below −60 m belongs to the lower world and is visible in the ray traces. Thus the apparent 3.5 m “floor” is the retained ceiling when viewed from inside; the 8 m floor is absent. No chunk size changed.
|
||||||
|
|
||||||
|
The owner’s decision and version-3 recipe say an underground strate has a ceiling at its configured upper boundary. “Farther to breathe” comes from a taller configured range. I updated the stale architecture-table wording “open void” to say the islands sit beneath the shared ceiling.
|
||||||
|
|
||||||
|
## Ceiling continuity
|
||||||
|
|
||||||
|
The final audit used 16×16 interior samples per tile and paired samples along same-LOD tile edges. Far LODs pass continuously:
|
||||||
|
|
||||||
|
| LOD | Tiles with cap | Interior hits | Max root delta | Edge gaps |
|
||||||
|
|---:|---:|---:|---:|---:|
|
||||||
|
| 4 | 48/48 | 12,288/12,288 | 0 cm | 0 |
|
||||||
|
| 5 | 48/48 | 12,288/12,288 | 0 cm | 0 |
|
||||||
|
| 6 | 48/48 | 12,288/12,288 | 0 cm | 0 |
|
||||||
|
| 7 | 48/48 | 12,288/12,288 | 0 cm | 0 |
|
||||||
|
| 8 | 153/153 | 39,168/39,168 | 0 cm | 0 |
|
||||||
|
|
||||||
|
LOD0, 2, and 3 also hit all tested samples. LOD1 missed 3 of 12,288 interior samples (0.387 cm maximum root delta) and had no shared-edge gaps. The readable upward view is `captures/floating-islands-ceiling-up.jpg`.
|
||||||
|
|
||||||
|
## Dark tile-aligned patches
|
||||||
|
|
||||||
|
They are lower SurfaceWorld triangles visible below the removed false lower cap, not empty rays and not a viewer dark-square overlay. Traces in the final island export found:
|
||||||
|
|
||||||
|
- Pixel (700,720): first hit at 104.924 m on non-skirt LOD2 tile `(-2,2,-2,2)`, z=−62.925 m, normal-z 0.266; another LOD2 tile hits z=−69.942 m.
|
||||||
|
- Pixel (1050,650): first hit at 168.051 m on non-skirt LOD3 tile `(0,2,-2,3)`, z=−72.594 m, normal-z 0.895; lower LOD4 geometry is also hit at z=−112.229 m and −120 m.
|
||||||
|
|
||||||
|
The ring probe measures real LOD height steps on those lower surfaces. I left them visible: suppressing those triangles would hide valid lower-stratum geometry, while changing the lower strata or SurfaceWorld would exceed this local fix. The sampled rectangles are not missing tiles or floor holes.
|
||||||
|
|
||||||
|
## Cross-LOD ring steps
|
||||||
|
|
||||||
|
Entries are median / p95 / maximum absolute edge-height difference in centimetres, then gap count. The L18 column is the final matched island camera. The large remaining steps are lower-stratum terrain, not the upper ceiling.
|
||||||
|
|
||||||
|
| LOD edge | L17 before | L17 after | L18 final |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| 0→1 | 3.451 / 8.920 / 449.158; 14 | —; 0 samples | —; 0 samples |
|
||||||
|
| 1→2 | 26.642 / 148.262 / 149.862; 0 | 0 / 0 / 0; 0 | 147.141 / 148.403 / 149.862; 0 |
|
||||||
|
| 2→3 | 0.134 / 198.730 / 773.318; 8 | 0 / 0 / 0; 0 | 128.603 / 200.789 / 773.318; 8 |
|
||||||
|
| 3→4 | 3.269 / 47.875 / 188.531; 27 | 0 / 0 / 0; 0 | 3.269 / 47.774 / 188.531; 27 |
|
||||||
|
| 4→5 | 29.975 / 130.496 / 445.444; 6 | 0 / 0 / 30.957; 1 | 29.992 / 124.373 / 443.482; 6 |
|
||||||
|
| 5→6 | 12.234 / 127.224 / 536.573; 32 | 12.234 / 127.224 / 536.573; 32 | 12.234 / 127.224 / 536.573; 21 |
|
||||||
|
| 6→7 | 29.013 / 301.752 / 42,624.479; 0 | 28.892 / 291.861 / 945.477; 0 | 29.013 / 301.752 / 42,624.479; 0 |
|
||||||
|
| 7→8 | 83.843 / 1,225.416 / 3,755.407; 32 | 83.843 / 1,225.416 / 3,755.407; 0 | 83.843 / 1,225.416 / 3,755.407; 0 |
|
||||||
|
|
||||||
|
## SurfaceWorld regression hashes
|
||||||
|
|
||||||
|
The 14-run check compared each final output to `Saved/Linux/l14-final-hash-sw-clang-off-w12-r5.log` from the L14/`8704129` baseline: Clang `-ffp-contract=off` and GCC default, W1/W8/W12, with five W12 repeats. All 14 matched the whole-view, full-detail, LOD0–8, and exact-query rows. The 24 m crossing check likewise passed all 14 compiler/worker configurations against their L14 compiler-specific references.
|
||||||
|
|
||||||
|
Whole view D/M/P/T: `c10697a8f48380c7 / e75c9119a1498cd5 / 49a9a8d1835a6c8c / edcb23288b5e8724`.
|
||||||
|
|
||||||
|
Full detail D/M/P/T: `0ee0579bd15cbbe4 / 32e5d4964d610fee / 1932b96ee4b1d910 / 69743c6bad3447dc`.
|
||||||
|
|
||||||
|
| LOD | Density | Mesh | Payload | Topology |
|
||||||
|
|---:|---|---|---|---|
|
||||||
|
| 0 | `769f2a706d585a72` | `33eeeb39fa443816` | `645621a1d968983f` | `c2457ed905036c9d` |
|
||||||
|
| 1 | `69cc5a5c0a2c60af` | `b48335f5410c57d5` | `d15c5480162ab8ba` | `feca1bd025823604` |
|
||||||
|
| 2 | `85deed5683a77202` | `53469895987aeea2` | `e55f8ae7e1532033` | `d5feb12a0e85c09b` |
|
||||||
|
| 3 | `6518caefb3be7e7f` | `ec513c90b853a614` | `f62fa1137ff17f82` | `ccf1dcf681965816` |
|
||||||
|
| 4 | `62d4be96021992f4` | `f1e4c6efcc2615a9` | `9dc5f9c88be02f33` | `fe6f55125a07c3bf` |
|
||||||
|
| 5 | `5e4a8c38b8cea6fc` | `08ee56998aaf21e5` | `541eea8558f26cc8` | `6539374a8b3336d8` |
|
||||||
|
| 6 | `e036e95b9006945b` | `624a33f941a99b02` | `81b73e14f68bdf86` | `4de4c63ce1f80a56` |
|
||||||
|
| 7 | `8ee358af56a30d8d` | `46ab3c6184048b7c` | `0a6164b7e68e80c5` | `0c26b85bb03bc831` |
|
||||||
|
| 8 | `c3458e1cbdb9c471` | `46c03d0994d75192` | `8f89fc5bde12d230` | `d16dc0cf62031898` |
|
||||||
|
|
||||||
|
Exact query: 9 probes; density/unclamped `e4d4a8eb95b35933`, detail `efcba20c0c70c415`, collision `e604823a249029bf`.
|
||||||
|
|
||||||
|
The 24 m crossing W12×5 hashes match those same aggregate whole-view, full-detail, and exact-query values on both compiler references; the full check compared all 12 rows per run.
|
||||||
|
|
||||||
|
FloatingIslands’ representative W12×5 whole-view D/M/P/T is `ca7f375cded78f1d / da003331c554f8f3 / 0d2e74862b4ee500 / b7a7177af8915a21`; full-detail is `fc71abf85f5b9373 / e139b32d42ecfa29 / 28a2414e41de59bd / d61ca68ca64f74ca`. Exact query is `4ef614485815ee30` density/unclamped, `efcba20c0c70c415` detail, `e604823a249029bf` collision. Clang and GCC representative hashes agree, and all nine LOD rows stayed stable across the 28-run matrix.
|
||||||
|
|
||||||
|
## Other invariants and Unreal edits
|
||||||
|
|
||||||
|
- Final matrix: 28/28 rows passed; repeated runs were stable within each compiler. Route parity had 0 mismatches across 782,026 FI and 910,651 SurfaceWorld exact samples. Proof audits had 0 violations: FI 9,048 blocks / 12,042,888 samples; SurfaceWorld 27,479 / 36,574,549. Readiness negative controls were caught; walkability passed 4,000/4,000 FI and 5,642/5,642 SurfaceWorld samples.
|
||||||
|
- L3 FP-contract preamble/self-test, L4 `FLazy`, L7 shared cache, L9/L12 proofs, L11 readiness/crash handling, L13 route binding, ARCHITECTURE §8.10, and async/MPSC/Epoch paths remain intact. Both final compiler runs started without the Core FP-contract self-test refusing the build. No async route was added.
|
||||||
|
- Unreal-side edits are limited to [VoxelForgeCoreAdapter.cpp](../../../../Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:453), bumping FI geometry version 2→3 so serialized recipes pick up the lower-seal removal, and the shared [VoxelDensityPrimitives.h](../../../../Source/VoxelForge/Public/VoxelDensityPrimitives.h:44), extracting the common upper-seal math used by v3. The Unreal target could not be compiled on this Linux host.
|
||||||
|
|
||||||
|
## Captures
|
||||||
|
|
||||||
|
All JPEGs are below 300 KB. Before/after pairs use matched cameras: FI flat square `(-11.6124,-118.2212,-19.8452), yaw/pitch 0/0`; FI island `(-22,-19,-22.8), 0/−4`; SurfaceWorld spawn `(0,-35,-487.94), 0/−22`; horizon uses the same spawn camera with pitch 0. The ceiling view uses the island camera with pitch 80. The SurfaceWorld pairs look the same; the exact hash checks certify its output bit-for-bit.
|
||||||
|
|
||||||
|
- FI flat square: `captures/floating-islands-flat-square-before.jpg`, `captures/floating-islands-flat-square-after.jpg`.
|
||||||
|
- FI low island: `captures/floating-islands-island-view-before.jpg`, `captures/floating-islands-island-view-after.jpg`.
|
||||||
|
- FI upward ceiling: `captures/floating-islands-ceiling-up.jpg`.
|
||||||
|
- SurfaceWorld L1 spawn: `captures/surface-world-l1-spawn-before.jpg`, `captures/surface-world-l1-spawn-after.jpg`.
|
||||||
|
- SurfaceWorld horizon: `captures/surface-world-horizon-before.jpg`, `captures/surface-world-horizon-after.jpg`.
|
||||||
|
- Flat-square diagnostic without tile edges: `captures/floating-islands-flat-square-no-edges.jpg`.
|
||||||
|
|
||||||
|
## Failed or retried runs
|
||||||
|
|
||||||
|
- The first density-probe link omitted legacy compatibility objects and failed with undefined references. Relinking with the legacy implementation objects succeeded; both boundary probes then ran.
|
||||||
|
- The initial 4×4 continuity audit failed at far LODs 4–6 and found edge gaps. The shared all-LOD boundary cap and final full-cell coverage check replaced the center-only deduplication; final 16×16 LOD4–8 continuity passed with zero edge gaps.
|
||||||
|
- An early matrix checker compared Clang contraction-off hashes directly with GCC default hashes and reported FI mismatches. The checker was corrected to test repeated runs within each compiler and the L14 compiler-specific references; the final 28-row matrix passed.
|
||||||
|
- An early offscreen capture drew all skirts and showed an artificial slab. The capture now applies the viewer’s hidden-skirt mask; final matched captures were regenerated.
|
||||||
|
- A GCC build interrupted by owner steering produced no binary and was rerun. A pre-correction Clang build was superseded by the final build. No final build or matrix run failed or exceeded the runtime limits.
|
||||||
|
|
||||||
|
## Resources and owner note
|
||||||
|
|
||||||
|
Final builds: Clang 19 exit 0 in 31.689 s, peak RSS 436,088 KiB; GCC 14 exit 0 in 48.648 s, peak RSS 903,168 KiB. The largest measured runtime process was 897,276 KiB RSS. All processes stayed below the 6 GiB address-space cap; CPU affinity was 0–11, worker maximum 12, normal priority, and governor `powersave`. No run exceeded 30 minutes or stalled for 15 minutes. See [BuildResources.md](BuildResources.md).
|
||||||
|
|
||||||
|
**Owner note:** SurfaceWorld stayed bit-identical to L14. FloatingIslands now retains a ceiling at its configured upper strate boundary while removing the false lower all-XY floor. At the current plan that is a z=+4 m boundary with a 0.5 m seal; its distance above the islands comes from the configured range. The small Unreal edits are listed above and need an owner-machine UBT compile.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- **Merged.** SurfaceWorld is bit-identical (14 configs plus the 24 m crossing); the FI false all-XY lower seal
|
||||||
|
is removed; the ceiling comes from the shared, strate-parameter upper-boundary seal and ceiling path
|
||||||
|
(owner rule: every strate has a ceiling, height from its own parameters). The Unreal edits are a pure
|
||||||
|
extraction of the existing upper-seal math plus the FI `geometryVersion` 2→3; they need a UBT compile on
|
||||||
|
the owner PC.
|
||||||
|
- **The scale answer:** both conventions are deliberate and agree between Unreal and the standalone
|
||||||
|
(SurfaceWorld 32 Core voxels per chunk; FloatingIslands 32 owner voxels = 16 Core voxels). Nothing
|
||||||
|
rescaled.
|
||||||
|
- **Still open (L19):**
|
||||||
|
1. Small black holes in the FI ceiling, visible near the top of `floating-islands-flat-square-after.jpg`;
|
||||||
|
compare LOD1 missing 3/12,288 continuity samples.
|
||||||
|
2. The owner's ledges are most likely the LOWER stratum's terrain seen through the floorless FI void:
|
||||||
|
ring steps of 147 cm median at LOD1→2 and 129 cm at 2→3 remain in that terrain. "Valid lower-stratum
|
||||||
|
geometry" with metre-scale cross-LOD steps is still a bug the owner can see.
|
||||||
|
- Exports (229 MB `.vfmesh`) are not committed.
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compare upward mesh heights on shared XY edges between adjacent LOD rings."""
|
||||||
|
import array
|
||||||
|
import collections
|
||||||
|
import math
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def read_scene(path):
|
||||||
|
tiles = []
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
if f.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit(f"{path}: invalid mesh header")
|
||||||
|
version, voxel_um, count = struct.unpack("<3I", f.read(12))
|
||||||
|
camera = struct.unpack("<3d", f.read(24))
|
||||||
|
yaw, pitch = struct.unpack("<2d", f.read(16))
|
||||||
|
voxel_m = voxel_um / 1_000_000.0
|
||||||
|
for _ in range(count):
|
||||||
|
key = struct.unpack("<4i", f.read(16))
|
||||||
|
surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", f.read(20))
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(f.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(f.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(f.read(index_count * 4))
|
||||||
|
masks = f.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap(); normals.byteswap(); indices.byteswap()
|
||||||
|
triangles = []
|
||||||
|
for i in range(0, index_count, 3):
|
||||||
|
if masks and masks[i // 3] != 0:
|
||||||
|
continue
|
||||||
|
a, b, c = indices[i:i + 3]
|
||||||
|
if max(a, b, c) >= surface_vertices:
|
||||||
|
continue
|
||||||
|
nz = (normals[a * 3 + 2] + normals[b * 3 + 2] + normals[c * 3 + 2]) / 3.0
|
||||||
|
if nz < 0.65:
|
||||||
|
continue
|
||||||
|
tri = tuple((positions[j * 3], positions[j * 3 + 1], positions[j * 3 + 2])
|
||||||
|
for j in (a, b, c))
|
||||||
|
triangles.append(tri)
|
||||||
|
if triangles:
|
||||||
|
lod = key[3]
|
||||||
|
extent = 32 * (1 << lod) * voxel_m
|
||||||
|
tiles.append({"key": key, "lod": lod, "extent": extent,
|
||||||
|
"x0": key[0] * extent, "y0": key[1] * extent,
|
||||||
|
"x1": (key[0] + 1) * extent, "y1": (key[1] + 1) * extent,
|
||||||
|
"z0": key[2] * extent, "z1": (key[2] + 1) * extent,
|
||||||
|
"triangles": triangles})
|
||||||
|
return camera, yaw, pitch, tiles
|
||||||
|
|
||||||
|
|
||||||
|
def height_at(tile, x, y):
|
||||||
|
best = None
|
||||||
|
for tri in tile["triangles"]:
|
||||||
|
(x0, y0, z0), (x1, y1, z1), (x2, y2, z2) = tri
|
||||||
|
den = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2)
|
||||||
|
if abs(den) < 1.0e-10:
|
||||||
|
continue
|
||||||
|
a = ((y1 - y2) * (x - x2) + (x2 - x1) * (y - y2)) / den
|
||||||
|
b = ((y2 - y0) * (x - x2) + (x0 - x2) * (y - y2)) / den
|
||||||
|
c = 1.0 - a - b
|
||||||
|
if min(a, b, c) >= -1.0e-5:
|
||||||
|
z = a * z0 + b * z1 + c * z2
|
||||||
|
if best is None or z > best:
|
||||||
|
best = z
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
path = sys.argv[1]
|
||||||
|
camera, yaw, pitch, tiles = read_scene(path)
|
||||||
|
by_lod = collections.defaultdict(list)
|
||||||
|
for tile in tiles:
|
||||||
|
by_lod[tile["lod"]].append(tile)
|
||||||
|
print(f"scene={path} camera_m={camera} yaw={yaw} pitch={pitch} nonempty_mesh_tiles={len(tiles)}")
|
||||||
|
print("lod_pair shared_edges samples_both median_abs_cm p95_abs_cm max_abs_cm median_delta_cm gaps")
|
||||||
|
for lod in range(8):
|
||||||
|
fine = by_lod[lod]
|
||||||
|
coarse = by_lod[lod + 1]
|
||||||
|
deltas = []
|
||||||
|
edge_rows = []
|
||||||
|
edge_count = 0
|
||||||
|
gap_count = 0
|
||||||
|
# Shared XY edges occur only where tile extents touch and overlap on the other axis.
|
||||||
|
for a in fine:
|
||||||
|
for b in coarse:
|
||||||
|
zlo, zhi = max(a["z0"], b["z0"]), min(a["z1"], b["z1"])
|
||||||
|
if zhi - zlo <= 1.0e-4:
|
||||||
|
continue
|
||||||
|
edge = None
|
||||||
|
if abs(a["x1"] - b["x0"]) < 1.0e-4 or abs(b["x1"] - a["x0"]) < 1.0e-4:
|
||||||
|
lo, hi = max(a["y0"], b["y0"]), min(a["y1"], b["y1"])
|
||||||
|
if hi - lo > 1.0e-4:
|
||||||
|
edge = ("x", a["x1"] if abs(a["x1"] - b["x0"]) < 1.0e-4 else a["x0"], lo, hi)
|
||||||
|
if edge is None and (abs(a["y1"] - b["y0"]) < 1.0e-4 or abs(b["y1"] - a["y0"]) < 1.0e-4):
|
||||||
|
lo, hi = max(a["x0"], b["x0"]), min(a["x1"], b["x1"])
|
||||||
|
if hi - lo > 1.0e-4:
|
||||||
|
edge = ("y", a["y1"] if abs(a["y1"] - b["y0"]) < 1.0e-4 else a["y0"], lo, hi)
|
||||||
|
if edge is None:
|
||||||
|
continue
|
||||||
|
edge_count += 1
|
||||||
|
axis, fixed, lo, hi = edge
|
||||||
|
epsilon = min(0.02, a["extent"] * 0.001, b["extent"] * 0.001)
|
||||||
|
samples = 9
|
||||||
|
matched = 0
|
||||||
|
edge_deltas = []
|
||||||
|
for i in range(samples):
|
||||||
|
t = (i + 0.5) / samples
|
||||||
|
varying = lo + (hi - lo) * t
|
||||||
|
if axis == "x":
|
||||||
|
side_a = 1.0 if abs(a["x1"] - fixed) < 1.0e-4 else -1.0
|
||||||
|
side_b = 1.0 if abs(b["x1"] - fixed) < 1.0e-4 else -1.0
|
||||||
|
za = height_at(a, fixed - side_a * epsilon, varying)
|
||||||
|
zb = height_at(b, fixed - side_b * epsilon, varying)
|
||||||
|
else:
|
||||||
|
side_a = 1.0 if abs(a["y1"] - fixed) < 1.0e-4 else -1.0
|
||||||
|
side_b = 1.0 if abs(b["y1"] - fixed) < 1.0e-4 else -1.0
|
||||||
|
za = height_at(a, varying, fixed - side_a * epsilon)
|
||||||
|
zb = height_at(b, varying, fixed - side_b * epsilon)
|
||||||
|
if za is not None and not zlo - 1.0e-3 <= za <= zhi + 1.0e-3:
|
||||||
|
za = None
|
||||||
|
if zb is not None and not zlo - 1.0e-3 <= zb <= zhi + 1.0e-3:
|
||||||
|
zb = None
|
||||||
|
if za is not None and zb is not None:
|
||||||
|
delta = (za - zb) * 100.0
|
||||||
|
deltas.append(delta)
|
||||||
|
edge_deltas.append((delta, za, zb, varying))
|
||||||
|
matched += 1
|
||||||
|
if matched == 0:
|
||||||
|
gap_count += 1
|
||||||
|
elif edge_deltas:
|
||||||
|
edge_rows.append((max(abs(row[0]) for row in edge_deltas),
|
||||||
|
a["key"], b["key"], edge, edge_deltas))
|
||||||
|
if deltas:
|
||||||
|
absolute = sorted(abs(x) for x in deltas)
|
||||||
|
signed = sorted(deltas)
|
||||||
|
p95 = absolute[math.floor(0.95 * (len(absolute) - 1))]
|
||||||
|
median_abs = absolute[len(absolute) // 2]
|
||||||
|
median_delta = signed[len(signed) // 2]
|
||||||
|
print(f"{lod}->{lod+1} {edge_count} {len(deltas)} {median_abs:.3f} {p95:.3f} {max(absolute):.3f} {median_delta:.3f} {gap_count}")
|
||||||
|
for worst, fine_key, coarse_key, edge, rows in sorted(edge_rows, reverse=True)[:2]:
|
||||||
|
row = max(rows, key=lambda item: abs(item[0]))
|
||||||
|
print(f" worst_edge max_abs_cm={worst:.3f} fine={fine_key} coarse={coarse_key} "
|
||||||
|
f"axis={edge[0]} fixed_m={edge[1]:.3f} span_m={edge[2]:.3f}..{edge[3]:.3f} "
|
||||||
|
f"sample_v_m={row[3]:.3f} fine_z_m={row[1]:.3f} "
|
||||||
|
f"coarse_z_m={row[2]:.3f} delta_cm={row[0]:.3f}")
|
||||||
|
else:
|
||||||
|
print(f"{lod}->{lod+1} {edge_count} 0 n/a n/a n/a n/a {gap_count}")
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the L18 24 m SurfaceWorld crossing hash matrix."""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l18-crossing"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not os.sched_getaffinity(0).issubset(set(range(12))):
|
||||||
|
raise SystemExit("run under taskset -c 0-11")
|
||||||
|
BINARIES = {
|
||||||
|
"clang-off": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux18-clang",
|
||||||
|
"gcc-default": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux18-gcc",
|
||||||
|
}
|
||||||
|
PATTERN = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
rows = []
|
||||||
|
passed = True
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for workers, repeat in [(1, 1), (8, 1), (12, 1), (12, 2), (12, 3), (12, 4), (12, 5)]:
|
||||||
|
name = f"{compiler}-w{workers}-r{repeat}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = ["prlimit", "--as=6442450944", "--", str(binary),
|
||||||
|
str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--stream-hash", "--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||||
|
f"--workers={workers}", "--camera=0,-35,-487.94,0,-22",
|
||||||
|
"--camera-step=24,0,0", "--region-crossing", "--recipe=surface-world"]
|
||||||
|
started = time.monotonic()
|
||||||
|
peak_rss = 0
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
process = subprocess.Popen(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
start_new_session=True)
|
||||||
|
while process.poll() is None:
|
||||||
|
pending = [process.pid]
|
||||||
|
seen = set()
|
||||||
|
while pending:
|
||||||
|
pid = pending.pop()
|
||||||
|
if pid in seen:
|
||||||
|
continue
|
||||||
|
seen.add(pid)
|
||||||
|
try:
|
||||||
|
pending.extend(int(value) for value in
|
||||||
|
Path(f"/proc/{pid}/task/{pid}/children").read_text().split())
|
||||||
|
for line in Path(f"/proc/{pid}/status").read_text().splitlines():
|
||||||
|
if line.startswith("VmRSS:"):
|
||||||
|
peak_rss = max(peak_rss, int(line.split()[1]))
|
||||||
|
break
|
||||||
|
except (FileNotFoundError, PermissionError, ProcessLookupError, ValueError):
|
||||||
|
pass
|
||||||
|
if time.monotonic() - started > 900:
|
||||||
|
os.killpg(process.pid, signal.SIGTERM)
|
||||||
|
try:
|
||||||
|
process.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
os.killpg(process.pid, signal.SIGKILL)
|
||||||
|
process.wait()
|
||||||
|
passed = False
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
code = process.wait()
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
actual = PATTERN.findall(logfile.read_text(encoding="utf-8", errors="replace"))
|
||||||
|
ok = code == 0 and len(actual) == 12
|
||||||
|
passed &= ok
|
||||||
|
rows.append((compiler, workers, repeat, code, elapsed, peak_rss, ok, actual))
|
||||||
|
print(f"[CrossingRun] {name} exit={code} seconds={elapsed:.3f} "
|
||||||
|
f"peak_rss_kib={peak_rss} hash_rows={len(actual)} status={'PASS' if ok else 'FAIL'}",
|
||||||
|
flush=True)
|
||||||
|
reference = None
|
||||||
|
for compiler, workers, repeat, code, elapsed, peak_rss, ok, hashes in rows:
|
||||||
|
if reference is None:
|
||||||
|
reference = hashes
|
||||||
|
elif hashes != reference:
|
||||||
|
passed = False
|
||||||
|
print(f"[CrossingHashMismatch] {compiler} workers={workers} repeat={repeat}", flush=True)
|
||||||
|
print(f"[CrossingSummary] rows={len(rows)} expected=14 result={'PASS' if passed else 'FAIL'} "
|
||||||
|
f"path={OUT}", flush=True)
|
||||||
|
raise SystemExit(0 if passed and len(rows) == 14 else 1)
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the L18 deterministic W1/W8/W12 x5 matrix on both supported Linux builds."""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l18-matrix"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not os.sched_getaffinity(0).issubset(set(range(12))):
|
||||||
|
raise SystemExit("run under taskset -c 0-11")
|
||||||
|
|
||||||
|
BINARIES = {
|
||||||
|
"clang-off": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux18-clang",
|
||||||
|
"gcc-default": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux18-gcc",
|
||||||
|
}
|
||||||
|
RECIPES = {
|
||||||
|
"floating-islands": "-10,-31,-22,0,-8",
|
||||||
|
"surface-world": "0,-35,-487.94,0,-22",
|
||||||
|
}
|
||||||
|
HASH_PATTERNS = {
|
||||||
|
"full": re.compile(r"^\[PipelineFullDetailHash\].*$", re.M),
|
||||||
|
"whole": re.compile(r"^\[PipelineWholeViewHash\].*$", re.M),
|
||||||
|
"lod": re.compile(r"^\[PipelineLODHash\].*$", re.M),
|
||||||
|
"exact": re.compile(r"^\[PipelineExactQueryHash\].*$", re.M),
|
||||||
|
}
|
||||||
|
rows = []
|
||||||
|
all_pass = True
|
||||||
|
|
||||||
|
for recipe, camera in RECIPES.items():
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
if not binary.exists():
|
||||||
|
raise SystemExit(f"missing binary: {binary}")
|
||||||
|
for workers, repeat in [(1, 1), (8, 1), (12, 1), (12, 2), (12, 3), (12, 4), (12, 5)]:
|
||||||
|
name = f"{recipe}-{compiler}-w{workers}-r{repeat}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = ["prlimit", "--as=6442450944", "--", str(binary),
|
||||||
|
str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--stream-hash", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", f"--workers={workers}", f"--camera={camera}",
|
||||||
|
f"--recipe={recipe}"]
|
||||||
|
started = time.monotonic()
|
||||||
|
peak_rss_kib = 0
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
process = subprocess.Popen(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
start_new_session=True)
|
||||||
|
while process.poll() is None:
|
||||||
|
pending = [process.pid]
|
||||||
|
seen = set()
|
||||||
|
while pending:
|
||||||
|
pid = pending.pop()
|
||||||
|
if pid in seen:
|
||||||
|
continue
|
||||||
|
seen.add(pid)
|
||||||
|
try:
|
||||||
|
children = Path(f"/proc/{pid}/task/{pid}/children").read_text().split()
|
||||||
|
pending.extend(int(value) for value in children)
|
||||||
|
for line in Path(f"/proc/{pid}/status").read_text().splitlines():
|
||||||
|
if line.startswith("VmRSS:"):
|
||||||
|
peak_rss_kib = max(peak_rss_kib, int(line.split()[1]))
|
||||||
|
break
|
||||||
|
except (FileNotFoundError, PermissionError, ProcessLookupError, ValueError):
|
||||||
|
pass
|
||||||
|
if time.monotonic() - started > 900:
|
||||||
|
os.killpg(process.pid, signal.SIGTERM)
|
||||||
|
try:
|
||||||
|
process.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
os.killpg(process.pid, signal.SIGKILL)
|
||||||
|
process.wait()
|
||||||
|
all_pass = False
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
code = process.wait()
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
matched = {key: pattern.findall(output) for key, pattern in HASH_PATTERNS.items()}
|
||||||
|
ok = code == 0 and all(matched.values()) and len(matched["lod"]) == 9
|
||||||
|
if not ok:
|
||||||
|
all_pass = False
|
||||||
|
row = {"recipe": recipe, "compiler": compiler, "workers": workers,
|
||||||
|
"repeat": repeat, "exit": code, "seconds": elapsed,
|
||||||
|
"peak_rss_kib": peak_rss_kib, "ok": ok,
|
||||||
|
"full": matched["full"][-1] if matched["full"] else "",
|
||||||
|
"whole": matched["whole"][-1] if matched["whole"] else "",
|
||||||
|
"lod": " || ".join(matched["lod"]),
|
||||||
|
"exact": matched["exact"][-1] if matched["exact"] else ""}
|
||||||
|
rows.append(row)
|
||||||
|
print(f"[MatrixRun] {name} exit={code} seconds={elapsed:.3f} "
|
||||||
|
f"peak_rss_kib={peak_rss_kib} hash_rows={sum(map(len,matched.values()))} "
|
||||||
|
f"status={'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
|
summary = OUT / "summary.tsv"
|
||||||
|
columns = ["recipe", "compiler", "workers", "repeat", "exit", "seconds", "peak_rss_kib",
|
||||||
|
"ok", "full", "whole", "lod", "exact"]
|
||||||
|
with summary.open("w", encoding="utf-8") as f:
|
||||||
|
f.write("\t".join(columns) + "\n")
|
||||||
|
for row in rows:
|
||||||
|
f.write("\t".join(str(row[key]) for key in columns) + "\n")
|
||||||
|
|
||||||
|
for recipe in RECIPES:
|
||||||
|
for compiler in BINARIES:
|
||||||
|
reference = None
|
||||||
|
for row in rows:
|
||||||
|
if row["recipe"] != recipe or row["compiler"] != compiler:
|
||||||
|
continue
|
||||||
|
signature = (row["full"], row["whole"], row["lod"], row["exact"])
|
||||||
|
if reference is None:
|
||||||
|
reference = signature
|
||||||
|
elif signature != reference:
|
||||||
|
print(f"[MatrixMismatch] recipe={recipe} workers={row['workers']} "
|
||||||
|
f"compiler={compiler} repeat={row['repeat']}", flush=True)
|
||||||
|
all_pass = False
|
||||||
|
# Compiler defaults are intentionally recorded separately: Clang uses contraction off,
|
||||||
|
# while GCC default is the owner-requested control. Report rather than hide any cross-build
|
||||||
|
# difference; worker/repeat determinism is asserted within each compiler above.
|
||||||
|
compiler_signatures = {}
|
||||||
|
for row in rows:
|
||||||
|
if row["recipe"] == recipe:
|
||||||
|
compiler_signatures.setdefault(row["compiler"],
|
||||||
|
(row["full"], row["whole"], row["lod"], row["exact"]))
|
||||||
|
if len(set(compiler_signatures.values())) > 1:
|
||||||
|
print(f"[MatrixCompilerDifference] recipe={recipe} compilers="
|
||||||
|
f"{','.join(compiler_signatures)}", flush=True)
|
||||||
|
print(f"[MatrixSummary] rows={len(rows)} expected=28 result={'PASS' if all_pass else 'FAIL'} "
|
||||||
|
f"path={summary}", flush=True)
|
||||||
|
raise SystemExit(0 if all_pass and len(rows) == 28 else 1)
|
||||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -16,6 +16,7 @@
|
|||||||
#include <limits>
|
#include <limits>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@@ -38,10 +39,69 @@ namespace
|
|||||||
float MaximumHeight=-std::numeric_limits<float>::infinity();
|
float MaximumHeight=-std::numeric_limits<float>::infinity();
|
||||||
std::vector<FVertex> Vertices;
|
std::vector<FVertex> Vertices;
|
||||||
std::vector<std::uint32_t> Indices;
|
std::vector<std::uint32_t> Indices;
|
||||||
|
struct FTileRange
|
||||||
|
{
|
||||||
|
std::array<std::int32_t,4> Key{};
|
||||||
|
std::size_t IndexBegin=0;
|
||||||
|
std::uint32_t IndexCount=0;
|
||||||
|
std::vector<std::uint8_t> FaceMasks;
|
||||||
|
};
|
||||||
|
std::vector<FTileRange> Tiles;
|
||||||
std::uint64_t Triangles=0,SkirtTriangles=0;
|
std::uint64_t Triangles=0,SkirtTriangles=0;
|
||||||
|
std::uint64_t VisibleSkirtTriangles=0,HiddenSameLodSkirtTriangles=0;
|
||||||
|
std::uint64_t DrawnTriangles=0;
|
||||||
std::uint32_t TileCount=0;
|
std::uint32_t TileCount=0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct FTileKeyHash
|
||||||
|
{
|
||||||
|
std::size_t operator()(const std::array<std::int32_t,4>& Key) const noexcept
|
||||||
|
{
|
||||||
|
std::size_t Hash=0xcbf29ce484222325ull;
|
||||||
|
for(const std::int32_t Value:Key)
|
||||||
|
{
|
||||||
|
Hash^=static_cast<std::uint32_t>(Value);
|
||||||
|
Hash*=0x100000001b3ull;
|
||||||
|
}
|
||||||
|
return Hash;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::int32_t FloorDiv(std::int64_t Value,std::int64_t Divisor)
|
||||||
|
{
|
||||||
|
const std::int64_t Quotient=Value/Divisor, Remainder=Value%Divisor;
|
||||||
|
return static_cast<std::int32_t>(Quotient-((Remainder<0)?1:0));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint8_t ComputeVisibleSkirtFaceMask(
|
||||||
|
const std::array<std::int32_t,4>& Tile,
|
||||||
|
const std::unordered_set<std::array<std::int32_t,4>,FTileKeyHash>& Desired)
|
||||||
|
{
|
||||||
|
const std::int64_t Extent=static_cast<std::int64_t>(32)<<Tile[3];
|
||||||
|
const std::int64_t Origin[3]={static_cast<std::int64_t>(Tile[0])*Extent,
|
||||||
|
static_cast<std::int64_t>(Tile[1])*Extent,static_cast<std::int64_t>(Tile[2])*Extent};
|
||||||
|
std::uint8_t Mask=0;
|
||||||
|
for(std::int32_t Axis=0;Axis<3;++Axis)for(std::int32_t Side=0;Side<2;++Side)
|
||||||
|
{
|
||||||
|
std::int64_t Probe[3]={Origin[0]+Extent/2,Origin[1]+Extent/2,
|
||||||
|
Origin[2]+Extent/2};
|
||||||
|
Probe[Axis]=Side?Origin[Axis]+Extent:Origin[Axis]-1;
|
||||||
|
std::int32_t NeighborLevel=-1;
|
||||||
|
for(std::int32_t Level=0;Level<=8;++Level)
|
||||||
|
{
|
||||||
|
const std::int64_t NeighborExtent=static_cast<std::int64_t>(32)<<Level;
|
||||||
|
const std::array<std::int32_t,4> Neighbor={
|
||||||
|
FloorDiv(Probe[0],NeighborExtent),FloorDiv(Probe[1],NeighborExtent),
|
||||||
|
FloorDiv(Probe[2],NeighborExtent),Level};
|
||||||
|
if(Desired.contains(Neighbor))
|
||||||
|
{NeighborLevel=Level;break;}
|
||||||
|
}
|
||||||
|
if(NeighborLevel<0||NeighborLevel!=Tile[3])
|
||||||
|
Mask|=static_cast<std::uint8_t>(1u<<(Axis*2+Side));
|
||||||
|
}
|
||||||
|
return Mask;
|
||||||
|
}
|
||||||
|
|
||||||
template<typename T> bool Read(std::ifstream& File,T& Value)
|
template<typename T> bool Read(std::ifstream& File,T& Value)
|
||||||
{
|
{
|
||||||
File.read(reinterpret_cast<char*>(&Value),sizeof(Value));
|
File.read(reinterpret_cast<char*>(&Value),sizeof(Value));
|
||||||
@@ -126,11 +186,36 @@ namespace
|
|||||||
}
|
}
|
||||||
Scene.Triangles+=IndexCount/3;
|
Scene.Triangles+=IndexCount/3;
|
||||||
Scene.SkirtTriangles+=SkirtCount;
|
Scene.SkirtTriangles+=SkirtCount;
|
||||||
|
Scene.Tiles.push_back({{Key[0],Key[1],Key[2],Key[3]},IndexOffset,
|
||||||
|
IndexCount,std::move(Masks)});
|
||||||
TotalVertices+=VertexCount;TotalIndices+=IndexCount;
|
TotalVertices+=VertexCount;TotalIndices+=IndexCount;
|
||||||
}
|
}
|
||||||
if(File.peek()!=std::char_traits<char>::eof())
|
if(File.peek()!=std::char_traits<char>::eof())
|
||||||
{Error="mesh scene has trailing bytes";return false;}
|
{Error="mesh scene has trailing bytes";return false;}
|
||||||
if(Scene.Indices.empty()){Error="mesh scene contains no triangles";return false;}
|
if(Scene.Indices.empty()){Error="mesh scene contains no triangles";return false;}
|
||||||
|
std::unordered_set<std::array<std::int32_t,4>,FTileKeyHash> Desired;
|
||||||
|
Desired.reserve(Scene.Tiles.size());
|
||||||
|
for(const auto& Tile:Scene.Tiles)Desired.insert(Tile.Key);
|
||||||
|
std::vector<std::uint32_t> VisibleIndices;
|
||||||
|
VisibleIndices.reserve(Scene.Indices.size());
|
||||||
|
for(const auto& Tile:Scene.Tiles)
|
||||||
|
{
|
||||||
|
const std::uint8_t VisibleMask=ComputeVisibleSkirtFaceMask(Tile.Key,Desired);
|
||||||
|
for(std::uint32_t Triangle=0;Triangle<Tile.IndexCount/3;++Triangle)
|
||||||
|
{
|
||||||
|
const std::size_t MaskIndex=Triangle;
|
||||||
|
const std::uint8_t FaceMask=MaskIndex<Tile.FaceMasks.size()
|
||||||
|
?Tile.FaceMasks[MaskIndex]:0;
|
||||||
|
if(FaceMask!=0&&(FaceMask&VisibleMask)==0)
|
||||||
|
{++Scene.HiddenSameLodSkirtTriangles;continue;}
|
||||||
|
if(FaceMask!=0)++Scene.VisibleSkirtTriangles;
|
||||||
|
const std::size_t Index=Tile.IndexBegin+static_cast<std::size_t>(Triangle)*3;
|
||||||
|
VisibleIndices.insert(VisibleIndices.end(),Scene.Indices.begin()+Index,
|
||||||
|
Scene.Indices.begin()+Index+3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Scene.Indices=std::move(VisibleIndices);
|
||||||
|
Scene.DrawnTriangles=Scene.Indices.size()/3;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,9 +484,14 @@ void main()
|
|||||||
{
|
{
|
||||||
std::error_code SizeError;
|
std::error_code SizeError;
|
||||||
const auto Bytes=std::filesystem::file_size(Output,SizeError);
|
const auto Bytes=std::filesystem::file_size(Output,SizeError);
|
||||||
std::printf("[OffscreenCapture] triangles=%llu tiles=%u skirts=%llu shading=%s tile_edges=%s edge_width_m=%.3f png_bytes=%llu status=PASS path=%s\n",
|
std::printf("[OffscreenCapture] source_triangles=%llu drawn_triangles=%llu tiles=%u "
|
||||||
static_cast<unsigned long long>(Scene.Triangles),Scene.TileCount,
|
"skirts=%llu visible_skirt_triangles=%llu hidden_same_lod_skirt_triangles=%llu "
|
||||||
|
"shading=%s tile_edges=%s edge_width_m=%.3f png_bytes=%llu status=PASS path=%s\n",
|
||||||
|
static_cast<unsigned long long>(Scene.Triangles),
|
||||||
|
static_cast<unsigned long long>(Scene.DrawnTriangles),Scene.TileCount,
|
||||||
static_cast<unsigned long long>(Scene.SkirtTriangles),
|
static_cast<unsigned long long>(Scene.SkirtTriangles),
|
||||||
|
static_cast<unsigned long long>(Scene.VisibleSkirtTriangles),
|
||||||
|
static_cast<unsigned long long>(Scene.HiddenSameLodSkirtTriangles),
|
||||||
ReadableMode?"readable":"normal",TileEdges?"on":"off",TileEdgeWidthMeters,
|
ReadableMode?"readable":"normal",TileEdges?"on":"off",TileEdgeWidthMeters,
|
||||||
static_cast<unsigned long long>(SizeError?0:Bytes),Output.string().c_str());
|
static_cast<unsigned long long>(SizeError?0:Bytes),Output.string().c_str());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,21 +41,27 @@
|
|||||||
// SealFactor > 0 et BaseDensity > 0, le résultat est solide GARANTI quelle qu'ait été l'entrée.
|
// SealFactor > 0 et BaseDensity > 0, le résultat est solide GARANTI quelle qu'ait été l'entrée.
|
||||||
// C'est exactement ce qu'encode `IVoxelDensityOp::ClassifyBox` (voir VoxelDensityOp.h), et la
|
// C'est exactement ce qu'encode `IVoxelDensityOp::ClassifyBox` (voir VoxelDensityOp.h), et la
|
||||||
// raison pour laquelle cette méthode existe.
|
// raison pour laquelle cette méthode existe.
|
||||||
FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
|
FORCEINLINE void VF_ApplyUpperBoundarySealValue(float& Density, float WorldZ,
|
||||||
float StrateTopZ, float StrateBottomZ,
|
float StrateTopZ, float Thickness, float BaseDensity)
|
||||||
float Thickness, float BaseDensity)
|
|
||||||
{
|
{
|
||||||
if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return;
|
const float DistTop = StrateTopZ - WorldZ;
|
||||||
|
|
||||||
const float DistTop = StrateTopZ - WorldZ; // + si on est sous le plafond
|
|
||||||
const float DistBot = WorldZ - StrateBottomZ; // + si on est au-dessus du sol
|
|
||||||
|
|
||||||
if (DistTop >= 0.0f && DistTop < Thickness)
|
if (DistTop >= 0.0f && DistTop < Thickness)
|
||||||
{
|
{
|
||||||
float SealFactor = 1.0f - (DistTop / Thickness);
|
float SealFactor = 1.0f - (DistTop / Thickness);
|
||||||
SealFactor = SmoothStep01(SealFactor);
|
SealFactor = SmoothStep01(SealFactor);
|
||||||
Density = FMath::Max(Density, SealFactor * BaseDensity);
|
Density = FMath::Max(Density, SealFactor * BaseDensity);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
|
||||||
|
float StrateTopZ, float StrateBottomZ,
|
||||||
|
float Thickness, float BaseDensity)
|
||||||
|
{
|
||||||
|
if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return;
|
||||||
|
|
||||||
|
const float DistBot = WorldZ - StrateBottomZ; // + si on est au-dessus du sol
|
||||||
|
|
||||||
|
VF_ApplyUpperBoundarySealValue(Density, WorldZ, StrateTopZ, Thickness, BaseDensity);
|
||||||
if (DistBot >= 0.0f && DistBot < Thickness)
|
if (DistBot >= 0.0f && DistBot < Thickness)
|
||||||
{
|
{
|
||||||
float SealFactor = 1.0f - (DistBot / Thickness);
|
float SealFactor = 1.0f - (DistBot / Thickness);
|
||||||
@@ -64,6 +70,16 @@ FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The same structural seal, limited to a strate's upper boundary. Native finite fields use this
|
||||||
|
// when their lower boundary intentionally has no seal; keeping the math here makes that ceiling
|
||||||
|
// use the same shared density primitive as every two-sided strate seal.
|
||||||
|
FORCEINLINE void VF_ApplyUpperBoundarySeal(float& Density, float WorldZ,
|
||||||
|
float StrateTopZ, float Thickness, float BaseDensity)
|
||||||
|
{
|
||||||
|
if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return;
|
||||||
|
VF_ApplyUpperBoundarySealValue(Density, WorldZ, StrateTopZ, Thickness, BaseDensity);
|
||||||
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// CARVE DE PASSAGE / PASSAGE CARVING
|
// CARVE DE PASSAGE / PASSAGE CARVING
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
|
|||||||
@@ -494,11 +494,9 @@ namespace StandaloneWorldAssemblyPrototype
|
|||||||
const std::int64_t coreVoxelsPerOwnerChunk=static_cast<std::int64_t>(CHUNK_SIZE)
|
const std::int64_t coreVoxelsPerOwnerChunk=static_cast<std::int64_t>(CHUNK_SIZE)
|
||||||
*recipe.ownerToCoreScaleNumerator/recipe.ownerToCoreScaleDenominator;
|
*recipe.ownerToCoreScaleNumerator/recipe.ownerToCoreScaleDenominator;
|
||||||
const std::int64_t minimumZ=static_cast<std::int64_t>(islandSlot->BottomChunkZ)
|
const std::int64_t minimumZ=static_cast<std::int64_t>(islandSlot->BottomChunkZ)
|
||||||
*coreVoxelsPerOwnerChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel
|
*coreVoxelsPerOwnerChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||||
+2*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
||||||
const std::int64_t maximumZ=static_cast<std::int64_t>(islandSlot->TopChunkZ+1)
|
const std::int64_t maximumZ=static_cast<std::int64_t>(islandSlot->TopChunkZ+1)
|
||||||
*coreVoxelsPerOwnerChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel
|
*coreVoxelsPerOwnerChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||||
-2*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
||||||
if(maximumZ<=minimumZ)
|
if(maximumZ<=minimumZ)
|
||||||
{error="FloatingIslands strate is too short for a bounded Core recipe";return false;}
|
{error="FloatingIslands strate is too short for a bounded Core recipe";return false;}
|
||||||
recipe.layoutMinimumZSubvoxels=minimumZ;
|
recipe.layoutMinimumZSubvoxels=minimumZ;
|
||||||
|
|||||||